Add persisted UI settings preferences

This commit is contained in:
Joe Julian
2026-04-01 17:38:02 -07:00
parent 2677f7db17
commit 2f13b0d42f
2 changed files with 426 additions and 318 deletions
+136 -110
View File
@@ -141,6 +141,7 @@ type uiPreferences struct {
GroupControlsHidden bool `json:"groupControlsHidden"` GroupControlsHidden bool `json:"groupControlsHidden"`
LifecycleAdvancedHidden bool `json:"lifecycleAdvancedHidden"` LifecycleAdvancedHidden bool `json:"lifecycleAdvancedHidden"`
HistoryHidden bool `json:"historyHidden"` HistoryHidden bool `json:"historyHidden"`
DenseLayout bool `json:"denseLayout"`
} }
type entriesSectionState struct { type entriesSectionState struct {
@@ -224,10 +225,8 @@ type ui struct {
toggleSyncMenu widget.Clickable toggleSyncMenu widget.Clickable
openAdvancedSync widget.Clickable openAdvancedSync widget.Clickable
openSecuritySettings widget.Clickable openSecuritySettings widget.Clickable
openRemotePrefsHelp widget.Clickable
closeAdvancedSync widget.Clickable closeAdvancedSync widget.Clickable
closeSecuritySettings widget.Clickable closeSecuritySettings widget.Clickable
closeRemotePrefsHelp widget.Clickable
runAdvancedSync widget.Clickable runAdvancedSync widget.Clickable
saveSecuritySettings widget.Clickable saveSecuritySettings widget.Clickable
editEntry widget.Clickable editEntry widget.Clickable
@@ -287,6 +286,10 @@ type ui struct {
apiPolicyAllow widget.Bool apiPolicyAllow widget.Bool
apiPolicyGroupScopeW widget.Bool apiPolicyGroupScopeW widget.Bool
apiTokenDisabled widget.Bool apiTokenDisabled widget.Bool
settingsGroupControls widget.Bool
settingsLifecycleAdvanced widget.Bool
settingsHistory widget.Bool
settingsDenseLayout widget.Bool
entryClicks []widget.Clickable entryClicks []widget.Clickable
apiTokenClicks []widget.Clickable apiTokenClicks []widget.Clickable
apiPolicyRemoves []widget.Clickable apiPolicyRemoves []widget.Clickable
@@ -344,7 +347,6 @@ type ui struct {
syncDialogOpen bool syncDialogOpen bool
syncMenuOpen bool syncMenuOpen bool
securityDialogOpen bool securityDialogOpen bool
remotePrefsDialogOpen bool
showSyncPassword bool showSyncPassword bool
keyboardFocus focusID keyboardFocus focusID
defaultSaveAsPath string defaultSaveAsPath string
@@ -356,6 +358,7 @@ type ui struct {
groupControlsHidden bool groupControlsHidden bool
lifecycleAdvancedHidden bool lifecycleAdvancedHidden bool
historyHidden bool historyHidden bool
denseLayout bool
recentVaults []string recentVaults []string
recentRemotes []recentRemoteRecord recentRemotes []recentRemoteRecord
recentVaultGroups map[string][]string recentVaultGroups map[string][]string
@@ -519,6 +522,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
u.loadRecentRemotes() u.loadRecentRemotes()
u.restoreStartupLifecycleTarget() u.restoreStartupLifecycleTarget()
u.loadUIPreferences() u.loadUIPreferences()
u.loadSettingsFormFromPreferences()
u.filter() u.filter()
u.syncAutofillCache() u.syncAutofillCache()
return u return u
@@ -1016,6 +1020,8 @@ func (u *ui) saveSecuritySettingsAction() error {
if err := u.state.ConfigureSecurity(settings); err != nil { if err := u.state.ConfigureSecurity(settings); err != nil {
return err return err
} }
u.applySettingsFormToPreferences()
u.saveUIPreferences()
u.securityDialogOpen = false u.securityDialogOpen = false
return nil return nil
} }
@@ -1292,6 +1298,7 @@ func (u *ui) loadUIPreferences() {
u.groupControlsHidden = prefs.GroupControlsHidden u.groupControlsHidden = prefs.GroupControlsHidden
u.lifecycleAdvancedHidden = prefs.LifecycleAdvancedHidden u.lifecycleAdvancedHidden = prefs.LifecycleAdvancedHidden
u.historyHidden = prefs.HistoryHidden u.historyHidden = prefs.HistoryHidden
u.denseLayout = prefs.DenseLayout
} }
func (u *ui) saveUIPreferences() { func (u *ui) saveUIPreferences() {
@@ -1305,6 +1312,7 @@ func (u *ui) saveUIPreferences() {
GroupControlsHidden: u.groupControlsHidden, GroupControlsHidden: u.groupControlsHidden,
LifecycleAdvancedHidden: u.lifecycleAdvancedHidden, LifecycleAdvancedHidden: u.lifecycleAdvancedHidden,
HistoryHidden: u.historyHidden, HistoryHidden: u.historyHidden,
DenseLayout: u.denseLayout,
}, "", " ") }, "", " ")
if err != nil { if err != nil {
return return
@@ -1312,6 +1320,20 @@ func (u *ui) saveUIPreferences() {
_ = os.WriteFile(u.uiPreferencesPath, content, 0o600) _ = os.WriteFile(u.uiPreferencesPath, content, 0o600)
} }
func (u *ui) loadSettingsFormFromPreferences() {
u.settingsGroupControls.Value = u.groupControlsHidden
u.settingsLifecycleAdvanced.Value = u.lifecycleAdvancedHidden
u.settingsHistory.Value = u.historyHidden
u.settingsDenseLayout.Value = u.denseLayout
}
func (u *ui) applySettingsFormToPreferences() {
u.groupControlsHidden = u.settingsGroupControls.Value
u.lifecycleAdvancedHidden = u.settingsLifecycleAdvanced.Value
u.historyHidden = u.settingsHistory.Value
u.denseLayout = u.settingsDenseLayout.Value
}
func (u *ui) noteRecentRemote(baseURL, path, username, password string, rememberAuth bool) { func (u *ui) noteRecentRemote(baseURL, path, username, password string, rememberAuth bool) {
baseURL = strings.TrimSpace(baseURL) baseURL = strings.TrimSpace(baseURL)
path = strings.TrimSpace(path) path = strings.TrimSpace(path)
@@ -1428,28 +1450,20 @@ func (u *ui) applyRecentRemoteRecord(record recentRemoteRecord) {
u.rememberRemoteAuth.Value = strings.TrimSpace(record.Username) != "" || record.Password != "" u.rememberRemoteAuth.Value = strings.TrimSpace(record.Username) != "" || record.Password != ""
} }
func (u *ui) remotePreferencesCurrentSummary() string { func (u *ui) remoteAuthStatusMessage() string {
selected, hasSelected := u.selectedRecentRemoteRecord() selected, hasSelected := u.selectedRecentRemoteRecord()
switch { switch {
case !u.rememberRemoteAuth.Value: case !u.rememberRemoteAuth.Value:
return "Current choice: KeePassGO will remember only the WebDAV location for this connection." return "Only the location will be saved in Recent Connections."
case hasSelected && (strings.TrimSpace(selected.Username) != "" || selected.Password != ""): case hasSelected && (strings.TrimSpace(selected.Username) != "" || selected.Password != ""):
return "Current choice: a successful open will update the saved sign-in for this connection on this device." return "Saved sign-in will be updated for this connection."
case strings.TrimSpace(u.remoteUsername.Text()) != "" || u.remotePassword.Text() != "": case strings.TrimSpace(u.remoteUsername.Text()) != "" || u.remotePassword.Text() != "":
return "Current choice: a successful open will save the entered sign-in for this connection on this device." return "This sign-in will be saved in Recent Connections after a successful open."
default: default:
return "Current choice: sign-in retention is enabled, but no username or password is entered yet." return "Enter a username or password to save sign-in details for this connection."
} }
} }
func (u *ui) remotePreferencesAlwaysSavedSummary() string {
return "Recent Connections always stores the WebDAV base URL, remote path, and the last group you opened for that connection."
}
func (u *ui) remotePreferencesRetentionSummary() string {
return "KeePassGO keeps up to six recent connections. Turning off Remember sign-in and reopening rewrites that connection without the saved username or password."
}
func (u *ui) noteCurrentRemotePath() { func (u *ui) noteCurrentRemotePath() {
status, ok := u.state.Session.(sessionStatus) status, ok := u.state.Session.(sessionStatus)
if !ok || !status.IsRemote() || status.IsLocked() { if !ok || !status.IsRemote() || status.IsLocked() {
@@ -2450,11 +2464,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
} }
for u.openSecuritySettings.Clicked(gtx) { for u.openSecuritySettings.Clicked(gtx) {
u.loadSecuritySettingsFromSession() u.loadSecuritySettingsFromSession()
u.loadSettingsFormFromPreferences()
u.securityDialogOpen = true u.securityDialogOpen = true
} }
for u.openRemotePrefsHelp.Clicked(gtx) {
u.remotePrefsDialogOpen = true
}
for u.closeAdvancedSync.Clicked(gtx) { for u.closeAdvancedSync.Clicked(gtx) {
u.syncDialogOpen = false u.syncDialogOpen = false
u.showSyncPassword = false u.showSyncPassword = false
@@ -2462,9 +2474,6 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.closeSecuritySettings.Clicked(gtx) { for u.closeSecuritySettings.Clicked(gtx) {
u.securityDialogOpen = false u.securityDialogOpen = false
} }
for u.closeRemotePrefsHelp.Clicked(gtx) {
u.remotePrefsDialogOpen = false
}
for u.runAdvancedSync.Clicked(gtx) { for u.runAdvancedSync.Clicked(gtx) {
u.runAction("advanced synchronize vault", u.advancedSyncAction) u.runAction("advanced synchronize vault", u.advancedSyncAction)
} }
@@ -2870,12 +2879,6 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
} }
return u.securityDialog(gtx) return u.securityDialog(gtx)
}), }),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
if !u.remotePrefsDialogOpen {
return layout.Dimensions{}
}
return u.remotePrefsDialog(gtx)
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions { layout.Stacked(func(gtx layout.Context) layout.Dimensions {
if _, ok := u.pendingApproval(); !ok { if _, ok := u.pendingApproval(); !ok {
return layout.Dimensions{} return layout.Dimensions{}
@@ -2958,43 +2961,55 @@ func (u *ui) securityDialog(gtx layout.Context) layout.Dimensions {
) )
} }
func (u *ui) remotePrefsDialog(gtx layout.Context) layout.Dimensions {
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
paint.FillShape(gtx.Ops, color.NRGBA{A: 90}, clip.Rect{Max: gtx.Constraints.Max}.Op())
return layout.Dimensions{Size: gtx.Constraints.Max}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
width := gtx.Dp(unit.Dp(660))
if width > gtx.Constraints.Max.X {
width = gtx.Constraints.Max.X - gtx.Dp(unit.Dp(24))
}
if width < 1 {
width = gtx.Constraints.Max.X
}
gtx.Constraints.Min.X = width
gtx.Constraints.Max.X = width
return card(gtx, u.remotePrefsDialogContent)
})
}),
)
}
func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions { func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(20), "Security Settings") lbl := material.Label(u.theme, unit.Sp(20), "Settings")
lbl.Color = accentColor lbl.Color = accentColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(14), "Choose the KDBX cipher and KDF family KeePassGO should use for new or future saves.") lbl := material.Label(u.theme, unit.Sp(14), "Choose how KeePassGO remembers UI layout behavior and which KDBX security settings it should use for new or future saves.")
lbl.Color = mutedColor lbl.Color = mutedColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(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 {
check := material.CheckBox(u.theme, &u.settingsGroupControls, "Keep Group Tools collapsed")
return check.Layout(gtx)
}),
layout.Rigid(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 {
check := material.CheckBox(u.theme, &u.settingsHistory, "Keep entry history collapsed")
return check.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
check := material.CheckBox(u.theme, &u.settingsDenseLayout, "Use dense entry layout")
return check.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "Dense layout reduces list and detail spacing. Leave it unchecked for the default comfortable spacing.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
layout.Rigid(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(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(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(labeledEditorHelp(u.theme, "KDF", "Supported values: "+strings.Join([]string{vault.KDFAES, vault.KDFArgon2}, ", "), &u.securityKDF, false)),
@@ -3006,49 +3021,13 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
}), }),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.saveSecuritySettings, "Save Security Settings") return tonedButton(gtx, u.theme, &u.saveSecuritySettings, "Save Settings")
}), }),
) )
}), }),
) )
} }
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 {
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 {
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 {
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 {
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 {
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 {
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 {
return tonedButton(gtx, u.theme, &u.closeRemotePrefsHelp, "Done")
}),
)
}
func (u *ui) approvalDialog(gtx layout.Context) layout.Dimensions { func (u *ui) approvalDialog(gtx layout.Context) layout.Dimensions {
return layout.Stack{}.Layout(gtx, return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions { layout.Expanded(func(gtx layout.Context) layout.Dimensions {
@@ -3355,7 +3334,7 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
layout.Rigid(u.syncButtonGroup), layout.Rigid(u.syncButtonGroup),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.IconButton(u.theme, &u.openSecuritySettings, u.settingsIcon, "Vault settings") btn := material.IconButton(u.theme, &u.openSecuritySettings, u.settingsIcon, "Settings")
btn.Background = selectedColor btn.Background = selectedColor
btn.Color = accentColor btn.Color = accentColor
btn.Size = unit.Dp(18) btn.Size = unit.Dp(18)
@@ -3422,12 +3401,58 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions {
}) })
} }
func (u *ui) sectionSpacing() unit.Dp {
if u.mode == "phone" {
if u.denseLayout {
return unit.Dp(4)
}
return unit.Dp(6)
}
if u.denseLayout {
return unit.Dp(8)
}
return unit.Dp(12)
}
func (u *ui) entryRowMetrics() (unit.Dp, unit.Sp, unit.Sp, unit.Sp, unit.Sp, unit.Dp) {
inset := unit.Dp(12)
titleSize := unit.Sp(17)
metaSize := unit.Sp(14)
urlSize := unit.Sp(12)
pathSize := unit.Sp(11)
dividerGap := unit.Dp(7)
if u.denseLayout {
inset = unit.Dp(9)
titleSize = unit.Sp(15)
metaSize = unit.Sp(12)
urlSize = unit.Sp(11)
pathSize = unit.Sp(10)
dividerGap = unit.Dp(5)
}
if u.mode == "phone" {
inset = unit.Dp(9)
titleSize = unit.Sp(15)
metaSize = unit.Sp(12)
urlSize = unit.Sp(11)
pathSize = unit.Sp(10)
dividerGap = unit.Dp(5)
if u.denseLayout {
inset = unit.Dp(8)
titleSize = unit.Sp(14)
metaSize = unit.Sp(11)
urlSize = unit.Sp(10)
pathSize = unit.Sp(9)
dividerGap = unit.Dp(4)
}
}
return inset, titleSize, metaSize, urlSize, pathSize, dividerGap
}
func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
panel := card panel := card
spacing := unit.Dp(12) spacing := u.sectionSpacing()
if u.mode == "phone" { if u.mode == "phone" {
panel = compactCard panel = compactCard
spacing = unit.Dp(6)
} }
u.ensureNavClickables() u.ensureNavClickables()
return panel(gtx, func(gtx layout.Context) layout.Dimensions { return panel(gtx, func(gtx layout.Context) layout.Dimensions {
@@ -3614,18 +3639,7 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
u.loadSelectedEntryIntoEditor() u.loadSelectedEntryIntoEditor()
} }
return click.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return click.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
inset := unit.Dp(12) inset, titleSize, metaSize, urlSize, pathSize, dividerGap := u.entryRowMetrics()
titleSize := unit.Sp(17)
metaSize := unit.Sp(14)
urlSize := unit.Sp(12)
pathSize := unit.Sp(11)
if u.mode == "phone" {
inset = unit.Dp(9)
titleSize = unit.Sp(15)
metaSize = unit.Sp(12)
urlSize = unit.Sp(11)
pathSize = unit.Sp(10)
}
selected := item.ID == u.state.SelectedEntryID selected := item.ID == u.state.SelectedEntryID
focused := u.isFocused(listFocusID(idx)) focused := u.isFocused(listFocusID(idx))
titleColor := accentColor titleColor := accentColor
@@ -3696,7 +3710,7 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
lbl.Color = secondaryColor lbl.Color = secondaryColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(7)}.Layout), layout.Rigid(layout.Spacer{Height: dividerGap}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
w := gtx.Constraints.Max.X w := gtx.Constraints.Max.X
if w < 1 { if w < 1 {
@@ -3808,7 +3822,7 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
layout.Rigid(u.syncButtonGroup), layout.Rigid(u.syncButtonGroup),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.IconButton(u.theme, &u.openSecuritySettings, u.settingsIcon, "Vault settings") btn := material.IconButton(u.theme, &u.openSecuritySettings, u.settingsIcon, "Settings")
btn.Background = selectedColor btn.Background = selectedColor
btn.Color = accentColor btn.Color = accentColor
btn.Size = unit.Dp(18) btn.Size = unit.Dp(18)
@@ -3947,10 +3961,22 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
titleSize := unit.Sp(26) titleSize := unit.Sp(26)
titlePad := unit.Dp(10) titlePad := unit.Dp(10)
sectionGap := unit.Dp(6) sectionGap := unit.Dp(6)
cardGap := unit.Dp(8)
if u.denseLayout {
titlePad = unit.Dp(6)
sectionGap = unit.Dp(4)
cardGap = unit.Dp(6)
}
if u.mode == "phone" { if u.mode == "phone" {
titleSize = unit.Sp(18) titleSize = unit.Sp(18)
titlePad = unit.Dp(4) titlePad = unit.Dp(4)
sectionGap = unit.Dp(4) sectionGap = unit.Dp(4)
cardGap = unit.Dp(6)
if u.denseLayout {
titlePad = unit.Dp(3)
sectionGap = unit.Dp(3)
cardGap = unit.Dp(4)
}
} }
rows := []layout.Widget{ rows := []layout.Widget{
func(gtx layout.Context) layout.Dimensions { func(gtx layout.Context) layout.Dimensions {
@@ -4068,11 +4094,11 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
) )
}) })
}, },
layout.Spacer{Height: unit.Dp(8)}.Layout, layout.Spacer{Height: cardGap}.Layout,
u.attachmentSummaryPanel, u.attachmentSummaryPanel,
layout.Spacer{Height: unit.Dp(8)}.Layout, layout.Spacer{Height: cardGap}.Layout,
u.historyPanel, u.historyPanel,
layout.Spacer{Height: unit.Dp(8)}.Layout, layout.Spacer{Height: cardGap}.Layout,
func(gtx layout.Context) layout.Dimensions { func(gtx layout.Context) layout.Dimensions {
switch u.state.Section { switch u.state.Section {
case appstate.SectionTemplates: case appstate.SectionTemplates:
+82
View File
@@ -865,6 +865,48 @@ func TestUISaveSecuritySettingsUpdatesExistingVault(t *testing.T) {
} }
} }
func TestUISaveSettingsPersistsUIPreferences(t *testing.T) {
t.Parallel()
dir := t.TempDir()
configPath := filepath.Join(dir, "ui-prefs.json")
u := newUIWithSession("desktop", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
UIPreferencesPath: configPath,
})
u.settingsGroupControls.Value = true
u.settingsLifecycleAdvanced.Value = false
u.settingsHistory.Value = false
u.settingsDenseLayout.Value = true
if err := u.saveSecuritySettingsAction(); err != nil {
t.Fatalf("saveSecuritySettingsAction() error = %v", err)
}
reloaded := newUIWithSession("desktop", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
UIPreferencesPath: configPath,
})
if !reloaded.groupControlsHidden {
t.Fatal("groupControlsHidden after reload = false, want true")
}
if reloaded.lifecycleAdvancedHidden {
t.Fatal("lifecycleAdvancedHidden after reload = true, want false")
}
if reloaded.historyHidden {
t.Fatal("historyHidden after reload = true, want false")
}
if !reloaded.denseLayout {
t.Fatal("denseLayout after reload = false, want true")
}
}
func TestUILockAndUnlockClearMasterPasswordField(t *testing.T) { func TestUILockAndUnlockClearMasterPasswordField(t *testing.T) {
t.Parallel() t.Parallel()
@@ -3420,6 +3462,46 @@ func TestUIGroupToolsDisclosureStatePersists(t *testing.T) {
} }
} }
func TestUIDenseLayoutPreferencePersists(t *testing.T) {
t.Parallel()
configPath := filepath.Join(t.TempDir(), "ui-prefs.json")
first := newUIWithSession("desktop", &session.Manager{})
first.uiPreferencesPath = configPath
first.denseLayout = true
first.saveUIPreferences()
second := newUIWithSession("desktop", &session.Manager{})
second.uiPreferencesPath = configPath
second.denseLayout = false
second.loadUIPreferences()
if !second.denseLayout {
t.Fatal("denseLayout = false after reload, want true")
}
}
func TestUIEntryRowMetricsUseDenseLayout(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
comfortableInset, comfortableTitle, _, _, _, comfortableGap := u.entryRowMetrics()
u.denseLayout = true
denseInset, denseTitle, _, _, _, denseGap := u.entryRowMetrics()
if denseInset >= comfortableInset {
t.Fatalf("dense inset = %v, want smaller than comfortable inset %v", denseInset, comfortableInset)
}
if denseTitle >= comfortableTitle {
t.Fatalf("dense title size = %v, want smaller than comfortable title size %v", denseTitle, comfortableTitle)
}
if denseGap >= comfortableGap {
t.Fatalf("dense divider gap = %v, want smaller than comfortable divider gap %v", denseGap, comfortableGap)
}
}
func TestSelectingRecentRemoteConnectionKeepsPasswordMasked(t *testing.T) { func TestSelectingRecentRemoteConnectionKeepsPasswordMasked(t *testing.T) {
t.Parallel() t.Parallel()