Explorar o código

[优化] 左滑触发合并操作,优化界面

HapeLee hai 8 meses
pai
achega
3c40135f12

+ 1 - 0
app/src/main/java/io/legado/app/data/repository/ReadRecordRepository.kt

@@ -212,6 +212,7 @@ class ReadRecordRepository(
         }
         }
     }
     }
 
 
+    @Transaction
     private suspend fun mergeSingleReadRecordInto(targetRecord: ReadRecord, sourceRecord: ReadRecord) {
     private suspend fun mergeSingleReadRecordInto(targetRecord: ReadRecord, sourceRecord: ReadRecord) {
         if (targetRecord == sourceRecord) return
         if (targetRecord == sourceRecord) return
         if (targetRecord.deviceId != sourceRecord.deviceId) return
         if (targetRecord.deviceId != sourceRecord.deviceId) return

+ 99 - 74
app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt

@@ -3,6 +3,7 @@ package io.legado.app.ui.book.readRecord
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.background
 import androidx.compose.foundation.background
+import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
@@ -23,6 +24,7 @@ import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.List
 import androidx.compose.material.icons.automirrored.filled.List
 import androidx.compose.material.icons.filled.CalendarMonth
 import androidx.compose.material.icons.filled.CalendarMonth
 import androidx.compose.material.icons.filled.Delete
 import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Merge
 import androidx.compose.material.icons.filled.Schedule
 import androidx.compose.material.icons.filled.Schedule
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.material.icons.filled.Timeline
 import androidx.compose.material.icons.filled.Timeline
@@ -36,6 +38,8 @@ import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.material3.TextButton
@@ -47,6 +51,7 @@ import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.produceState
 import androidx.compose.runtime.produceState
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
@@ -56,8 +61,11 @@ import androidx.compose.ui.draw.rotate
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.withStyle
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
 import androidx.compose.ui.zIndex
 import cn.hutool.core.date.DateUtil
 import cn.hutool.core.date.DateUtil
@@ -75,6 +83,7 @@ import io.legado.app.ui.widget.components.button.SmallTopBarButton
 import io.legado.app.ui.widget.components.swipe.SwipeAction
 import io.legado.app.ui.widget.components.swipe.SwipeAction
 import io.legado.app.ui.widget.components.swipe.SwipeActionContainer
 import io.legado.app.ui.widget.components.swipe.SwipeActionContainer
 import io.legado.app.utils.StringUtils.formatFriendlyDate
 import io.legado.app.utils.StringUtils.formatFriendlyDate
+import kotlinx.coroutines.launch
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
 import java.time.LocalDate
 import java.time.LocalDate
 import java.time.format.DateTimeFormatter
 import java.time.format.DateTimeFormatter
@@ -87,6 +96,9 @@ fun ReadRecordScreen(
     onBackClick: () -> Unit,
     onBackClick: () -> Unit,
     onBookClick: (String, String) -> Unit
     onBookClick: (String, String) -> Unit
 ) {
 ) {
+    val snackbarHostState = remember { SnackbarHostState() }
+    val scope = rememberCoroutineScope()
+
     val state by viewModel.uiState.collectAsState()
     val state by viewModel.uiState.collectAsState()
     val displayMode by viewModel.displayMode.collectAsState()
     val displayMode by viewModel.displayMode.collectAsState()
     var showSearch by remember { mutableStateOf(false) }
     var showSearch by remember { mutableStateOf(false) }
@@ -96,7 +108,7 @@ fun ReadRecordScreen(
 
 
     var skipDeleteConfirm by remember { mutableStateOf(false) }
     var skipDeleteConfirm by remember { mutableStateOf(false) }
     var pendingDeleteAction by remember { mutableStateOf<(() -> Unit)?>(null) }
     var pendingDeleteAction by remember { mutableStateOf<(() -> Unit)?>(null) }
-    var mergeTargetRecord by remember { mutableStateOf<ReadRecord?>(null) }
+    var mergeDialogData by remember { mutableStateOf<Pair<ReadRecord, List<ReadRecord>>?>(null) }
     val onConfirmDelete: (() -> Unit) -> Unit = { action ->
     val onConfirmDelete: (() -> Unit) -> Unit = { action ->
         if (skipDeleteConfirm) {
         if (skipDeleteConfirm) {
             action()
             action()
@@ -113,6 +125,7 @@ fun ReadRecordScreen(
 
 
     Scaffold(
     Scaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+        snackbarHost = { SnackbarHost(snackbarHostState) },
         topBar = {
         topBar = {
             Column {
             Column {
                 GlassMediumFlexibleTopAppBar(
                 GlassMediumFlexibleTopAppBar(
@@ -228,7 +241,16 @@ fun ReadRecordScreen(
                                 viewModel = viewModel,
                                 viewModel = viewModel,
                                 onBookClick = onBookClick,
                                 onBookClick = onBookClick,
                                 onConfirmDelete = onConfirmDelete,
                                 onConfirmDelete = onConfirmDelete,
-                                onMergeClick = { mergeTargetRecord = it }
+                                onMergeClick = { record ->
+                                    scope.launch {
+                                        val candidates = viewModel.getMergeCandidates(record)
+                                        if (candidates.isEmpty()) {
+                                            snackbarHostState.showSnackbar("没有可合并的同名记录")
+                                        } else {
+                                            mergeDialogData = record to candidates
+                                        }
+                                    }
+                                }
                             )
                             )
                         }
                         }
                     }
                     }
@@ -289,49 +311,40 @@ fun ReadRecordScreen(
         )
         )
     }
     }
 
 
-    mergeTargetRecord?.let { targetRecord ->
-        val candidates by produceState(
-            initialValue = emptyList<ReadRecord>(),
-            key1 = targetRecord.bookName,
-            key2 = targetRecord.bookAuthor
-        ) {
-            value = viewModel.getMergeCandidates(targetRecord)
-        }
-        var selectedAuthors by remember(targetRecord.bookName, targetRecord.bookAuthor, candidates) {
+    mergeDialogData?.let { (targetRecord, candidates) ->
+
+        var selectedAuthors by remember(targetRecord, candidates) {
             mutableStateOf(candidates.map { it.bookAuthor }.toSet())
             mutableStateOf(candidates.map { it.bookAuthor }.toSet())
         }
         }
 
 
         AlertDialog(
         AlertDialog(
-            onDismissRequest = { mergeTargetRecord = null },
+            onDismissRequest = { mergeDialogData = null },
             title = { Text("合并阅读记录") },
             title = { Text("合并阅读记录") },
             text = {
             text = {
-                if (candidates.isEmpty()) {
-                    Text("没有可合并的同名记录")
-                } else {
-                    Column {
-                        Text("将以下作者的“${targetRecord.bookName}”合并到 ${targetRecord.bookAuthor.ifBlank { "未知作者" }}")
-                        Spacer(modifier = Modifier.height(8.dp))
-                        candidates.forEach { candidate ->
-                            val author = candidate.bookAuthor.ifBlank { "未知作者" }
-                            val isChecked = selectedAuthors.contains(candidate.bookAuthor)
-                            Row(
-                                modifier = Modifier
-                                    .fillMaxWidth()
-                                    .clickable {
-                                        selectedAuthors =
-                                            if (isChecked) selectedAuthors - candidate.bookAuthor
-                                            else selectedAuthors + candidate.bookAuthor
-                                    }
-                                    .padding(vertical = 6.dp),
-                                verticalAlignment = Alignment.CenterVertically
-                            ) {
-                                Checkbox(checked = isChecked, onCheckedChange = null)
-                                Text(
-                                    text = "$author(${formatDuring(candidate.readTime)})",
-                                    modifier = Modifier.padding(start = 8.dp),
-                                    style = MaterialTheme.typography.bodyMedium
-                                )
-                            }
+                Column {
+                    Text("将以下作者的“${targetRecord.bookName}”合并到 ${targetRecord.bookAuthor.ifBlank { "未知作者" }}")
+                    Spacer(modifier = Modifier.height(8.dp))
+
+                    candidates.forEach { candidate ->
+                        val author = candidate.bookAuthor.ifBlank { "未知作者" }
+                        val isChecked = selectedAuthors.contains(candidate.bookAuthor)
+                        Row(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .clickable {
+                                    selectedAuthors =
+                                        if (isChecked) selectedAuthors - candidate.bookAuthor
+                                        else selectedAuthors + candidate.bookAuthor
+                                }
+                                .padding(vertical = 6.dp),
+                            verticalAlignment = Alignment.CenterVertically
+                        ) {
+                            Checkbox(checked = isChecked, onCheckedChange = null)
+                            Text(
+                                text = "$author(${formatDuring(candidate.readTime)})",
+                                modifier = Modifier.padding(start = 8.dp),
+                                style = MaterialTheme.typography.bodyMedium
+                            )
                         }
                         }
                     }
                     }
                 }
                 }
@@ -339,20 +352,18 @@ fun ReadRecordScreen(
             confirmButton = {
             confirmButton = {
                 TextButton(
                 TextButton(
                     onClick = {
                     onClick = {
-                        if (candidates.isNotEmpty()) {
-                            viewModel.mergeReadRecords(
-                                targetRecord,
-                                candidates.filter { selectedAuthors.contains(it.bookAuthor) }
-                            )
-                        }
-                        mergeTargetRecord = null
+                        viewModel.mergeReadRecords(
+                            targetRecord,
+                            candidates.filter { selectedAuthors.contains(it.bookAuthor) }
+                        )
+                        mergeDialogData = null
                     }
                     }
                 ) {
                 ) {
                     Text("合并")
                     Text("合并")
                 }
                 }
             },
             },
             dismissButton = {
             dismissButton = {
-                TextButton(onClick = { mergeTargetRecord = null }) {
+                TextButton(onClick = { mergeDialogData = null }) {
                     Text("取消")
                     Text("取消")
                 }
                 }
             }
             }
@@ -409,6 +420,7 @@ fun LazyListScope.renderListByMode(
     onConfirmDelete: (() -> Unit) -> Unit,
     onConfirmDelete: (() -> Unit) -> Unit,
     onMergeClick: (ReadRecord) -> Unit
     onMergeClick: (ReadRecord) -> Unit
 ) {
 ) {
+
     when (displayMode) {
     when (displayMode) {
         DisplayMode.AGGREGATE -> {
         DisplayMode.AGGREGATE -> {
             state.groupedRecords.forEach { (date, details) ->
             state.groupedRecords.forEach { (date, details) ->
@@ -469,13 +481,19 @@ fun LazyListScope.renderListByMode(
                         onSwipe = {
                         onSwipe = {
                             onConfirmDelete { viewModel.deleteReadRecord(record) }
                             onConfirmDelete { viewModel.deleteReadRecord(record) }
                         }
                         }
+                    ),
+                    endAction = SwipeAction(
+                        icon = Icons.Default.Merge,
+                        background = MaterialTheme.colorScheme.primary,
+                        onSwipe = {
+                            onMergeClick(record)
+                        }
                     )
                     )
                 ) {
                 ) {
                         LatestReadItem(
                         LatestReadItem(
                             record = record,
                             record = record,
                             viewModel = viewModel,
                             viewModel = viewModel,
-                            onClick = { onBookClick(record.bookName, record.bookAuthor) },
-                            onMergeClick = { onMergeClick(record) }
+                            onClick = { onBookClick(record.bookName, record.bookAuthor) }
                         )
                         )
                     }
                     }
                 }
                 }
@@ -488,7 +506,6 @@ fun LatestReadItem(
     record: ReadRecord,
     record: ReadRecord,
     viewModel: ReadRecordViewModel,
     viewModel: ReadRecordViewModel,
     onClick: () -> Unit,
     onClick: () -> Unit,
-    onMergeClick: () -> Unit,
     modifier: Modifier = Modifier
     modifier: Modifier = Modifier
 ) {
 ) {
     var coverPath by remember { mutableStateOf<String?>(null) }
     var coverPath by remember { mutableStateOf<String?>(null) }
@@ -512,30 +529,38 @@ fun LatestReadItem(
             Text(
             Text(
                 text = record.bookName,
                 text = record.bookName,
                 style = MaterialTheme.typography.titleMedium,
                 style = MaterialTheme.typography.titleMedium,
-                maxLines = 2
+                maxLines = 1,
+                overflow = TextOverflow.Ellipsis
             )
             )
             Text(
             Text(
                 text = record.bookAuthor.ifBlank { "未知作者" },
                 text = record.bookAuthor.ifBlank { "未知作者" },
                 style = MaterialTheme.typography.bodySmall,
                 style = MaterialTheme.typography.bodySmall,
-                color = Color.Gray,
+                color = MaterialTheme.colorScheme.outline,
                 maxLines = 1,
                 maxLines = 1,
                 overflow = TextOverflow.Ellipsis
                 overflow = TextOverflow.Ellipsis
             )
             )
-            Spacer(modifier = Modifier.height(4.dp))
+            Spacer(modifier = Modifier.height(8.dp))
             Text(
             Text(
-                text = "总时长: ${formatDuring(record.readTime)}",
-                style = MaterialTheme.typography.bodySmall,
-                color = Color.Gray
-            )
-            Text(
-                text = "最后阅读: ${DateUtil.format(Date(record.lastRead), "yyyy-MM-dd HH:mm")}",
-                style = MaterialTheme.typography.bodySmall,
-                color = MaterialTheme.colorScheme.primary
+                modifier = Modifier
+                    .basicMarquee(
+                        iterations = Int.MAX_VALUE,
+                        repeatDelayMillis = 2000,
+                        initialDelayMillis = 1000
+                    ),
+                text = buildAnnotatedString {
+                    withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.outline)) {
+                        append(formatDuring(record.readTime))
+                        append(" • ")
+                    }
+                    withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
+                        append("${DateUtil.format(Date(record.lastRead), "yyyy-MM-dd HH:mm")}")
+                    }
+                },
+                style = MaterialTheme.typography.labelSmall,
+                maxLines = 1,
+                overflow = TextOverflow.Ellipsis
             )
             )
         }
         }
-        TextButton(onClick = onMergeClick) {
-            Text("合并")
-        }
     }
     }
 }
 }
 
 
@@ -613,21 +638,21 @@ fun TimelineSessionItem(
                     Text(
                     Text(
                         text = session.bookName,
                         text = session.bookName,
                         style = MaterialTheme.typography.titleMedium,
                         style = MaterialTheme.typography.titleMedium,
-                        maxLines = 2,
-                        minLines = 2,
+                        maxLines = 1,
                         overflow = TextOverflow.Ellipsis
                         overflow = TextOverflow.Ellipsis
                     )
                     )
                     Text(
                     Text(
                         text = session.bookAuthor.ifBlank { "未知作者" },
                         text = session.bookAuthor.ifBlank { "未知作者" },
                         style = MaterialTheme.typography.bodySmall,
                         style = MaterialTheme.typography.bodySmall,
-                        color = Color.Gray,
+                        color = MaterialTheme.colorScheme.outline,
                         maxLines = 1,
                         maxLines = 1,
                         overflow = TextOverflow.Ellipsis
                         overflow = TextOverflow.Ellipsis
                     )
                     )
+                    Spacer(modifier = Modifier.height(8.dp))
                     Text(
                     Text(
                         text = chapterTitle.orEmpty(),
                         text = chapterTitle.orEmpty(),
-                        style = MaterialTheme.typography.bodySmall,
-                        color = Color.Gray,
+                        style = MaterialTheme.typography.labelSmall,
+                        color = MaterialTheme.colorScheme.outline,
                         maxLines = 1,
                         maxLines = 1,
                         overflow = TextOverflow.Ellipsis
                         overflow = TextOverflow.Ellipsis
                     )
                     )
@@ -665,18 +690,18 @@ fun ReadRecordItem(
             Text(
             Text(
                 text = detail.bookName,
                 text = detail.bookName,
                 style = MaterialTheme.typography.titleMedium,
                 style = MaterialTheme.typography.titleMedium,
-                maxLines = 2
+                maxLines = 1
             )
             )
             Text(
             Text(
                 text = detail.bookAuthor.ifBlank { "未知作者" },
                 text = detail.bookAuthor.ifBlank { "未知作者" },
                 style = MaterialTheme.typography.bodySmall,
                 style = MaterialTheme.typography.bodySmall,
-                color = Color.Gray
+                color = MaterialTheme.colorScheme.outline,
             )
             )
-            Spacer(modifier = Modifier.height(4.dp))
+            Spacer(modifier = Modifier.height(8.dp))
             Text(
             Text(
                 text = "阅读时长: ${formatDuring(detail.readTime)}",
                 text = "阅读时长: ${formatDuring(detail.readTime)}",
-                style = MaterialTheme.typography.bodySmall,
-                color = Color.Gray
+                color = MaterialTheme.colorScheme.outline,
+                style = MaterialTheme.typography.labelSmall
             )
             )
         }
         }
     }
     }

+ 83 - 36
app/src/main/java/io/legado/app/ui/widget/components/swipe/SwipeActionContainer.kt

@@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp
 fun SwipeActionContainer(
 fun SwipeActionContainer(
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
     startAction: SwipeAction? = null,
     startAction: SwipeAction? = null,
+    endAction: SwipeAction? = null,
     content: @Composable () -> Unit
     content: @Composable () -> Unit
 ) {
 ) {
     val haptic = LocalHapticFeedback.current
     val haptic = LocalHapticFeedback.current
@@ -39,8 +40,6 @@ fun SwipeActionContainer(
             totalDistance * 0.6f
             totalDistance * 0.6f
         }
         }
     )
     )
-    val isThresholdReached =
-        dismissState.progress > 0.5f && dismissState.targetValue == SwipeToDismissBoxValue.StartToEnd
 
 
     LaunchedEffect(dismissState.targetValue) {
     LaunchedEffect(dismissState.targetValue) {
         if (dismissState.targetValue == SwipeToDismissBoxValue.StartToEnd) {
         if (dismissState.targetValue == SwipeToDismissBoxValue.StartToEnd) {
@@ -51,9 +50,18 @@ fun SwipeActionContainer(
     }
     }
 
 
     LaunchedEffect(dismissState.settledValue) {
     LaunchedEffect(dismissState.settledValue) {
-        if (dismissState.settledValue == SwipeToDismissBoxValue.StartToEnd) {
-            startAction?.onSwipe?.invoke()
-            dismissState.reset()
+        when (dismissState.settledValue) {
+            SwipeToDismissBoxValue.StartToEnd -> {
+                startAction?.onSwipe?.invoke()
+                dismissState.reset()
+            }
+
+            SwipeToDismissBoxValue.EndToStart -> {
+                endAction?.onSwipe?.invoke()
+                dismissState.reset()
+            }
+
+            else -> {}
         }
         }
     }
     }
 
 
@@ -61,44 +69,34 @@ fun SwipeActionContainer(
         state = dismissState,
         state = dismissState,
         modifier = modifier,
         modifier = modifier,
         enableDismissFromStartToEnd = startAction != null,
         enableDismissFromStartToEnd = startAction != null,
-        enableDismissFromEndToStart = false,
+        enableDismissFromEndToStart = endAction != null,
         backgroundContent = {
         backgroundContent = {
             val direction = dismissState.dismissDirection
             val direction = dismissState.dismissDirection
             val progress = dismissState.progress
             val progress = dismissState.progress
 
 
-            if (direction == SwipeToDismissBoxValue.StartToEnd) {
-                val backgroundColor by animateColorAsState(
-                    targetValue = if (isThresholdReached) startAction!!.background
-                    else MaterialTheme.colorScheme.surfaceVariant,
-                    label = "bgColor"
-                )
+            when (direction) {
 
 
-                Box(
-                    modifier = Modifier
-                        .fillMaxSize()
-                        .background(backgroundColor)
-                        .padding(horizontal = 24.dp),
-                    contentAlignment = Alignment.CenterStart
-                ) {
-                    val iconScale by animateFloatAsState(
-                        targetValue = if (isThresholdReached) 1.3f else progress.coerceIn(0.5f, 1f),
-                        label = "iconScale"
-                    )
+                SwipeToDismissBoxValue.StartToEnd -> {
+                    startAction?.let { action ->
+                        SwipeBackground(
+                            action = action,
+                            progress = progress,
+                            alignStart = true
+                        )
+                    }
+                }
 
 
-                    Icon(
-                        imageVector = startAction!!.icon,
-                        contentDescription = null,
-                        modifier = Modifier
-                            .size(24.dp)
-                            .graphicsLayer {
-                                scaleX = iconScale
-                                scaleY = iconScale
-                            },
-                        tint = if (isThresholdReached)
-                            contentColorFor(startAction.background)
-                        else MaterialTheme.colorScheme.onSurfaceVariant
-                    )
+                SwipeToDismissBoxValue.EndToStart -> {
+                    endAction?.let { action ->
+                        SwipeBackground(
+                            action = action,
+                            progress = progress,
+                            alignStart = false
+                        )
+                    }
                 }
                 }
+
+                else -> {}
             }
             }
         },
         },
         content = {
         content = {
@@ -111,4 +109,53 @@ fun SwipeActionContainer(
             }
             }
         }
         }
     )
     )
+}
+
+@Composable
+private fun SwipeBackground(
+    action: SwipeAction,
+    progress: Float,
+    alignStart: Boolean
+) {
+    val isThresholdReached = progress > 0.6f
+
+    val backgroundColor by animateColorAsState(
+        targetValue = if (isThresholdReached)
+            action.background
+        else
+            MaterialTheme.colorScheme.surfaceVariant,
+        label = "bgColor"
+    )
+
+    val iconScale by animateFloatAsState(
+        targetValue = if (isThresholdReached) 1.3f
+        else progress.coerceIn(0.5f, 1f),
+        label = "iconScale"
+    )
+
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .background(backgroundColor)
+            .padding(horizontal = 24.dp),
+        contentAlignment = if (alignStart)
+            Alignment.CenterStart
+        else
+            Alignment.CenterEnd
+    ) {
+        Icon(
+            imageVector = action.icon,
+            contentDescription = null,
+            modifier = Modifier
+                .size(24.dp)
+                .graphicsLayer {
+                    scaleX = iconScale
+                    scaleY = iconScale
+                },
+            tint = if (isThresholdReached)
+                contentColorFor(action.background)
+            else
+                MaterialTheme.colorScheme.onSurfaceVariant
+        )
+    }
 }
 }