CopyButton.kt

3.6 KB · raw

1package com.peachpasswords.ui.components23import androidx.compose.animation.core.LinearEasing4import androidx.compose.animation.core.animateFloatAsState5import androidx.compose.animation.core.tween6import androidx.compose.foundation.background7import androidx.compose.foundation.clickable8import androidx.compose.foundation.layout.Box9import androidx.compose.foundation.layout.size10import androidx.compose.foundation.shape.RoundedCornerShape11import androidx.compose.material.icons.Icons12import androidx.compose.material.icons.rounded.Check13import androidx.compose.material.icons.rounded.ContentCopy14import androidx.compose.material3.Icon15import androidx.compose.runtime.Composable16import androidx.compose.runtime.getValue17import androidx.compose.ui.Alignment18import androidx.compose.ui.Modifier19import androidx.compose.ui.draw.clip20import androidx.compose.ui.draw.drawBehind21import androidx.compose.ui.geometry.Offset22import androidx.compose.ui.geometry.Size23import androidx.compose.ui.graphics.Brush24import androidx.compose.ui.graphics.Color25import androidx.compose.ui.unit.dp26import com.peachpasswords.ui.theme.LocalPeachAccent27import com.peachpasswords.ui.theme.LocalReduceMotion28import com.peachpasswords.ui.theme.peachColors2930/**31 * Copy button with the two-effect copy feedback (spec §4.4): a diagonal sheen32 * sweeps once over ~600ms and an accent drain bar fills 0→100% over 1.4s. Both33 * collapse instantly when the copied latch clears, and are skipped under34 * reduce-motion.35 *36 * Extracted from EntryDetailScreen so the generator drawer (EntryEditScreen,37 * detail bottom sheet) can reuse the identical drain animation.38 */39@Composable40fun CopyButton(copied: Boolean, onCopy: () -> Unit) {41    val accent = LocalPeachAccent.current42    val reduceMotion = LocalReduceMotion.current43    val animate = copied && !reduceMotion44    val drain by animateFloatAsState(45        targetValue = if (copied) 1f else 0f,46        animationSpec = if (animate) tween(1400, easing = LinearEasing) else tween(0),47        label = "copy-drain",48    )49    val sheen by animateFloatAsState(50        targetValue = if (copied) 1f else 0f,51        animationSpec = if (animate) tween(600, easing = LinearEasing) else tween(0),52        label = "copy-sheen",53    )54    Box(55        Modifier56            .size(36.dp)57            .clip(RoundedCornerShape(6.dp))58            .background(peachColors.S2)59            .clickable(onClick = onCopy)60            .drawBehind {61                if (animate && sheen < 1f) {62                    val sweepX = (sheen * 2f - 0.5f) * size.width63                    val band = size.width * 0.5f64                    drawRect(65                        brush = Brush.linearGradient(66                            colors = listOf(Color.Transparent, accent.copy(alpha = 0.35f), Color.Transparent),67                            start = Offset(sweepX - band, 0f),68                            end = Offset(sweepX + band, size.height),69                        ),70                    )71                }72                if (drain > 0f) {73                    val h = 2.dp.toPx()74                    drawRect(75                        color = accent,76                        topLeft = Offset(0f, size.height - h),77                        size = Size(size.width * drain, h),78                    )79                }80            },81        contentAlignment = Alignment.Center,82    ) {83        Icon(84            if (copied) Icons.Rounded.Check else Icons.Rounded.ContentCopy,85            contentDescription = "copy",86            tint = if (copied) accent else peachColors.T1,87            modifier = Modifier.size(15.dp),88        )89    }90}