feat(config): make the shell's destinations configurable
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.
This commit is contained in:
+76
-26
@@ -5,27 +5,13 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// DefaultPage identifies which view the app opens to on launch.
|
||||
type DefaultPage string
|
||||
|
||||
// Valid DefaultPage values, matching the frontend's top-level route ids.
|
||||
const (
|
||||
DefaultPageHome DefaultPage = "home"
|
||||
DefaultPageTracks DefaultPage = "tracks"
|
||||
DefaultPageAlbums DefaultPage = "albums"
|
||||
DefaultPageArtists DefaultPage = "artists"
|
||||
DefaultPageGenres DefaultPage = "genres"
|
||||
DefaultPagePlaylists DefaultPage = "playlists"
|
||||
DefaultPageExplore DefaultPage = "explore"
|
||||
DefaultPageDownloads DefaultPage = "downloads"
|
||||
DefaultPageAutotag DefaultPage = "autotag"
|
||||
DefaultPageJobs DefaultPage = "jobs"
|
||||
)
|
||||
|
||||
// DefaultDefaultPage is the launch page for a fresh install.
|
||||
const DefaultDefaultPage = DefaultPageHome
|
||||
const DefaultDefaultPage = ViewHome
|
||||
|
||||
var errUnknownDefaultPage = errors.New("unknown default page")
|
||||
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.
|
||||
@@ -46,8 +32,22 @@ 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 DefaultPage `toml:"DefaultPage"`
|
||||
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,
|
||||
@@ -71,15 +71,17 @@ func (c *GeneralConfig) ApplyDefaults() {
|
||||
func (c *GeneralConfig) Validate() error {
|
||||
c.ApplyDefaults()
|
||||
|
||||
switch c.DefaultPage {
|
||||
case DefaultPageHome, DefaultPageTracks, DefaultPageAlbums, DefaultPageArtists,
|
||||
DefaultPageGenres, DefaultPagePlaylists, DefaultPageExplore, DefaultPageDownloads,
|
||||
DefaultPageAutotag, DefaultPageJobs:
|
||||
// Valid.
|
||||
default:
|
||||
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.
|
||||
@@ -89,3 +91,51 @@ func (c *GeneralConfig) Validate() error {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user