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
5 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 −0
VERSION+1 −1
@@ -1 +1 @@1-2.15.11+2.15.2

view file

app/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                 )

view file

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 

view file

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,

view file

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() {

view file