Add persisted UI settings preferences
This commit is contained in:
@@ -141,6 +141,7 @@ type uiPreferences struct {
|
||||
GroupControlsHidden bool `json:"groupControlsHidden"`
|
||||
LifecycleAdvancedHidden bool `json:"lifecycleAdvancedHidden"`
|
||||
HistoryHidden bool `json:"historyHidden"`
|
||||
DenseLayout bool `json:"denseLayout"`
|
||||
}
|
||||
|
||||
type entriesSectionState struct {
|
||||
@@ -165,217 +166,219 @@ const (
|
||||
)
|
||||
|
||||
type ui struct {
|
||||
mode string
|
||||
theme *material.Theme
|
||||
logoHorizontal paint.ImageOp
|
||||
splashSquare paint.ImageOp
|
||||
search widget.Editor
|
||||
vaultPath widget.Editor
|
||||
saveAsPath widget.Editor
|
||||
remoteBaseURL widget.Editor
|
||||
remotePath widget.Editor
|
||||
remoteUsername widget.Editor
|
||||
remotePassword widget.Editor
|
||||
masterPassword widget.Editor
|
||||
keyFilePath widget.Editor
|
||||
apiTokenName widget.Editor
|
||||
apiTokenClientName widget.Editor
|
||||
apiTokenExpiresAt widget.Editor
|
||||
apiPolicyOperation widget.Editor
|
||||
apiPolicyPath widget.Editor
|
||||
apiPolicyEntryID widget.Editor
|
||||
securityCipher widget.Editor
|
||||
securityKDF widget.Editor
|
||||
entryID widget.Editor
|
||||
entryTitle widget.Editor
|
||||
entryUsername widget.Editor
|
||||
entryPassword widget.Editor
|
||||
entryURL widget.Editor
|
||||
entryNotes widget.Editor
|
||||
entryTags widget.Editor
|
||||
entryPath widget.Editor
|
||||
entryFields widget.Editor
|
||||
customFieldKeys []widget.Editor
|
||||
customFieldValues []widget.Editor
|
||||
historyIndex widget.Editor
|
||||
groupName widget.Editor
|
||||
groupParentPath widget.Editor
|
||||
passwordProfile widget.Editor
|
||||
attachmentName widget.Editor
|
||||
attachmentPath widget.Editor
|
||||
exportAttachmentPath widget.Editor
|
||||
list widget.List
|
||||
groupList widget.List
|
||||
detailList widget.List
|
||||
apiPolicyList widget.List
|
||||
lifecycleList widget.List
|
||||
copyUser widget.Clickable
|
||||
copyPass widget.Clickable
|
||||
copyURL widget.Clickable
|
||||
lockVault widget.Clickable
|
||||
unlockVault widget.Clickable
|
||||
createVault widget.Clickable
|
||||
openVault widget.Clickable
|
||||
saveVault widget.Clickable
|
||||
saveAsVault widget.Clickable
|
||||
openRemote widget.Clickable
|
||||
changeMasterKey widget.Clickable
|
||||
synchronizeVault widget.Clickable
|
||||
toggleSyncMenu widget.Clickable
|
||||
openAdvancedSync widget.Clickable
|
||||
openSecuritySettings widget.Clickable
|
||||
openRemotePrefsHelp widget.Clickable
|
||||
closeAdvancedSync widget.Clickable
|
||||
closeSecuritySettings widget.Clickable
|
||||
closeRemotePrefsHelp widget.Clickable
|
||||
runAdvancedSync widget.Clickable
|
||||
saveSecuritySettings widget.Clickable
|
||||
editEntry widget.Clickable
|
||||
cancelEdit widget.Clickable
|
||||
pickVaultPath widget.Clickable
|
||||
pickKeyFile widget.Clickable
|
||||
pickSyncLocalPath widget.Clickable
|
||||
clearVaultSelection widget.Clickable
|
||||
clearRemoteSelection widget.Clickable
|
||||
dismissBanner widget.Clickable
|
||||
addEntry widget.Clickable
|
||||
saveEntry widget.Clickable
|
||||
duplicateEntry widget.Clickable
|
||||
deleteEntry widget.Clickable
|
||||
restoreEntry widget.Clickable
|
||||
saveTemplate widget.Clickable
|
||||
deleteTemplate widget.Clickable
|
||||
instantiateTemplate widget.Clickable
|
||||
addAttachment widget.Clickable
|
||||
replaceAttachment widget.Clickable
|
||||
removeAttachment widget.Clickable
|
||||
exportAttachment widget.Clickable
|
||||
restoreHistory widget.Clickable
|
||||
generatePassword widget.Clickable
|
||||
goToRootGroup widget.Clickable
|
||||
goToParentGroup widget.Clickable
|
||||
createGroup widget.Clickable
|
||||
moveGroup widget.Clickable
|
||||
renameGroup widget.Clickable
|
||||
deleteGroup widget.Clickable
|
||||
confirmDeleteGroup widget.Clickable
|
||||
cancelDeleteGroup widget.Clickable
|
||||
addCustomField widget.Clickable
|
||||
toggleGroupControls widget.Clickable
|
||||
toggleLifecycleAdvanced widget.Clickable
|
||||
toggleHistory widget.Clickable
|
||||
togglePasswordInline widget.Clickable
|
||||
toggleSyncPassword widget.Clickable
|
||||
showEntries widget.Clickable
|
||||
showTemplates widget.Clickable
|
||||
showRecycle widget.Clickable
|
||||
showAPITokens widget.Clickable
|
||||
showAPIAudit widget.Clickable
|
||||
showLocalLifecycle widget.Clickable
|
||||
showRemoteLifecycle widget.Clickable
|
||||
showSyncLocal widget.Clickable
|
||||
showSyncRemote widget.Clickable
|
||||
showSyncPull widget.Clickable
|
||||
showSyncPush widget.Clickable
|
||||
allowApproval widget.Clickable
|
||||
denyApproval widget.Clickable
|
||||
cancelApproval widget.Clickable
|
||||
cancelLifecycleProgress widget.Clickable
|
||||
retryLifecycleOpen widget.Clickable
|
||||
approvalPermanent widget.Bool
|
||||
rememberRemoteAuth widget.Bool
|
||||
apiPolicyAllow widget.Bool
|
||||
apiPolicyGroupScopeW widget.Bool
|
||||
apiTokenDisabled widget.Bool
|
||||
entryClicks []widget.Clickable
|
||||
apiTokenClicks []widget.Clickable
|
||||
apiPolicyRemoves []widget.Clickable
|
||||
apiAuditClicks []widget.Clickable
|
||||
apiAuditTokenFilters []widget.Clickable
|
||||
apiAuditDecisionFilters []widget.Clickable
|
||||
apiAuditOperationFilters []widget.Clickable
|
||||
clearAPIAuditFilters widget.Clickable
|
||||
historyClicks []widget.Clickable
|
||||
attachmentClicks []widget.Clickable
|
||||
breadcrumbs []widget.Clickable
|
||||
groupClicks []widget.Clickable
|
||||
recentVaultClicks []widget.Clickable
|
||||
recentRemoteClicks []widget.Clickable
|
||||
removeCustomFields []widget.Clickable
|
||||
state appstate.State
|
||||
visible []entry
|
||||
currentPath []string
|
||||
syncedPath []string
|
||||
selectedHistoryIndex int
|
||||
showPassword bool
|
||||
generatedPasswordDraft bool
|
||||
togglePassword widget.Clickable
|
||||
copyAPITokenSecret widget.Clickable
|
||||
issueAPIToken widget.Clickable
|
||||
saveAPIToken widget.Clickable
|
||||
rotateAPIToken widget.Clickable
|
||||
disableAPIToken widget.Clickable
|
||||
revokeAPIToken widget.Clickable
|
||||
deleteAPIToken widget.Clickable
|
||||
addAPIPolicyRule widget.Clickable
|
||||
phoneSplit widget.Float
|
||||
splitDrag gesture.Drag
|
||||
splitBase float32
|
||||
splitStartY float32
|
||||
phoneSpan int
|
||||
eyeIcon *widget.Icon
|
||||
eyeOffIcon *widget.Icon
|
||||
copyIcon *widget.Icon
|
||||
expandMoreIcon *widget.Icon
|
||||
expandLessIcon *widget.Icon
|
||||
chevronDownIcon *widget.Icon
|
||||
settingsIcon *widget.Icon
|
||||
clipboardWriter clipboard.Writer
|
||||
loadingMessage string
|
||||
loadingActionLabel string
|
||||
lifecycleMode string
|
||||
syncSourceMode syncSourceMode
|
||||
syncDirection syncDirection
|
||||
syncLocalPath widget.Editor
|
||||
syncRemoteBaseURL widget.Editor
|
||||
syncRemotePath widget.Editor
|
||||
syncRemoteUsername widget.Editor
|
||||
syncRemotePassword widget.Editor
|
||||
syncDialogOpen bool
|
||||
syncMenuOpen bool
|
||||
securityDialogOpen bool
|
||||
remotePrefsDialogOpen bool
|
||||
showSyncPassword bool
|
||||
keyboardFocus focusID
|
||||
defaultSaveAsPath string
|
||||
recentVaultsPath string
|
||||
uiPreferencesPath string
|
||||
recentRemotesPath string
|
||||
autofillCachePath string
|
||||
editingEntry bool
|
||||
groupControlsHidden bool
|
||||
lifecycleAdvancedHidden bool
|
||||
historyHidden bool
|
||||
recentVaults []string
|
||||
recentRemotes []recentRemoteRecord
|
||||
recentVaultGroups map[string][]string
|
||||
recentVaultUsedAt map[string]time.Time
|
||||
entriesState entriesSectionState
|
||||
deleteGroupPath []string
|
||||
apiPolicyGroupScope bool
|
||||
apiTokenSecret string
|
||||
selectedAuditIndex int
|
||||
statusExpiresAt time.Time
|
||||
now func() time.Time
|
||||
apiHost *api.Host
|
||||
auditLog *apiaudit.Log
|
||||
grpcAddress string
|
||||
backgroundResults chan backgroundActionResult
|
||||
backgroundActionSerial int
|
||||
activeBackgroundAction int
|
||||
lastLifecycleAction string
|
||||
requestMasterPassFocus bool
|
||||
invalidate func()
|
||||
mode string
|
||||
theme *material.Theme
|
||||
logoHorizontal paint.ImageOp
|
||||
splashSquare paint.ImageOp
|
||||
search widget.Editor
|
||||
vaultPath widget.Editor
|
||||
saveAsPath widget.Editor
|
||||
remoteBaseURL widget.Editor
|
||||
remotePath widget.Editor
|
||||
remoteUsername widget.Editor
|
||||
remotePassword widget.Editor
|
||||
masterPassword widget.Editor
|
||||
keyFilePath widget.Editor
|
||||
apiTokenName widget.Editor
|
||||
apiTokenClientName widget.Editor
|
||||
apiTokenExpiresAt widget.Editor
|
||||
apiPolicyOperation widget.Editor
|
||||
apiPolicyPath widget.Editor
|
||||
apiPolicyEntryID widget.Editor
|
||||
securityCipher widget.Editor
|
||||
securityKDF widget.Editor
|
||||
entryID widget.Editor
|
||||
entryTitle widget.Editor
|
||||
entryUsername widget.Editor
|
||||
entryPassword widget.Editor
|
||||
entryURL widget.Editor
|
||||
entryNotes widget.Editor
|
||||
entryTags widget.Editor
|
||||
entryPath widget.Editor
|
||||
entryFields widget.Editor
|
||||
customFieldKeys []widget.Editor
|
||||
customFieldValues []widget.Editor
|
||||
historyIndex widget.Editor
|
||||
groupName widget.Editor
|
||||
groupParentPath widget.Editor
|
||||
passwordProfile widget.Editor
|
||||
attachmentName widget.Editor
|
||||
attachmentPath widget.Editor
|
||||
exportAttachmentPath widget.Editor
|
||||
list widget.List
|
||||
groupList widget.List
|
||||
detailList widget.List
|
||||
apiPolicyList widget.List
|
||||
lifecycleList widget.List
|
||||
copyUser widget.Clickable
|
||||
copyPass widget.Clickable
|
||||
copyURL widget.Clickable
|
||||
lockVault widget.Clickable
|
||||
unlockVault widget.Clickable
|
||||
createVault widget.Clickable
|
||||
openVault widget.Clickable
|
||||
saveVault widget.Clickable
|
||||
saveAsVault widget.Clickable
|
||||
openRemote widget.Clickable
|
||||
changeMasterKey widget.Clickable
|
||||
synchronizeVault widget.Clickable
|
||||
toggleSyncMenu widget.Clickable
|
||||
openAdvancedSync widget.Clickable
|
||||
openSecuritySettings widget.Clickable
|
||||
closeAdvancedSync widget.Clickable
|
||||
closeSecuritySettings widget.Clickable
|
||||
runAdvancedSync widget.Clickable
|
||||
saveSecuritySettings widget.Clickable
|
||||
editEntry widget.Clickable
|
||||
cancelEdit widget.Clickable
|
||||
pickVaultPath widget.Clickable
|
||||
pickKeyFile widget.Clickable
|
||||
pickSyncLocalPath widget.Clickable
|
||||
clearVaultSelection widget.Clickable
|
||||
clearRemoteSelection widget.Clickable
|
||||
dismissBanner widget.Clickable
|
||||
addEntry widget.Clickable
|
||||
saveEntry widget.Clickable
|
||||
duplicateEntry widget.Clickable
|
||||
deleteEntry widget.Clickable
|
||||
restoreEntry widget.Clickable
|
||||
saveTemplate widget.Clickable
|
||||
deleteTemplate widget.Clickable
|
||||
instantiateTemplate widget.Clickable
|
||||
addAttachment widget.Clickable
|
||||
replaceAttachment widget.Clickable
|
||||
removeAttachment widget.Clickable
|
||||
exportAttachment widget.Clickable
|
||||
restoreHistory widget.Clickable
|
||||
generatePassword widget.Clickable
|
||||
goToRootGroup widget.Clickable
|
||||
goToParentGroup widget.Clickable
|
||||
createGroup widget.Clickable
|
||||
moveGroup widget.Clickable
|
||||
renameGroup widget.Clickable
|
||||
deleteGroup widget.Clickable
|
||||
confirmDeleteGroup widget.Clickable
|
||||
cancelDeleteGroup widget.Clickable
|
||||
addCustomField widget.Clickable
|
||||
toggleGroupControls widget.Clickable
|
||||
toggleLifecycleAdvanced widget.Clickable
|
||||
toggleHistory widget.Clickable
|
||||
togglePasswordInline widget.Clickable
|
||||
toggleSyncPassword widget.Clickable
|
||||
showEntries widget.Clickable
|
||||
showTemplates widget.Clickable
|
||||
showRecycle widget.Clickable
|
||||
showAPITokens widget.Clickable
|
||||
showAPIAudit widget.Clickable
|
||||
showLocalLifecycle widget.Clickable
|
||||
showRemoteLifecycle widget.Clickable
|
||||
showSyncLocal widget.Clickable
|
||||
showSyncRemote widget.Clickable
|
||||
showSyncPull widget.Clickable
|
||||
showSyncPush widget.Clickable
|
||||
allowApproval widget.Clickable
|
||||
denyApproval widget.Clickable
|
||||
cancelApproval widget.Clickable
|
||||
cancelLifecycleProgress widget.Clickable
|
||||
retryLifecycleOpen widget.Clickable
|
||||
approvalPermanent widget.Bool
|
||||
rememberRemoteAuth widget.Bool
|
||||
apiPolicyAllow widget.Bool
|
||||
apiPolicyGroupScopeW widget.Bool
|
||||
apiTokenDisabled widget.Bool
|
||||
settingsGroupControls widget.Bool
|
||||
settingsLifecycleAdvanced widget.Bool
|
||||
settingsHistory widget.Bool
|
||||
settingsDenseLayout widget.Bool
|
||||
entryClicks []widget.Clickable
|
||||
apiTokenClicks []widget.Clickable
|
||||
apiPolicyRemoves []widget.Clickable
|
||||
apiAuditClicks []widget.Clickable
|
||||
apiAuditTokenFilters []widget.Clickable
|
||||
apiAuditDecisionFilters []widget.Clickable
|
||||
apiAuditOperationFilters []widget.Clickable
|
||||
clearAPIAuditFilters widget.Clickable
|
||||
historyClicks []widget.Clickable
|
||||
attachmentClicks []widget.Clickable
|
||||
breadcrumbs []widget.Clickable
|
||||
groupClicks []widget.Clickable
|
||||
recentVaultClicks []widget.Clickable
|
||||
recentRemoteClicks []widget.Clickable
|
||||
removeCustomFields []widget.Clickable
|
||||
state appstate.State
|
||||
visible []entry
|
||||
currentPath []string
|
||||
syncedPath []string
|
||||
selectedHistoryIndex int
|
||||
showPassword bool
|
||||
generatedPasswordDraft bool
|
||||
togglePassword widget.Clickable
|
||||
copyAPITokenSecret widget.Clickable
|
||||
issueAPIToken widget.Clickable
|
||||
saveAPIToken widget.Clickable
|
||||
rotateAPIToken widget.Clickable
|
||||
disableAPIToken widget.Clickable
|
||||
revokeAPIToken widget.Clickable
|
||||
deleteAPIToken widget.Clickable
|
||||
addAPIPolicyRule widget.Clickable
|
||||
phoneSplit widget.Float
|
||||
splitDrag gesture.Drag
|
||||
splitBase float32
|
||||
splitStartY float32
|
||||
phoneSpan int
|
||||
eyeIcon *widget.Icon
|
||||
eyeOffIcon *widget.Icon
|
||||
copyIcon *widget.Icon
|
||||
expandMoreIcon *widget.Icon
|
||||
expandLessIcon *widget.Icon
|
||||
chevronDownIcon *widget.Icon
|
||||
settingsIcon *widget.Icon
|
||||
clipboardWriter clipboard.Writer
|
||||
loadingMessage string
|
||||
loadingActionLabel string
|
||||
lifecycleMode string
|
||||
syncSourceMode syncSourceMode
|
||||
syncDirection syncDirection
|
||||
syncLocalPath widget.Editor
|
||||
syncRemoteBaseURL widget.Editor
|
||||
syncRemotePath widget.Editor
|
||||
syncRemoteUsername widget.Editor
|
||||
syncRemotePassword widget.Editor
|
||||
syncDialogOpen bool
|
||||
syncMenuOpen bool
|
||||
securityDialogOpen bool
|
||||
showSyncPassword bool
|
||||
keyboardFocus focusID
|
||||
defaultSaveAsPath string
|
||||
recentVaultsPath string
|
||||
uiPreferencesPath string
|
||||
recentRemotesPath string
|
||||
autofillCachePath string
|
||||
editingEntry bool
|
||||
groupControlsHidden bool
|
||||
lifecycleAdvancedHidden bool
|
||||
historyHidden bool
|
||||
denseLayout bool
|
||||
recentVaults []string
|
||||
recentRemotes []recentRemoteRecord
|
||||
recentVaultGroups map[string][]string
|
||||
recentVaultUsedAt map[string]time.Time
|
||||
entriesState entriesSectionState
|
||||
deleteGroupPath []string
|
||||
apiPolicyGroupScope bool
|
||||
apiTokenSecret string
|
||||
selectedAuditIndex int
|
||||
statusExpiresAt time.Time
|
||||
now func() time.Time
|
||||
apiHost *api.Host
|
||||
auditLog *apiaudit.Log
|
||||
grpcAddress string
|
||||
backgroundResults chan backgroundActionResult
|
||||
backgroundActionSerial int
|
||||
activeBackgroundAction int
|
||||
lastLifecycleAction string
|
||||
requestMasterPassFocus bool
|
||||
invalidate func()
|
||||
}
|
||||
|
||||
type backgroundActionResult struct {
|
||||
@@ -519,6 +522,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
u.loadRecentRemotes()
|
||||
u.restoreStartupLifecycleTarget()
|
||||
u.loadUIPreferences()
|
||||
u.loadSettingsFormFromPreferences()
|
||||
u.filter()
|
||||
u.syncAutofillCache()
|
||||
return u
|
||||
@@ -1016,6 +1020,8 @@ func (u *ui) saveSecuritySettingsAction() error {
|
||||
if err := u.state.ConfigureSecurity(settings); err != nil {
|
||||
return err
|
||||
}
|
||||
u.applySettingsFormToPreferences()
|
||||
u.saveUIPreferences()
|
||||
u.securityDialogOpen = false
|
||||
return nil
|
||||
}
|
||||
@@ -1292,6 +1298,7 @@ func (u *ui) loadUIPreferences() {
|
||||
u.groupControlsHidden = prefs.GroupControlsHidden
|
||||
u.lifecycleAdvancedHidden = prefs.LifecycleAdvancedHidden
|
||||
u.historyHidden = prefs.HistoryHidden
|
||||
u.denseLayout = prefs.DenseLayout
|
||||
}
|
||||
|
||||
func (u *ui) saveUIPreferences() {
|
||||
@@ -1305,6 +1312,7 @@ func (u *ui) saveUIPreferences() {
|
||||
GroupControlsHidden: u.groupControlsHidden,
|
||||
LifecycleAdvancedHidden: u.lifecycleAdvancedHidden,
|
||||
HistoryHidden: u.historyHidden,
|
||||
DenseLayout: u.denseLayout,
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
return
|
||||
@@ -1312,6 +1320,20 @@ func (u *ui) saveUIPreferences() {
|
||||
_ = 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) {
|
||||
baseURL = strings.TrimSpace(baseURL)
|
||||
path = strings.TrimSpace(path)
|
||||
@@ -1428,28 +1450,20 @@ func (u *ui) applyRecentRemoteRecord(record recentRemoteRecord) {
|
||||
u.rememberRemoteAuth.Value = strings.TrimSpace(record.Username) != "" || record.Password != ""
|
||||
}
|
||||
|
||||
func (u *ui) remotePreferencesCurrentSummary() string {
|
||||
func (u *ui) remoteAuthStatusMessage() string {
|
||||
selected, hasSelected := u.selectedRecentRemoteRecord()
|
||||
switch {
|
||||
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 != ""):
|
||||
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() != "":
|
||||
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:
|
||||
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() {
|
||||
status, ok := u.state.Session.(sessionStatus)
|
||||
if !ok || !status.IsRemote() || status.IsLocked() {
|
||||
@@ -2450,11 +2464,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
for u.openSecuritySettings.Clicked(gtx) {
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.loadSettingsFormFromPreferences()
|
||||
u.securityDialogOpen = true
|
||||
}
|
||||
for u.openRemotePrefsHelp.Clicked(gtx) {
|
||||
u.remotePrefsDialogOpen = true
|
||||
}
|
||||
for u.closeAdvancedSync.Clicked(gtx) {
|
||||
u.syncDialogOpen = false
|
||||
u.showSyncPassword = false
|
||||
@@ -2462,9 +2474,6 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
for u.closeSecuritySettings.Clicked(gtx) {
|
||||
u.securityDialogOpen = false
|
||||
}
|
||||
for u.closeRemotePrefsHelp.Clicked(gtx) {
|
||||
u.remotePrefsDialogOpen = false
|
||||
}
|
||||
for u.runAdvancedSync.Clicked(gtx) {
|
||||
u.runAction("advanced synchronize vault", u.advancedSyncAction)
|
||||
}
|
||||
@@ -2870,12 +2879,6 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
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 {
|
||||
if _, ok := u.pendingApproval(); !ok {
|
||||
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 {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
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
|
||||
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), "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
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
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(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)),
|
||||
@@ -3006,49 +3021,13 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
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 {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
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(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
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.Color = accentColor
|
||||
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 {
|
||||
panel := card
|
||||
spacing := unit.Dp(12)
|
||||
spacing := u.sectionSpacing()
|
||||
if u.mode == "phone" {
|
||||
panel = compactCard
|
||||
spacing = unit.Dp(6)
|
||||
}
|
||||
u.ensureNavClickables()
|
||||
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()
|
||||
}
|
||||
return click.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
inset := unit.Dp(12)
|
||||
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)
|
||||
}
|
||||
inset, titleSize, metaSize, urlSize, pathSize, dividerGap := u.entryRowMetrics()
|
||||
selected := item.ID == u.state.SelectedEntryID
|
||||
focused := u.isFocused(listFocusID(idx))
|
||||
titleColor := accentColor
|
||||
@@ -3696,7 +3710,7 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
|
||||
lbl.Color = secondaryColor
|
||||
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 {
|
||||
w := gtx.Constraints.Max.X
|
||||
if w < 1 {
|
||||
@@ -3808,7 +3822,7 @@ 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, "Vault settings")
|
||||
btn := material.IconButton(u.theme, &u.openSecuritySettings, u.settingsIcon, "Settings")
|
||||
btn.Background = selectedColor
|
||||
btn.Color = accentColor
|
||||
btn.Size = unit.Dp(18)
|
||||
@@ -3947,10 +3961,22 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
|
||||
titleSize := unit.Sp(26)
|
||||
titlePad := unit.Dp(10)
|
||||
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" {
|
||||
titleSize = unit.Sp(18)
|
||||
titlePad = 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{
|
||||
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,
|
||||
layout.Spacer{Height: unit.Dp(8)}.Layout,
|
||||
layout.Spacer{Height: cardGap}.Layout,
|
||||
u.historyPanel,
|
||||
layout.Spacer{Height: unit.Dp(8)}.Layout,
|
||||
layout.Spacer{Height: cardGap}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
switch u.state.Section {
|
||||
case appstate.SectionTemplates:
|
||||
|
||||
@@ -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) {
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user