From 16f603ccba549df04d25188097ed6590531f18ff Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Wed, 8 Apr 2026 23:27:47 -0700 Subject: [PATCH] Move sync menu state decisions out of renderers --- main.go | 67 +++++--------------- ui_layout_header.go | 84 ++++++++++++------------- ui_sync_menu_model.go | 142 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 97 deletions(-) create mode 100644 ui_sync_menu_model.go diff --git a/main.go b/main.go index fd92f4e..fbe0339 100644 --- a/main.go +++ b/main.go @@ -2740,98 +2740,61 @@ func (u *ui) shouldShowSavedRemoteBindingSelectors() bool { } func (u *ui) savedRemoteBindingSummary() (profileLabel, credentialLabel, syncLabel string, ok bool) { - profile, ok := u.selectedVaultRemoteProfile() - if !ok { - return "", "", "", false - } - entry, ok := u.selectedVaultRemoteCredentialEntry() - if !ok { - return "", "", "", false - } - credentialLabel = entry.Title - if strings.TrimSpace(entry.Username) != "" { - credentialLabel += " · " + strings.TrimSpace(entry.Username) - } - syncLabel = "Sync manually when you choose Use Remote Sync." - if normalizeUISyncMode(u.selectedVaultRemoteSyncMode) == appstate.SyncModeAutomaticOnOpenSave { - syncLabel = "Syncs automatically on open and save." - } - return profile.Name, credentialLabel, syncLabel, true + summary := u.computeSavedRemoteBindingSummary() + return summary.profileLabel, summary.credentialLabel, summary.syncLabel, summary.ok } func (u *ui) savedRemoteBindingHeading() string { - if !u.shouldShowSavedRemoteBindingSelectors() { - return "Use this vault's saved remote sync target" - } - return "Use a saved remote profile from this vault" + return u.buildSyncMenuModel().savedBindingHeading() } func (u *ui) openSelectedVaultRemoteButtonLabel() string { - if !u.shouldShowSavedRemoteBindingSelectors() { - return "Use Remote Sync" - } - return "Open Saved Remote" + return u.buildSyncMenuModel().openSelectedButtonLabel() } func (u *ui) shouldShowDirectRemoteSyncShortcut() bool { if !u.hasOpenVault() || u.isVaultLocked() || u.state.Section != appstate.SectionEntries { return false } - _, ok := u.selectedVaultRemoteBinding() - return ok + return u.buildSyncMenuModel().showDirectRemoteSyncShortcut() } func (u *ui) directRemoteSyncShortcutLabel() string { - return "Use Remote Sync" + return u.buildSyncMenuModel().directRemoteSyncShortcutLabel() } func (u *ui) shouldShowRemoteSyncSettingsShortcut() bool { if !u.hasOpenVault() || u.isVaultLocked() || u.state.Section != appstate.SectionEntries { return false } - _, ok := u.selectedVaultRemoteBinding() - return ok + return u.buildSyncMenuModel().showRemoteSyncSettingsShortcut() } func (u *ui) remoteSyncSettingsShortcutLabel() string { - return "Remote Sync Settings" + return u.buildSyncMenuModel().remoteSyncSettingsShortcutLabel() } func (u *ui) shouldShowRemoveRemoteSyncShortcut() bool { - return u.shouldShowRemoteSyncSettingsShortcut() + return u.buildSyncMenuModel().showRemoveRemoteSyncShortcut() } func (u *ui) removeRemoteSyncShortcutLabel() string { - return "Stop Using Remote Sync" + return u.buildSyncMenuModel().removeRemoteSyncShortcutLabel() } func (u *ui) shouldShowRemoteSyncSetupShortcut() bool { if !u.hasOpenVault() || u.isVaultLocked() || u.state.Section != appstate.SectionEntries { return false } - _, ok := u.selectedVaultRemoteBinding() - return !ok + return u.buildSyncMenuModel().showRemoteSyncSetupShortcut() } func (u *ui) remoteSyncSetupShortcutLabel() string { - return "Set Up Remote Sync" + return u.buildSyncMenuModel().remoteSyncSetupShortcutLabel() } func (u *ui) syncMenuActionLabels() []string { - labels := []string{"Open Advanced Sync"} - if u.shouldShowRemoteSyncSetupShortcut() { - labels = append(labels, u.remoteSyncSetupShortcutLabel()) - } - if u.shouldShowDirectRemoteSyncShortcut() { - labels = append(labels, u.directRemoteSyncShortcutLabel()) - } - if u.shouldShowRemoteSyncSettingsShortcut() { - labels = append(labels, u.remoteSyncSettingsShortcutLabel()) - } - if u.shouldShowRemoveRemoteSyncShortcut() { - labels = append(labels, u.removeRemoteSyncShortcutLabel()) - } - return labels + return u.buildSyncMenuModel().actionLabels() } func remoteBindingSuffix(baseURL, path, username string) string { @@ -2939,11 +2902,11 @@ func (u *ui) removeSelectedRemoteBindingAction() error { } func (u *ui) saveCurrentRemoteBindingHeading() string { - return "Bind this local vault to the current remote target" + return u.buildSyncMenuModel().saveCurrentRemoteBindingHeading() } func (u *ui) saveCurrentRemoteBindingButtonLabel() string { - return "Save Remote In Vault" + return u.buildSyncMenuModel().saveCurrentRemoteBindingButtonLabel() } func (u *ui) materializeCurrentRemoteCache() error { diff --git a/ui_layout_header.go b/ui_layout_header.go index 3b81d7e..8961231 100644 --- a/ui_layout_header.go +++ b/ui_layout_header.go @@ -3,7 +3,6 @@ package main import ( "image" "image/color" - "runtime" "strings" "gioui.org/layout" @@ -192,6 +191,7 @@ func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions { } func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { + model := u.buildSyncMenuModel() profiles := u.availableRemoteProfiles() credentials := u.availableRemoteCredentialEntries() if len(u.vaultRemoteProfileClicks) < len(profiles) { @@ -205,29 +205,29 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync") }, } - if supportsVaultShare(runtime.GOOS) && u.vaultSharer != nil && strings.TrimSpace(u.currentShareableVaultPath()) != "" { + if model.showShare { actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault") }) } - if u.shouldShowRemoteSyncSetupShortcut() { + if model.showRemoteSyncSetupShortcut() { actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel()) + return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.remoteSyncSetupShortcutLabel()) }) } - if u.shouldShowDirectRemoteSyncShortcut() { + if model.showDirectRemoteSyncShortcut() { actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel()) + return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, model.directRemoteSyncShortcutLabel()) }) } - if u.shouldShowRemoteSyncSettingsShortcut() { + if model.showRemoteSyncSettingsShortcut() { actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSettingsShortcutLabel()) + return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.remoteSyncSettingsShortcutLabel()) }) } - if u.shouldShowRemoveRemoteSyncShortcut() { + if model.showRemoveRemoteSyncShortcut() { actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, u.removeRemoteSyncShortcutLabel()) + return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, model.removeRemoteSyncShortcutLabel()) }) } actionWidth := menuActionWidth(gtx, actionRows) @@ -240,7 +240,7 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !supportsVaultShare(runtime.GOOS) || u.vaultSharer == nil || strings.TrimSpace(u.currentShareableVaultPath()) == "" { + if !model.showShare { return layout.Dimensions{} } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, @@ -258,42 +258,42 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { }) }), } - if u.shouldShowRemoteSyncSetupShortcut() { + if model.showRemoteSyncSetupShortcut() { rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel()) + return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.remoteSyncSetupShortcutLabel()) }) }), ) } - if u.shouldShowDirectRemoteSyncShortcut() { + if model.showDirectRemoteSyncShortcut() { rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel()) + return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, model.directRemoteSyncShortcutLabel()) }) }), ) } - if u.shouldShowRemoteSyncSettingsShortcut() { + if model.showRemoteSyncSettingsShortcut() { rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSettingsShortcutLabel()) + return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.remoteSyncSettingsShortcutLabel()) }) }), ) } - if u.shouldShowRemoveRemoteSyncShortcut() { + if model.showRemoveRemoteSyncShortcut() { rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, u.removeRemoteSyncShortcutLabel()) + return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, model.removeRemoteSyncShortcutLabel()) }) }), ) @@ -302,36 +302,36 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), u.savedRemoteBindingHeading()) + lbl := material.Label(u.theme, unit.Sp(11), model.savedBindingHeading()) lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), ) - if !u.shouldShowSavedRemoteBindingSelectors() { + if !model.showSelectors { rows = append(rows, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - profileLabel, credentialLabel, syncLabel, ok := u.savedRemoteBindingSummary() - if !ok { + summary := model.savedBindingSummary + if !summary.ok { return layout.Dimensions{} } return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 242, G: 245, B: 240, A: 255}), func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(13), profileLabel) + lbl := material.Label(u.theme, unit.Sp(13), summary.profileLabel) lbl.Color = accentColor return lbl.Layout(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), "Credential: "+credentialLabel) + lbl := material.Label(u.theme, unit.Sp(12), "Credential: "+summary.credentialLabel) lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), syncLabel) + lbl := material.Label(u.theme, unit.Sp(12), summary.syncLabel) lbl.Color = mutedColor return lbl.Layout(gtx) }), @@ -394,25 +394,19 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { } } } - if u.hasOpenVault() { - baseURL := strings.TrimSpace(u.remoteBaseURL.Text()) - remotePath := strings.TrimSpace(u.remotePath.Text()) - username := strings.TrimSpace(u.remoteUsername.Text()) - password := u.remotePassword.Text() - if baseURL != "" && remotePath != "" && username != "" && password != "" { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), u.saveCurrentRemoteBindingHeading()) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.saveCurrentRemoteBinding, u.saveCurrentRemoteBindingButtonLabel()) - }), - ) - } + if model.showSaveCurrentBinding { + rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), model.saveCurrentRemoteBindingHeading()) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.saveCurrentRemoteBinding, model.saveCurrentRemoteBindingButtonLabel()) + }), + ) } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...) }) diff --git a/ui_sync_menu_model.go b/ui_sync_menu_model.go new file mode 100644 index 0000000..34893be --- /dev/null +++ b/ui_sync_menu_model.go @@ -0,0 +1,142 @@ +package main + +import ( + "runtime" + "strings" + + "git.julianfamily.org/keepassgo/appstate" +) + +type syncMenuModel struct { + hasOpenVault bool + hasSelectedBinding bool + showSelectors bool + showShare bool + showSaveCurrentBinding bool + savedBindingSummary syncMenuBindingSummary + remoteBaseURL string + remotePath string + remoteUsername string + remotePassword string + selectedVaultSyncMode appstate.SyncMode +} + +type syncMenuBindingSummary struct { + profileLabel string + credentialLabel string + syncLabel string + ok bool +} + +func (u *ui) buildSyncMenuModel() syncMenuModel { + model := syncMenuModel{ + hasOpenVault: u.hasOpenVault(), + showSelectors: u.shouldShowSavedRemoteBindingSelectors(), + showShare: supportsVaultShare(runtime.GOOS) && u.vaultSharer != nil && strings.TrimSpace(u.currentShareableVaultPath()) != "", + remoteBaseURL: strings.TrimSpace(u.remoteBaseURL.Text()), + remotePath: strings.TrimSpace(u.remotePath.Text()), + remoteUsername: strings.TrimSpace(u.remoteUsername.Text()), + remotePassword: u.remotePassword.Text(), + selectedVaultSyncMode: normalizeUISyncMode(u.selectedVaultRemoteSyncMode), + } + _, model.hasSelectedBinding = u.selectedVaultRemoteBinding() + model.savedBindingSummary = u.computeSavedRemoteBindingSummary() + model.showSaveCurrentBinding = model.hasOpenVault && model.remoteBaseURL != "" && model.remotePath != "" && model.remoteUsername != "" && model.remotePassword != "" + return model +} + +func (u *ui) computeSavedRemoteBindingSummary() syncMenuBindingSummary { + profile, ok := u.selectedVaultRemoteProfile() + if !ok { + return syncMenuBindingSummary{} + } + entry, ok := u.selectedVaultRemoteCredentialEntry() + if !ok { + return syncMenuBindingSummary{} + } + credentialLabel := entry.Title + if strings.TrimSpace(entry.Username) != "" { + credentialLabel += " · " + strings.TrimSpace(entry.Username) + } + syncLabel := "Sync manually when you choose Use Remote Sync." + if normalizeUISyncMode(u.selectedVaultRemoteSyncMode) == appstate.SyncModeAutomaticOnOpenSave { + syncLabel = "Syncs automatically on open and save." + } + return syncMenuBindingSummary{ + profileLabel: profile.Name, + credentialLabel: credentialLabel, + syncLabel: syncLabel, + ok: true, + } +} + +func (m syncMenuModel) savedBindingHeading() string { + if !m.showSelectors { + return "Use this vault's saved remote sync target" + } + return "Use a saved remote profile from this vault" +} + +func (m syncMenuModel) openSelectedButtonLabel() string { + if !m.showSelectors { + return "Use Remote Sync" + } + return "Open Saved Remote" +} + +func (m syncMenuModel) showDirectRemoteSyncShortcut() bool { + return m.hasOpenVault && m.hasSelectedBinding +} + +func (m syncMenuModel) directRemoteSyncShortcutLabel() string { + return "Use Remote Sync" +} + +func (m syncMenuModel) showRemoteSyncSettingsShortcut() bool { + return m.hasOpenVault && m.hasSelectedBinding +} + +func (m syncMenuModel) remoteSyncSettingsShortcutLabel() string { + return "Remote Sync Settings" +} + +func (m syncMenuModel) showRemoveRemoteSyncShortcut() bool { + return m.showRemoteSyncSettingsShortcut() +} + +func (m syncMenuModel) removeRemoteSyncShortcutLabel() string { + return "Stop Using Remote Sync" +} + +func (m syncMenuModel) showRemoteSyncSetupShortcut() bool { + return m.hasOpenVault && !m.hasSelectedBinding +} + +func (m syncMenuModel) remoteSyncSetupShortcutLabel() string { + return "Set Up Remote Sync" +} + +func (m syncMenuModel) actionLabels() []string { + labels := []string{"Open Advanced Sync"} + if m.showRemoteSyncSetupShortcut() { + labels = append(labels, m.remoteSyncSetupShortcutLabel()) + } + if m.showDirectRemoteSyncShortcut() { + labels = append(labels, m.directRemoteSyncShortcutLabel()) + } + if m.showRemoteSyncSettingsShortcut() { + labels = append(labels, m.remoteSyncSettingsShortcutLabel()) + } + if m.showRemoveRemoteSyncShortcut() { + labels = append(labels, m.removeRemoteSyncShortcutLabel()) + } + return labels +} + +func (m syncMenuModel) saveCurrentRemoteBindingHeading() string { + return "Bind this local vault to the current remote target" +} + +func (m syncMenuModel) saveCurrentRemoteBindingButtonLabel() string { + return "Save Remote In Vault" +}