82a4e7b3b85b
feat: add password and passphrase generator controls (v2.15.2)
- time
- 2026-09-05 12:53 UTC · 1 month ago
- machine
- koh2
- agent
- gpt-6
- session
- Vault V4 password and passphrase support
- parent
- eb8841cdb3be
↓ peach-android-82a4e7b3.tar.gzatlas at this save
files changed5 files
VERSION+1 −1app/src/main/java/com/peachpasswords/ui/vault/EntryDetailGenerator.kt+23 −10app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt+58 −13app/src/main/java/com/peachpasswords/ui/vault/EntryEditViewModel.kt+26 −8app/src/test/java/com/peachpasswords/ui/vault/EntryEditScreenTest.kt+9 −0app/src/main/java/com/peachpasswords/ui/vault/EntryDetailGenerator.kt+23 −10
@@ -95,7 +95,10 @@9595 val accent = LocalPeachAccent.current9696 val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)9797 98+ var mode by remember { mutableStateOf(GeneratorMode.Password) }9899 var length by remember { mutableStateOf(20) }100+ var wordCount by remember { mutableStateOf(PasswordGenerator.DEFAULT_PASSPHRASE_WORDS) }101+ var separator by remember { mutableStateOf("-") }99102 var upper by remember { mutableStateOf(true) }100103 var lower by remember { mutableStateOf(true) }101104 var numbers by remember { mutableStateOf(true) }@@ -103,15 +106,19 @@103106 var draft by remember { mutableStateOf("") }104107 105108 fun gen() {106- draft = PasswordGenerator.generate(107- PasswordGenerator.Options(108- length = length,109- includeUppercase = upper,110- includeLowercase = lower,111- includeNumbers = numbers,112- includeSymbols = symbols,113- ),114- )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+ }115122 }116123 117124 ModalBottomSheet(@@ -143,10 +150,16 @@143150 144151 // Shared generator controls + [Copy] drain button on the generate row.145152 GeneratorBody(153+ mode = mode,146154 length = length,155+ wordCount = wordCount,156+ separator = separator,147157 upper = upper, lower = lower, numbers = numbers, symbols = symbols,148158 password = draft, copied = copied,159+ onMode = { mode = it; gen() },149160 onLength = { length = it },161+ onWordCount = { wordCount = it },162+ onSeparator = { separator = it; gen() },150163 onUpper = { upper = it }, onLower = { lower = it },151164 onNumbers = { numbers = it }, onSymbols = { symbols = it },152165 onGenerate = { gen() },@@ -163,7 +176,7 @@163176 contentAlignment = Alignment.Center,164177 ) {165178 Text(166- "use this password",179+ "use this ${if (mode == GeneratorMode.Passphrase) "passphrase" else "password"}",167180 color = if (draft.isEmpty()) accent.copy(alpha = 0.5f) else accent,168181 fontFamily = PeachType.Mono, fontSize = 12.sp,169182 )app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt+58 −13
@@ -165,11 +165,17 @@165165 }166166 GeneratorDrawer(167167 open = f.generatorOpen,168+ mode = f.genMode,168169 length = f.genLength,170+ wordCount = f.genWordCount,171+ separator = f.genSeparator,169172 upper = f.genUpper, lower = f.genLower, numbers = f.genNumbers, symbols = f.genSymbols,170173 password = f.password,171174 copied = f.genCopied,175+ onMode = vm::setGenMode,172176 onLength = vm::setGenLength,177+ onWordCount = vm::setGenWordCount,178+ onSeparator = vm::setGenSeparator,173179 onUpper = vm::setGenUpper, onLower = vm::setGenLower,174180 onNumbers = vm::setGenNumbers, onSymbols = vm::setGenSymbols,175181 onGenerate = vm::generate,@@ -430,11 +436,17 @@430436 @Composable431437 private fun GeneratorDrawer(432438 open: Boolean,439+ mode: GeneratorMode,433440 length: Int,441+ wordCount: Int,442+ separator: String,434443 upper: Boolean, lower: Boolean, numbers: Boolean, symbols: Boolean,435444 password: String,436445 copied: Boolean,446+ onMode: (GeneratorMode) -> Unit,437447 onLength: (Int) -> Unit,448+ onWordCount: (Int) -> Unit,449+ onSeparator: (String) -> Unit,438450 onUpper: (Boolean) -> Unit, onLower: (Boolean) -> Unit,439451 onNumbers: (Boolean) -> Unit, onSymbols: (Boolean) -> Unit,440452 onGenerate: () -> Unit,@@ -442,7 +454,7 @@442454 onUse: () -> Unit,443455 ) {444456 val height by animateDpAsState(445- targetValue = if (open) 326.dp else 0.dp,457+ targetValue = if (open) 370.dp else 0.dp,446458 animationSpec = tween(320, easing = PeachMotion.Spring),447459 label = "gen-drawer",448460 )@@ -453,10 +465,16 @@453465 .border(1.dp, peachColors.T3, RoundedCornerShape(12.dp)),454466 ) {455467 GeneratorBody(468+ mode = mode,456469 length = length,470+ wordCount = wordCount,471+ separator = separator,457472 upper = upper, lower = lower, numbers = numbers, symbols = symbols,458473 password = password, copied = copied,474+ onMode = onMode,459475 onLength = onLength,476+ onWordCount = onWordCount,477+ onSeparator = onSeparator,460478 onUpper = onUpper, onLower = onLower, onNumbers = onNumbers, onSymbols = onSymbols,461479 onGenerate = onGenerate, onCopy = onCopy,462480 showOutput = true,@@ -472,11 +490,17 @@472490 */473491 @Composable474492 internal fun GeneratorBody(493+ mode: GeneratorMode,475494 length: Int,495+ wordCount: Int,496+ separator: String,476497 upper: Boolean, lower: Boolean, numbers: Boolean, symbols: Boolean,477498 password: String,478499 copied: Boolean,500+ onMode: (GeneratorMode) -> Unit,479501 onLength: (Int) -> Unit,502+ onWordCount: (Int) -> Unit,503+ onSeparator: (String) -> Unit,480504 onUpper: (Boolean) -> Unit, onLower: (Boolean) -> Unit,481505 onNumbers: (Boolean) -> Unit, onSymbols: (Boolean) -> Unit,482506 onGenerate: () -> Unit,@@ -486,6 +510,11 @@486510 ) {487511 val accent = LocalPeachAccent.current488512 Column(Modifier.fillMaxWidth().padding(16.dp)) {513+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {514+ ChoicePill("password", mode == GeneratorMode.Password) { onMode(GeneratorMode.Password) }515+ ChoicePill("passphrase", mode == GeneratorMode.Passphrase) { onMode(GeneratorMode.Passphrase) }516+ }517+ Spacer(Modifier.height(10.dp))489518 if (showOutput) {490519 Box(491520 Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp))@@ -506,21 +535,32 @@506535 }507536 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {508537 Text("GENERATOR", color = peachColors.T1, fontFamily = PeachType.Mono, fontSize = 8.sp)509- Text("length $length", color = accent, fontFamily = PeachType.Mono, fontSize = 9.sp)538+ Text(539+ if (mode == GeneratorMode.Passphrase) "word count $wordCount" else "length $length",540+ color = accent, fontFamily = PeachType.Mono, fontSize = 9.sp,541+ )510542 }511543 Slider(512- value = length.toFloat(),513- onValueChange = { onLength(it.toInt()) },514- valueRange = 8f..128f,544+ value = if (mode == GeneratorMode.Passphrase) wordCount.toFloat() else length.toFloat(),545+ onValueChange = {546+ if (mode == GeneratorMode.Passphrase) onWordCount(it.toInt()) else onLength(it.toInt())547+ },548+ valueRange = if (mode == GeneratorMode.Passphrase) 4f..12f else 8f..256f,515549 colors = SliderDefaults.colors(516550 thumbColor = accent, activeTrackColor = accent, inactiveTrackColor = peachColors.T3,517551 ),518552 )519553 Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {520- TogglePill("A-Z", upper, onUpper)521- TogglePill("a-z", lower, onLower)522- TogglePill("0-9", numbers, onNumbers)523- TogglePill("!@#", symbols, onSymbols)554+ if (mode == GeneratorMode.Passphrase) {555+ listOf("-", " ", ".", "_").forEach { value ->556+ ChoicePill(if (value == " ") "space" else value, separator == value) { onSeparator(value) }557+ }558+ } else {559+ TogglePill("A-Z", upper, onUpper)560+ TogglePill("a-z", lower, onLower)561+ TogglePill("0-9", numbers, onNumbers)562+ TogglePill("!@#", symbols, onSymbols)563+ }524564 }525565 Spacer(Modifier.height(12.dp))526566 Row(verticalAlignment = Alignment.CenterVertically) {@@ -554,15 +594,20 @@554594 555595 @Composable556596 private fun TogglePill(label: String, on: Boolean, onToggle: (Boolean) -> Unit) {597+ ChoicePill(label, on) { onToggle(!on) }598+}599+600+@Composable601+private fun ChoicePill(label: String, selected: Boolean, onClick: () -> Unit) {557602 val accent = LocalPeachAccent.current558603 Box(559604 Modifier.clip(RoundedCornerShape(PeachRadius.TypeTab))560- .border(1.dp, if (on) accent.copy(alpha = 0.5f) else peachColors.T3, RoundedCornerShape(PeachRadius.TypeTab))561- .background(if (on) accent.copy(alpha = 0.10f) else Color.Transparent)562- .clickable { onToggle(!on) }605+ .border(1.dp, if (selected) accent.copy(alpha = 0.5f) else peachColors.T3, RoundedCornerShape(PeachRadius.TypeTab))606+ .background(if (selected) accent.copy(alpha = 0.10f) else Color.Transparent)607+ .clickable(onClick = onClick)563608 .padding(horizontal = 10.dp, vertical = 6.dp),564609 ) {565- Text(label, color = if (on) accent else peachColors.T2, fontFamily = PeachType.Mono, fontSize = 9.sp)610+ Text(label, color = if (selected) accent else peachColors.T2, fontFamily = PeachType.Mono, fontSize = 9.sp)566611 }567612 }568613 app/src/main/java/com/peachpasswords/ui/vault/EntryEditViewModel.kt+26 −8
@@ -52,7 +52,10 @@5252 val genCopied: Boolean = false,5353 val history: List<PasswordHistoryItem> = emptyList(),5454 val showHistory: Boolean = false,55+ val genMode: GeneratorMode = GeneratorMode.Password,5556 val genLength: Int = BROWSER_ADD_GENERATOR_LENGTH,57+ val genWordCount: Int = PasswordGenerator.DEFAULT_PASSPHRASE_WORDS,58+ val genSeparator: String = "-",5659 val genUpper: Boolean = true,5760 val genLower: Boolean = true,5861 val genNumbers: Boolean = true,@@ -243,7 +246,13 @@243246 fun copyHistoryPassword(pw: String) {244247 PeachClipboard.copy(appContext, "peach", pw)245248 }246- fun setGenLength(n: Int) = _form.update { it.copy(genLength = n.coerceIn(8, 128)).withGeneratedPassword() }249+ fun setGenMode(mode: GeneratorMode) = _form.update { it.copy(genMode = mode).withGeneratedPassword() }250+ fun setGenLength(n: Int) = _form.update { it.copy(genLength = n.coerceIn(8, 256)).withGeneratedPassword() }251+ fun setGenWordCount(n: Int) = _form.update {252+ it.copy(genWordCount = n.coerceIn(PasswordGenerator.MIN_PASSPHRASE_WORDS, PasswordGenerator.MAX_PASSPHRASE_WORDS))253+ .withGeneratedPassword()254+ }255+ fun setGenSeparator(separator: String) = _form.update { it.copy(genSeparator = separator).withGeneratedPassword() }247256 fun setGenUpper(b: Boolean) = _form.update { it.copy(genUpper = b).withGeneratedPassword() }248257 fun setGenLower(b: Boolean) = _form.update { it.copy(genLower = b).withGeneratedPassword() }249258 fun setGenNumbers(b: Boolean) = _form.update { it.copy(genNumbers = b).withGeneratedPassword() }@@ -343,18 +352,27 @@343352 .replace("\r", "\\r") + "\""344353 345354 internal const val BROWSER_ADD_GENERATOR_LENGTH = 64355+enum class GeneratorMode { Password, Passphrase }346356 347357 private fun EditForm.withGeneratedPassword(): EditForm =348358 copy(349- password = generateEditPassword(350- length = genLength,351- upper = genUpper,352- lower = genLower,353- numbers = genNumbers,354- symbols = genSymbols,355- ),359+ password = generateEditCredential(this),356360 showPassword = true,357361 )362+363+internal fun generateEditCredential(form: EditForm): String = when (form.genMode) {364+ GeneratorMode.Password -> generateEditPassword(365+ length = form.genLength,366+ upper = form.genUpper,367+ lower = form.genLower,368+ numbers = form.genNumbers,369+ symbols = form.genSymbols,370+ )371+ GeneratorMode.Passphrase -> PasswordGenerator.generatePassphrase(372+ wordCount = form.genWordCount,373+ separator = form.genSeparator,374+ )375+}358376 359377 private fun generateEditPassword(360378 length: Int,app/src/test/java/com/peachpasswords/ui/vault/EntryEditScreenTest.kt+9 −0
@@ -86,8 +86,17 @@8686 8787 @Test fun generator_defaults_and_toggle_label_match_browser_add_view() {8888 assertEquals(64, BROWSER_ADD_GENERATOR_LENGTH)89+ assertEquals(GeneratorMode.Password, EditForm().genMode)90+ assertEquals(5, EditForm().genWordCount)8991 assertEquals("generate", generatorToggleLabel(open = false))9092 assertEquals("hide generator", generatorToggleLabel(open = true))93+ }94+95+ @Test fun edit_generator_uses_passphrase_word_count_and_separator() {96+ val generated = generateEditCredential(97+ EditForm(genMode = GeneratorMode.Passphrase, genWordCount = 12, genSeparator = "."),98+ )99+ assertEquals(12, generated.split(".").size)91100 }92101 93102 @Test fun tag_suggestions_match_browser_add_view_limit_and_filtering() {