ui appearance: default worker orbs OFF on Firefox for first-time users

The blurred 60fps worker-orb canvas is the main remaining Firefox lag source after the
#935 sweep (multiple Discord lag reports). So for a FIRST-TIME user with no saved
preference, default the orbs OFF on Firefox (smooth first impression where it's needed)
and ON everywhere else (full polish where the browser handles it). An explicit saved
choice ALWAYS wins — this only picks the default when the user hasn't chosen.

Done kettui-style with a SINGLE source of truth, not the dual browser-detection I first
floated (server UA + client _isFirefox would be the same fact in two places that can
drift — exactly the server/client class #943's green-flash fix just cleaned up):

- core/ui_appearance.py (new, pure + importable): is_firefox_user_agent +
  resolve_worker_orbs_default(explicit, is_firefox) — explicit wins, unset → !firefox.
- web_server: the SERVER decides (UA via _request_is_firefox, request-context-safe) and
  injects initial_worker_orbs_enabled; config default flipped None so "unset" is
  distinguishable from an explicit False. The client just consumes the injected value
  (init.js unchanged) — no client-side re-derivation of "is Firefox".
- settings.js: the orb checkbox default now reflects the server value when unset, so
  saving Settings can't silently flip a first-time Firefox user's orbs back on.

No regression: Chrome users unchanged; users with an explicit setting unchanged (it
wins regardless of browser); /api/settings returns raw config so it can't clobber the
default for an unset value. Verified end-to-end through a real Flask request context
(Firefox→off, Chrome→on, explicit wins both ways, no crash outside a request). 8 pure
seam tests pin the contract; ruff clean.
This commit is contained in:
BoulderBadgeDad 2026-06-28 16:50:22 -07:00
parent 3207310448
commit a62d2d4310
4 changed files with 126 additions and 3 deletions

40
core/ui_appearance.py Normal file
View file

@ -0,0 +1,40 @@
"""Pure decisions for UI-appearance defaults.
Kept here (importable, no Flask/config coupling) so the rules are unit-testable in
isolation; web_server does only the request/config plumbing around them.
Worker orbs are a blurred 60fps canvas the main remaining Firefox lag source after
the #935 sweep. So for a FIRST-TIME user (no saved preference) we default them OFF on
Firefox and ON everywhere else: a smooth first impression where it's needed, full
polish where the browser handles it. An explicit saved choice ALWAYS wins this only
picks the default when the user hasn't chosen.
"""
from __future__ import annotations
from typing import Optional
def is_firefox_user_agent(user_agent: Optional[str]) -> bool:
"""True when a User-Agent string is Firefox.
Used ONLY to pick a performance-friendly default never to gate functionality
so a spoofed or unusual UA simply gets the default and the user can toggle. Chrome,
Edge, Safari, Opera, Brave do not carry 'firefox' in their UA; Firefox does
('… Gecko/… Firefox/<ver>')."""
return 'firefox' in str(user_agent or '').lower()
def resolve_worker_orbs_default(explicit: object, is_firefox: bool) -> bool:
"""Whether worker orbs should be on.
``explicit`` is the saved config value: ``True``/``False`` when the user has chosen,
``None`` when unset. An explicit choice always wins; when unset, default OFF on
Firefox (perf) and ON elsewhere.
"""
if explicit is None:
return not is_firefox
return explicit is not False
__all__ = ['is_firefox_user_agent', 'resolve_worker_orbs_default']

View file

@ -0,0 +1,56 @@
"""Pure UI-appearance default rules (core/ui_appearance.py).
Pins the worker-orbs default contract: explicit saved choice ALWAYS wins; when unset,
default OFF on Firefox (the blurred orb canvas is the main remaining Firefox lag
source) and ON elsewhere."""
from core.ui_appearance import is_firefox_user_agent, resolve_worker_orbs_default
_FIREFOX_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) "
"Gecko/20100101 Firefox/128.0")
_CHROME_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
_SAFARI_UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
"(KHTML, like Gecko) Version/17.0 Safari/605.1.15")
_EDGE_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0")
# ── is_firefox_user_agent ──
def test_detects_firefox():
assert is_firefox_user_agent(_FIREFOX_UA) is True
def test_non_firefox_browsers_are_false():
for ua in (_CHROME_UA, _SAFARI_UA, _EDGE_UA):
assert is_firefox_user_agent(ua) is False
def test_empty_or_none_ua_is_not_firefox():
assert is_firefox_user_agent('') is False
assert is_firefox_user_agent(None) is False
# ── resolve_worker_orbs_default: explicit ALWAYS wins ──
def test_unset_defaults_off_on_firefox():
assert resolve_worker_orbs_default(None, is_firefox=True) is False
def test_unset_defaults_on_elsewhere():
assert resolve_worker_orbs_default(None, is_firefox=False) is True
def test_explicit_true_wins_even_on_firefox():
# A Firefox user who explicitly enabled orbs keeps them — default never overrides.
assert resolve_worker_orbs_default(True, is_firefox=True) is True
def test_explicit_false_wins_even_off_firefox():
assert resolve_worker_orbs_default(False, is_firefox=False) is False
def test_explicit_values_ignore_browser():
assert resolve_worker_orbs_default(True, is_firefox=False) is True
assert resolve_worker_orbs_default(False, is_firefox=True) is False

View file

@ -84,6 +84,7 @@ if not pp_logger.handlers:
pp_logger.propagate = False
from core.spotify_client import SpotifyClient, Playlist as SpotifyPlaylist, Track as SpotifyTrack, _is_globally_rate_limited as _spotify_rate_limited
from core.plex_client import PlexClient
from core.ui_appearance import is_firefox_user_agent, resolve_worker_orbs_default
from plexapi.myplex import MyPlexAccount, MyPlexPinLogin
from core.jellyfin_client import JellyfinClient
from core.navidrome_client import NavidromeClient
@ -359,12 +360,32 @@ def _hsl_to_rgb(hue, saturation, lightness):
)
def _request_is_firefox() -> bool:
"""Whether the current request's browser is Firefox (UA-based). Used ONLY to pick a
performance-friendly default; an explicit saved setting always wins. Safe outside a
request context (returns False)."""
try:
from flask import has_request_context, request
if not has_request_context():
return False
return is_firefox_user_agent(request.headers.get('User-Agent'))
except Exception:
return False
def _initial_appearance_context():
preset = config_manager.get('ui_appearance.accent_preset', '#1db954')
custom = config_manager.get('ui_appearance.accent_color', '#1db954')
accent = _valid_hex_color(custom if preset == 'custom' else preset)
particles_enabled = config_manager.get('ui_appearance.particles_enabled', False) is True
worker_orbs_enabled = config_manager.get('ui_appearance.worker_orbs_enabled', True) is not False
# Worker orbs: explicit choice wins; unset → OFF on Firefox (the blurred orb canvas
# is the main remaining Firefox lag source), ON elsewhere. config default None so we
# can tell "unset" from an explicit False. (#kettui — single source: the server
# decides, the client consumes the injected value below.)
worker_orbs_enabled = resolve_worker_orbs_default(
config_manager.get('ui_appearance.worker_orbs_enabled', None),
_request_is_firefox(),
)
reduce_effects = config_manager.get('ui_appearance.reduce_effects', False) is True
r, g, b = _hex_to_rgb(accent)
hue, saturation, lightness = _rgb_to_hsl(r, g, b)

View file

@ -1548,8 +1548,14 @@ async function loadSettingsData() {
if (particlesCheckbox) particlesCheckbox.checked = particlesEnabled;
applyParticlesSetting(particlesEnabled);
// Worker orbs toggle
const workerOrbsEnabled = settings.ui_appearance?.worker_orbs_enabled !== false; // default true
// Worker orbs toggle. When the user hasn't saved a preference, reflect the
// server-decided browser-aware default (window._workerOrbsEnabled — OFF on
// Firefox for perf) so saving settings doesn't silently flip a first-time
// Firefox user's orbs back on. An explicit saved config value always wins.
const _orbsCfg = settings.ui_appearance?.worker_orbs_enabled;
const workerOrbsEnabled = (_orbsCfg === undefined || _orbsCfg === null)
? (window._workerOrbsEnabled !== false)
: (_orbsCfg !== false);
const workerOrbsCheckbox = document.getElementById('worker-orbs-enabled');
if (workerOrbsCheckbox) workerOrbsCheckbox.checked = workerOrbsEnabled;
applyWorkerOrbsSetting(workerOrbsEnabled);