feat: add Cancel and Defaults buttons to Settings

Cancel discards unsaved edits by reloading the saved config; Defaults
loads built-in default values into the form for review before saving.
Extracts the default config into domain.DefaultConfig() so the store
and settings UI share one source of truth.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mixeme
2026-07-26 10:15:36 +03:00
parent 84212764e4
commit d3e3d30b42
3 changed files with 59 additions and 18 deletions
+19
View File
@@ -62,6 +62,25 @@ type Config struct {
Theme Theme `json:"theme,omitempty"`
}
// DefaultConfig returns the built-in default settings. It is the config used
// when gosentry.json does not yet exist, and is also what the Settings UI
// offers to restore via its "Defaults" button.
func DefaultConfig() Config {
return Config{
JobsDir: ".",
LogsDir: "logs",
MaxLogFiles: 100,
MaxLogAgeDays: 30,
StartOnLogin: false,
KeepRunningInTray: true,
NotifyOnFailure: true,
ExecutionMode: ExecutionModeParallel,
OverlapPolicy: OverlapPolicySkip,
Theme: ThemeDefault,
DefaultTimeoutSeconds: 30,
}
}
// JobsFile is the on-disk shape of jobs.json. Wrapping the slice in a top-level
// object leaves room for future metadata without breaking the basic file format.
type JobsFile struct {
+1 -14
View File
@@ -68,20 +68,7 @@ func (s *Store) SaveJobs(jobs []domain.Job) error {
func loadOrCreateConfig(paths Paths) (domain.Config, error) {
// Defaults favor a portable installation: settings and jobs begin next to the
// executable, while logs are grouped under a dedicated subdirectory.
config := domain.Config{
JobsDir: ".",
LogsDir: "logs",
MaxLogFiles: 100,
MaxLogAgeDays: 30,
StartOnLogin: false,
KeepRunningInTray: true,
NotifyOnFailure: true,
ExecutionMode: domain.ExecutionModeParallel,
OverlapPolicy: domain.OverlapPolicySkip,
Theme: domain.ThemeDefault,
DefaultTimeoutSeconds: 30,
}
config := domain.DefaultConfig()
if _, err := os.Stat(paths.ConfigPath); errors.Is(err, os.ErrNotExist) {
return config, writeJSON(paths.ConfigPath, config)
+39 -4
View File
@@ -35,6 +35,9 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
// when something differs. It is defined below (once Save and every field
// exist) but declared here so the field change handlers can reference it.
var updateSaveState func()
// loadFields populates every form control from the given config. It backs
// both the initial load and the Cancel/Defaults buttons below.
var loadFields func(domain.Config)
startOnLogin := widget.NewCheck("Start on login", nil)
startOnLogin.SetChecked(store.Config.StartOnLogin)
autostartStatus := widget.NewLabel("")
@@ -188,6 +191,38 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
}
updateSaveState()
// loadFields populates every form control from a config without saving it,
// backing both the Cancel button (reload the saved config, discarding edits)
// and the Defaults button (load the built-in defaults for review before
// Save is clicked).
loadFields = func(c domain.Config) {
startOnLogin.SetChecked(c.StartOnLogin)
minimizeToTray.SetChecked(c.KeepRunningInTray)
notifications.SetChecked(c.NotifyOnFailure)
themeSelect.SetSelected(themeLabel(c.Theme))
applyTheme(fyne.CurrentApp(), themeFromLabel(themeSelect.Selected))
executionModeSelect.SetSelected(string(c.ExecutionMode))
overlapPolicySelect.SetSelected(string(c.OverlapPolicy))
defaultTimeout.SetText(strconv.Itoa(c.DefaultTimeoutSeconds))
jobsDir.SetText(c.JobsDir)
logsDir.SetText(c.LogsDir)
maxLogFiles.SetText(strconv.Itoa(c.MaxLogFiles))
maxLogAgeDays.SetText(strconv.Itoa(c.MaxLogAgeDays))
if startOnLogin.Checked != store.Config.StartOnLogin {
autostartStatus.SetText("Pending: save settings to apply")
} else {
refreshAutostartStatus()
}
settingsStatus.SetText("")
updateSaveState()
}
cancelSettings := widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func() {
loadFields(store.Config)
})
restoreDefaults := widget.NewButtonWithIcon("Defaults", theme.MediaReplayIcon(), func() {
loadFields(domain.DefaultConfig())
})
// The form is split into two columns so a wide window uses its horizontal
// space instead of stretching into one tall strip. The left column holds the
// toggles (Application, Queue); the right holds the editable Storage fields and
@@ -241,10 +276,10 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
return container.NewVScroll(container.NewPadded(container.NewVBox(
container.NewGridWithColumns(2, leftColumn, rightColumn),
widget.NewSeparator(),
// Save button and its status share one row so an empty status (the common
// case) does not leave a blank line above the separator. The status appears
// beside the button once a save reports a result.
container.NewHBox(saveSettings, settingsStatus),
// Save/Cancel/Defaults share one row with the status so an empty status
// (the common case) does not leave a blank line above the separator. The
// status appears beside the buttons once a save reports a result.
container.NewHBox(saveSettings, cancelSettings, restoreDefaults, settingsStatus),
)))
}