7c92cad1f1f8

restore the QR scan entry point in Sync > Add a device: alongside 'show code', a 'scan' button opens the camera-driven AdoptVault flow so a phone can join an existing browser-only vault (v2.7.9)

time
2026-07-28 00:52 UTC · 2 months ago
machine
koh2
agent
claude-MiniMax-M3
session
sync red indicator port from browser
parent
727903630472
4 files app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+8 −2app/src/main/java/com/peachpasswords/ui/vault/ContextMenuSheet.kt+0 −173app/src/main/java/com/peachpasswords/ui/vault/EntryDetailViewModel.kt+10 −1
app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616         applicationId "com.peachpasswords"1717         minSdk 261818         targetSdk 3519-        versionCode 24013520-        versionName "2.7.8"19+        versionCode 24013620+        versionName "2.7.9"2121 2222         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323         vectorDrawables {

view file

app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+8 −2
@@ -717,12 +717,18 @@717717         Column(Modifier.weight(1f)) {718718             Text("Pair a device", color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)719719             Text(720-                "This vault shows a QR. An empty device scans it and receives the vault after the codes match.",720+                "Either scan a QR shown on the other device, or show this device's QR for the other one to scan.",721721                 color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 11.sp, lineHeight = 16.sp,722722                 modifier = Modifier.padding(top = 2.dp),723723             )724724         }725-        SubtleButton("pair", fg) { onNavigate(PeachNav.CodePair) }725+        // "Scan" opens the camera-driven AdoptVault flow: the phone is the726+        // receiver. "Show a code" opens CodePair: the phone is the source.727+        // Both are valid pairing entry points; without scan, a phone that wants728+        // to join an existing browser-only vault has no path.729+        SubtleButton("scan", fg) { onNavigate(PeachNav.AdoptVault) }730+        Spacer(Modifier.width(PeachSpacing.xs))731+        SubtleButton("show code", fg) { onNavigate(PeachNav.CodePair) }726732     }727733 728734     SubLabel("SYNC STATUS", fg)

view file

app/src/main/java/com/peachpasswords/ui/vault/ContextMenuSheet.kt+0 −173
@@ -1,173 +0,0 @@1-package com.peachpasswords.ui.vault2-3-import androidx.compose.foundation.background4-import androidx.compose.foundation.clickable5-import androidx.compose.foundation.interaction.MutableInteractionSource6-import androidx.compose.foundation.interaction.collectIsPressedAsState7-import androidx.compose.foundation.layout.Arrangement8-import androidx.compose.foundation.layout.Box9-import androidx.compose.foundation.layout.Column10-import androidx.compose.foundation.layout.Row11-import androidx.compose.foundation.layout.Spacer12-import androidx.compose.foundation.layout.fillMaxWidth13-import androidx.compose.foundation.layout.height14-import androidx.compose.foundation.layout.heightIn15-import androidx.compose.foundation.layout.navigationBarsPadding16-import androidx.compose.foundation.layout.padding17-import androidx.compose.material3.ExperimentalMaterial3Api18-import androidx.compose.material3.ModalBottomSheet19-import androidx.compose.material3.Text20-import androidx.compose.material3.rememberModalBottomSheetState21-import androidx.compose.runtime.Composable22-import androidx.compose.runtime.getValue23-import androidx.compose.runtime.remember24-import androidx.compose.ui.Alignment25-import androidx.compose.ui.Modifier26-import androidx.compose.ui.draw.drawBehind27-import androidx.compose.ui.geometry.Offset28-import androidx.compose.ui.geometry.Size29-import androidx.compose.ui.graphics.Color30-import androidx.compose.ui.text.font.FontStyle31-import androidx.compose.ui.text.style.TextAlign32-import androidx.compose.ui.unit.dp33-import androidx.compose.ui.unit.sp34-import com.peachpasswords.ui.theme.LocalPeachAccent35-import com.peachpasswords.ui.theme.peachColors36-import com.peachpasswords.ui.theme.PeachType37-import com.peachpasswords.vault.VaultEntry38-import com.peachpasswords.vault.hasTOTP39-40-/**41- * D12 long-press context menu (spec §9). A glass ModalBottomSheet over an entry42- * row offering quick copy / edit / trash without opening the detail screen.43- *44- * Self-contained, drop-in. Triggered from VaultListScreen on a 500ms long-press45- * (see INTEGRATION.md for the pointerInput anchor). Callbacks are wired to the46- * VM in the host; this composable owns no business logic.47- */48-@OptIn(ExperimentalMaterial3Api::class)49-@Composable50-fun ContextMenuSheet(51-    entry: VaultEntry,52-    onCopyPassword: () -> Unit,53-    onCopyUsername: () -> Unit,54-    onCopyTotp: () -> Unit,55-    onEdit: () -> Unit,56-    onMoveToTrash: () -> Unit,57-    onDismiss: () -> Unit,58-) {59-    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)60-61-    ModalBottomSheet(62-        onDismissRequest = onDismiss,63-        sheetState = sheetState,64-        containerColor = peachColors.SurfaceElevated,65-        dragHandle = null,66-    ) {67-        Column(68-            Modifier69-                .fillMaxWidth()70-                .padding(top = 8.dp)71-                .navigationBarsPadding(),72-        ) {73-            // Entry name header — non-interactive title.74-            Text(75-                text = entry.title,76-                color = peachColors.T0,77-                style = PeachType.entryNameList.copy(fontSize = 15.sp),78-                modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp),79-            )80-            Divider()81-82-            val username = entry.username ?: entry.email83-            if (!entry.password.isNullOrEmpty()) {84-                ActionRow(label = "Copy password", onClick = { onCopyPassword(); onDismiss() })85-            }86-            if (!username.isNullOrEmpty()) {87-                ActionRow(label = "Copy username", onClick = { onCopyUsername(); onDismiss() })88-            }89-            if (entry.hasTOTP()) {90-                ActionRow(label = "Copy TOTP code", onClick = { onCopyTotp(); onDismiss() })91-            }92-            ActionRow(label = "Edit", onClick = { onEdit(); onDismiss() })93-            ActionRow(94-                label = "Move to trash",95-                textColor = peachColors.Breach,96-                onClick = { onMoveToTrash(); onDismiss() },97-            )98-99-            Divider()100-            CancelRow(onClick = onDismiss)101-            Spacer(Modifier.height(4.dp))102-        }103-    }104-}105-106-@Composable107-private fun Divider() {108-    Box(109-        Modifier110-            .fillMaxWidth()111-            .height(1.dp)112-            .background(peachColors.T3),113-    )114-}115-116-/**117- * A single action row: 44dp min height, IBM Plex Mono 12sp, with a 2dp accent118- * left-border that fades in on press (the spec's "accent left-border on press").119- */120-@Composable121-private fun ActionRow(122-    label: String,123-    textColor: Color = peachColors.T0,124-    onClick: () -> Unit,125-) {126-    val accent = LocalPeachAccent.current127-    val interaction = remember { MutableInteractionSource() }128-    val pressed by interaction.collectIsPressedAsState()129-    Row(130-        modifier = Modifier131-            .fillMaxWidth()132-            .heightIn(min = 44.dp)133-            .clickable(interactionSource = interaction, indication = null, onClick = onClick)134-            .background(if (pressed) peachColors.T4 else Color.Transparent)135-            .drawBehind {136-                if (pressed) {137-                    drawRect(138-                        color = accent,139-                        topLeft = Offset.Zero,140-                        size = Size(2.dp.toPx(), size.height),141-                    )142-                }143-            }144-            .padding(horizontal = 20.dp, vertical = 12.dp),145-        verticalAlignment = Alignment.CenterVertically,146-    ) {147-        Text(148-            text = label,149-            color = textColor,150-            style = PeachType.bodyMono.copy(fontSize = 12.sp),151-        )152-    }153-}154-155-@Composable156-private fun CancelRow(onClick: () -> Unit) {157-    Row(158-        modifier = Modifier159-            .fillMaxWidth()160-            .heightIn(min = 44.dp)161-            .clickable(onClick = onClick)162-            .padding(horizontal = 20.dp, vertical = 12.dp),163-        horizontalArrangement = Arrangement.Center,164-        verticalAlignment = Alignment.CenterVertically,165-    ) {166-        Text(167-            text = "Cancel",168-            color = peachColors.T2,169-            style = PeachType.labelMono.copy(fontSize = 11.sp),170-            textAlign = TextAlign.Center,171-        )172-    }173-}

view file at the parent

app/src/main/java/com/peachpasswords/ui/vault/EntryDetailViewModel.kt+10 −1
@@ -61,7 +61,14 @@6161             .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), vault.getEntry(id))6262 6363     val activeEntries: StateFlow<List<VaultEntry>> =64-        vault.entries.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), vault.getActiveEntries())64+        // Notes are not a destination of the Entry view (NotesMode is the65+        // notes surface). Filtering once here means the prev/next buttons,66+        // the index lookup and the initial entry picker all share the same67+        // view of "what is navigable" — mirroring the browser's68+        // `nonNoteEntries` memo so a user can never get stuck inside a note.69+        vault.entries70+            .map { it.filter(::isNavigable) }71+            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), vault.getActiveEntries().filter(::isNavigable))6572 6673     /** Distinct prior username/email values for the identity-suggest dropdown. */6774     val identitySuggestions: StateFlow<List<String>> =@@ -140,6 +147,8 @@140147         val nextIndex = (currentIndex + offset + list.size) % list.size141148         return entryTapDestination(list[nextIndex])142149     }150+151+    private fun isNavigable(entry: VaultEntry): Boolean = entry.deletedAt == null && entry.type != "note"143152 144153     /**145154      * Apply a generated password via updateEntry — VaultRepository captures the

view file