ad56fa5783bb
fix: Repair focused search and discard blank note drafts (v2.14.22)
- time
- 2026-08-14 16:27 UTC · 2 months ago
- machine
- koh2
- agent
- gpt-5.6-sol
- session
- search repair and empty notes
- parent
- 42c96fc32955
↓ peach-android-ad56fa57.tar.gzatlas at this save
files changed6 files
VERSION+1 −1app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt+9 −7app/src/main/java/com/peachpasswords/ui/vault/NotesModeViewModel.kt+27 −3app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+23 −13app/src/test/java/com/peachpasswords/ui/vault/NotesModeTest.kt+17 −1app/src/test/java/com/peachpasswords/ui/vault/UntouchedNoteDraftTest.kt+14 −0app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt+9 −7
@@ -133,6 +133,8 @@133133 // "- eggs" into a shopping list never turns it into a <ul> behind their back.134134 var isHtml by remember(e?.id) { mutableStateOf(isNoteHtml(stored)) }135135 var editing by remember(e?.id) { mutableStateOf(stored.isBlank()) }136+ fun finishNote(onDone: () -> Unit) = vm.finishNote(title, noteStorageText(body.text, isHtml), onDone)137+ BackHandler { finishNote(vm::showToc) }136138 137139 ShellBackground {138140 Column(@@ -141,7 +143,7 @@141143 .statusBarsPadding()142144 .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),143145 ) {144- PeachVaultIdentity("notes") { onNavigate(PeachNav.VaultList) }146+ PeachVaultIdentity("notes") { finishNote { onNavigate(PeachNav.VaultList) } }145147 FieldShell("title") {146148 Box(Modifier.weight(1f)) {147149 if (title.isEmpty()) {@@ -233,12 +235,12 @@233235 }234236 }235237 PeachDeckBottomBar(236- onSearch = { vm.saveNow(title, noteStorageText(body.text, isHtml)); vm.showToc() },237- onAdd = vm::createNote,238- onNotes = { vm.saveNow(title, noteStorageText(body.text, isHtml)); vm.showToc() },239- onSettings = { onNavigate(PeachNav.Settings) },240- onLock = vm::lock,241- onSwipeUp = { onNavigate(PeachNav.VaultList) },238+ onSearch = { finishNote(vm::showToc) },239+ onAdd = { finishNote(vm::createNote) },240+ onNotes = { finishNote(vm::showToc) },241+ onSettings = { finishNote { onNavigate(PeachNav.Settings) } },242+ onLock = { finishNote(vm::lock) },243+ onSwipeUp = { finishNote { onNavigate(PeachNav.VaultList) } },242244 notesSelected = true,243245 )244246 }app/src/main/java/com/peachpasswords/ui/vault/NotesModeViewModel.kt+27 −3
@@ -60,17 +60,22 @@6060 val savedPulse: StateFlow<Long> = _savedPulse.asStateFlow()6161 6262 private var saveJob: Job? = null63+ private val untouchedDraftIds = mutableSetOf<String>()6364 6465 /** Debounced note-body save. Reschedules on every keystroke; fires 800ms after the last. */65- fun onContentChanged(plainText: String) =66+ fun onContentChanged(plainText: String) {67+ if (plainText.isNotBlank()) _activeNoteId.value?.let(untouchedDraftIds::remove)6668 scheduleSave { it.copy(notes = plainText.ifBlank { null }) }69+ }6770 6871 /**6972 * Title save. Debounced like the body so blur-after-typing still lands;7073 * callers may also force an immediate save on blur via [saveTitleNow].7174 */72- fun onTitleChanged(title: String) =75+ fun onTitleChanged(title: String) {76+ if (title.isNotBlank()) _activeNoteId.value?.let(untouchedDraftIds::remove)7377 scheduleSave { it.copy(title = title) }78+ }7479 7580 /** Commit the title immediately (e.g. on field blur), bypassing the debounce. */7681 fun saveTitleNow(title: String) {@@ -98,16 +103,32 @@98103 VaultEntry(99104 id = "",100105 type = "note",101- title = "Untitled",106+ title = "",102107 notes = null,103108 createdAt = 0L,104109 modifiedAt = 0L,105110 ),106111 )112+ untouchedDraftIds += created.id107113 _activeNoteId.value = created.id108114 }109115 }110116 117+ fun finishNote(title: String, plainText: String, onDone: () -> Unit) {118+ saveJob?.cancel()119+ val activeId = _activeNoteId.value120+ if (shouldDiscardUntouchedNote(activeId in untouchedDraftIds, title, plainText)) {121+ viewModelScope.launch {122+ activeId?.let { runCatching { vault.deleteEntry(it) } }123+ untouchedDraftIds.remove(activeId)124+ onDone()125+ }126+ } else {127+ saveNow(title, plainText)128+ onDone()129+ }130+ }131+111132 /** Browser parity: back navigation commits both title and body before leaving. */112133 fun saveNow(title: String, plainText: String) {113134 saveJob?.cancel()@@ -170,3 +191,6 @@170191 }171192 }172193 }194+195+internal fun shouldDiscardUntouchedNote(isUntouchedDraft: Boolean, title: String, body: String): Boolean =196+ isUntouchedDraft && title.isBlank() && body.isBlank()app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+23 −13
@@ -80,6 +80,7 @@8080 import androidx.compose.ui.input.pointer.positionChange8181 import androidx.compose.ui.focus.FocusRequester8282 import androidx.compose.ui.focus.focusRequester83+import androidx.compose.ui.focus.onFocusChanged8384 import androidx.compose.ui.platform.LocalContext8485 import androidx.compose.ui.platform.LocalDensity8586 import androidx.compose.ui.text.TextStyle@@ -169,6 +170,7 @@169170 val undoEvent by vm.undoEvent.collectAsState()170171 val proNotice by vm.proNotice.collectAsState()171172 val snackbarHostState = remember { SnackbarHostState() }173+ var searchActive by remember { mutableStateOf(initialSearchActive) }172174 // Pull-to-refresh drives a manual sync round against paired devices.173175 val syncVm: SyncViewModel = hiltViewModel()174176 val syncStatus by syncVm.status.collectAsState()@@ -208,7 +210,9 @@208210 209211 ShellBackground(modifier) {210212 Box(Modifier.fillMaxSize()) {211- Column(Modifier.fillMaxSize()) {213+ Column(214+ Modifier.fillMaxSize().then(if (isMultiSelect) Modifier else Modifier.statusBarsPadding()),215+ ) {212216 if (isMultiSelect) {213217 MultiSelectTopBar(214218 count = selectedIds.size,@@ -294,23 +298,26 @@294298 .padding(top = PeachSpacing.lg)295299 .navigationBarsPadding(),296300 ) {297- VaultActionCluster(298- noteOnly = noteOnly,299- onToggleNote = vm::toggleNoteFilter,300- onTrash = { onNavigate(PeachNav.Trash) },301- onSettings = { onNavigate(PeachNav.Settings) },302- onAdd = {303- if (noteOnly) vm.createNote { id -> onNavigate(PeachNav.NotesMode(id)) }304- else onNavigate(PeachNav.EntryEdit(null))305- },306- onLock = vm::lock,307- )301+ if (!searchActive) {302+ VaultActionCluster(303+ noteOnly = noteOnly,304+ onToggleNote = vm::toggleNoteFilter,305+ onTrash = { onNavigate(PeachNav.Trash) },306+ onSettings = { onNavigate(PeachNav.Settings) },307+ onAdd = {308+ if (noteOnly) vm.createNote { id -> onNavigate(PeachNav.NotesMode(id)) }309+ else onNavigate(PeachNav.EntryEdit(null))310+ },311+ onLock = vm::lock,312+ )313+ }308314 SearchBar(309315 value = search,310316 onChange = vm::setSearch,311317 count = if (search.isNotBlank()) "${entries.size}/$total" else pluralEntries(total),312318 onCollapse = onBack,313319 requestFocus = initialSearchActive,320+ onFocusChange = { searchActive = it },314321 syncStatus = syncStatus,315322 pairedCount = pairedDevices.size,316323 )@@ -381,6 +388,7 @@381388 count: String,382389 onCollapse: (() -> Unit)? = null,383390 requestFocus: Boolean = false,391+ onFocusChange: (Boolean) -> Unit = {},384392 syncStatus: SyncOutcomeSummary? = null,385393 pairedCount: Int = 0,386394 ) {@@ -432,7 +440,9 @@432440 singleLine = true,433441 textStyle = PeachType.searchInput.copy(color = peachColors.T0),434442 cursorBrush = SolidColor(accent),435- modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),443+ modifier = Modifier.fillMaxWidth()444+ .focusRequester(focusRequester)445+ .onFocusChanged { onFocusChange(it.isFocused) },436446 )437447 }438448 if (syncStatus != null) {app/src/test/java/com/peachpasswords/ui/vault/NotesModeTest.kt+17 −1
@@ -190,8 +190,24 @@190190 advanceUntilIdle()191191 192192 val opened = vm.activeNoteId.value193- assertEquals("Untitled", repo.getEntry(opened!!)?.title)193+ assertEquals("", repo.getEntry(opened!!)?.title)194194 assertEquals("note", repo.getEntry(opened)?.type)195+ }196+197+ @Test fun leaving_an_untouched_new_note_discards_it() = runTest(dispatcher) {198+ val repo = VaultRepository(FakeStorage())199+ repo.createVault("correct horse battery")200+ val vm = NotesModeViewModel(repo, SavedStateHandle(mapOf("id" to "")))201+202+ vm.createNote()203+ advanceUntilIdle()204+ val opened = vm.activeNoteId.value!!205+206+ vm.finishNote("", "") { vm.showToc() }207+ advanceUntilIdle()208+209+ assertNull(repo.getEntry(opened))210+ assertNull(vm.activeNoteId.value)195211 }196212 197213 @Test fun notes_delete_removes_note_from_active_lookup() = runTest(dispatcher) {app/src/test/java/com/peachpasswords/ui/vault/UntouchedNoteDraftTest.kt+14 −0
@@ -0,0 +1,14 @@1+package com.peachpasswords.ui.vault2+3+import org.junit.Assert.assertFalse4+import org.junit.Assert.assertTrue5+import org.junit.Test6+7+class UntouchedNoteDraftTest {8+ @Test fun only_untouched_blank_drafts_are_discarded() {9+ assertTrue(shouldDiscardUntouchedNote(true, "", ""))10+ assertFalse(shouldDiscardUntouchedNote(true, "title", ""))11+ assertFalse(shouldDiscardUntouchedNote(true, "", "body"))12+ assertFalse(shouldDiscardUntouchedNote(false, "", ""))13+ }14+}