Add accessibility settings preferences
This commit is contained in:
@@ -153,8 +153,28 @@ type uiPreferences struct {
|
|||||||
DenseLayout bool `json:"denseLayout"`
|
DenseLayout bool `json:"denseLayout"`
|
||||||
StatusBannerMillis int `json:"statusBannerMillis,omitempty"`
|
StatusBannerMillis int `json:"statusBannerMillis,omitempty"`
|
||||||
AutofillNoticeMode string `json:"autofillNoticeMode,omitempty"`
|
AutofillNoticeMode string `json:"autofillNoticeMode,omitempty"`
|
||||||
|
DisplayDensity string `json:"displayDensity,omitempty"`
|
||||||
|
Contrast string `json:"contrast,omitempty"`
|
||||||
|
ReducedMotion bool `json:"reducedMotion,omitempty"`
|
||||||
|
KeyboardFocus string `json:"keyboardFocus,omitempty"`
|
||||||
|
AutofillPrivacy autofillPrivacySettings `json:"autofillPrivacy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type autofillPrivacySettings struct {
|
||||||
|
FirstFillApprovalMode string `json:"firstFillApprovalMode,omitempty"`
|
||||||
|
BrowserAllowlist []string `json:"browserAllowlist,omitempty"`
|
||||||
|
AppAllowlist []string `json:"appAllowlist,omitempty"`
|
||||||
|
PackageRules []string `json:"packageRules,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type autofillFirstFillApprovalMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
autofillFirstFillApprovalAsk autofillFirstFillApprovalMode = "ask"
|
||||||
|
autofillFirstFillApprovalAllow autofillFirstFillApprovalMode = "allow"
|
||||||
|
autofillFirstFillApprovalBlock autofillFirstFillApprovalMode = "block"
|
||||||
|
)
|
||||||
|
|
||||||
type entriesSectionState struct {
|
type entriesSectionState struct {
|
||||||
Path []string
|
Path []string
|
||||||
SearchQuery string
|
SearchQuery string
|
||||||
@@ -216,6 +236,9 @@ type ui struct {
|
|||||||
attachmentName widget.Editor
|
attachmentName widget.Editor
|
||||||
attachmentPath widget.Editor
|
attachmentPath widget.Editor
|
||||||
exportAttachmentPath widget.Editor
|
exportAttachmentPath widget.Editor
|
||||||
|
autofillBrowserAllowlist widget.Editor
|
||||||
|
autofillAppAllowlist widget.Editor
|
||||||
|
autofillPackageRules widget.Editor
|
||||||
list widget.List
|
list widget.List
|
||||||
groupList widget.List
|
groupList widget.List
|
||||||
detailList widget.List
|
detailList widget.List
|
||||||
@@ -240,6 +263,14 @@ type ui struct {
|
|||||||
closeSecuritySettings widget.Clickable
|
closeSecuritySettings widget.Clickable
|
||||||
runAdvancedSync widget.Clickable
|
runAdvancedSync widget.Clickable
|
||||||
saveSecuritySettings widget.Clickable
|
saveSecuritySettings widget.Clickable
|
||||||
|
settingsDensityDense widget.Clickable
|
||||||
|
settingsDensityComfortable widget.Clickable
|
||||||
|
settingsContrastStandard widget.Clickable
|
||||||
|
settingsContrastHigh widget.Clickable
|
||||||
|
settingsReducedMotionOff widget.Clickable
|
||||||
|
settingsReducedMotionOn widget.Clickable
|
||||||
|
settingsKeyboardFocusStandard widget.Clickable
|
||||||
|
settingsKeyboardFocusProminent widget.Clickable
|
||||||
editEntry widget.Clickable
|
editEntry widget.Clickable
|
||||||
cancelEdit widget.Clickable
|
cancelEdit widget.Clickable
|
||||||
pickVaultPath widget.Clickable
|
pickVaultPath widget.Clickable
|
||||||
@@ -293,6 +324,9 @@ type ui struct {
|
|||||||
showSyncRemote widget.Clickable
|
showSyncRemote widget.Clickable
|
||||||
showSyncPull widget.Clickable
|
showSyncPull widget.Clickable
|
||||||
showSyncPush widget.Clickable
|
showSyncPush widget.Clickable
|
||||||
|
showAutofillApprovalAsk widget.Clickable
|
||||||
|
showAutofillApprovalAllow widget.Clickable
|
||||||
|
showAutofillApprovalBlock widget.Clickable
|
||||||
allowApproval widget.Clickable
|
allowApproval widget.Clickable
|
||||||
denyApproval widget.Clickable
|
denyApproval widget.Clickable
|
||||||
cancelApproval widget.Clickable
|
cancelApproval widget.Clickable
|
||||||
@@ -378,6 +412,9 @@ type ui struct {
|
|||||||
denseLayout bool
|
denseLayout bool
|
||||||
statusBannerTTL time.Duration
|
statusBannerTTL time.Duration
|
||||||
autofillNoticePreference autofillNoticeMode
|
autofillNoticePreference autofillNoticeMode
|
||||||
|
autofillFirstFillApprovalMode autofillFirstFillApprovalMode
|
||||||
|
accessibilityPrefs accessibilityPreferences
|
||||||
|
settingsDraft settingsDraft
|
||||||
recentVaults []string
|
recentVaults []string
|
||||||
recentRemotes []recentRemoteRecord
|
recentRemotes []recentRemoteRecord
|
||||||
recentVaultGroups map[string][]string
|
recentVaultGroups map[string][]string
|
||||||
@@ -486,6 +523,9 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
|||||||
attachmentName: widget.Editor{SingleLine: true, Submit: false},
|
attachmentName: widget.Editor{SingleLine: true, Submit: false},
|
||||||
attachmentPath: widget.Editor{SingleLine: true, Submit: false},
|
attachmentPath: widget.Editor{SingleLine: true, Submit: false},
|
||||||
exportAttachmentPath: widget.Editor{SingleLine: true, Submit: false},
|
exportAttachmentPath: widget.Editor{SingleLine: true, Submit: false},
|
||||||
|
autofillBrowserAllowlist: widget.Editor{SingleLine: false, Submit: false},
|
||||||
|
autofillAppAllowlist: widget.Editor{SingleLine: false, Submit: false},
|
||||||
|
autofillPackageRules: widget.Editor{SingleLine: false, Submit: false},
|
||||||
list: widget.List{
|
list: widget.List{
|
||||||
List: layout.List{Axis: layout.Vertical},
|
List: layout.List{Axis: layout.Vertical},
|
||||||
},
|
},
|
||||||
@@ -515,6 +555,8 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
|||||||
lifecycleAdvancedHidden: true,
|
lifecycleAdvancedHidden: true,
|
||||||
historyHidden: true,
|
historyHidden: true,
|
||||||
statusBannerTTL: statusBannerDuration,
|
statusBannerTTL: statusBannerDuration,
|
||||||
|
accessibilityPrefs: defaultAccessibilityPreferences(),
|
||||||
|
autofillFirstFillApprovalMode: autofillFirstFillApprovalAsk,
|
||||||
now: time.Now,
|
now: time.Now,
|
||||||
syncSourceMode: syncSourceLocal,
|
syncSourceMode: syncSourceLocal,
|
||||||
syncDirection: syncDirectionPull,
|
syncDirection: syncDirectionPull,
|
||||||
@@ -544,6 +586,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
|||||||
u.restoreStartupLifecycleTarget()
|
u.restoreStartupLifecycleTarget()
|
||||||
u.loadUIPreferences()
|
u.loadUIPreferences()
|
||||||
u.loadSettingsFormFromPreferences()
|
u.loadSettingsFormFromPreferences()
|
||||||
|
u.loadSettingsDraft()
|
||||||
u.filter()
|
u.filter()
|
||||||
u.syncAutofillCache()
|
u.syncAutofillCache()
|
||||||
return u
|
return u
|
||||||
@@ -1033,20 +1076,6 @@ func (u *ui) loadSecuritySettingsFromSession() {
|
|||||||
u.securityKDF.SetText(settings.KDF)
|
u.securityKDF.SetText(settings.KDF)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) saveSecuritySettingsAction() error {
|
|
||||||
settings := vault.SecuritySettings{
|
|
||||||
Cipher: strings.TrimSpace(u.securityCipher.Text()),
|
|
||||||
KDF: strings.TrimSpace(u.securityKDF.Text()),
|
|
||||||
}
|
|
||||||
if err := u.state.ConfigureSecurity(settings); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
u.applySettingsFormToPreferences()
|
|
||||||
u.saveUIPreferences()
|
|
||||||
u.securityDialogOpen = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ui) clearMasterPassword() {
|
func (u *ui) clearMasterPassword() {
|
||||||
u.masterPassword.SetText("")
|
u.masterPassword.SetText("")
|
||||||
}
|
}
|
||||||
@@ -1322,6 +1351,22 @@ func (u *ui) loadUIPreferences() {
|
|||||||
u.denseLayout = prefs.DenseLayout
|
u.denseLayout = prefs.DenseLayout
|
||||||
u.statusBannerTTL = normalizedStatusBannerTTL(prefs.StatusBannerMillis)
|
u.statusBannerTTL = normalizedStatusBannerTTL(prefs.StatusBannerMillis)
|
||||||
u.autofillNoticePreference = normalizedAutofillNoticeMode(prefs.AutofillNoticeMode)
|
u.autofillNoticePreference = normalizedAutofillNoticeMode(prefs.AutofillNoticeMode)
|
||||||
|
displayDensity := strings.TrimSpace(prefs.DisplayDensity)
|
||||||
|
if displayDensity == "" {
|
||||||
|
displayDensity = displayDensityForDenseLayout(prefs.DenseLayout)
|
||||||
|
}
|
||||||
|
u.applyAccessibilityPreferences(accessibilityPreferences{
|
||||||
|
DisplayDensity: displayDensity,
|
||||||
|
Contrast: prefs.Contrast,
|
||||||
|
ReducedMotion: prefs.ReducedMotion,
|
||||||
|
KeyboardFocus: prefs.KeyboardFocus,
|
||||||
|
})
|
||||||
|
if mode := parseAutofillFirstFillApprovalMode(prefs.AutofillPrivacy.FirstFillApprovalMode); mode != "" {
|
||||||
|
u.autofillFirstFillApprovalMode = mode
|
||||||
|
}
|
||||||
|
u.autofillBrowserAllowlist.SetText(joinAutofillPrivacyLines(prefs.AutofillPrivacy.BrowserAllowlist))
|
||||||
|
u.autofillAppAllowlist.SetText(joinAutofillPrivacyLines(prefs.AutofillPrivacy.AppAllowlist))
|
||||||
|
u.autofillPackageRules.SetText(joinAutofillPrivacyLines(prefs.AutofillPrivacy.PackageRules))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) saveUIPreferences() {
|
func (u *ui) saveUIPreferences() {
|
||||||
@@ -1338,6 +1383,16 @@ func (u *ui) saveUIPreferences() {
|
|||||||
DenseLayout: u.denseLayout,
|
DenseLayout: u.denseLayout,
|
||||||
StatusBannerMillis: int(u.statusBannerTTL / time.Millisecond),
|
StatusBannerMillis: int(u.statusBannerTTL / time.Millisecond),
|
||||||
AutofillNoticeMode: string(u.autofillNoticePreference),
|
AutofillNoticeMode: string(u.autofillNoticePreference),
|
||||||
|
DisplayDensity: u.accessibilityPrefs.DisplayDensity,
|
||||||
|
Contrast: u.accessibilityPrefs.Contrast,
|
||||||
|
ReducedMotion: u.accessibilityPrefs.ReducedMotion,
|
||||||
|
KeyboardFocus: u.accessibilityPrefs.KeyboardFocus,
|
||||||
|
AutofillPrivacy: autofillPrivacySettings{
|
||||||
|
FirstFillApprovalMode: string(u.autofillFirstFillApprovalMode),
|
||||||
|
BrowserAllowlist: autofillPrivacyLines(u.autofillBrowserAllowlist.Text()),
|
||||||
|
AppAllowlist: autofillPrivacyLines(u.autofillAppAllowlist.Text()),
|
||||||
|
PackageRules: autofillPrivacyLines(u.autofillPackageRules.Text()),
|
||||||
|
},
|
||||||
}, "", " ")
|
}, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -1381,6 +1436,57 @@ func normalizedAutofillNoticeMode(value string) autofillNoticeMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseAutofillFirstFillApprovalMode(raw string) autofillFirstFillApprovalMode {
|
||||||
|
switch autofillFirstFillApprovalMode(strings.TrimSpace(raw)) {
|
||||||
|
case autofillFirstFillApprovalAsk, autofillFirstFillApprovalAllow, autofillFirstFillApprovalBlock:
|
||||||
|
return autofillFirstFillApprovalMode(strings.TrimSpace(raw))
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func autofillPrivacyLines(text string) []string {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
result := make([]string, 0, len(lines))
|
||||||
|
seen := make(map[string]struct{}, len(lines))
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[line]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[line] = struct{}{}
|
||||||
|
result = append(result, line)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinAutofillPrivacyLines(lines []string) string {
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join(autofillPrivacyLines(strings.Join(lines, "\n")), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) autofillRuleCount() int {
|
||||||
|
return len(autofillPrivacyLines(u.autofillBrowserAllowlist.Text())) +
|
||||||
|
len(autofillPrivacyLines(u.autofillAppAllowlist.Text())) +
|
||||||
|
len(autofillPrivacyLines(u.autofillPackageRules.Text()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) autofillFirstFillApprovalSummary() string {
|
||||||
|
switch u.autofillFirstFillApprovalMode {
|
||||||
|
case autofillFirstFillApprovalAllow:
|
||||||
|
return "New apps and packages can fill immediately until a persistent rule is created."
|
||||||
|
case autofillFirstFillApprovalBlock:
|
||||||
|
return "New apps and packages stay blocked until you add an allowlist entry or a package rule."
|
||||||
|
default:
|
||||||
|
return "KeePassGO asks before the first fill into a newly seen app or package."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) setStatusBannerTTL(value time.Duration) {
|
func (u *ui) setStatusBannerTTL(value time.Duration) {
|
||||||
u.statusBannerTTL = normalizedStatusBannerTTL(int(value / time.Millisecond))
|
u.statusBannerTTL = normalizedStatusBannerTTL(int(value / time.Millisecond))
|
||||||
u.saveUIPreferences()
|
u.saveUIPreferences()
|
||||||
@@ -1761,8 +1867,7 @@ func (u *ui) armDeleteCurrentGroupAction() {
|
|||||||
u.syncCurrentPath()
|
u.syncCurrentPath()
|
||||||
u.deleteGroupPath = append([]string(nil), u.currentPath...)
|
u.deleteGroupPath = append([]string(nil), u.currentPath...)
|
||||||
u.state.ErrorMessage = ""
|
u.state.ErrorMessage = ""
|
||||||
u.state.StatusMessage = fmt.Sprintf("Confirm deleting empty group %q.", strings.Join(u.displayPath(), " / "))
|
u.showStatusMessage(fmt.Sprintf("Confirm deleting empty group %q.", strings.Join(u.displayPath(), " / ")))
|
||||||
u.statusExpiresAt = u.now().Add(u.statusBannerTTL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) runAction(label string, action func() error) {
|
func (u *ui) runAction(label string, action func() error) {
|
||||||
@@ -1788,8 +1893,7 @@ func (u *ui) runAction(label string, action func() error) {
|
|||||||
u.statusExpiresAt = time.Time{}
|
u.statusExpiresAt = time.Time{}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
u.state.StatusMessage = label + " complete"
|
u.showStatusMessage(label + " complete")
|
||||||
u.statusExpiresAt = u.now().Add(u.statusBannerTTL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) runBackgroundAction(label string, prepare func() (func() error, error)) {
|
func (u *ui) runBackgroundAction(label string, prepare func() (func() error, error)) {
|
||||||
@@ -1847,8 +1951,7 @@ func (u *ui) applyBackgroundResult(result backgroundActionResult) {
|
|||||||
u.statusExpiresAt = time.Time{}
|
u.statusExpiresAt = time.Time{}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
u.state.StatusMessage = result.label + " complete"
|
u.showStatusMessage(result.label + " complete")
|
||||||
u.statusExpiresAt = u.now().Add(u.statusBannerTTL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) cancelLifecycleBusyState() {
|
func (u *ui) cancelLifecycleBusyState() {
|
||||||
@@ -2528,6 +2631,7 @@ 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.loadSettingsFormFromPreferences()
|
||||||
|
u.loadSettingsDraft()
|
||||||
u.securityDialogOpen = true
|
u.securityDialogOpen = true
|
||||||
}
|
}
|
||||||
for u.setStatusBannerShort.Clicked(gtx) {
|
for u.setStatusBannerShort.Clicked(gtx) {
|
||||||
@@ -2559,7 +2663,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
u.runAction("advanced synchronize vault", u.advancedSyncAction)
|
u.runAction("advanced synchronize vault", u.advancedSyncAction)
|
||||||
}
|
}
|
||||||
for u.saveSecuritySettings.Clicked(gtx) {
|
for u.saveSecuritySettings.Clicked(gtx) {
|
||||||
u.runAction("save security settings", u.saveSecuritySettingsAction)
|
u.runAction("save settings", u.saveSecuritySettingsAction)
|
||||||
}
|
}
|
||||||
for u.unlockVault.Clicked(gtx) {
|
for u.unlockVault.Clicked(gtx) {
|
||||||
u.startUnlockAction()
|
u.startUnlockAction()
|
||||||
@@ -2624,6 +2728,15 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
for u.showSyncPush.Clicked(gtx) {
|
for u.showSyncPush.Clicked(gtx) {
|
||||||
u.syncDirection = syncDirectionPush
|
u.syncDirection = syncDirectionPush
|
||||||
}
|
}
|
||||||
|
for u.showAutofillApprovalAsk.Clicked(gtx) {
|
||||||
|
u.autofillFirstFillApprovalMode = autofillFirstFillApprovalAsk
|
||||||
|
}
|
||||||
|
for u.showAutofillApprovalAllow.Clicked(gtx) {
|
||||||
|
u.autofillFirstFillApprovalMode = autofillFirstFillApprovalAllow
|
||||||
|
}
|
||||||
|
for u.showAutofillApprovalBlock.Clicked(gtx) {
|
||||||
|
u.autofillFirstFillApprovalMode = autofillFirstFillApprovalBlock
|
||||||
|
}
|
||||||
for u.allowApproval.Clicked(gtx) {
|
for u.allowApproval.Clicked(gtx) {
|
||||||
u.runAction("allow API request", func() error {
|
u.runAction("allow API request", func() error {
|
||||||
outcome := apiapproval.OutcomeAllowOnce
|
outcome := apiapproval.OutcomeAllowOnce
|
||||||
@@ -3049,9 +3162,55 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
|
|||||||
lbl.Color = accentColor
|
lbl.Color = accentColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(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 {
|
||||||
|
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,
|
||||||
|
choiceSpec{Click: &u.settingsDensityDense, Label: "Dense", Active: u.settingsDraft.Accessibility.DisplayDensity == displayDensityDense},
|
||||||
|
choiceSpec{Click: &u.settingsDensityComfortable, Label: "Comfortable", Active: u.settingsDraft.Accessibility.DisplayDensity == displayDensityComfortable},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}),
|
||||||
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 how KeePassGO remembers UI layout behavior, tune noncritical feedback, and set the KDBX security defaults used for new or future saves.")
|
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,
|
||||||
|
choiceSpec{Click: &u.settingsContrastStandard, Label: "Standard", Active: u.settingsDraft.Accessibility.Contrast == contrastStandard},
|
||||||
|
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 {
|
||||||
|
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,
|
||||||
|
choiceSpec{Click: &u.settingsReducedMotionOff, Label: "Off", Active: !u.settingsDraft.Accessibility.ReducedMotion},
|
||||||
|
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 {
|
||||||
|
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,
|
||||||
|
choiceSpec{Click: &u.settingsKeyboardFocusStandard, Label: "Standard", Active: u.settingsDraft.Accessibility.KeyboardFocus == keyboardFocusStandard},
|
||||||
|
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 {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(14), "Choose how KeePassGO remembers UI layout behavior, tunes noncritical feedback, and sets the KDBX security defaults used for new or future saves.")
|
||||||
lbl.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
@@ -3078,12 +3237,6 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
|
|||||||
check := material.CheckBox(u.theme, &u.settingsDenseLayout, "Use dense entry layout")
|
check := material.CheckBox(u.theme, &u.settingsDenseLayout, "Use dense entry layout")
|
||||||
return check.Layout(gtx)
|
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(layout.Spacer{Height: unit.Dp(14)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
lbl := material.Label(u.theme, unit.Sp(16), "Vault Security")
|
lbl := material.Label(u.theme, unit.Sp(16), "Vault Security")
|
||||||
@@ -3153,6 +3306,52 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncChoiceButton(gtx, u.theme, &u.showAutofillApprovalAllow, "Allow First Fill", u.autofillFirstFillApprovalMode == autofillFirstFillApprovalAllow)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(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 {
|
||||||
|
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.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -3394,7 +3593,7 @@ func (u *ui) syncPasswordField(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
return outlinedFieldState(gtx, false, field)
|
return u.outlinedFieldState(gtx, false, field)
|
||||||
}),
|
}),
|
||||||
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 {
|
||||||
@@ -3640,7 +3839,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
if u.mode == "phone" {
|
if u.mode == "phone" {
|
||||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||||
}
|
}
|
||||||
return outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
|
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
|
||||||
editor := material.Editor(u.theme, &u.search, "Search vault")
|
editor := material.Editor(u.theme, &u.search, "Search vault")
|
||||||
editor.Color = u.theme.Palette.Fg
|
editor.Color = u.theme.Palette.Fg
|
||||||
editor.HintColor = mutedColor
|
editor.HintColor = mutedColor
|
||||||
@@ -3781,20 +3980,7 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
|
|||||||
inset, titleSize, metaSize, urlSize, pathSize, dividerGap := u.entryRowMetrics()
|
inset, titleSize, metaSize, urlSize, pathSize, dividerGap := u.entryRowMetrics()
|
||||||
selected := item.ID == u.state.SelectedEntryID
|
selected := item.ID == u.state.SelectedEntryID
|
||||||
focused := u.isFocused(listFocusID(idx))
|
focused := u.isFocused(listFocusID(idx))
|
||||||
titleColor := accentColor
|
rowColors := u.listRowColors(selected, focused, u.state.Section == appstate.SectionRecycleBin)
|
||||||
metaColor := color.NRGBA{R: 61, G: 60, B: 56, A: 255}
|
|
||||||
secondaryColor := mutedColor
|
|
||||||
dividerColor := color.NRGBA{R: 225, G: 219, B: 210, A: 255}
|
|
||||||
if selected {
|
|
||||||
titleColor = color.NRGBA{R: 19, G: 57, B: 43, A: 255}
|
|
||||||
metaColor = color.NRGBA{R: 31, G: 53, B: 44, A: 255}
|
|
||||||
secondaryColor = color.NRGBA{R: 72, G: 88, B: 80, A: 255}
|
|
||||||
dividerColor = color.NRGBA{R: 173, G: 196, B: 184, A: 255}
|
|
||||||
} else if focused {
|
|
||||||
metaColor = color.NRGBA{R: 49, G: 74, B: 63, A: 255}
|
|
||||||
secondaryColor = color.NRGBA{R: 86, G: 102, B: 95, A: 255}
|
|
||||||
dividerColor = color.NRGBA{R: 190, G: 208, B: 199, A: 255}
|
|
||||||
}
|
|
||||||
row := func(gtx layout.Context) layout.Dimensions {
|
row := func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.UniformInset(inset).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.UniformInset(inset).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
showPath := strings.TrimSpace(u.search.Text()) != "" || len(u.displayPath()) == 0 || u.state.Section == appstate.SectionRecycleBin
|
showPath := strings.TrimSpace(u.search.Text()) != "" || len(u.displayPath()) == 0 || u.state.Section == appstate.SectionRecycleBin
|
||||||
@@ -3804,7 +3990,7 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
|
|||||||
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, titleSize, item.Title)
|
lbl := material.Label(u.theme, titleSize, item.Title)
|
||||||
lbl.Color = titleColor
|
lbl.Color = rowColors.Title
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -3818,7 +4004,7 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
|
|||||||
return layout.Dimensions{}
|
return layout.Dimensions{}
|
||||||
}
|
}
|
||||||
lbl := material.Label(u.theme, metaSize, item.Username)
|
lbl := material.Label(u.theme, metaSize, item.Username)
|
||||||
lbl.Color = metaColor
|
lbl.Color = rowColors.Meta
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -3832,7 +4018,7 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
|
|||||||
return layout.Dimensions{}
|
return layout.Dimensions{}
|
||||||
}
|
}
|
||||||
lbl := material.Label(u.theme, urlSize, item.URL)
|
lbl := material.Label(u.theme, urlSize, item.URL)
|
||||||
lbl.Color = secondaryColor
|
lbl.Color = rowColors.Secondary
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -3846,7 +4032,7 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
|
|||||||
return layout.Dimensions{}
|
return layout.Dimensions{}
|
||||||
}
|
}
|
||||||
lbl := material.Label(u.theme, pathSize, pathText)
|
lbl := material.Label(u.theme, pathSize, pathText)
|
||||||
lbl.Color = secondaryColor
|
lbl.Color = rowColors.Secondary
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: dividerGap}.Layout),
|
layout.Rigid(layout.Spacer{Height: dividerGap}.Layout),
|
||||||
@@ -3855,7 +4041,7 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
|
|||||||
if w < 1 {
|
if w < 1 {
|
||||||
w = 1
|
w = 1
|
||||||
}
|
}
|
||||||
paint.FillShape(gtx.Ops, dividerColor, clip.Rect{Max: image.Pt(w, 1)}.Op())
|
paint.FillShape(gtx.Ops, rowColors.Divider, clip.Rect{Max: image.Pt(w, 1)}.Op())
|
||||||
return layout.Dimensions{Size: image.Pt(w, 1)}
|
return layout.Dimensions{Size: image.Pt(w, 1)}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -3871,18 +4057,8 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
|
|||||||
if size.Y == 0 {
|
if size.Y == 0 {
|
||||||
size.Y = gtx.Constraints.Max.Y
|
size.Y = gtx.Constraints.Max.Y
|
||||||
}
|
}
|
||||||
fillColor := color.NRGBA{R: 212, G: 228, B: 220, A: 255}
|
paint.FillShape(gtx.Ops, rowColors.Fill, clip.Rect{Max: size}.Op())
|
||||||
edgeColor := color.NRGBA{R: 46, G: 106, B: 82, A: 255}
|
paint.FillShape(gtx.Ops, rowColors.Edge, clip.Rect{Max: image.Pt(5, size.Y)}.Op())
|
||||||
if u.state.Section == appstate.SectionRecycleBin {
|
|
||||||
fillColor = color.NRGBA{R: 244, G: 229, B: 219, A: 255}
|
|
||||||
edgeColor = color.NRGBA{R: 133, G: 65, B: 41, A: 255}
|
|
||||||
}
|
|
||||||
if focused && !selected {
|
|
||||||
fillColor = color.NRGBA{R: 231, G: 239, B: 235, A: 255}
|
|
||||||
edgeColor = color.NRGBA{R: 69, G: 118, B: 97, A: 255}
|
|
||||||
}
|
|
||||||
paint.FillShape(gtx.Ops, fillColor, clip.Rect{Max: size}.Op())
|
|
||||||
paint.FillShape(gtx.Ops, edgeColor, clip.Rect{Max: image.Pt(5, size.Y)}.Op())
|
|
||||||
return layout.Dimensions{Size: size}
|
return layout.Dimensions{Size: size}
|
||||||
}),
|
}),
|
||||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -4715,7 +4891,7 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
|
|||||||
u.filter()
|
u.filter()
|
||||||
}
|
}
|
||||||
btn := material.Button(u.theme, &u.breadcrumbs[index], label)
|
btn := material.Button(u.theme, &u.breadcrumbs[index], label)
|
||||||
btn.Background, btn.Color = buttonFocusColors(u.isFocused(breadcrumbFocusID(index)))
|
btn.Background, btn.Color = buttonFocusColors(u.accessibilityPrefs, u.isFocused(breadcrumbFocusID(index)))
|
||||||
btn.TextSize = unit.Sp(11)
|
btn.TextSize = unit.Sp(11)
|
||||||
if u.mode == "phone" {
|
if u.mode == "phone" {
|
||||||
btn.TextSize = unit.Sp(9)
|
btn.TextSize = unit.Sp(9)
|
||||||
@@ -4982,8 +5158,8 @@ func emptyStatePanel(gtx layout.Context, th *material.Theme, state emptyState) l
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func outlinedFieldState(gtx layout.Context, focused bool, w layout.Widget) layout.Dimensions {
|
func outlinedFieldStateWithPrefs(gtx layout.Context, prefs accessibilityPreferences, focused bool, w layout.Widget) layout.Dimensions {
|
||||||
appearance := fieldFocusAppearance(gtx.Metric, focused)
|
appearance := fieldFocusAppearance(gtx.Metric, prefs, focused)
|
||||||
size := gtx.Constraints.Min
|
size := gtx.Constraints.Min
|
||||||
if size.X == 0 {
|
if size.X == 0 {
|
||||||
size.X = gtx.Constraints.Max.X
|
size.X = gtx.Constraints.Max.X
|
||||||
@@ -5025,9 +5201,13 @@ func outlinedFieldState(gtx layout.Context, focused bool, w layout.Widget) layou
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) outlinedFieldState(gtx layout.Context, focused bool, w layout.Widget) layout.Dimensions {
|
||||||
|
return outlinedFieldStateWithPrefs(gtx, u.accessibilityPrefs, focused, w)
|
||||||
|
}
|
||||||
|
|
||||||
func tonedButton(gtx layout.Context, th *material.Theme, click *widget.Clickable, label string) layout.Dimensions {
|
func tonedButton(gtx layout.Context, th *material.Theme, click *widget.Clickable, label string) layout.Dimensions {
|
||||||
btn := material.Button(th, click, label)
|
btn := material.Button(th, click, label)
|
||||||
btn.Background, btn.Color = buttonFocusColors(false)
|
btn.Background, btn.Color = buttonFocusColors(defaultAccessibilityPreferences(), false)
|
||||||
btn.CornerRadius = unit.Dp(10)
|
btn.CornerRadius = unit.Dp(10)
|
||||||
btn.TextSize = unit.Sp(15)
|
btn.TextSize = unit.Sp(15)
|
||||||
if gtx.Constraints.Max.X <= gtx.Dp(unit.Dp(460)) {
|
if gtx.Constraints.Max.X <= gtx.Dp(unit.Dp(460)) {
|
||||||
|
|||||||
+77
-3
@@ -2671,9 +2671,9 @@ func TestUIAccessibilityLabelsDescribeFocusableControls(t *testing.T) {
|
|||||||
func TestFieldFocusAppearanceScalesForHighDPI(t *testing.T) {
|
func TestFieldFocusAppearanceScalesForHighDPI(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
lo := fieldFocusAppearance(unit.Metric{PxPerDp: 1, PxPerSp: 1}, true)
|
lo := fieldFocusAppearance(unit.Metric{PxPerDp: 1, PxPerSp: 1}, defaultAccessibilityPreferences(), true)
|
||||||
hi := fieldFocusAppearance(unit.Metric{PxPerDp: 2.5, PxPerSp: 2.5}, true)
|
hi := fieldFocusAppearance(unit.Metric{PxPerDp: 2.5, PxPerSp: 2.5}, defaultAccessibilityPreferences(), true)
|
||||||
unfocused := fieldFocusAppearance(unit.Metric{PxPerDp: 1, PxPerSp: 1}, false)
|
unfocused := fieldFocusAppearance(unit.Metric{PxPerDp: 1, PxPerSp: 1}, defaultAccessibilityPreferences(), false)
|
||||||
|
|
||||||
if got := lo.MinHeight; got != 44 {
|
if got := lo.MinHeight; got != 44 {
|
||||||
t.Fatalf("fieldFocusAppearance(low).MinHeight = %d, want 44", got)
|
t.Fatalf("fieldFocusAppearance(low).MinHeight = %d, want 44", got)
|
||||||
@@ -2935,6 +2935,26 @@ func TestUIStatusToastExpiresAfterConfiguredTimeout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIReducedMotionKeepsStatusToastVisible(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
now := time.Date(2026, time.March, 29, 12, 0, 0, 0, time.UTC)
|
||||||
|
u := newUIWithModel("desktop", vault.Model{})
|
||||||
|
u.now = func() time.Time { return now }
|
||||||
|
u.statusBannerTTL = statusBannerLong
|
||||||
|
u.applyAccessibilityPreferences(accessibilityPreferences{ReducedMotion: true})
|
||||||
|
|
||||||
|
u.showStatusMessage("synchronize vault complete")
|
||||||
|
if !u.statusExpiresAt.IsZero() {
|
||||||
|
t.Fatalf("statusExpiresAt with reduced motion = %v, want zero", u.statusExpiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
now = now.Add(statusBannerLong * 2)
|
||||||
|
if got := u.statusToastSurface(); got.Kind != bannerStatus || got.Message != "synchronize vault complete" {
|
||||||
|
t.Fatalf("statusToastSurface() with reduced motion = %#v, want persistent status toast", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUIAutofillStatusSurfaceUsesPendingApproval(t *testing.T) {
|
func TestUIAutofillStatusSurfaceUsesPendingApproval(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -3538,6 +3558,60 @@ func TestUIDenseLayoutPreferencePersists(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIAccessibilityPreferencesPersist(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
configPath := filepath.Join(t.TempDir(), "ui-prefs.json")
|
||||||
|
|
||||||
|
first := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
first.uiPreferencesPath = configPath
|
||||||
|
first.applyAccessibilityPreferences(accessibilityPreferences{
|
||||||
|
DisplayDensity: displayDensityComfortable,
|
||||||
|
Contrast: contrastHigh,
|
||||||
|
ReducedMotion: true,
|
||||||
|
KeyboardFocus: keyboardFocusProminent,
|
||||||
|
})
|
||||||
|
first.saveUIPreferences()
|
||||||
|
|
||||||
|
second := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
second.uiPreferencesPath = configPath
|
||||||
|
second.loadUIPreferences()
|
||||||
|
|
||||||
|
if second.denseLayout {
|
||||||
|
t.Fatal("denseLayout after reload = true, want comfortable layout preference")
|
||||||
|
}
|
||||||
|
if got := second.accessibilityPrefs; got != (accessibilityPreferences{
|
||||||
|
DisplayDensity: displayDensityComfortable,
|
||||||
|
Contrast: contrastHigh,
|
||||||
|
ReducedMotion: true,
|
||||||
|
KeyboardFocus: keyboardFocusProminent,
|
||||||
|
}) {
|
||||||
|
t.Fatalf("accessibilityPrefs after reload = %#v, want comfortable/high/reduced/prominent", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldFocusAppearanceUsesAccessibilityPreferences(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
metric := unit.Metric{PxPerDp: 1, PxPerSp: 1}
|
||||||
|
base := fieldFocusAppearance(metric, defaultAccessibilityPreferences(), true)
|
||||||
|
comfortable := fieldFocusAppearance(metric, accessibilityPreferences{
|
||||||
|
DisplayDensity: displayDensityComfortable,
|
||||||
|
Contrast: contrastHigh,
|
||||||
|
KeyboardFocus: keyboardFocusProminent,
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
if comfortable.MinHeight <= base.MinHeight {
|
||||||
|
t.Fatalf("fieldFocusAppearance(comfortable).MinHeight = %d, want > %d", comfortable.MinHeight, base.MinHeight)
|
||||||
|
}
|
||||||
|
if comfortable.OutlineWidth <= base.OutlineWidth {
|
||||||
|
t.Fatalf("fieldFocusAppearance(prominent).OutlineWidth = %d, want > %d", comfortable.OutlineWidth, base.OutlineWidth)
|
||||||
|
}
|
||||||
|
if comfortable.OutlineColor.A <= base.OutlineColor.A {
|
||||||
|
t.Fatalf("fieldFocusAppearance(high contrast).OutlineColor.A = %d, want > %d", comfortable.OutlineColor.A, base.OutlineColor.A)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUIEntryRowMetricsUseDenseLayout(t *testing.T) {
|
func TestUIEntryRowMetricsUseDenseLayout(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
+25
-2
@@ -19,26 +19,49 @@ type focusAppearance struct {
|
|||||||
MinHeight int
|
MinHeight int
|
||||||
}
|
}
|
||||||
|
|
||||||
func fieldFocusAppearance(metric unit.Metric, focused bool) focusAppearance {
|
func fieldFocusAppearance(metric unit.Metric, prefs accessibilityPreferences, focused bool) focusAppearance {
|
||||||
|
prefs = normalizeAccessibilityPreferences(prefs)
|
||||||
appearance := focusAppearance{
|
appearance := focusAppearance{
|
||||||
BorderColor: color.NRGBA{R: 202, G: 194, B: 180, A: 255},
|
BorderColor: color.NRGBA{R: 202, G: 194, B: 180, A: 255},
|
||||||
OutlineColor: color.NRGBA{A: 0},
|
OutlineColor: color.NRGBA{A: 0},
|
||||||
OutlineWidth: max(1, metric.Dp(unit.Dp(1))),
|
OutlineWidth: max(1, metric.Dp(unit.Dp(1))),
|
||||||
MinHeight: metric.Dp(unit.Dp(44)),
|
MinHeight: metric.Dp(unit.Dp(44)),
|
||||||
}
|
}
|
||||||
|
if prefs.DisplayDensity == displayDensityComfortable {
|
||||||
|
appearance.MinHeight = metric.Dp(unit.Dp(52))
|
||||||
|
}
|
||||||
|
if prefs.Contrast == contrastHigh {
|
||||||
|
appearance.BorderColor = color.NRGBA{R: 108, G: 101, B: 90, A: 255}
|
||||||
|
}
|
||||||
if focused {
|
if focused {
|
||||||
appearance.BorderColor = accentColor
|
appearance.BorderColor = accentColor
|
||||||
appearance.OutlineColor = color.NRGBA{R: 28, G: 83, B: 63, A: 72}
|
appearance.OutlineColor = color.NRGBA{R: 28, G: 83, B: 63, A: 72}
|
||||||
appearance.OutlineWidth = max(2, metric.Dp(unit.Dp(2)))
|
appearance.OutlineWidth = max(2, metric.Dp(unit.Dp(2)))
|
||||||
|
if prefs.Contrast == contrastHigh {
|
||||||
|
appearance.BorderColor = color.NRGBA{R: 16, G: 60, B: 44, A: 255}
|
||||||
|
appearance.OutlineColor = color.NRGBA{R: 20, G: 74, B: 55, A: 124}
|
||||||
|
}
|
||||||
|
if prefs.KeyboardFocus == keyboardFocusProminent {
|
||||||
|
appearance.OutlineWidth = max(3, metric.Dp(unit.Dp(3)))
|
||||||
|
appearance.OutlineColor = color.NRGBA{R: 20, G: 74, B: 55, A: 148}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return appearance
|
return appearance
|
||||||
}
|
}
|
||||||
|
|
||||||
func buttonFocusColors(focused bool) (background color.NRGBA, text color.NRGBA) {
|
func buttonFocusColors(prefs accessibilityPreferences, focused bool) (background color.NRGBA, text color.NRGBA) {
|
||||||
|
prefs = normalizeAccessibilityPreferences(prefs)
|
||||||
background = color.NRGBA{R: 231, G: 239, B: 235, A: 255}
|
background = color.NRGBA{R: 231, G: 239, B: 235, A: 255}
|
||||||
text = accentColor
|
text = accentColor
|
||||||
|
if prefs.Contrast == contrastHigh {
|
||||||
|
background = color.NRGBA{R: 225, G: 235, B: 230, A: 255}
|
||||||
|
text = color.NRGBA{R: 19, G: 57, B: 43, A: 255}
|
||||||
|
}
|
||||||
if focused {
|
if focused {
|
||||||
background = color.NRGBA{R: 214, G: 229, B: 221, A: 255}
|
background = color.NRGBA{R: 214, G: 229, B: 221, A: 255}
|
||||||
|
if prefs.Contrast == contrastHigh || prefs.KeyboardFocus == keyboardFocusProminent {
|
||||||
|
background = color.NRGBA{R: 202, G: 222, B: 212, A: 255}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return background, text
|
return background, text
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-16
@@ -969,15 +969,15 @@ func (u *ui) entryEditorPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return sectionCard(gtx, u.theme, "BASICS", "Core entry identity and navigation fields.", func(gtx layout.Context) layout.Dimensions {
|
return sectionCard(gtx, u.theme, "BASICS", "Core entry identity and navigation fields.", func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(labeledEditorWithFocus(u.theme, "Title", &u.entryTitle, false, u.isFocused(detailFocusID(detailFieldTitle)))),
|
layout.Rigid(labeledEditorWithFocus(u.theme, u.accessibilityPrefs, "Title", &u.entryTitle, false, u.isFocused(detailFocusID(detailFieldTitle)))),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(labeledEditorWithFocus(u.theme, "Username", &u.entryUsername, false, u.isFocused(detailFocusID(detailFieldUsername)))),
|
layout.Rigid(labeledEditorWithFocus(u.theme, u.accessibilityPrefs, "Username", &u.entryUsername, false, u.isFocused(detailFocusID(detailFieldUsername)))),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(labeledEditorWithFocus(u.theme, "URL", &u.entryURL, false, u.isFocused(detailFocusID(detailFieldURL)))),
|
layout.Rigid(labeledEditorWithFocus(u.theme, u.accessibilityPrefs, "URL", &u.entryURL, false, u.isFocused(detailFocusID(detailFieldURL)))),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(labeledEditorWithFocus(u.theme, "Path", &u.entryPath, false, u.isFocused(detailFocusID(detailFieldPath)))),
|
layout.Rigid(labeledEditorWithFocus(u.theme, u.accessibilityPrefs, "Path", &u.entryPath, false, u.isFocused(detailFocusID(detailFieldPath)))),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(labeledEditorWithFocus(u.theme, "Tags", &u.entryTags, false, u.isFocused(detailFocusID(detailFieldTags)))),
|
layout.Rigid(labeledEditorWithFocus(u.theme, u.accessibilityPrefs, "Tags", &u.entryTags, false, u.isFocused(detailFocusID(detailFieldTags)))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
@@ -985,9 +985,9 @@ func (u *ui) entryEditorPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return sectionCard(gtx, u.theme, "PASSWORD", "Generate, review, and keep track of password changes before you save.", func(gtx layout.Context) layout.Dimensions {
|
return sectionCard(gtx, u.theme, "PASSWORD", "Generate, review, and keep track of password changes before you save.", func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(labeledEditorWithFocus(u.theme, "Password", &u.entryPassword, true, u.isFocused(detailFocusID(detailFieldPassword)))),
|
layout.Rigid(labeledEditorWithFocus(u.theme, u.accessibilityPrefs, "Password", &u.entryPassword, true, u.isFocused(detailFocusID(detailFieldPassword)))),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(labeledEditorWithFocus(u.theme, "Password Profile", &u.passwordProfile, false, u.isFocused(detailFocusID(detailFieldPasswordProfile)))),
|
layout.Rigid(labeledEditorWithFocus(u.theme, u.accessibilityPrefs, "Password Profile", &u.passwordProfile, false, u.isFocused(detailFocusID(detailFieldPasswordProfile)))),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
lbl := material.Label(u.theme, unit.Sp(11), u.passwordProfileOptionsText())
|
lbl := material.Label(u.theme, unit.Sp(11), u.passwordProfileOptionsText())
|
||||||
@@ -1057,7 +1057,7 @@ func (u *ui) entryEditorPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
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 {
|
||||||
return sectionCard(gtx, u.theme, "NOTES", "Long-form context for this entry.", func(gtx layout.Context) layout.Dimensions {
|
return sectionCard(gtx, u.theme, "NOTES", "Long-form context for this entry.", func(gtx layout.Context) layout.Dimensions {
|
||||||
return labeledMultilineEditorWithFocus(u.theme, "Notes", &u.entryNotes, false, u.isFocused(detailFocusID(detailFieldNotes)), unit.Dp(108))(gtx)
|
return labeledMultilineEditorWithFocus(u.theme, u.accessibilityPrefs, "Notes", &u.entryNotes, false, u.isFocused(detailFocusID(detailFieldNotes)), unit.Dp(108))(gtx)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
@@ -1065,7 +1065,7 @@ func (u *ui) entryEditorPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
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 {
|
||||||
return sectionCard(gtx, u.theme, "HISTORY", "Pick a saved version index to restore into the current entry.", func(gtx layout.Context) layout.Dimensions {
|
return sectionCard(gtx, u.theme, "HISTORY", "Pick a saved version index to restore into the current entry.", func(gtx layout.Context) layout.Dimensions {
|
||||||
return labeledEditorWithFocus(u.theme, "History Index", &u.historyIndex, false, u.isFocused(detailFocusID(detailFieldHistoryIndex)))(gtx)
|
return labeledEditorWithFocus(u.theme, u.accessibilityPrefs, "History Index", &u.historyIndex, false, u.isFocused(detailFocusID(detailFieldHistoryIndex)))(gtx)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
@@ -1142,17 +1142,17 @@ func (u *ui) entryEditorPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func labeledEditor(th *material.Theme, label string, editor *widget.Editor, sensitive bool) layout.Widget {
|
func labeledEditor(th *material.Theme, label string, editor *widget.Editor, sensitive bool) layout.Widget {
|
||||||
return labeledEditorWithFocus(th, label, editor, sensitive, false)
|
return labeledEditorWithFocus(th, defaultAccessibilityPreferences(), label, editor, sensitive, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func labeledEditorHelp(th *material.Theme, label, help string, editor *widget.Editor, sensitive bool) layout.Widget {
|
func labeledEditorHelp(th *material.Theme, label, help string, editor *widget.Editor, sensitive bool) layout.Widget {
|
||||||
return labeledEditorHelpFocus(th, label, help, editor, sensitive, false)
|
return labeledEditorHelpFocus(th, defaultAccessibilityPreferences(), label, help, editor, sensitive, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func labeledEditorHelpFocus(th *material.Theme, label, help string, editor *widget.Editor, sensitive bool, focused bool) layout.Widget {
|
func labeledEditorHelpFocus(th *material.Theme, prefs accessibilityPreferences, label, help string, editor *widget.Editor, sensitive bool, focused bool) layout.Widget {
|
||||||
return func(gtx layout.Context) layout.Dimensions {
|
return func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(labeledEditorWithFocus(th, label, editor, sensitive, focused)),
|
layout.Rigid(labeledEditorWithFocus(th, prefs, label, editor, sensitive, focused)),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
lbl := material.Label(th, unit.Sp(11), help)
|
lbl := material.Label(th, unit.Sp(11), help)
|
||||||
@@ -1276,7 +1276,7 @@ func (u *ui) masterPasswordField(gtx layout.Context, help string) layout.Dimensi
|
|||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return outlinedFieldState(gtx, false, func(gtx layout.Context) layout.Dimensions {
|
return u.outlinedFieldState(gtx, false, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -1313,6 +1313,7 @@ func (u *ui) masterPasswordField(gtx layout.Context, help string) layout.Dimensi
|
|||||||
|
|
||||||
func labeledEditorWithFocus(
|
func labeledEditorWithFocus(
|
||||||
th *material.Theme,
|
th *material.Theme,
|
||||||
|
prefs accessibilityPreferences,
|
||||||
label string,
|
label string,
|
||||||
editor *widget.Editor,
|
editor *widget.Editor,
|
||||||
sensitive bool,
|
sensitive bool,
|
||||||
@@ -1326,7 +1327,7 @@ func labeledEditorWithFocus(
|
|||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return outlinedFieldState(gtx, focused, func(gtx layout.Context) layout.Dimensions {
|
return outlinedFieldStateWithPrefs(gtx, prefs, focused, func(gtx layout.Context) layout.Dimensions {
|
||||||
mask := editor.Mask
|
mask := editor.Mask
|
||||||
if sensitive {
|
if sensitive {
|
||||||
editor.Mask = '•'
|
editor.Mask = '•'
|
||||||
@@ -1343,6 +1344,7 @@ func labeledEditorWithFocus(
|
|||||||
|
|
||||||
func labeledMultilineEditorWithFocus(
|
func labeledMultilineEditorWithFocus(
|
||||||
th *material.Theme,
|
th *material.Theme,
|
||||||
|
prefs accessibilityPreferences,
|
||||||
label string,
|
label string,
|
||||||
editor *widget.Editor,
|
editor *widget.Editor,
|
||||||
sensitive bool,
|
sensitive bool,
|
||||||
@@ -1357,7 +1359,7 @@ func labeledMultilineEditorWithFocus(
|
|||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return outlinedFieldState(gtx, focused, func(gtx layout.Context) layout.Dimensions {
|
return outlinedFieldStateWithPrefs(gtx, prefs, focused, func(gtx layout.Context) layout.Dimensions {
|
||||||
mask := editor.Mask
|
mask := editor.Mask
|
||||||
if sensitive {
|
if sensitive {
|
||||||
editor.Mask = '•'
|
editor.Mask = '•'
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gioui.org/layout"
|
||||||
|
"gioui.org/unit"
|
||||||
|
"gioui.org/widget"
|
||||||
|
"gioui.org/widget/material"
|
||||||
|
"git.julianfamily.org/keepassgo/vault"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
displayDensityDense = "dense"
|
||||||
|
displayDensityComfortable = "comfortable"
|
||||||
|
|
||||||
|
contrastStandard = "standard"
|
||||||
|
contrastHigh = "high"
|
||||||
|
|
||||||
|
keyboardFocusStandard = "standard"
|
||||||
|
keyboardFocusProminent = "prominent"
|
||||||
|
)
|
||||||
|
|
||||||
|
type accessibilityPreferences struct {
|
||||||
|
DisplayDensity string
|
||||||
|
Contrast string
|
||||||
|
ReducedMotion bool
|
||||||
|
KeyboardFocus string
|
||||||
|
}
|
||||||
|
|
||||||
|
type settingsDraft struct {
|
||||||
|
Accessibility accessibilityPreferences
|
||||||
|
}
|
||||||
|
|
||||||
|
type choiceSpec struct {
|
||||||
|
Click *widget.Clickable
|
||||||
|
Label string
|
||||||
|
Active bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultAccessibilityPreferences() accessibilityPreferences {
|
||||||
|
return accessibilityPreferences{
|
||||||
|
DisplayDensity: displayDensityForDenseLayout(true),
|
||||||
|
Contrast: contrastStandard,
|
||||||
|
KeyboardFocus: keyboardFocusStandard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayDensityForDenseLayout(dense bool) string {
|
||||||
|
if dense {
|
||||||
|
return displayDensityDense
|
||||||
|
}
|
||||||
|
return displayDensityComfortable
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAccessibilityPreferences(prefs accessibilityPreferences) accessibilityPreferences {
|
||||||
|
normalized := defaultAccessibilityPreferences()
|
||||||
|
switch prefs.DisplayDensity {
|
||||||
|
case displayDensityDense, displayDensityComfortable:
|
||||||
|
normalized.DisplayDensity = prefs.DisplayDensity
|
||||||
|
}
|
||||||
|
switch prefs.Contrast {
|
||||||
|
case contrastStandard, contrastHigh:
|
||||||
|
normalized.Contrast = prefs.Contrast
|
||||||
|
}
|
||||||
|
switch prefs.KeyboardFocus {
|
||||||
|
case keyboardFocusStandard, keyboardFocusProminent:
|
||||||
|
normalized.KeyboardFocus = prefs.KeyboardFocus
|
||||||
|
}
|
||||||
|
normalized.ReducedMotion = prefs.ReducedMotion
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) applyAccessibilityPreferences(prefs accessibilityPreferences) {
|
||||||
|
normalized := normalizeAccessibilityPreferences(prefs)
|
||||||
|
u.denseLayout = normalized.DisplayDensity == displayDensityDense
|
||||||
|
u.accessibilityPrefs = normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) loadSettingsDraft() {
|
||||||
|
u.settingsDraft = settingsDraft{
|
||||||
|
Accessibility: accessibilityPreferences{
|
||||||
|
DisplayDensity: displayDensityForDenseLayout(u.denseLayout),
|
||||||
|
Contrast: u.accessibilityPrefs.Contrast,
|
||||||
|
ReducedMotion: u.accessibilityPrefs.ReducedMotion,
|
||||||
|
KeyboardFocus: u.accessibilityPrefs.KeyboardFocus,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) saveSecuritySettingsAction() error {
|
||||||
|
settings := vault.SecuritySettings{
|
||||||
|
Cipher: strings.TrimSpace(u.securityCipher.Text()),
|
||||||
|
KDF: strings.TrimSpace(u.securityKDF.Text()),
|
||||||
|
}
|
||||||
|
if err := u.state.ConfigureSecurity(settings); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if u.settingsDraft.Accessibility.DisplayDensity == displayDensityForDenseLayout(u.denseLayout) {
|
||||||
|
u.settingsDraft.Accessibility.DisplayDensity = displayDensityForDenseLayout(u.settingsDenseLayout.Value)
|
||||||
|
}
|
||||||
|
u.settingsDenseLayout.Value = u.settingsDraft.Accessibility.DisplayDensity == displayDensityDense
|
||||||
|
u.applySettingsFormToPreferences()
|
||||||
|
u.applyAccessibilityPreferences(u.settingsDraft.Accessibility)
|
||||||
|
u.saveUIPreferences()
|
||||||
|
u.securityDialogOpen = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) showStatusMessage(message string) {
|
||||||
|
u.state.StatusMessage = message
|
||||||
|
if u.accessibilityPrefs.ReducedMotion {
|
||||||
|
u.statusExpiresAt = time.Time{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.statusExpiresAt = u.now().Add(u.statusBannerTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) settingsPreferenceCard(gtx layout.Context, title, detail string, body layout.Widget) layout.Dimensions {
|
||||||
|
return sectionCard(gtx, u.theme, title, detail, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func settingsSummaryCard(gtx layout.Context, th *material.Theme, title, body string) 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 {
|
||||||
|
lbl := material.Label(th, unit.Sp(12), title)
|
||||||
|
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(th, unit.Sp(13), body)
|
||||||
|
lbl.Color = th.Palette.Fg
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) settingsChoiceRow(gtx layout.Context, choices ...choiceSpec) layout.Dimensions {
|
||||||
|
children := make([]layout.FlexChild, 0, len(choices)*2)
|
||||||
|
for i, choice := range choices {
|
||||||
|
if i > 0 {
|
||||||
|
children = append(children, layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout))
|
||||||
|
}
|
||||||
|
current := choice
|
||||||
|
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncChoiceButton(gtx, u.theme, current.Click, current.Label, current.Active)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type listRowColors struct {
|
||||||
|
Title color.NRGBA
|
||||||
|
Meta color.NRGBA
|
||||||
|
Secondary color.NRGBA
|
||||||
|
Divider color.NRGBA
|
||||||
|
Fill color.NRGBA
|
||||||
|
Edge color.NRGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) listRowColors(selected, focused, recycleBin bool) listRowColors {
|
||||||
|
colors := listRowColors{
|
||||||
|
Title: accentColor,
|
||||||
|
Meta: color.NRGBA{R: 61, G: 60, B: 56, A: 255},
|
||||||
|
Secondary: mutedColor,
|
||||||
|
Divider: color.NRGBA{R: 225, G: 219, B: 210, A: 255},
|
||||||
|
Fill: color.NRGBA{R: 231, G: 239, B: 235, A: 255},
|
||||||
|
Edge: color.NRGBA{R: 69, G: 118, B: 97, A: 255},
|
||||||
|
}
|
||||||
|
if selected {
|
||||||
|
colors.Title = color.NRGBA{R: 19, G: 57, B: 43, A: 255}
|
||||||
|
colors.Meta = color.NRGBA{R: 31, G: 53, B: 44, A: 255}
|
||||||
|
colors.Secondary = color.NRGBA{R: 72, G: 88, B: 80, A: 255}
|
||||||
|
colors.Divider = color.NRGBA{R: 173, G: 196, B: 184, A: 255}
|
||||||
|
colors.Fill = color.NRGBA{R: 212, G: 228, B: 220, A: 255}
|
||||||
|
colors.Edge = color.NRGBA{R: 46, G: 106, B: 82, A: 255}
|
||||||
|
}
|
||||||
|
if recycleBin {
|
||||||
|
colors.Fill = color.NRGBA{R: 244, G: 229, B: 219, A: 255}
|
||||||
|
colors.Edge = color.NRGBA{R: 133, G: 65, B: 41, A: 255}
|
||||||
|
}
|
||||||
|
if focused && !selected {
|
||||||
|
colors.Meta = color.NRGBA{R: 49, G: 74, B: 63, A: 255}
|
||||||
|
colors.Secondary = color.NRGBA{R: 86, G: 102, B: 95, A: 255}
|
||||||
|
colors.Divider = color.NRGBA{R: 190, G: 208, B: 199, A: 255}
|
||||||
|
}
|
||||||
|
if u.accessibilityPrefs.Contrast == contrastHigh {
|
||||||
|
colors.Meta = color.NRGBA{R: 39, G: 39, B: 36, A: 255}
|
||||||
|
colors.Secondary = color.NRGBA{R: 58, G: 57, B: 52, A: 255}
|
||||||
|
if focused || selected {
|
||||||
|
colors.Fill = color.NRGBA{R: 211, G: 228, B: 219, A: 255}
|
||||||
|
colors.Edge = color.NRGBA{R: 16, G: 60, B: 44, A: 255}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.accessibilityPrefs.KeyboardFocus == keyboardFocusProminent && focused && !selected {
|
||||||
|
colors.Fill = color.NRGBA{R: 220, G: 234, B: 226, A: 255}
|
||||||
|
colors.Edge = color.NRGBA{R: 20, G: 74, B: 55, A: 255}
|
||||||
|
}
|
||||||
|
if recycleBin && (focused || selected) && u.accessibilityPrefs.Contrast == contrastHigh {
|
||||||
|
colors.Fill = color.NRGBA{R: 242, G: 223, B: 209, A: 255}
|
||||||
|
colors.Edge = color.NRGBA{R: 116, G: 43, B: 19, A: 255}
|
||||||
|
}
|
||||||
|
return colors
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user