0.2.76 - Icon Packs
This commit is contained in:
@@ -68,6 +68,39 @@ const (
|
||||
decorationBlossom = "blossom"
|
||||
)
|
||||
|
||||
// The icon packs a theme may draw its marks from. Slugs, for the reason the decorations
|
||||
// above are slugs: the shapes live on the television, in ui/theme/MembyIconPacks.kt, and
|
||||
// the gateway has no business describing geometry to it. A client that does not recognise
|
||||
// one draws the marks it shipped with — so this list may gain a pack before the fleet has
|
||||
// the build that knows it, the MembyHeroLabel precedent again.
|
||||
//
|
||||
// The reason to want any of this is that Material's marks are the marks every Android app
|
||||
// on the television already wears. Moving a household off them is a decision an operator
|
||||
// should be able to make in the gateway, not one that waits on an APK reaching every set.
|
||||
const (
|
||||
iconPackMaterial = "material"
|
||||
iconPackLucide = "lucide"
|
||||
iconPackFontAwesome = "fontawesome"
|
||||
)
|
||||
|
||||
// defaultIconPackID is the marks the app shipped with, so nothing changes appearance on the
|
||||
// day this lands.
|
||||
const defaultIconPackID = iconPackMaterial
|
||||
|
||||
// iconPackOptions is the vocabulary of the `iconSet` preference, and the only place a pack
|
||||
// is declared. A pack this list does not name is one no viewer and no operator can select.
|
||||
//
|
||||
// The wording is about how the marks read from a sofa, because that is the whole of the
|
||||
// choice: stroke sets are drawn for 16-24px on a monitor, and on a rail chip at three
|
||||
// metres they go thin where a solid mark keeps its shape.
|
||||
func iconPackOptions() []preferenceOption {
|
||||
return []preferenceOption{
|
||||
option(iconPackMaterial, "Material — Android's own marks"),
|
||||
option(iconPackLucide, "Lucide — lighter, drawn as outlines"),
|
||||
option(iconPackFontAwesome, "Font Awesome — solid, clearest at a distance"),
|
||||
}
|
||||
}
|
||||
|
||||
type themeDefinition struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -83,6 +116,14 @@ type themeDefinition struct {
|
||||
// it. It is empty on every selectable theme by construction rather than by a check at
|
||||
// the point of use.
|
||||
Decoration string `json:"decoration,omitempty"`
|
||||
// IconSet lets a theme bring its own marks, and like Decoration only a seasonal theme
|
||||
// carries one. Empty means the viewer's own choice stands.
|
||||
//
|
||||
// The asymmetry with the palette is deliberate. A season *is* a look, so it may say
|
||||
// what the marks are; a scheme somebody picked to live with all year must not silently
|
||||
// take their marks away, because there would be no way to tell which of the two
|
||||
// choices had done it.
|
||||
IconSet string `json:"iconSet,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -164,6 +205,7 @@ var themeCatalogue = []themeDefinition{
|
||||
ID: themeHalloween, Name: "Halloween", Seasonal: true,
|
||||
Description: "Pumpkin orange on black, for the last week of October.",
|
||||
Decoration: decorationBats,
|
||||
IconSet: iconPackFontAwesome,
|
||||
Palette: themePalette{
|
||||
Surface: "#FF0A0704", SurfaceRaised: "#FF17100A", Accent: "#FFFF8A1F",
|
||||
OnSurface: "#FFF2E7DA", MutedText: "#FFE2D2BE", QuietText: "#FFBBA48C",
|
||||
@@ -341,6 +383,12 @@ type resolvedTheme struct {
|
||||
// theme by the television — a set holding a cached Christmas palette must not keep
|
||||
// snowing after the switch has been thrown.
|
||||
Decoration string `json:"decoration,omitempty"`
|
||||
// IconSet is the pack the television draws its marks from: "material", "lucide",
|
||||
// "fontawesome". Resolved here rather than derived on the set from the theme id,
|
||||
// exactly as Decoration is — a television holding a cached seasonal answer must stop
|
||||
// using that season's marks when the switch is thrown, and it has no way to know that
|
||||
// on its own.
|
||||
IconSet string `json:"iconSet,omitempty"`
|
||||
// Reason is the sentence the picker prints while it is locked. The gateway's wording,
|
||||
// the MembyHeroLabel precedent, so a season invented later reads correctly on today's
|
||||
// build rather than as a blank space where an explanation should be.
|
||||
@@ -364,6 +412,7 @@ type resolvedTheme struct {
|
||||
// a season. The only switch is seasonalEnabled, and that is the operator's feature flag.
|
||||
func resolveTheme(
|
||||
chosen string,
|
||||
chosenIconPack string,
|
||||
allowed []string,
|
||||
seasonalEnabled bool,
|
||||
decorationsEnabled bool,
|
||||
@@ -394,15 +443,38 @@ func resolveTheme(
|
||||
decoration = applied.Decoration
|
||||
}
|
||||
|
||||
// The viewer's marks, unless the season brought its own. Note that this is *not* gated
|
||||
// on decorationsEnabled: that switch is about the cost of a continuous animation on a
|
||||
// weak box, and a set of icons costs nothing to draw.
|
||||
iconPack := chosenIconPack
|
||||
if !knownIconPack(iconPack) {
|
||||
iconPack = defaultIconPackID
|
||||
}
|
||||
if seasonal && applied.IconSet != "" {
|
||||
iconPack = applied.IconSet
|
||||
}
|
||||
|
||||
resolved := resolvedTheme{
|
||||
ID: applied.ID, Name: applied.Name, Palette: applied.Palette,
|
||||
Seasonal: seasonal, Locked: seasonal, Chosen: pick.ID, Reason: reason,
|
||||
Decoration: decoration,
|
||||
Decoration: decoration, IconSet: iconPack,
|
||||
}
|
||||
resolved.Revision = themeRevision(resolved)
|
||||
return resolved
|
||||
}
|
||||
|
||||
// knownIconPack keeps an unreadable or retired slug out of the answer. The television
|
||||
// would fall back on its own — membyIconPackFor answers with Material for anything it does
|
||||
// not know — but a gateway that echoed a pack nobody can draw would make every set in the
|
||||
// house look broken in the same way while reporting that it had done what it was asked.
|
||||
func knownIconPack(id string) bool {
|
||||
switch id {
|
||||
case iconPackMaterial, iconPackLucide, iconPackFontAwesome:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// themeAllowed applies the operator's per-user list. An empty list is *permissive*: no row
|
||||
// has ever been written for the great majority of households, and reading that as "this
|
||||
// person may have no themes" would empty every picker in the house the day this ships.
|
||||
@@ -422,7 +494,7 @@ func themeRevision(resolved resolvedTheme) string {
|
||||
for _, part := range []string{
|
||||
strconv.Itoa(themeSchemaVersion), resolved.ID, resolved.Chosen,
|
||||
strconv.FormatBool(resolved.Seasonal), strconv.FormatBool(resolved.Locked), resolved.Reason,
|
||||
resolved.Decoration,
|
||||
resolved.Decoration, resolved.IconSet,
|
||||
palette.Surface, palette.SurfaceRaised, palette.Accent, palette.OnSurface,
|
||||
palette.MutedText, palette.QuietText, palette.Hairline, palette.RatingsSurface,
|
||||
} {
|
||||
@@ -442,12 +514,17 @@ func themeRevision(resolved resolvedTheme) string {
|
||||
// palette it falls back to is the one the app shipped with.
|
||||
func (s *Server) themeFor(ctx context.Context, sess store.Session) resolvedTheme {
|
||||
chosen, _ := preferenceDefault("themeId").(string)
|
||||
iconPack, _ := preferenceDefault("iconSet").(string)
|
||||
allowed := []string(nil)
|
||||
if s.store != nil && sess.EmbyUserID != "" {
|
||||
if stored, err := s.store.UserPreferences(ctx, sess.EmbyUserID); err == nil {
|
||||
if value, ok := decodePreferences(stored.Preferences)["themeId"].(string); ok {
|
||||
decoded := decodePreferences(stored.Preferences)
|
||||
if value, ok := decoded["themeId"].(string); ok {
|
||||
chosen = value
|
||||
}
|
||||
if value, ok := decoded["iconSet"].(string); ok {
|
||||
iconPack = value
|
||||
}
|
||||
} else {
|
||||
s.loggerFor(ctx).Warn("theme preference unavailable", "error", err)
|
||||
}
|
||||
@@ -458,7 +535,7 @@ func (s *Server) themeFor(ctx context.Context, sess store.Session) resolvedTheme
|
||||
}
|
||||
}
|
||||
return resolveTheme(
|
||||
chosen, allowed,
|
||||
chosen, iconPack, allowed,
|
||||
s.featureEnabled(ctx, featureSeasonalThemes),
|
||||
s.featureEnabled(ctx, featureSeasonalDecorations),
|
||||
s.now(),
|
||||
|
||||
Reference in New Issue
Block a user