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
6 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 −0
VERSION+1 −1
@@ -1 +1 @@1-2.14.211+2.14.22

view file

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

view file

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

view file

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

view file

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

view file

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+}

view file