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:
parent
3207310448
commit
a62d2d4310
4 changed files with 126 additions and 3 deletions
40
core/ui_appearance.py
Normal file
40
core/ui_appearance.py
Normal 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']
|
||||||
56
tests/test_ui_appearance.py
Normal file
56
tests/test_ui_appearance.py
Normal 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
|
||||||
|
|
@ -84,6 +84,7 @@ if not pp_logger.handlers:
|
||||||
pp_logger.propagate = False
|
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.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.plex_client import PlexClient
|
||||||
|
from core.ui_appearance import is_firefox_user_agent, resolve_worker_orbs_default
|
||||||
from plexapi.myplex import MyPlexAccount, MyPlexPinLogin
|
from plexapi.myplex import MyPlexAccount, MyPlexPinLogin
|
||||||
from core.jellyfin_client import JellyfinClient
|
from core.jellyfin_client import JellyfinClient
|
||||||
from core.navidrome_client import NavidromeClient
|
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():
|
def _initial_appearance_context():
|
||||||
preset = config_manager.get('ui_appearance.accent_preset', '#1db954')
|
preset = config_manager.get('ui_appearance.accent_preset', '#1db954')
|
||||||
custom = config_manager.get('ui_appearance.accent_color', '#1db954')
|
custom = config_manager.get('ui_appearance.accent_color', '#1db954')
|
||||||
accent = _valid_hex_color(custom if preset == 'custom' else preset)
|
accent = _valid_hex_color(custom if preset == 'custom' else preset)
|
||||||
particles_enabled = config_manager.get('ui_appearance.particles_enabled', False) is True
|
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
|
reduce_effects = config_manager.get('ui_appearance.reduce_effects', False) is True
|
||||||
r, g, b = _hex_to_rgb(accent)
|
r, g, b = _hex_to_rgb(accent)
|
||||||
hue, saturation, lightness = _rgb_to_hsl(r, g, b)
|
hue, saturation, lightness = _rgb_to_hsl(r, g, b)
|
||||||
|
|
|
||||||
|
|
@ -1548,8 +1548,14 @@ async function loadSettingsData() {
|
||||||
if (particlesCheckbox) particlesCheckbox.checked = particlesEnabled;
|
if (particlesCheckbox) particlesCheckbox.checked = particlesEnabled;
|
||||||
applyParticlesSetting(particlesEnabled);
|
applyParticlesSetting(particlesEnabled);
|
||||||
|
|
||||||
// Worker orbs toggle
|
// Worker orbs toggle. When the user hasn't saved a preference, reflect the
|
||||||
const workerOrbsEnabled = settings.ui_appearance?.worker_orbs_enabled !== false; // default true
|
// 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');
|
const workerOrbsCheckbox = document.getElementById('worker-orbs-enabled');
|
||||||
if (workerOrbsCheckbox) workerOrbsCheckbox.checked = workerOrbsEnabled;
|
if (workerOrbsCheckbox) workerOrbsCheckbox.checked = workerOrbsEnabled;
|
||||||
applyWorkerOrbsSetting(workerOrbsEnabled);
|
applyWorkerOrbsSetting(workerOrbsEnabled);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue