Add autofill privacy settings controls
This commit is contained in:
@@ -127,6 +127,7 @@ type statePaths struct {
|
||||
DefaultSaveAsPath string
|
||||
RecentVaultsPath string
|
||||
RecentRemotesPath string
|
||||
SettingsPath string
|
||||
UIPreferencesPath string
|
||||
AutofillCachePath string
|
||||
}
|
||||
@@ -150,6 +151,8 @@ type uiPreferences struct {
|
||||
GroupControlsHidden bool `json:"groupControlsHidden"`
|
||||
LifecycleAdvancedHidden bool `json:"lifecycleAdvancedHidden"`
|
||||
HistoryHidden bool `json:"historyHidden"`
|
||||
SyncSourceDefault string `json:"syncSourceDefault,omitempty"`
|
||||
SyncDirectionDefault string `json:"syncDirectionDefault,omitempty"`
|
||||
DenseLayout bool `json:"denseLayout"`
|
||||
StatusBannerMillis int `json:"statusBannerMillis,omitempty"`
|
||||
AutofillNoticeMode string `json:"autofillNoticeMode,omitempty"`
|
||||
@@ -271,6 +274,10 @@ type ui struct {
|
||||
settingsReducedMotionOn widget.Clickable
|
||||
settingsKeyboardFocusStandard widget.Clickable
|
||||
settingsKeyboardFocusProminent widget.Clickable
|
||||
showSettingsSyncLocal widget.Clickable
|
||||
showSettingsSyncRemote widget.Clickable
|
||||
showSettingsSyncPull widget.Clickable
|
||||
showSettingsSyncPush widget.Clickable
|
||||
editEntry widget.Clickable
|
||||
cancelEdit widget.Clickable
|
||||
pickVaultPath widget.Clickable
|
||||
@@ -402,10 +409,13 @@ type ui struct {
|
||||
keyboardFocus focusID
|
||||
defaultSaveAsPath string
|
||||
recentVaultsPath string
|
||||
settingsPath string
|
||||
uiPreferencesPath string
|
||||
recentRemotesPath string
|
||||
autofillCachePath string
|
||||
editingEntry bool
|
||||
syncDefaultSourceMode syncSourceMode
|
||||
syncDefaultDirection syncDirection
|
||||
groupControlsHidden bool
|
||||
lifecycleAdvancedHidden bool
|
||||
historyHidden bool
|
||||
@@ -547,6 +557,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
lifecycleMode: "local",
|
||||
defaultSaveAsPath: paths.DefaultSaveAsPath,
|
||||
recentVaultsPath: paths.RecentVaultsPath,
|
||||
settingsPath: paths.SettingsPath,
|
||||
uiPreferencesPath: paths.UIPreferencesPath,
|
||||
recentRemotesPath: paths.RecentRemotesPath,
|
||||
autofillCachePath: paths.AutofillCachePath,
|
||||
@@ -560,6 +571,8 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
now: time.Now,
|
||||
syncSourceMode: syncSourceLocal,
|
||||
syncDirection: syncDirectionPull,
|
||||
syncDefaultSourceMode: syncSourceLocal,
|
||||
syncDefaultDirection: syncDirectionPull,
|
||||
apiPolicyGroupScope: true,
|
||||
autofillNoticePreference: autofillNoticeAll,
|
||||
backgroundResults: make(chan backgroundActionResult, 8),
|
||||
@@ -585,6 +598,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
u.loadRecentRemotes()
|
||||
u.restoreStartupLifecycleTarget()
|
||||
u.loadUIPreferences()
|
||||
u.loadSettings()
|
||||
u.loadSettingsFormFromPreferences()
|
||||
u.loadSettingsDraft()
|
||||
u.filter()
|
||||
@@ -621,6 +635,7 @@ func defaultStatePaths(stateDir string) statePaths {
|
||||
DefaultSaveAsPath: filepath.Join(baseDir, "vault.kdbx"),
|
||||
RecentVaultsPath: filepath.Join(baseDir, "recent-vaults.json"),
|
||||
RecentRemotesPath: filepath.Join(baseDir, "recent-remotes.json"),
|
||||
SettingsPath: filepath.Join(baseDir, "settings.json"),
|
||||
UIPreferencesPath: filepath.Join(baseDir, "ui-prefs.json"),
|
||||
AutofillCachePath: filepath.Join(baseDir, "autofill-cache.json"),
|
||||
}
|
||||
@@ -1093,11 +1108,31 @@ func (u *ui) openAdvancedSyncDialog() {
|
||||
u.syncDialogOpen = true
|
||||
u.syncMenuOpen = false
|
||||
u.showSyncPassword = false
|
||||
u.syncSourceMode = u.syncDefaultSourceMode
|
||||
u.syncDirection = u.syncDefaultDirection
|
||||
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
|
||||
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeSyncSourceMode(mode syncSourceMode) syncSourceMode {
|
||||
switch mode {
|
||||
case syncSourceRemote:
|
||||
return syncSourceRemote
|
||||
default:
|
||||
return syncSourceLocal
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeSyncDirection(direction syncDirection) syncDirection {
|
||||
switch direction {
|
||||
case syncDirectionPush:
|
||||
return syncDirectionPush
|
||||
default:
|
||||
return syncDirectionPull
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) advancedSyncAction() error {
|
||||
switch u.syncDirection {
|
||||
case syncDirectionPush:
|
||||
@@ -2728,6 +2763,18 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
for u.showSyncPush.Clicked(gtx) {
|
||||
u.syncDirection = syncDirectionPush
|
||||
}
|
||||
for u.showSettingsSyncLocal.Clicked(gtx) {
|
||||
u.settingsDraft.Sync.SourceDefault = syncSourceLocal
|
||||
}
|
||||
for u.showSettingsSyncRemote.Clicked(gtx) {
|
||||
u.settingsDraft.Sync.SourceDefault = syncSourceRemote
|
||||
}
|
||||
for u.showSettingsSyncPull.Clicked(gtx) {
|
||||
u.settingsDraft.Sync.DirectionDefault = syncDirectionPull
|
||||
}
|
||||
for u.showSettingsSyncPush.Clicked(gtx) {
|
||||
u.settingsDraft.Sync.DirectionDefault = syncDirectionPush
|
||||
}
|
||||
for u.showAutofillApprovalAsk.Clicked(gtx) {
|
||||
u.autofillFirstFillApprovalMode = autofillFirstFillApprovalAsk
|
||||
}
|
||||
@@ -3210,7 +3257,7 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
}),
|
||||
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 := material.Label(u.theme, unit.Sp(14), "Choose how KeePassGO remembers UI layout behavior, sync defaults, and KDBX security defaults without crowding the main vault flow.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
@@ -3248,6 +3295,56 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Supported values: "+strings.Join([]string{vault.KDFAES, vault.KDFArgon2}, ", "), &u.securityKDF, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(syncDialogSectionLabel(u.theme, "Sync Defaults")),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), "Advanced Sync starts from these defaults. You can still change the source or direction before a single run.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncPull, "Pull Into Current Vault", u.settingsDraft.Sync.DirectionDefault == syncDirectionPull)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncPush, "Push Current Vault Out", u.settingsDraft.Sync.DirectionDefault == syncDirectionPush)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncLocal, "Local File", u.settingsDraft.Sync.SourceDefault == syncSourceLocal)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncRemote, "Remote WebDAV", u.settingsDraft.Sync.SourceDefault == syncSourceRemote)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncDialogSummaryCard(gtx, u.theme, u.settingsDraft.Sync.SourceDefault, u.settingsDraft.Sync.DirectionDefault)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Conflict handling stays retry-safe: merged entry changes keep history, while remote save conflicts still require reopening the vault and retrying the save.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(syncDialogSectionLabel(u.theme, "Background Sync")),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Future background sync controls belong here so source, direction, and unattended behavior stay in one settings surface.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(syncDialogSectionLabel(u.theme, "Feedback")),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -3451,7 +3548,7 @@ func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
}),
|
||||
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), "Pick direction, choose the other vault, and then run the merge. The quick sync button keeps using the current source.")
|
||||
lbl := material.Label(u.theme, unit.Sp(14), "Pick direction, choose the other vault, and then run the merge. Saved source and direction defaults now live in Settings.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
|
||||
+190
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"image"
|
||||
"io"
|
||||
@@ -3558,6 +3559,118 @@ func TestUIDenseLayoutPreferencePersists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUISyncDefaultsPersistInSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), "settings.json")
|
||||
|
||||
first := newUIWithSession("desktop", &session.Manager{})
|
||||
first.settingsPath = configPath
|
||||
first.syncDefaultSourceMode = syncSourceRemote
|
||||
first.syncDefaultDirection = syncDirectionPush
|
||||
first.saveSettings()
|
||||
|
||||
second := newUIWithSession("desktop", &session.Manager{})
|
||||
second.settingsPath = configPath
|
||||
second.syncDefaultSourceMode = syncSourceLocal
|
||||
second.syncDefaultDirection = syncDirectionPull
|
||||
second.loadSettings()
|
||||
|
||||
if got := second.syncDefaultSourceMode; got != syncSourceRemote {
|
||||
t.Fatalf("syncDefaultSourceMode = %q, want remote", got)
|
||||
}
|
||||
if got := second.syncDefaultDirection; got != syncDirectionPush {
|
||||
t.Fatalf("syncDefaultDirection = %q, want push", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUILoadSettingsFallsBackToLegacySyncDefaultsInUIPreferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
legacyPath := filepath.Join(dir, "ui-prefs.json")
|
||||
content, err := json.MarshalIndent(uiPreferences{
|
||||
SyncSourceDefault: string(syncSourceRemote),
|
||||
SyncDirectionDefault: string(syncDirectionPush),
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("json.MarshalIndent() error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(legacyPath, content, 0o600); err != nil {
|
||||
t.Fatalf("os.WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
reloaded := newUIWithSession("desktop", &session.Manager{})
|
||||
reloaded.uiPreferencesPath = legacyPath
|
||||
reloaded.settingsPath = filepath.Join(dir, "settings.json")
|
||||
reloaded.syncDefaultSourceMode = syncSourceLocal
|
||||
reloaded.syncDefaultDirection = syncDirectionPull
|
||||
reloaded.loadSettings()
|
||||
|
||||
if got := reloaded.syncDefaultSourceMode; got != syncSourceRemote {
|
||||
t.Fatalf("syncDefaultSourceMode = %q after legacy load, want remote", got)
|
||||
}
|
||||
if got := reloaded.syncDefaultDirection; got != syncDirectionPush {
|
||||
t.Fatalf("syncDefaultDirection = %q after legacy load, want push", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIOpenAdvancedSyncDialogUsesSavedSyncDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", &session.Manager{})
|
||||
u.syncDefaultSourceMode = syncSourceRemote
|
||||
u.syncDefaultDirection = syncDirectionPush
|
||||
u.syncSourceMode = syncSourceLocal
|
||||
u.syncDirection = syncDirectionPull
|
||||
u.vaultPath.SetText("/vaults/current.kdbx")
|
||||
|
||||
u.openAdvancedSyncDialog()
|
||||
|
||||
if got := u.syncSourceMode; got != syncSourceRemote {
|
||||
t.Fatalf("syncSourceMode = %q after open, want remote default", got)
|
||||
}
|
||||
if got := u.syncDirection; got != syncDirectionPush {
|
||||
t.Fatalf("syncDirection = %q after open, want push default", got)
|
||||
}
|
||||
if got := u.syncLocalPath.Text(); got != "/vaults/current.kdbx" {
|
||||
t.Fatalf("syncLocalPath = %q after open, want current vault path", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUISaveSecuritySettingsPersistsSyncDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
manager := &session.Manager{}
|
||||
u := newUIWithSession("desktop", manager)
|
||||
u.masterPassword.SetText("correct horse battery staple")
|
||||
if err := u.createVaultAction(); err != nil {
|
||||
t.Fatalf("createVaultAction() error = %v", err)
|
||||
}
|
||||
u.securityCipher.SetText(vault.CipherAES256)
|
||||
u.securityKDF.SetText(vault.KDFAES)
|
||||
u.loadSettingsDraft()
|
||||
u.settingsDraft.Sync.SourceDefault = syncSourceRemote
|
||||
u.settingsDraft.Sync.DirectionDefault = syncDirectionPush
|
||||
u.settingsPath = filepath.Join(t.TempDir(), "settings.json")
|
||||
u.uiPreferencesPath = filepath.Join(t.TempDir(), "ui-prefs.json")
|
||||
|
||||
if err := u.saveSecuritySettingsAction(); err != nil {
|
||||
t.Fatalf("saveSecuritySettingsAction() error = %v", err)
|
||||
}
|
||||
|
||||
reloaded := newUIWithSession("desktop", &session.Manager{})
|
||||
reloaded.settingsPath = u.settingsPath
|
||||
reloaded.loadSettings()
|
||||
|
||||
if got := reloaded.syncDefaultSourceMode; got != syncSourceRemote {
|
||||
t.Fatalf("reloaded syncDefaultSourceMode = %q, want remote", got)
|
||||
}
|
||||
if got := reloaded.syncDefaultDirection; got != syncDirectionPush {
|
||||
t.Fatalf("reloaded syncDefaultDirection = %q, want push", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAccessibilityPreferencesPersist(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -3657,6 +3770,83 @@ func TestUINotificationPreferencesPersist(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutofillPrivacyLinesNormalizesEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := autofillPrivacyLines(" com.android.chrome \n\ncom.example.app\ncom.android.chrome\n org.keepassgo.browser ")
|
||||
want := []string{"com.android.chrome", "com.example.app", "org.keepassgo.browser"}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("autofillPrivacyLines() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinAutofillPrivacyLines(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := joinAutofillPrivacyLines([]string{"com.android.chrome", "com.example.app"})
|
||||
if got != "com.android.chrome\ncom.example.app" {
|
||||
t.Fatalf("joinAutofillPrivacyLines() = %q, want %q", got, "com.android.chrome\ncom.example.app")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAutofillPrivacyPreferencesPersist(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), "ui-prefs.json")
|
||||
|
||||
first := newUIWithSession("desktop", &session.Manager{})
|
||||
first.uiPreferencesPath = configPath
|
||||
first.autofillFirstFillApprovalMode = autofillFirstFillApprovalBlock
|
||||
first.autofillBrowserAllowlist.SetText("https://accounts.example.com\nhttps://login.example.org\nhttps://accounts.example.com")
|
||||
first.autofillAppAllowlist.SetText("org.mozilla.firefox\ncom.android.chrome")
|
||||
first.autofillPackageRules.SetText("com.android.chrome=hostname\norg.keepassgo.browser=view-id")
|
||||
first.saveUIPreferences()
|
||||
|
||||
second := newUIWithSession("desktop", &session.Manager{})
|
||||
second.uiPreferencesPath = configPath
|
||||
second.autofillFirstFillApprovalMode = autofillFirstFillApprovalAsk
|
||||
second.loadUIPreferences()
|
||||
|
||||
if got := second.autofillFirstFillApprovalMode; got != autofillFirstFillApprovalBlock {
|
||||
t.Fatalf("autofillFirstFillApprovalMode = %q, want %q", got, autofillFirstFillApprovalBlock)
|
||||
}
|
||||
if got := second.autofillBrowserAllowlist.Text(); got != "https://accounts.example.com\nhttps://login.example.org" {
|
||||
t.Fatalf("autofillBrowserAllowlist = %q, want normalized browser allowlist", got)
|
||||
}
|
||||
if got := second.autofillAppAllowlist.Text(); got != "org.mozilla.firefox\ncom.android.chrome" {
|
||||
t.Fatalf("autofillAppAllowlist = %q, want preserved allowlist entries", got)
|
||||
}
|
||||
if got := second.autofillPackageRules.Text(); got != "com.android.chrome=hostname\norg.keepassgo.browser=view-id" {
|
||||
t.Fatalf("autofillPackageRules = %q, want persisted package rules", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUILoadUIPreferencesKeepsDefaultAutofillApprovalWhenMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), "ui-prefs.json")
|
||||
content, err := json.Marshal(uiPreferences{
|
||||
GroupControlsHidden: true,
|
||||
LifecycleAdvancedHidden: true,
|
||||
HistoryHidden: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal(uiPreferences) error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(configPath, content, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(uiPreferences) error = %v", err)
|
||||
}
|
||||
|
||||
u := newUIWithSession("desktop", &session.Manager{})
|
||||
u.uiPreferencesPath = configPath
|
||||
u.autofillFirstFillApprovalMode = autofillFirstFillApprovalAsk
|
||||
u.loadUIPreferences()
|
||||
|
||||
if got := u.autofillFirstFillApprovalMode; got != autofillFirstFillApprovalAsk {
|
||||
t.Fatalf("autofillFirstFillApprovalMode = %q, want %q when preference missing", got, autofillFirstFillApprovalAsk)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectingRecentRemoteConnectionKeepsPasswordMasked(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"image/color"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -30,8 +33,23 @@ type accessibilityPreferences struct {
|
||||
KeyboardFocus string
|
||||
}
|
||||
|
||||
type settingsFile struct {
|
||||
Sync syncSettings `json:"sync,omitempty"`
|
||||
}
|
||||
|
||||
type syncSettings struct {
|
||||
SourceDefault string `json:"sourceDefault,omitempty"`
|
||||
DirectionDefault string `json:"directionDefault,omitempty"`
|
||||
}
|
||||
|
||||
type syncSettingsDraft struct {
|
||||
SourceDefault syncSourceMode
|
||||
DirectionDefault syncDirection
|
||||
}
|
||||
|
||||
type settingsDraft struct {
|
||||
Accessibility accessibilityPreferences
|
||||
Sync syncSettingsDraft
|
||||
}
|
||||
|
||||
type choiceSpec struct {
|
||||
@@ -87,6 +105,10 @@ func (u *ui) loadSettingsDraft() {
|
||||
ReducedMotion: u.accessibilityPrefs.ReducedMotion,
|
||||
KeyboardFocus: u.accessibilityPrefs.KeyboardFocus,
|
||||
},
|
||||
Sync: syncSettingsDraft{
|
||||
SourceDefault: u.syncDefaultSourceMode,
|
||||
DirectionDefault: u.syncDefaultDirection,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,13 +124,70 @@ func (u *ui) saveSecuritySettingsAction() error {
|
||||
u.settingsDraft.Accessibility.DisplayDensity = displayDensityForDenseLayout(u.settingsDenseLayout.Value)
|
||||
}
|
||||
u.settingsDenseLayout.Value = u.settingsDraft.Accessibility.DisplayDensity == displayDensityDense
|
||||
u.syncDefaultSourceMode = sanitizeSyncSourceMode(u.settingsDraft.Sync.SourceDefault)
|
||||
u.syncDefaultDirection = sanitizeSyncDirection(u.settingsDraft.Sync.DirectionDefault)
|
||||
u.applySettingsFormToPreferences()
|
||||
u.applyAccessibilityPreferences(u.settingsDraft.Accessibility)
|
||||
u.saveSettings()
|
||||
u.saveUIPreferences()
|
||||
u.securityDialogOpen = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) loadSettings() {
|
||||
u.syncDefaultSourceMode = syncSourceLocal
|
||||
u.syncDefaultDirection = syncDirectionPull
|
||||
|
||||
if strings.TrimSpace(u.settingsPath) != "" {
|
||||
content, err := os.ReadFile(u.settingsPath)
|
||||
if err == nil {
|
||||
var settings settingsFile
|
||||
if json.Unmarshal(content, &settings) == nil {
|
||||
u.syncDefaultSourceMode = sanitizeSyncSourceMode(syncSourceMode(settings.Sync.SourceDefault))
|
||||
u.syncDefaultDirection = sanitizeSyncDirection(syncDirection(settings.Sync.DirectionDefault))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u.loadLegacySyncDefaultsFromUIPreferences()
|
||||
}
|
||||
|
||||
func (u *ui) loadLegacySyncDefaultsFromUIPreferences() {
|
||||
if strings.TrimSpace(u.uiPreferencesPath) == "" {
|
||||
return
|
||||
}
|
||||
content, err := os.ReadFile(u.uiPreferencesPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var prefs uiPreferences
|
||||
if err := json.Unmarshal(content, &prefs); err != nil {
|
||||
return
|
||||
}
|
||||
u.syncDefaultSourceMode = sanitizeSyncSourceMode(syncSourceMode(prefs.SyncSourceDefault))
|
||||
u.syncDefaultDirection = sanitizeSyncDirection(syncDirection(prefs.SyncDirectionDefault))
|
||||
}
|
||||
|
||||
func (u *ui) saveSettings() {
|
||||
if strings.TrimSpace(u.settingsPath) == "" {
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(u.settingsPath), 0o700); err != nil {
|
||||
return
|
||||
}
|
||||
content, err := json.MarshalIndent(settingsFile{
|
||||
Sync: syncSettings{
|
||||
SourceDefault: string(u.syncDefaultSourceMode),
|
||||
DirectionDefault: string(u.syncDefaultDirection),
|
||||
},
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = os.WriteFile(u.settingsPath, content, 0o600)
|
||||
}
|
||||
|
||||
func (u *ui) showStatusMessage(message string) {
|
||||
u.state.StatusMessage = message
|
||||
if u.accessibilityPrefs.ReducedMotion {
|
||||
|
||||
Reference in New Issue
Block a user