The sidebar's eleven entries are more than most libraries need, and Autotag rewrites tags on disk, which is not what a fresh install should be one click from. Stored as a map keyed by view id, where an absent key means that view's own default. A `HiddenViews []string` cannot express "Autotag off by default" -- its zero value is *hide nothing* -- and a boolean per view turns a view that later stops existing into stored garbage. With a map, an unknown key is dropped on load, a view added later gets its own default, and no install needs migrating in either direction. Same polarity as AllowMeteredCatalogDownload: the zero value is the intended answer. `Views` is also what DefaultPage now validates against, so which views exist and which may be the launch page are one list rather than two. Two states the user could not get out of are refused rather than allowed: Settings is never hideable, and the launch page is not hideable while it is the launch page. Both refuse in the *config*, not in the UI, because `config.toml` is hand-editable. On load the launch page is instead un-hidden -- there is nobody to tell, and the honest reading of "my launch page is Autotag" is that this user wants Autotag, not that their launch page should be silently reset.
142 lines
4.8 KiB
Go
142 lines
4.8 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.
|
|
func (c *GeneralConfig) ApplyDefaults() {
|
|
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
|
|
}
|