832e5cf806cf

feat: Match browser note tools and polish entry editing (v2.14.51)

time
2026-08-14 22:56 UTC · 2 months ago
machine
koh2
parent
084684b358fd
8 files VERSION+1 −1app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+2 −0app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt+29 −13app/src/main/java/com/peachpasswords/ui/vault/NoteHtml.kt+7 −3app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt+14 −3app/src/main/java/com/peachpasswords/ui/vault/TagChipEditor.kt+4 −3app/src/test/java/com/peachpasswords/ui/vault/NoteHtmlTest.kt+9 −1app/src/test/java/com/peachpasswords/ui/vault/NotesModeTest.kt+10 −0
VERSION+1 −1
@@ -1 +1 @@1-2.14.501+2.14.51

view file

app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+2 −0
@@ -186,6 +186,7 @@186186                 modifier = Modifier.weight(1f).clickable { beginEdit() },187187             )188188             RevealButton(revealed = reveal, onClick = { reveal = !reveal })189+            Spacer(Modifier.width(6.dp))189190             CopyButton(copied, onCopy)190191         } else {191192             Column(Modifier.weight(1f)) {@@ -201,6 +202,7 @@201202                         if (show) "hide" else "show", color = accent, style = PeachType.labelMono,202203                         modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { show = !show }.padding(6.dp),203204                     )205+                    Spacer(Modifier.width(6.dp))204206                     // Generate belongs to the moment you are choosing a205207                     // password, not to the resting row — a wrench parked next206208                     // to a saved password invites overwriting it by accident.

view file

app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt+29 −13
@@ -139,12 +139,6 @@139139                                 joined = true,140140                             )141141                         }142-                        AdditionalUrlsEditor(143-                            urls = f.additionalUrls,144-                            onChange = vm::setAdditionalUrl,145-                            onAdd = vm::addAdditionalUrl,146-                            onRemove = vm::removeAdditionalUrl,147-                        )148142                         GeneratorDrawer(149143                             open = f.generatorOpen,150144                             length = f.genLength,@@ -158,6 +152,12 @@158152                             onCopy = vm::copyGenerated,159153                             onUse = vm::useGeneratedPassword,160154                         )155+                        AdditionalUrlsEditor(156+                            urls = f.additionalUrls,157+                            onChange = vm::setAdditionalUrl,158+                            onAdd = vm::addAdditionalUrl,159+                            onRemove = vm::removeAdditionalUrl,160+                        )161161                         PasswordHistorySection(162162                             history = f.history,163163                             expanded = f.showHistory,@@ -379,16 +379,30 @@379379         transformation = if (show) VisualTransformation.None else PasswordVisualTransformation(),380380         joined = joined,381381         trailing = {382-            Text(if (show) "hide" else "show", color = LocalPeachAccent.current,383-                fontFamily = PeachType.Mono, fontSize = 9.sp,384-                modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable(onClick = onToggleShow).padding(6.dp))385-            Text(generatorToggleLabel(generatorOpen), color = LocalPeachAccent.current,386-                fontFamily = PeachType.Mono, fontSize = 9.sp,387-                modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable(onClick = onToggleGenerator).padding(6.dp))382+            Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {383+                PasswordAction(if (show) "hide" else "show", onToggleShow)384+                PasswordAction(generatorToggleLabel(generatorOpen), onToggleGenerator)385+            }388386         },389387     )390388 }391389 390+@Composable391+private fun PasswordAction(label: String, onClick: () -> Unit) {392+    val accent = LocalPeachAccent.current393+    Text(394+        label,395+        color = accent,396+        fontFamily = PeachType.Mono,397+        fontSize = 9.sp,398+        modifier = Modifier399+            .clip(RoundedCornerShape(6.dp))400+            .background(accent.copy(alpha = 0.08f))401+            .clickable(onClick = onClick)402+            .padding(horizontal = 7.dp, vertical = 5.dp),403+    )404+}405+392406 internal fun generatorToggleLabel(open: Boolean): String = if (open) "hide generator" else "generate"393407 394408 @Composable@@ -412,7 +426,9 @@412426     )413427     Box(414428         Modifier.fillMaxWidth().height(height).clipToBounds()415-            .clip(RoundedCornerShape(12.dp)).background(peachColors.S2),429+            .clip(RoundedCornerShape(12.dp))430+            .background(peachColors.S2)431+            .border(1.dp, peachColors.T3, RoundedCornerShape(12.dp)),416432     ) {417433         GeneratorBody(418434             length = length,

view file

app/src/main/java/com/peachpasswords/ui/vault/NoteHtml.kt+7 −3
@@ -12,18 +12,18 @@1212 /*1313  * Note bodies are authored in the browser's contenteditable, so a synced note1414  * arrives as HTML: the extension's sanitizer allow-list (NoteEditor.tsx) is15- * b, strong, i, em, u, s, h2, h3, ul, ol, li, br, hr, p — attribute-less — plus15+ * b, strong, i, em, u, s, h2, h3, blockquote, ul, ol, li, br, hr, p — attribute-less — plus1616  * the div wrappers Chrome's contenteditable inserts for new lines. Android read1717  * that field as plain text, which is why the phone showed raw tags.1818  *19- * Parsed here rather than through HtmlCompat: the vocabulary is fourteen19+ * Parsed here rather than through HtmlCompat: the vocabulary is fifteen2020  * attribute-less tags, host-JVM unit tests cannot touch android.text, and2121  * Html.toHtml serialises back as `<p dir="ltr" style="…">` — outside the2222  * allow-list, so it could not round-trip to the browser.2323  *2424  * Compose drops AnnotatedString spans on every keystroke, so there is no sound2525  * WYSIWYG editing surface. Editing therefore goes through a plain-text markup26- * (`**bold**`, `## heading`, `- item`) that converts back to the same tag26+ * (`**bold**`, `## heading`, `> quote`, `- item`) that converts back to the same tag2727  * vocabulary on save. The markup never leaves the device — it exists only2828  * between load and save.2929  */@@ -104,6 +104,7 @@104104             "s", "strike", "del" -> out.append(Mark.STRIKE.markup)105105             "h2" -> { endLine(); if (!closing) out.append("## ") }106106             "h3" -> { endLine(); if (!closing) out.append("### ") }107+            "blockquote" -> { endLine(); if (!closing) out.append("> ") }107108             "ul" -> { endLine(); if (closing) lists.removeLastOrNull() else lists.addLast(-1) }108109             "ol" -> { endLine(); if (closing) lists.removeLastOrNull() else lists.addLast(1) }109110             "li" -> {@@ -219,6 +220,7 @@219220             line.trim() == "---" -> { closeList(); out.append("<hr>"); false }220221             line.startsWith("## ") -> { closeList(); out.append("<h2>${inlineHtml(line.removePrefix("## "))}</h2>"); false }221222             line.startsWith("### ") -> { closeList(); out.append("<h3>${inlineHtml(line.removePrefix("### "))}</h3>"); false }223+            line.startsWith("> ") -> { closeList(); out.append("<blockquote>${inlineHtml(line.removePrefix("> "))}</blockquote>"); false }222224             line.startsWith("- ") -> { startList("ul"); out.append("<li>${inlineHtml(line.removePrefix("- "))}</li>"); false }223225             ORDERED_ITEM.containsMatchIn(line) -> { startList("ol"); out.append("<li>${inlineHtml(line.replaceFirst(ORDERED_ITEM, ""))}</li>"); false }224226             else -> {@@ -236,6 +238,7 @@236238 237239 private val HEADING2 = SpanStyle(fontWeight = FontWeight.SemiBold, fontSize = 19.sp)238240 private val HEADING3 = SpanStyle(fontWeight = FontWeight.SemiBold, fontSize = 16.sp)241+private val QUOTE = SpanStyle(fontStyle = FontStyle.Italic)239242 240243 private fun markupToAnnotated(markup: String): AnnotatedString = buildAnnotatedString {241244     markup.split("\n").forEachIndexed { index, line ->@@ -244,6 +247,7 @@244247             line.trim() == "---" -> append("──────────")245248             line.startsWith("## ") -> appendRuns(line.removePrefix("## "), HEADING2)246249             line.startsWith("### ") -> appendRuns(line.removePrefix("### "), HEADING3)250+            line.startsWith("> ") -> appendRuns(line.removePrefix("> "), QUOTE)247251             line.startsWith("- ") -> { append("•  "); appendRuns(line.removePrefix("- "), null) }248252             ORDERED_ITEM.containsMatchIn(line) -> {249253                 append(ORDERED_ITEM.find(line)!!.value.trimEnd() + "  ")

view file

app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt+14 −3
@@ -213,10 +213,13 @@213213                         FormatButton("B", FontWeight.Bold) { format(wrapSelection(body, "**")) }214214                         FormatButton("I", fontStyle = FontStyle.Italic) { format(wrapSelection(body, "*")) }215215                         FormatButton("U", underline = true) { format(wrapSelection(body, "__")) }216-                        Spacer(Modifier.width(4.dp))217-                        FormatButton("H") { format(applyLinePrefix(body, "## ")) }216+                        FormatButton("S", strikeThrough = true) { format(wrapSelection(body, "~~")) }217+                        Box(Modifier.width(1.dp).height(24.dp).background(peachColors.T3))218+                        FormatButton("H₂") { format(applyLinePrefix(body, "## ")) }219+                        FormatButton("❞") { format(applyLinePrefix(body, "> ")) }218220                         FormatButton("•") { format(applyLinePrefix(body, "- ")) }219221                         FormatButton("1.") { format(applyLinePrefix(body, "1. ")) }222+                        FormatButton("—") { format(applyLinePrefix(body, "---")) }220223                     }221224                     Box(Modifier.fillMaxWidth().height(1.dp).background(peachColors.T3))222225                 }@@ -506,6 +509,7 @@506509     fontWeight: FontWeight? = null,507510     fontStyle: FontStyle? = null,508511     underline: Boolean = false,512+    strikeThrough: Boolean = false,509513     onClick: () -> Unit,510514 ) {511515     Box(@@ -524,7 +528,14 @@524528                 fontSize = 14.sp,525529                 fontWeight = fontWeight,526530                 fontStyle = fontStyle,527-                textDecoration = if (underline) TextDecoration.Underline else null,531+                textDecoration = when {532+                    underline && strikeThrough -> TextDecoration.combine(533+                        listOf(TextDecoration.Underline, TextDecoration.LineThrough),534+                    )535+                    underline -> TextDecoration.Underline536+                    strikeThrough -> TextDecoration.LineThrough537+                    else -> null538+                },528539             ),529540         )530541     }

view file

app/src/main/java/com/peachpasswords/ui/vault/TagChipEditor.kt+4 −3
@@ -145,13 +145,14 @@145145                 suggestions.forEach { s ->146146                     Text(147147                         s,148-                        color = peachColors.T1,148+                        color = accent,149149                         style = TextStyle(fontFamily = PeachType.Mono, fontSize = 11.sp),150150                         modifier = Modifier151-                            .fillMaxWidth()152151                             .clip(RoundedCornerShape(6.dp))152+                            .background(accent.copy(alpha = 0.08f))153+                            .border(1.dp, accent.copy(alpha = 0.28f), RoundedCornerShape(6.dp))153154                             .clickable { commit(s) }154-                            .padding(horizontal = 8.dp, vertical = 6.dp),155+                            .padding(horizontal = 10.dp, vertical = 6.dp),155156                     )156157                 }157158             }

view file

app/src/test/java/com/peachpasswords/ui/vault/NoteHtmlTest.kt+9 −1
@@ -12,7 +12,7 @@1212 /**1313  * The vocabulary under test is the browser sanitizer's allow-list1414  * (NoteEditor.tsx ALLOWED_NOTE_TAGS): b, i, u, s, strong, em, h2, h3, ul, ol,15- * li, br, hr, p — plus the div wrappers contenteditable adds on its own.15+ * li, br, hr, blockquote, p — plus the div wrappers contenteditable adds on its own.1616  */1717 class NoteHtmlTest {1818 @@ -31,6 +31,7 @@3131             "<s>struck</s>" to "struck",3232             "<h2>Title</h2>" to "Title",3333             "<h3>Sub</h3>" to "Sub",34+            "<blockquote>quoted</blockquote>" to "quoted",3435             "<ul><li>one</li></ul>" to "•  one",3536             "<ol><li>one</li></ol>" to "1.  one",3637             "<p>para</p>" to "para",@@ -59,6 +60,13 @@5960     @Test fun nested_inline_tags_keep_both_styles() {6061         val styles = noteRendered("<b>a<i>b</i></b>").spanStyles6162         assertTrue(styles.any { it.item.fontWeight == FontWeight.Bold && it.item.fontStyle == FontStyle.Italic })63+    }64+65+    @Test fun blockquote_round_trips_and_renders_italic() {66+        val html = "<blockquote>quoted text</blockquote>"67+        assertEquals("> quoted text", noteEditorText(html))68+        assertEquals(html, noteStorageText(noteEditorText(html), isHtml = true))69+        assertTrue(noteRendered(html).spanStyles.any { it.item.fontStyle == FontStyle.Italic })6270     }6371 6472     @Test fun entities_decode_and_re_encode() {

view file

app/src/test/java/com/peachpasswords/ui/vault/NotesModeTest.kt+10 −0
@@ -103,6 +103,16 @@103103         assertEquals("alpha\nbeta", unbulleted.text)104104     }105105 106+    @Test fun note_toolbar_quote_and_rule_markers_round_trip() {107+        val quote = applyLinePrefix(TextFieldValue("alpha"), "> ")108+        assertEquals("> alpha", quote.text)109+        assertEquals("<blockquote>alpha</blockquote>", noteStorageText(quote.text, isHtml = true))110+111+        val rule = applyLinePrefix(TextFieldValue(""), "---")112+        assertEquals("---", rule.text)113+        assertEquals("<hr>", noteStorageText(rule.text, isHtml = true))114+    }115+106116     // ── Auto-save debounce (NotesModeViewModel) ───────────────────────────────107117 108118     @Test fun notes_auto_save_debounces_800ms() = runTest(dispatcher) {

view file