PasswordHistorySection.kt

6.5 KB · raw

1package com.peachpasswords.ui.vault23import androidx.compose.foundation.background4import androidx.compose.foundation.border5import androidx.compose.foundation.clickable6import androidx.compose.foundation.layout.Arrangement7import androidx.compose.foundation.layout.Box8import androidx.compose.foundation.layout.Column9import androidx.compose.foundation.layout.Row10import androidx.compose.foundation.layout.Spacer11import androidx.compose.foundation.layout.fillMaxWidth12import androidx.compose.foundation.layout.height13import androidx.compose.foundation.layout.padding14import androidx.compose.foundation.layout.width15import androidx.compose.foundation.shape.RoundedCornerShape16import androidx.compose.material3.Text17import androidx.compose.runtime.Composable18import androidx.compose.runtime.getValue19import androidx.compose.runtime.mutableStateOf20import androidx.compose.runtime.remember21import androidx.compose.runtime.setValue22import androidx.compose.ui.Alignment23import androidx.compose.ui.Modifier24import androidx.compose.ui.draw.clip25import androidx.compose.ui.text.font.FontStyle26import androidx.compose.ui.unit.dp27import androidx.compose.ui.unit.sp28import com.peachpasswords.ui.theme.LocalPeachAccent29import com.peachpasswords.ui.theme.PeachSpacing30import com.peachpasswords.ui.theme.peachColors31import com.peachpasswords.ui.theme.PeachType32import com.peachpasswords.vault.PasswordHistoryItem3334/**35 * Shared past-password history UI, extracted from EntryEditScreen so both the36 * editor and EntryDetailScreen render the same rows. Behavior is unchanged from37 * the original private copy.38 *39 * - [PasswordHistorySection] is the original combined toggle + content block used40 *   by EntryEditScreen (toggle always present so the affordance is discoverable;41 *   empty state explains how it fills).42 * - [PasswordHistoryToggle] + [PasswordHistoryList] split the same pieces apart so43 *   EntryDetailScreen can drive expand/collapse with AnimatedVisibility itself.44 *45 * Rows are masked until revealed; "use" hands the value to [onRestore].46 */47@Composable48internal fun PasswordHistorySection(49    history: List<PasswordHistoryItem>,50    expanded: Boolean,51    onToggle: () -> Unit,52    onRestore: (String) -> Unit,53    onCopy: (String) -> Unit,54) {55    PasswordHistoryToggle(56        count = history.size,57        expanded = expanded,58        onToggle = onToggle,59        modifier = Modifier.fillMaxWidth().padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.sm),60    )61    if (expanded) {62        Box(Modifier.fillMaxWidth().height(1.dp).background(peachColors.T3))63        Column(Modifier.fillMaxWidth().padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md)) {64            PasswordHistoryList(history = history, onRestore = onRestore, onCopy = onCopy)65        }66    }67}6869/**70 * The "↺ history (N)" affordance — Cormorant italic, accent-tinted. Shows a bare71 * "↺ history" when empty and "↺ hide history" when [expanded].72 */73@Composable74internal fun PasswordHistoryToggle(75    count: Int,76    expanded: Boolean,77    onToggle: () -> Unit,78    modifier: Modifier = Modifier,79) {80    val accent = LocalPeachAccent.current81    val label = if (count == 0) "↺ history" else "↺ history ($count)"82    Text(83        if (expanded) "↺ hide history" else label,84        color = accent.copy(alpha = 0.7f),85        fontFamily = PeachType.Display,86        fontStyle = FontStyle.Italic,87        fontSize = 15.sp,88        modifier = modifier.clip(RoundedCornerShape(6.dp)).clickable(onClick = onToggle).padding(vertical = 4.dp),89    )90}9192/**93 * The expanded body: either the empty-state explainer or the list of past94 * passwords. Caller owns the expand/collapse (and any AnimatedVisibility wrap).95 */96@Composable97internal fun PasswordHistoryList(98    history: List<PasswordHistoryItem>,99    onRestore: (String) -> Unit,100    onCopy: (String) -> Unit,101) {102    val accent = LocalPeachAccent.current103    if (history.isEmpty()) {104        Text(105            "No earlier passwords yet. When you change this password, the old one is saved here so you can copy or restore it.",106            color = peachColors.T2,107            fontFamily = PeachType.Mono,108            fontSize = 10.sp,109            modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),110        )111    } else {112        Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {113            history.forEach { item -> HistoryRow(item, onRestore, onCopy) }114        }115    }116}117118@Composable119private fun HistoryRow(120    item: PasswordHistoryItem,121    onRestore: (String) -> Unit,122    onCopy: (String) -> Unit,123) {124    val accent = LocalPeachAccent.current125    var revealed by remember { mutableStateOf(false) }126    Row(127        Modifier.fillMaxWidth().clip(RoundedCornerShape(8.dp))128            .border(1.dp, accent.copy(alpha = 0.12f), RoundedCornerShape(8.dp))129            .background(accent.copy(alpha = 0.06f)).padding(horizontal = 10.dp, vertical = 8.dp),130        verticalAlignment = Alignment.CenterVertically,131        horizontalArrangement = Arrangement.SpaceBetween,132    ) {133        Column(Modifier.weight(1f)) {134            Text(135                if (revealed) item.password else "••••••••••••",136                color = peachColors.T0, fontFamily = PeachType.Mono, fontSize = 12.sp, maxLines = 1,137            )138            Text(139                timeSince(item.changedAt),140                color = accent.copy(alpha = 0.55f), fontFamily = PeachType.Mono, fontSize = 8.sp,141            )142        }143        Spacer(Modifier.width(6.dp))144        HistoryAction(if (revealed) "hide" else "show") { revealed = !revealed }145        HistoryAction("copy") { onCopy(item.password) }146        HistoryAction("use") { onRestore(item.password) }147    }148}149150@Composable151private fun HistoryAction(label: String, onClick: () -> Unit) {152    Text(153        label, color = LocalPeachAccent.current.copy(alpha = 0.7f),154        fontFamily = PeachType.Mono, fontSize = 9.sp,155        modifier = Modifier.clip(RoundedCornerShape(5.dp)).clickable(onClick = onClick).padding(horizontal = 5.dp, vertical = 3.dp),156    )157}158159/** Coarse "time since" label for the history list. Mirrors the browser helper. */160private fun timeSince(whenMs: Long): String {161    val secs = ((System.currentTimeMillis() - whenMs) / 1000).coerceAtLeast(0)162    if (secs < 60) return "just now"163    val mins = secs / 60164    if (mins < 60) return "${mins}m ago"165    val hours = mins / 60166    if (hours < 24) return "${hours}h ago"167    val days = hours / 24168    if (days < 30) return "${days}d ago"169    val months = days / 30170    if (months < 12) return "${months}mo ago"171    return "${months / 12}y ago"172}