EntryDetailGenerator.kt

7.7 KB · raw

1package com.peachpasswords.ui.vault23/*4 * D4 drafts — generator access from EntryDetailScreen.5 *6 * Provides:7 *   - WrenchButton: a 18dp wrench icon to place beside the password copy button.8 *   - GeneratorSheet: a ModalBottomSheet wrapping the shared GeneratorBody. It9 *     holds LOCAL generator option + draft-password state, never auto-saves, and10 *     exposes [Copy] (drain) + [Use this password] (accent). "Use this password"11 *     calls onUse(draft) which the screen wires to the VM's setPassword path.12 *13 * This is a DRAFT. The orchestrator integrates per INTEGRATION.md: it must add a14 * `setPassword`/`updateEntry` method to EntryDetailViewModel (see INTEGRATION.md)15 * and a `copy(...)`-backed copied latch for the sheet's draft.16 */1718import androidx.compose.foundation.background19import androidx.compose.foundation.border20import androidx.compose.foundation.clickable21import androidx.compose.foundation.layout.Box22import androidx.compose.foundation.layout.Column23import androidx.compose.foundation.layout.Spacer24import androidx.compose.foundation.layout.fillMaxWidth25import androidx.compose.foundation.layout.height26import androidx.compose.foundation.layout.padding27import androidx.compose.foundation.layout.size28import androidx.compose.foundation.shape.RoundedCornerShape29import androidx.compose.material.icons.Icons30import androidx.compose.material.icons.outlined.Build31import androidx.compose.material3.ExperimentalMaterial3Api32import androidx.compose.material3.Icon33import androidx.compose.material3.ModalBottomSheet34import androidx.compose.material3.Text35import androidx.compose.material3.rememberModalBottomSheetState36import androidx.compose.runtime.Composable37import androidx.compose.runtime.getValue38import androidx.compose.runtime.mutableStateOf39import androidx.compose.runtime.remember40import androidx.compose.runtime.setValue41import androidx.compose.ui.Alignment42import androidx.compose.ui.Modifier43import androidx.compose.ui.draw.clip44import androidx.compose.ui.text.font.FontStyle45import androidx.compose.ui.unit.dp46import androidx.compose.ui.unit.sp47import com.peachpasswords.ui.theme.LocalPeachAccent48import com.peachpasswords.ui.theme.peachColors49import com.peachpasswords.ui.theme.PeachType50import com.peachpasswords.vault.PasswordGenerator5152/**53 * Opens the generator sheet from the detail password row. Matches CopyButton's54 * icon-button shape (36dp, 6dp rounded, filled background) — this used to be a55 * bare icon on a plain CircleShape clip with no background or press feedback,56 * visibly inconsistent with the copy button sitting right next to it.57 */58@Composable59internal fun WrenchButton(onClick: () -> Unit) {60    Box(61        Modifier62            .size(36.dp)63            .clip(RoundedCornerShape(6.dp))64            .background(peachColors.S2)65            .clickable(onClick = onClick),66        contentAlignment = Alignment.Center,67    ) {68        Icon(69            Icons.Outlined.Build,70            contentDescription = "generate password",71            tint = peachColors.T2,72            modifier = Modifier.size(15.dp),73        )74    }75}7677/**78 * Generator bottom sheet for EntryDetailScreen. Local-only: generating populates79 * a draft password (never persisted). The user copies it (drain) or commits it80 * via [Use this password], which fires onUse(draft) and dismisses.81 *82 * @param copied        latch for the [Copy] drain animation (driven by the screen/VM).83 * @param onCopy        copy the current draft to clipboard (sets the latch).84 * @param onUse         commit the draft password (VM updateEntry + history capture).85 * @param onDismiss     close the sheet without committing.86 */87@OptIn(ExperimentalMaterial3Api::class)88@Composable89internal fun GeneratorSheet(90    copied: Boolean,91    onCopy: (String) -> Unit,92    onUse: (String) -> Unit,93    onDismiss: () -> Unit,94) {95    val accent = LocalPeachAccent.current96    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)9798    var mode by remember { mutableStateOf(GeneratorMode.Password) }99    var length by remember { mutableStateOf(20) }100    var wordCount by remember { mutableStateOf(PasswordGenerator.DEFAULT_PASSPHRASE_WORDS) }101    var separator by remember { mutableStateOf("-") }102    var upper by remember { mutableStateOf(true) }103    var lower by remember { mutableStateOf(true) }104    var numbers by remember { mutableStateOf(true) }105    var symbols by remember { mutableStateOf(true) }106    var draft by remember { mutableStateOf("") }107108    fun gen() {109        draft = if (mode == GeneratorMode.Passphrase) {110            PasswordGenerator.generatePassphrase(wordCount, separator)111        } else {112            PasswordGenerator.generate(113                PasswordGenerator.Options(114                    length = length,115                    includeUppercase = upper,116                    includeLowercase = lower,117                    includeNumbers = numbers,118                    includeSymbols = symbols,119                ),120            )121        }122    }123124    ModalBottomSheet(125        onDismissRequest = onDismiss,126        sheetState = sheetState,127        containerColor = peachColors.S1,128    ) {129        Column(Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 4.dp)) {130            Text(131                "generate password",132                color = peachColors.T0,133                fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 18.sp,134            )135            Spacer(Modifier.height(8.dp))136137            // Draft preview — empty until first generate.138            Box(139                Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp))140                    .border(1.dp, peachColors.T3, RoundedCornerShape(10.dp))141                    .padding(horizontal = 12.dp, vertical = 14.dp),142            ) {143                Text(144                    text = draft.ifEmpty { "tap generate" },145                    color = if (draft.isEmpty()) peachColors.T2 else peachColors.T0,146                    fontFamily = PeachType.Mono, fontSize = 13.sp,147                )148            }149            Spacer(Modifier.height(6.dp))150151            // Shared generator controls + [Copy] drain button on the generate row.152            GeneratorBody(153                mode = mode,154                length = length,155                wordCount = wordCount,156                separator = separator,157                upper = upper, lower = lower, numbers = numbers, symbols = symbols,158                password = draft, copied = copied,159                onMode = { mode = it; gen() },160                onLength = { length = it },161                onWordCount = { wordCount = it },162                onSeparator = { separator = it; gen() },163                onUpper = { upper = it }, onLower = { lower = it },164                onNumbers = { numbers = it }, onSymbols = { symbols = it },165                onGenerate = { gen() },166                onCopy = { if (draft.isNotEmpty()) onCopy(draft) },167            )168169            // [Use this password] — accent button, commits + closes. No-op while empty.170            Box(171                Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp))172                    .background(if (draft.isEmpty()) accent.copy(alpha = 0.10f) else accent.copy(alpha = 0.18f))173                    .border(1.dp, accent.copy(alpha = 0.5f), RoundedCornerShape(10.dp))174                    .clickable(enabled = draft.isNotEmpty()) { onUse(draft) }175                    .padding(vertical = 14.dp),176                contentAlignment = Alignment.Center,177            ) {178                Text(179                    "use this ${if (mode == GeneratorMode.Passphrase) "passphrase" else "password"}",180                    color = if (draft.isEmpty()) accent.copy(alpha = 0.5f) else accent,181                    fontFamily = PeachType.Mono, fontSize = 12.sp,182                )183            }184            Spacer(Modifier.height(20.dp))185        }186    }187}