Nothing it carried is gone -- the two commits before this put all of it somewhere the work is already being done. A retired destination is the one shape #25's storage decision does not make free. A visibility entry is a map key and an unknown key is dropped on load; a launch page is a *value*, and an unknown one fails validation -- which on the load path means the app refuses to start for whoever had Jobs selected. `RetiredViews` is that list, read by `ApplyDefaults`, which treats a retired name as a zero value. An unknown-but-not-retired name still errors, because that is a typo and saying so is the useful answer.
152 lines
5.2 KiB
Go
152 lines
5.2 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// DefaultDefaultPage is the launch page for a fresh install.
|
|
const DefaultDefaultPage = ViewHome
|
|
|
|
var (
|
|
errUnknownDefaultPage = errors.New("unknown default page")
|
|
errViewCannotLaunch = errors.New("view cannot be the launch page")
|
|
)
|
|
|
|
// QueueFallback identifies what plays, if anything, once the queue
|
|
// runs out with nothing left to auto-advance to.
|
|
type QueueFallback string
|
|
|
|
// Valid QueueFallback values.
|
|
const (
|
|
QueueFallbackStop QueueFallback = "stop"
|
|
QueueFallbackFavorites QueueFallback = "favorites"
|
|
QueueFallbackDynamicMix QueueFallback = "dynamicMix"
|
|
)
|
|
|
|
// DefaultQueueFallback is the fallback behavior for a fresh install.
|
|
const DefaultQueueFallback = QueueFallbackFavorites
|
|
|
|
var errUnknownQueueFallback = errors.New("unknown queue fallback")
|
|
|
|
// GeneralConfig holds general application preferences that don't
|
|
// belong to a more specific subsystem.
|
|
type GeneralConfig struct {
|
|
DefaultPage View `toml:"DefaultPage"`
|
|
QueueFallback QueueFallback `toml:"QueueFallback"`
|
|
// ViewVisibility says which sidebar destinations are shown, keyed by
|
|
// view id.
|
|
//
|
|
// **An absent key means that view's own default** (`Views`), and that
|
|
// is the whole reason this is a map rather than a `HiddenViews
|
|
// []string` or a struct of booleans. A list's zero value is "hide
|
|
// nothing", which cannot express Autotag being off by default without
|
|
// a migration; a struct field for a view that later stops existing is
|
|
// stored garbage somebody has to deprecate. Here a view added later
|
|
// gets its own default rather than being invisible or forcibly
|
|
// visible, an unknown key is dropped on load, and no install needs
|
|
// migrating in either direction. Same polarity rule as
|
|
// AllowMeteredCatalogDownload: the zero value is the intended answer.
|
|
ViewVisibility map[string]bool `toml:"ViewVisibility"`
|
|
// AllowMeteredCatalogDownload permits the ~0.6 GB Explore catalog to
|
|
// be fetched on a connection the platform calls cellular. It defaults
|
|
// to false, which is the whole point: the zero value is the safe one,
|
|
// so an existing config with no such key refuses by default rather
|
|
// than needing a migration to become careful.
|
|
AllowMeteredCatalogDownload bool `toml:"AllowMeteredCatalogDownload"`
|
|
}
|
|
|
|
// ApplyDefaults fills zero-value fields with sensible defaults.
|
|
//
|
|
// A launch page naming a *retired* view is treated as a zero value
|
|
// rather than as an error, because the alternative is an app that will
|
|
// not start for anyone who had that page selected when it was removed.
|
|
// An unknown-but-not-retired name still fails Validate: that is a typo,
|
|
// and telling someone about it is the useful answer.
|
|
func (c *GeneralConfig) ApplyDefaults() {
|
|
if _, retired := RetiredViews[c.DefaultPage]; retired {
|
|
c.DefaultPage = ""
|
|
}
|
|
|
|
if c.DefaultPage == "" {
|
|
c.DefaultPage = DefaultDefaultPage
|
|
}
|
|
|
|
if c.QueueFallback == "" {
|
|
c.QueueFallback = DefaultQueueFallback
|
|
}
|
|
}
|
|
|
|
// Validate checks that all values are well-formed.
|
|
func (c *GeneralConfig) Validate() error {
|
|
c.ApplyDefaults()
|
|
|
|
spec, known := LookupView(string(c.DefaultPage))
|
|
if !known {
|
|
return fmt.Errorf("%w: %q", errUnknownDefaultPage, c.DefaultPage)
|
|
}
|
|
|
|
if !spec.CanLaunch {
|
|
return fmt.Errorf("%w: %q", errViewCannotLaunch, c.DefaultPage)
|
|
}
|
|
|
|
c.normalizeViewVisibility()
|
|
|
|
switch c.QueueFallback {
|
|
case QueueFallbackStop, QueueFallbackFavorites, QueueFallbackDynamicMix:
|
|
// Valid.
|
|
default:
|
|
return fmt.Errorf("%w: %q", errUnknownQueueFallback, c.QueueFallback)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// normalizeViewVisibility drops what the stored map may not say, and
|
|
// repairs the one invariant the shell depends on.
|
|
//
|
|
// Three things are dropped or forced, and all three are reachable only
|
|
// from a hand-edited config or from a version that knew different
|
|
// views: an unknown id (a view removed since, e.g. when #27 folds Jobs
|
|
// into Settings) says nothing to anybody; a view that is not Hideable
|
|
// cannot be false; and **the launch page is always visible**, because
|
|
// otherwise an install lands on a page with no nav item pointing at it.
|
|
//
|
|
// That last one is a *repair* here and an *error* at the setter
|
|
// (SetViewVisible), deliberately. On load there is nobody to tell and
|
|
// the honest reading of "my launch page is Autotag" is that this user
|
|
// wants Autotag, so it is un-hidden rather than the launch page being
|
|
// silently reset to something they did not choose. At the setter the
|
|
// user is right there and can act, so it refuses and says why.
|
|
func (c *GeneralConfig) normalizeViewVisibility() {
|
|
for id := range c.ViewVisibility {
|
|
spec, known := LookupView(id)
|
|
if !known || !spec.Hideable {
|
|
delete(c.ViewVisibility, id)
|
|
}
|
|
}
|
|
|
|
if visible, ok := c.ViewVisibility[string(c.DefaultPage)]; ok && !visible {
|
|
c.ViewVisibility[string(c.DefaultPage)] = true
|
|
}
|
|
}
|
|
|
|
// ResolvedViewVisibility answers for every known view, so no caller has
|
|
// to know the defaults -- the frontend included, which is why the
|
|
// binding returns this rather than the stored map.
|
|
func (c *GeneralConfig) ResolvedViewVisibility() map[string]bool {
|
|
resolved := make(map[string]bool, len(Views))
|
|
|
|
for _, v := range Views {
|
|
visible := v.VisibleByDefault
|
|
|
|
if stored, ok := c.ViewVisibility[string(v.ID)]; ok && v.Hideable {
|
|
visible = stored
|
|
}
|
|
|
|
resolved[string(v.ID)] = visible
|
|
}
|
|
|
|
return resolved
|
|
}
|