0.2.76 - Icon Packs

This commit is contained in:
ponzischeme89
2026-08-18 14:59:29 +12:00
parent 36d171e51b
commit 8c847c59b8
70 changed files with 5267 additions and 673 deletions
+81 -4
View File
@@ -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(),