package app import ( "encoding/json" "errors" "io" "sync" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/internal" ) type preferences struct { *internal.InMemoryPreferences prefLock sync.RWMutex savedRecently bool changedDuringSaving bool app *fyneApp needsSaveBeforeExit bool } // Declare conformity with Preferences interface var _ fyne.Preferences = (*preferences)(nil) // sentinel error to signal an empty preferences storage backend was loaded var errEmptyPreferencesStore = errors.New("empty preferences store") // returned from storageWriter() - may be a file, browser local storage, etc type writeSyncCloser interface { io.WriteCloser Sync() error } // forceImmediateSave writes preferences to storage immediately, ignoring the debouncing // logic in the change listener. Does nothing if preferences are not backed with a persistent store. func (p *preferences) forceImmediateSave() { if !p.needsSaveBeforeExit { return } err := p.save() if err != nil { fyne.LogError("Failed on force saving preferences", err) } } func (p *preferences) resetSavedRecently() { go func() { time.Sleep(time.Millisecond * 100) // writes are not always atomic. 10ms worked, 100 is safer. // For test reasons we need to use current app not what we were initialised with as they can differ fyne.DoAndWait(func() { p.prefLock.Lock() p.savedRecently = false changedDuringSaving := p.changedDuringSaving p.changedDuringSaving = false p.prefLock.Unlock() if changedDuringSaving { p.save() } }) }() } func (p *preferences) save() error { storage, err := p.storageWriter() if err != nil { return err } return p.saveToStorage(storage) } func (p *preferences) saveToStorage(writer writeSyncCloser) error { p.prefLock.Lock() p.savedRecently = true p.prefLock.Unlock() defer p.resetSavedRecently() defer writer.Close() encode := json.NewEncoder(writer) var err error p.InMemoryPreferences.ReadValues(func(values map[string]any) { err = encode.Encode(&values) }) err2 := writer.Sync() if err == nil { err = err2 } return err } func (p *preferences) load() { storage, err := p.storageReader() if err == nil { err = p.loadFromStorage(storage) } if err != nil && err != errEmptyPreferencesStore { fyne.LogError("Preferences load error:", err) } } func (p *preferences) loadFromStorage(storage io.ReadCloser) (err error) { defer func() { if r := storage.Close(); r != nil && err == nil { err = r } }() decode := json.NewDecoder(storage) p.InMemoryPreferences.WriteValues(func(values map[string]any) { err = decode.Decode(&values) if err != nil { return } convertLists(values) }) return err } func newPreferences(app *fyneApp) *preferences { p := &preferences{} p.app = app p.InMemoryPreferences = internal.NewInMemoryPreferences() // don't load or watch if not setup if app.uniqueID == "" && app.Metadata().ID == "" { return p } p.needsSaveBeforeExit = true p.AddChangeListener(func() { if p != app.prefs { return } p.prefLock.Lock() shouldIgnoreChange := p.savedRecently if p.savedRecently { p.changedDuringSaving = true } p.prefLock.Unlock() if shouldIgnoreChange { // callback after loading from storage, or too many updates in a row return } err := p.save() if err != nil { fyne.LogError("Failed on saving preferences", err) } }) p.watch() return p } func convertLists(values map[string]any) { for k, v := range values { if items, ok := v.([]any); ok { if len(items) == 0 { continue } switch items[0].(type) { case bool: bools := make([]bool, len(items)) for i, item := range items { bools[i] = item.(bool) } values[k] = bools case float64: floats := make([]float64, len(items)) for i, item := range items { floats[i] = item.(float64) } values[k] = floats //case int: // json has no int! case string: strings := make([]string, len(items)) for i, item := range items { strings[i] = item.(string) } values[k] = strings } } } }