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
↓ peach-android-7c92cad1.tar.gzatlas at this save
files changed4 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 −1app/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 {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)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-}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