diff --git a/main.go b/main.go index c915865..85ee630 100644 --- a/main.go +++ b/main.go @@ -246,6 +246,8 @@ type ui struct { apiPolicyList widget.List lifecycleList widget.List phonePanelList widget.List + securityDialogList widget.List + remotePrefsDialogList widget.List recentVaultListState widget.List recentRemoteListState widget.List copyUser widget.Clickable @@ -261,6 +263,7 @@ type ui struct { changeMasterKey widget.Clickable synchronizeVault widget.Clickable toggleSyncMenu widget.Clickable + toggleMainMenu widget.Clickable openAdvancedSync widget.Clickable openSecuritySettings widget.Clickable openRemotePrefsHelp widget.Clickable @@ -396,6 +399,7 @@ type ui struct { expandLessIcon *widget.Icon chevronDownIcon *widget.Icon settingsIcon *widget.Icon + menuIcon *widget.Icon clipboardWriter clipboard.Writer loadingMessage string loadingActionLabel string @@ -409,6 +413,7 @@ type ui struct { syncRemotePassword widget.Editor syncDialogOpen bool syncMenuOpen bool + mainMenuOpen bool securityDialogOpen bool remotePrefsDialogOpen bool showSyncPassword bool @@ -560,6 +565,12 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) phonePanelList: widget.List{ List: layout.List{Axis: layout.Vertical}, }, + securityDialogList: widget.List{ + List: layout.List{Axis: layout.Vertical}, + }, + remotePrefsDialogList: widget.List{ + List: layout.List{Axis: layout.Vertical}, + }, recentVaultListState: widget.List{ List: layout.List{Axis: layout.Vertical}, }, @@ -604,6 +615,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) u.expandLessIcon, _ = widget.NewIcon(icons.NavigationExpandLess) u.chevronDownIcon, _ = widget.NewIcon(icons.NavigationArrowDropDown) u.settingsIcon, _ = widget.NewIcon(icons.ActionSettings) + u.menuIcon, _ = widget.NewIcon(icons.NavigationMenu) u.passwordProfile.SetText("strong") u.securityCipher.SetText(vault.CipherChaCha20) u.securityKDF.SetText(vault.KDFArgon2) @@ -721,6 +733,7 @@ func (u *ui) selectedAttachmentNames() []string { func (u *ui) showEntriesSection() { u.resetPasswordPeek() u.state.ShowSection(appstate.SectionEntries) + u.mainMenuOpen = false u.restoreEntriesSectionState() u.filter() } @@ -729,6 +742,7 @@ func (u *ui) showTemplatesSection() { u.resetPasswordPeek() u.rememberEntriesSectionState() u.state.ShowSection(appstate.SectionTemplates) + u.mainMenuOpen = false u.filter() } @@ -736,6 +750,7 @@ func (u *ui) showRecycleBinSection() { u.resetPasswordPeek() u.rememberEntriesSectionState() u.state.ShowSection(appstate.SectionRecycleBin) + u.mainMenuOpen = false u.filter() } @@ -743,6 +758,7 @@ func (u *ui) showAPITokensSection() { u.resetPasswordPeek() u.rememberEntriesSectionState() u.state.ShowSection(appstate.SectionAPITokens) + u.mainMenuOpen = false u.loadSelectedAPITokenIntoEditor() u.filter() } @@ -751,10 +767,37 @@ func (u *ui) showAPIAuditSection() { u.resetPasswordPeek() u.rememberEntriesSectionState() u.state.ShowSection(appstate.SectionAPIAudit) + u.mainMenuOpen = false u.selectedAuditIndex = -1 u.filter() } +func (u *ui) returnToMainEntries() { + u.clearDeleteGroupConfirmation() + u.showEntriesSection() +} + +func (u *ui) handlePhoneBack() bool { + if u.mode != "phone" { + return false + } + switch { + case u.securityDialogOpen: + u.securityDialogOpen = false + case u.remotePrefsDialogOpen: + u.remotePrefsDialogOpen = false + case u.syncDialogOpen: + u.syncDialogOpen = false + case u.mainMenuOpen: + u.mainMenuOpen = false + case u.state.Section != appstate.SectionEntries: + u.returnToMainEntries() + default: + return false + } + return true +} + func (u *ui) resetPasswordPeek() { u.showPassword = false } @@ -2706,6 +2749,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { for u.toggleSyncMenu.Clicked(gtx) { u.syncMenuOpen = !u.syncMenuOpen } + for u.toggleMainMenu.Clicked(gtx) { + u.mainMenuOpen = !u.mainMenuOpen + } for u.openAdvancedSync.Clicked(gtx) { u.openAdvancedSyncDialog() } @@ -2713,6 +2759,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { u.loadSecuritySettingsFromSession() u.loadSettingsFormFromPreferences() u.loadSettingsDraft() + u.mainMenuOpen = false u.securityDialogOpen = true } for u.openRemotePrefsHelp.Clicked(gtx) { @@ -3308,20 +3355,20 @@ func (u *ui) remotePrefsDialog(gtx layout.Context) layout.Dimensions { } func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + rows := []layout.Widget{ + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(20), "Settings") lbl.Color = accentColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(6)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "ACCESSIBILITY") lbl.Color = mutedColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(4)}.Layout, + func(gtx layout.Context) layout.Dimensions { return u.settingsPreferenceCard(gtx, "Display Density", "Adjust editor height and control spacing for denser or roomier forms.", func(gtx layout.Context) layout.Dimensions { return u.settingsChoiceRow( gtx, @@ -3329,9 +3376,9 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { choiceSpec{Click: &u.settingsDensityComfortable, Label: "Comfortable", Active: u.settingsDraft.Accessibility.DisplayDensity == displayDensityComfortable}, ) }) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { return u.settingsPreferenceCard(gtx, "Contrast", "Increase focus and selection contrast without changing unrelated vault behavior.", func(gtx layout.Context) layout.Dimensions { return u.settingsChoiceRow( gtx, @@ -3339,9 +3386,9 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { choiceSpec{Click: &u.settingsContrastHigh, Label: "High Contrast", Active: u.settingsDraft.Accessibility.Contrast == contrastHigh}, ) }) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { return u.settingsPreferenceCard(gtx, "Reduced Motion", "Keep transient status toasts steady instead of auto-dismissing after a short timeout.", func(gtx layout.Context) layout.Dimensions { return u.settingsChoiceRow( gtx, @@ -3349,9 +3396,9 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { choiceSpec{Click: &u.settingsReducedMotionOn, Label: "On", Active: u.settingsDraft.Accessibility.ReducedMotion}, ) }) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { return u.settingsPreferenceCard(gtx, "Keyboard Focus", "Strengthen the visible focus ring and focused selection treatment for keyboard-first navigation.", func(gtx layout.Context) layout.Dimensions { return u.settingsChoiceRow( gtx, @@ -3359,52 +3406,52 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { choiceSpec{Click: &u.settingsKeyboardFocusProminent, Label: "Prominent", Active: u.settingsDraft.Accessibility.KeyboardFocus == keyboardFocusProminent}, ) }) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(12)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(14), "Choose how KeePassGO remembers UI layout behavior, sync defaults, and KDBX security defaults without crowding the main vault flow.") lbl.Color = mutedColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(12)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(16), "UI Preferences") lbl.Color = accentColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(6)}.Layout, + func(gtx layout.Context) layout.Dimensions { check := material.CheckBox(u.theme, &u.settingsGroupControls, "Keep Group Tools collapsed") return check.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + func(gtx layout.Context) layout.Dimensions { check := material.CheckBox(u.theme, &u.settingsLifecycleAdvanced, "Keep advanced lifecycle controls collapsed") return check.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + func(gtx layout.Context) layout.Dimensions { check := material.CheckBox(u.theme, &u.settingsHistory, "Keep entry history collapsed") return check.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(14)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(16), "Vault Security") lbl.Color = accentColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(labeledEditorHelp(u.theme, "Cipher", "Supported values: "+strings.Join([]string{vault.CipherAES256, vault.CipherChaCha20}, ", "), &u.securityCipher, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Supported values: "+strings.Join([]string{vault.KDFAES, vault.KDFArgon2}, ", "), &u.securityKDF, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), - layout.Rigid(syncDialogSectionLabel(u.theme, "Sync Defaults")), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + labeledEditorHelp(u.theme, "Cipher", "Supported values: "+strings.Join([]string{vault.CipherAES256, vault.CipherChaCha20}, ", "), &u.securityCipher, false), + layout.Spacer{Height: unit.Dp(8)}.Layout, + labeledEditorHelp(u.theme, "KDF", "Supported values: "+strings.Join([]string{vault.KDFAES, vault.KDFArgon2}, ", "), &u.securityKDF, false), + layout.Spacer{Height: unit.Dp(12)}.Layout, + syncDialogSectionLabel(u.theme, "Sync Defaults"), + layout.Spacer{Height: unit.Dp(6)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(13), "Advanced Sync starts from these defaults. You can still change the source or direction before a single run.") lbl.Color = mutedColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncPull, "Pull Into Current Vault", u.settingsDraft.Sync.DirectionDefault == syncDirectionPull) @@ -3414,9 +3461,9 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncPush, "Push Current Vault Out", u.settingsDraft.Sync.DirectionDefault == syncDirectionPush) }), ) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncLocal, "Local File", u.settingsDraft.Sync.SourceDefault == syncSourceLocal) @@ -3426,41 +3473,41 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncRemote, "Remote WebDAV", u.settingsDraft.Sync.SourceDefault == syncSourceRemote) }), ) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { return syncDialogSummaryCard(gtx, u.theme, u.settingsDraft.Sync.SourceDefault, u.settingsDraft.Sync.DirectionDefault) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "Conflict handling stays retry-safe: merged entry changes keep history, while remote save conflicts still require reopening the vault and retrying the save.") lbl.Color = mutedColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), - layout.Rigid(syncDialogSectionLabel(u.theme, "Background Sync")), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(12)}.Layout, + syncDialogSectionLabel(u.theme, "Background Sync"), + layout.Spacer{Height: unit.Dp(6)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "Future background sync controls belong here so source, direction, and unattended behavior stay in one settings surface.") lbl.Color = mutedColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), - layout.Rigid(syncDialogSectionLabel(u.theme, "Feedback")), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(12)}.Layout, + syncDialogSectionLabel(u.theme, "Feedback"), + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(14), "Success and reminder banners") lbl.Color = accentColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(4)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "Choose how long noncritical status banners stay visible.") lbl.Color = mutedColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(6)}.Layout, + func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return syncChoiceButton(gtx, u.theme, &u.setStatusBannerShort, "Short", u.statusBannerTTL == 2*time.Second) @@ -3474,21 +3521,21 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { return syncChoiceButton(gtx, u.theme, &u.setStatusBannerLong, "Long", u.statusBannerTTL == statusBannerLong) }), ) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(10)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(14), "Autofill notices") lbl.Color = accentColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(4)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "Keep recent autofill results visible, reduce them to approval-only, or hide them.") lbl.Color = mutedColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(6)}.Layout, + func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return syncChoiceButton(gtx, u.theme, &u.showAllAutofillNotices, "All", u.autofillNoticePreference == autofillNoticeAll) @@ -3502,27 +3549,27 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { return syncChoiceButton(gtx, u.theme, &u.hideAutofillNotices, "Hidden", u.autofillNoticePreference == autofillNoticeSuppressed) }), ) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), - layout.Rigid(syncDialogSectionLabel(u.theme, "Privacy")), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(12)}.Layout, + syncDialogSectionLabel(u.theme, "Privacy"), + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { return settingsSummaryCard(gtx, u.theme, "PRIVACY PLAN", "Use first-fill approval plus browser/app rules to keep autofill constrained to trusted targets.") - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(14), "First-fill approval") lbl.Color = accentColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(4)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), u.autofillFirstFillApprovalSummary()) lbl.Color = mutedColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(6)}.Layout, + func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return syncChoiceButton(gtx, u.theme, &u.showAutofillApprovalAsk, "Ask First", u.autofillFirstFillApprovalMode == autofillFirstFillApprovalAsk) @@ -3536,21 +3583,21 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { return syncChoiceButton(gtx, u.theme, &u.showAutofillApprovalBlock, "Block Until Allowed", u.autofillFirstFillApprovalMode == autofillFirstFillApprovalBlock) }), ) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(10)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), fmt.Sprintf("%d autofill rule entries configured across browsers, apps, and package-specific overrides.", u.autofillRuleCount())) lbl.Color = mutedColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(labeledEditorHelp(u.theme, "Browser allowlist", "One origin or hostname per line for trusted browser surfaces.", &u.autofillBrowserAllowlist, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(labeledEditorHelp(u.theme, "App and package allowlist", "One Android package name or trusted app identifier per line.", &u.autofillAppAllowlist, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(labeledEditorHelp(u.theme, "Package rules", "One rule per line, for example `com.android.chrome=hostname` or `org.keepassgo.browser=view-id`.", &u.autofillPackageRules, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + labeledEditorHelp(u.theme, "Browser allowlist", "One origin or hostname per line for trusted browser surfaces.", &u.autofillBrowserAllowlist, false), + layout.Spacer{Height: unit.Dp(8)}.Layout, + labeledEditorHelp(u.theme, "App and package allowlist", "One Android package name or trusted app identifier per line.", &u.autofillAppAllowlist, false), + layout.Spacer{Height: unit.Dp(8)}.Layout, + labeledEditorHelp(u.theme, "Package rules", "One rule per line, for example `com.android.chrome=hostname` or `org.keepassgo.browser=view-id`.", &u.autofillPackageRules, false), + layout.Spacer{Height: unit.Dp(12)}.Layout, + func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.closeSecuritySettings, "Close") @@ -3560,44 +3607,50 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.saveSecuritySettings, "Save Settings") }), ) - }), - ) + }, + } + return material.List(u.theme, &u.securityDialogList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions { + return rows[i](gtx) + }) } func (u *ui) remotePrefsDialogContent(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + rows := []layout.Widget{ + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(20), "Remote Connection Settings & Help") lbl.Color = accentColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(14), "Use Recent Connections to reopen WebDAV-backed vaults quickly without cluttering the main open flow.") lbl.Color = mutedColor return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(12)}.Layout, + func(gtx layout.Context) layout.Dimensions { return approvalFact(u.theme, "Current", u.remotePreferencesCurrentSummary(), "")(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { return approvalFact(u.theme, "Always Saved", u.remotePreferencesAlwaysSavedSummary(), "")(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { return approvalFact(u.theme, "Retention", u.remotePreferencesRetentionSummary(), "")(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + func(gtx layout.Context) layout.Dimensions { return approvalFact(u.theme, "When Sign-in Saves", "Username and password or app token are only stored after a successful remote open when Remember sign-in is enabled.", "")(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + }, + layout.Spacer{Height: unit.Dp(14)}.Layout, + func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.closeRemotePrefsHelp, "Done") - }), - ) + }, + } + return material.List(u.theme, &u.remotePrefsDialogList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions { + return rows[i](gtx) + }) } func (u *ui) approvalDialog(gtx layout.Context) layout.Dimensions { @@ -3852,35 +3905,20 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions { if u.shouldShowLifecycleSetup() || u.isVaultLocked() { return layout.Dimensions{} } - return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - summary := u.currentVaultSummary() - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), "VAULT") - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(1)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(15), summary.Title) - lbl.Color = accentColor - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if strings.TrimSpace(summary.Detail) == "" || summary.Detail == summary.Title { - return layout.Dimensions{} - } - lbl := material.Label(u.theme, unit.Sp(11), summary.Detail) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - ) - }), - layout.Rigid(u.headerActions), - ) - }) + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { return layout.Dimensions{} }), + layout.Rigid(u.headerActions), + ) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !u.mainMenuOpen { + return layout.Dimensions{} + } + return layout.Inset{Top: unit.Dp(8)}.Layout(gtx, u.mainMenu) + }), + ) } if u.shouldShowDesktopWorkingHeader() { return layout.Dimensions{} @@ -3909,7 +3947,11 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions { layout.Rigid(u.syncButtonGroup), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - btn := material.IconButton(u.theme, &u.openSecuritySettings, u.settingsIcon, "Settings") + icon := u.menuIcon + if icon == nil { + icon = u.settingsIcon + } + btn := material.IconButton(u.theme, &u.toggleMainMenu, icon, "Menu") btn.Background = selectedColor btn.Color = accentColor btn.Size = unit.Dp(18) @@ -3924,6 +3966,32 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions { ) } +func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions { + return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.showEntries, "Entries") + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.showRecycle, "Recycle Bin") + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.showAPITokens, "API Tokens") + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit") + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings") + }), + ) + }) +} + func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions { label := "Sync" spacing := unit.Dp(4) @@ -4033,11 +4101,25 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { if u.mode == "phone" { return panel(gtx, func(gtx layout.Context) layout.Dimensions { rows := make([]layout.Widget, 0, 16+len(u.visible)) + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Min.X = gtx.Constraints.Max.X + return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions { + editor := material.Editor(u.theme, &u.search, "Search vault") + editor.Color = u.theme.Palette.Fg + editor.HintColor = mutedColor + return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout) + }) + }) + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return layout.Spacer{Height: spacing}.Layout(gtx) + }) if !u.isVaultLocked() { rows = append(rows, u.navigationHeader) - rows = append(rows, func(gtx layout.Context) layout.Dimensions { - return layout.Spacer{Height: spacing}.Layout(gtx) - }) + if u.state.Section == appstate.SectionEntries { + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return layout.Spacer{Height: spacing}.Layout(gtx) + }) + } } if !u.isVaultLocked() && u.state.Section == appstate.SectionRecycleBin { rows = append(rows, u.recycleBinSectionNotice) @@ -4061,18 +4143,6 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { return layout.Spacer{Height: spacing}.Layout(gtx) }) } - rows = append(rows, func(gtx layout.Context) layout.Dimensions { - gtx.Constraints.Min.X = gtx.Constraints.Max.X - return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions { - editor := material.Editor(u.theme, &u.search, "Search vault") - editor.Color = u.theme.Palette.Fg - editor.HintColor = mutedColor - return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout) - }) - }) - rows = append(rows, func(gtx layout.Context) layout.Dimensions { - return layout.Spacer{Height: spacing}.Layout(gtx) - }) if !u.isVaultLocked() { rows = append(rows, func(gtx layout.Context) layout.Dimensions { switch u.state.Section { @@ -4206,17 +4276,10 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions { if u.mode == "phone" { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return u.sectionBar(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.state.Section != appstate.SectionEntries { - return layout.Dimensions{} - } - return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, u.groupControlsDisclosure) - }), - ) + if u.state.Section != appstate.SectionEntries { + return layout.Dimensions{} + } + return u.groupControlsDisclosure(gtx) } return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { @@ -4455,13 +4518,23 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions { layout.Rigid(u.syncButtonGroup), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - btn := material.IconButton(u.theme, &u.openSecuritySettings, u.settingsIcon, "Settings") + icon := u.menuIcon + if icon == nil { + icon = u.settingsIcon + } + btn := material.IconButton(u.theme, &u.toggleMainMenu, icon, "Menu") btn.Background = selectedColor btn.Color = accentColor btn.Size = unit.Dp(18) btn.Inset = layout.UniformInset(unit.Dp(8)) return btn.Layout(gtx) }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !u.mainMenuOpen { + return layout.Dimensions{} + } + return layout.Inset{Left: unit.Dp(6)}.Layout(gtx, u.mainMenu) + }), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { btn := material.Button(u.theme, &u.lockVault, "Lock") diff --git a/main_test.go b/main_test.go index 59d5d7e..110571d 100644 --- a/main_test.go +++ b/main_test.go @@ -24,6 +24,7 @@ import ( "git.julianfamily.org/keepassgo/apiapproval" "git.julianfamily.org/keepassgo/apiaudit" "git.julianfamily.org/keepassgo/apitokens" + "git.julianfamily.org/keepassgo/appstate" "git.julianfamily.org/keepassgo/clipboard" "git.julianfamily.org/keepassgo/passwords" "git.julianfamily.org/keepassgo/session" @@ -806,6 +807,41 @@ func TestUIPhoneListPanelWithExpandedGroupControlsAndEntriesDoesNotPanic(t *test _ = u.listPanel(gtx) } +func TestUIPhoneBackReturnsFromSubscreenToEntries(t *testing.T) { + t.Parallel() + + u := newUIWithModel("phone", vault.Model{ + Entries: []vault.Entry{{ID: "entry-1", Title: "Git Server", Path: []string{"Root", "Internet"}}}, + }) + u.showAPITokensSection() + + if !u.handlePhoneBack() { + t.Fatal("handlePhoneBack() = false, want true for phone subsection") + } + if u.state.Section != appstate.SectionEntries { + t.Fatalf("state.Section = %q, want entries", u.state.Section) + } +} + +func TestUISecurityDialogContentDoesNotPanicWithSmallViewport(t *testing.T) { + t.Parallel() + + u := newUIWithModel("phone", vault.Model{}) + u.securityDialogOpen = true + ops := new(op.Ops) + gtx := layout.Context{ + Ops: ops, + Constraints: layout.Exact(image.Pt(540, 420)), + } + defer func() { + if r := recover(); r != nil { + t.Fatalf("securityDialogContent() panicked in small viewport: %v", r) + } + }() + + _ = u.securityDialogContent(gtx) +} + func TestUIAPIAuditSectionShowsRecordedEvents(t *testing.T) { t.Parallel() diff --git a/ui_shortcuts.go b/ui_shortcuts.go index b52d581..9d8e304 100644 --- a/ui_shortcuts.go +++ b/ui_shortcuts.go @@ -37,6 +37,8 @@ func (u *ui) processShortcuts(gtx layout.Context) { key.Filter{Name: key.NameUpArrow}, key.Filter{Name: key.NameDownArrow}, key.Filter{Name: key.NameReturn}, + key.Filter{Name: key.NameBack}, + key.Filter{Name: key.NameEscape}, ) if !ok { break @@ -48,6 +50,9 @@ func (u *ui) processShortcuts(gtx layout.Context) { } u.handleKeyPress(ke.Name, ke.Modifiers) + if ke.Name == key.NameBack || ke.Name == key.NameEscape { + _ = u.handlePhoneBack() + } } }