SyncIndicatorDot.kt

2.9 KB · raw

1package com.peachpasswords.ui.components23import androidx.compose.animation.core.RepeatMode4import androidx.compose.animation.core.animateFloat5import androidx.compose.animation.core.infiniteRepeatable6import androidx.compose.animation.core.rememberInfiniteTransition7import androidx.compose.animation.core.tween8import androidx.compose.animation.animateColorAsState9import androidx.compose.foundation.background10import androidx.compose.foundation.layout.Box11import androidx.compose.foundation.layout.size12import androidx.compose.foundation.shape.CircleShape13import androidx.compose.runtime.Composable14import androidx.compose.runtime.getValue15import androidx.compose.ui.Modifier16import androidx.compose.ui.draw.alpha17import androidx.compose.ui.draw.clip18import androidx.compose.ui.semantics.contentDescription19import androidx.compose.ui.semantics.semantics20import androidx.compose.ui.unit.Dp21import androidx.compose.ui.unit.dp22import com.peachpasswords.sync.SyncIndicator23import com.peachpasswords.ui.theme.LocalReduceMotion24import com.peachpasswords.ui.theme.peachColors2526/**27 * The sync indicator light, shared by the vault list and the Sync settings card28 * so the two can never disagree. Green pulsing while a sync is running, solid29 * green on a healthy pair, red on a problem, unlit when there is nothing to30 * report — the same four states as the browser's `stk-pip`.31 *32 * [label] is not decoration: colour alone is never the signal, so it carries33 * the same claim in words for TalkBack and for a long-press tooltip.34 */35@Composable36fun SyncIndicatorDot(37    indicator: SyncIndicator,38    label: String,39    modifier: Modifier = Modifier,40    size: Dp = 6.dp,41) {42    val color by animateColorAsState(43        targetValue = when (indicator) {44            // Busy is green too, not amber: it is the same healthy sync, in45            // progress. The pulse carries "in progress", the hue carries "well".46            SyncIndicator.BUSY, SyncIndicator.OK -> peachColors.SyncOk47            SyncIndicator.WARN -> peachColors.SyncWarn48            SyncIndicator.ERROR -> peachColors.SyncErr49            SyncIndicator.IDLE -> peachColors.T250        },51        label = "sync-indicator",52    )53    val pulsing = indicator == SyncIndicator.BUSY && !LocalReduceMotion.current54    val alpha = if (pulsing) {55        val transition = rememberInfiniteTransition(label = "sync-pulse")56        val value by transition.animateFloat(57            initialValue = 1f,58            targetValue = 0.3f,59            animationSpec = infiniteRepeatable(60                animation = tween(900),61                repeatMode = RepeatMode.Reverse,62            ),63            label = "sync-pulse-alpha",64        )65        value66    } else {67        1f68    }69    Box(70        modifier71            .size(size)72            .alpha(alpha)73            .clip(CircleShape)74            .background(color)75            .semantics { contentDescription = label },76    )77}