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
↓ peach-android-832e5cf8.tar.gzatlas at this save
files changed8 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 −0app/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.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,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() + " ")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 }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 }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() {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) {