Merge pull request #948 from nick2000713/codex/fix-startup-freeze
Fix post-update UI freeze: stop password managers re-scanning the DOM + add Max Performance mode
This commit is contained in:
commit
b05b641521
8 changed files with 258 additions and 21 deletions
|
|
@ -387,6 +387,7 @@ def _initial_appearance_context():
|
||||||
_request_is_firefox(),
|
_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
|
||||||
|
max_performance = config_manager.get('ui_appearance.max_performance', 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)
|
||||||
light = _hsl_to_rgb(hue, saturation, min(lightness + 0.16, 0.95))
|
light = _hsl_to_rgb(hue, saturation, min(lightness + 0.16, 0.95))
|
||||||
|
|
@ -399,6 +400,7 @@ def _initial_appearance_context():
|
||||||
'initial_particles_enabled': particles_enabled,
|
'initial_particles_enabled': particles_enabled,
|
||||||
'initial_worker_orbs_enabled': worker_orbs_enabled,
|
'initial_worker_orbs_enabled': worker_orbs_enabled,
|
||||||
'initial_reduce_effects': reduce_effects,
|
'initial_reduce_effects': reduce_effects,
|
||||||
|
'initial_max_performance': max_performance,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,12 @@
|
||||||
window._particlesEnabled = {{ initial_particles_enabled|tojson }};
|
window._particlesEnabled = {{ initial_particles_enabled|tojson }};
|
||||||
window._workerOrbsEnabled = {{ initial_worker_orbs_enabled|tojson }};
|
window._workerOrbsEnabled = {{ initial_worker_orbs_enabled|tojson }};
|
||||||
window._reduceEffectsActive = {{ initial_reduce_effects|tojson }};
|
window._reduceEffectsActive = {{ initial_reduce_effects|tojson }};
|
||||||
|
window._maxPerfActive = {{ initial_max_performance|tojson }};
|
||||||
</script>
|
</script>
|
||||||
{{ vite_assets('head')|safe }}
|
{{ vite_assets('head')|safe }}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body{% if initial_reduce_effects %} class="reduce-effects"{% endif %}>
|
<body class="{% if initial_reduce_effects %}reduce-effects {% endif %}{% if initial_max_performance %}max-performance{% endif %}">
|
||||||
<!-- Setup Wizard Overlay -->
|
<!-- Setup Wizard Overlay -->
|
||||||
<div id="setup-wizard-overlay" class="setup-wizard-overlay" style="display: none;">
|
<div id="setup-wizard-overlay" class="setup-wizard-overlay" style="display: none;">
|
||||||
<div class="setup-wizard-container">
|
<div class="setup-wizard-container">
|
||||||
|
|
@ -6438,7 +6439,14 @@
|
||||||
<input type="checkbox" id="reduce-effects-enabled">
|
<input type="checkbox" id="reduce-effects-enabled">
|
||||||
Reduce Visual Effects
|
Reduce Visual Effects
|
||||||
</label>
|
</label>
|
||||||
<small class="settings-hint">Disables backdrop blur, animations, transitions, and shadows. Significantly reduces GPU/CPU usage on low-end devices.</small>
|
<small class="settings-hint">Disables backdrop blur and shadows (the GPU-heavy bits). Keeps functional motion like loading spinners. Good for low-end devices.</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="max-performance-enabled">
|
||||||
|
Max Performance
|
||||||
|
</label>
|
||||||
|
<small class="settings-hint">Nuclear low-power mode for software-rendered / no-GPU setups (e.g. Docker, remote desktop). Turns off Worker Orbs, Particles, all blur/shadows AND every animation & transition (loading spinners go static). Overrides the three options above.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -386,8 +386,8 @@ function _renderEqualizerBars(grid, data) {
|
||||||
// Call embers: tiny accent sparks rise off the fill tip, spawned per
|
// Call embers: tiny accent sparks rise off the fill tip, spawned per
|
||||||
// socket update in proportion to REAL traffic — motion strictly means
|
// socket update in proportion to REAL traffic — motion strictly means
|
||||||
// API calls are happening right now. Suppressed during cooldown and
|
// API calls are happening right now. Suppressed during cooldown and
|
||||||
// under reduced-effects.
|
// under reduced-effects / max-performance.
|
||||||
if (!window._reduceEffectsActive && !cooling && realPct > 0.03) {
|
if (!window._reduceEffectsActive && !window._maxPerfActive && !cooling && realPct > 0.03) {
|
||||||
_spawnEmbers(bar, pct, realPct > 0.6 ? 3 : realPct > 0.25 ? 2 : 1);
|
_spawnEmbers(bar, pct, realPct > 0.6 ? 3 : realPct > 0.25 ? 2 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,14 @@ function initAccentColorListeners() {
|
||||||
applyReduceEffects(reduceEffectsCheckbox.checked);
|
applyReduceEffects(reduceEffectsCheckbox.checked);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Max Performance toggle — apply immediately on change
|
||||||
|
const maxPerfCheckbox = document.getElementById('max-performance-enabled');
|
||||||
|
if (maxPerfCheckbox) {
|
||||||
|
maxPerfCheckbox.addEventListener('change', () => {
|
||||||
|
applyMaxPerformance(maxPerfCheckbox.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyReduceEffects(enabled) {
|
function applyReduceEffects(enabled) {
|
||||||
|
|
@ -211,6 +219,67 @@ function applyReduceEffects(enabled) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Max Performance overrides Worker Orbs / Particles / Reduce Effects, so while it's
|
||||||
|
// on we lock those checkboxes (greyed + visually off) and restore them when it's
|
||||||
|
// off. We never fire their change handlers, so the user's real saved prefs
|
||||||
|
// (window._workerOrbsEnabled / _particlesEnabled / the reduce-effects localStorage)
|
||||||
|
// stay intact — saving reads those, not these forced-off boxes.
|
||||||
|
function _syncMaxPerfDependentToggles(maxPerfOn) {
|
||||||
|
const ids = ['worker-orbs-enabled', 'particles-enabled', 'reduce-effects-enabled'];
|
||||||
|
ids.forEach(id => {
|
||||||
|
const cb = document.getElementById(id);
|
||||||
|
if (!cb) return;
|
||||||
|
const group = cb.closest('.form-group');
|
||||||
|
if (maxPerfOn) {
|
||||||
|
cb.disabled = true;
|
||||||
|
cb.checked = false;
|
||||||
|
if (group) group.classList.add('setting-overridden');
|
||||||
|
} else {
|
||||||
|
cb.disabled = false;
|
||||||
|
if (group) group.classList.remove('setting-overridden');
|
||||||
|
// Restore each box to the user's real per-device preference.
|
||||||
|
if (id === 'worker-orbs-enabled') cb.checked = window._workerOrbsEnabled !== false;
|
||||||
|
else if (id === 'particles-enabled') cb.checked = window._particlesEnabled === true;
|
||||||
|
else if (id === 'reduce-effects-enabled') cb.checked = localStorage.getItem('soulsync-reduce-effects') === '1';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max Performance — the nuclear low-power switch for software-rendered / no-GPU
|
||||||
|
// setups (e.g. Docker). Superset of Reduce Visual Effects: body.max-performance CSS
|
||||||
|
// kills the expensive GPU properties AND all animation/transitions, while here we
|
||||||
|
// halt every JS canvas loop (particles + worker orbs; cursor-glow + API sparks gate
|
||||||
|
// on window._maxPerfActive themselves).
|
||||||
|
function applyMaxPerformance(enabled) {
|
||||||
|
if (enabled) {
|
||||||
|
document.body.classList.add('max-performance');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('max-performance');
|
||||||
|
}
|
||||||
|
window._maxPerfActive = enabled;
|
||||||
|
localStorage.setItem('soulsync-max-performance', enabled ? '1' : '0');
|
||||||
|
|
||||||
|
const pcanvas = document.getElementById('page-particles-canvas');
|
||||||
|
if (enabled) {
|
||||||
|
if (window.pageParticles) window.pageParticles.stop();
|
||||||
|
if (pcanvas) pcanvas.style.display = 'none';
|
||||||
|
if (window.workerOrbs) window.workerOrbs.setPage('_disabled');
|
||||||
|
} else {
|
||||||
|
// Restore whatever the user's own toggles (and reduce-effects) still allow.
|
||||||
|
const reduce = window._reduceEffectsActive === true;
|
||||||
|
const activePage = document.querySelector('.page.active');
|
||||||
|
const activeId = activePage ? activePage.id.replace('-page', '') : null;
|
||||||
|
if (!reduce && window._particlesEnabled !== false) {
|
||||||
|
if (pcanvas) pcanvas.style.display = '';
|
||||||
|
if (window.pageParticles && activeId) window.pageParticles.setPage(activeId);
|
||||||
|
}
|
||||||
|
if (window._workerOrbsEnabled !== false && window.workerOrbs && activeId) {
|
||||||
|
window.workerOrbs.setPage(activeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_syncMaxPerfDependentToggles(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
// Bootstrap accent and reduce-effects from localStorage instantly (prevents flash)
|
// Bootstrap accent and reduce-effects from localStorage instantly (prevents flash)
|
||||||
(function () {
|
(function () {
|
||||||
// Auto performance mode on likely-weak hardware. Only acts when this device has
|
// Auto performance mode on likely-weak hardware. Only acts when this device has
|
||||||
|
|
@ -264,6 +333,19 @@ function applyReduceEffects(enabled) {
|
||||||
} else if (window._reduceEffectsActive) {
|
} else if (window._reduceEffectsActive) {
|
||||||
document.body.classList.add('reduce-effects');
|
document.body.classList.add('reduce-effects');
|
||||||
}
|
}
|
||||||
|
// Max Performance — device-scoped (localStorage wins over the server default,
|
||||||
|
// same as reduce-effects). The window flag is seeded server-side in index.html
|
||||||
|
// for a flash-free first paint; localStorage reconciles it here.
|
||||||
|
const maxPerfSaved = localStorage.getItem('soulsync-max-performance');
|
||||||
|
if (maxPerfSaved === '1') {
|
||||||
|
document.body.classList.add('max-performance');
|
||||||
|
window._maxPerfActive = true;
|
||||||
|
} else if (maxPerfSaved === '0') {
|
||||||
|
document.body.classList.remove('max-performance');
|
||||||
|
window._maxPerfActive = false;
|
||||||
|
} else if (window._maxPerfActive) {
|
||||||
|
document.body.classList.add('max-performance');
|
||||||
|
}
|
||||||
const saved = localStorage.getItem('soulsync-accent');
|
const saved = localStorage.getItem('soulsync-accent');
|
||||||
if (saved) applyAccentColor(saved);
|
if (saved) applyAccentColor(saved);
|
||||||
// Bootstrap particles setting from localStorage — OFF by default (continuous
|
// Bootstrap particles setting from localStorage — OFF by default (continuous
|
||||||
|
|
@ -319,6 +401,90 @@ async function bootstrapServerAppearanceSettings() {
|
||||||
|
|
||||||
bootstrapServerAppearanceSettings();
|
bootstrapServerAppearanceSettings();
|
||||||
|
|
||||||
|
// ── Password-manager autofill suppression ──────────────────────────────
|
||||||
|
// Bitwarden / 1Password / LastPass etc. attach an inline autofill overlay to
|
||||||
|
// every <input>/<select>/<textarea> and REBUILD it on every DOM mutation. This
|
||||||
|
// app mutates the DOM continuously (live service status, download/automation
|
||||||
|
// progress bars, the per-second "next run" countdown, innerHTML hub rebuilds),
|
||||||
|
// so the managers' whole-document MutationObserver storms the main thread. A
|
||||||
|
// captured DevTools trace (2026-06-29) showed Bitwarden's
|
||||||
|
// bootstrap-autofill-overlay.js (setupOverlayOnField / setupOverlayListeners)
|
||||||
|
// using ~6× the CPU of the entire SoulSync app — almost the whole freeze.
|
||||||
|
//
|
||||||
|
// None of these fields are credentials (they're search boxes, filters, config),
|
||||||
|
// so we mark them ignored and the managers skip them: once a field carries the
|
||||||
|
// ignore hint, the overlay is never (re)attached, so the mutation→re-setup storm
|
||||||
|
// stops. Real sign-in fields (password type + the auth overlays) are left alone
|
||||||
|
// so the user can still autofill the login / PIN screen. Purely additive data-*
|
||||||
|
// attributes — no functional effect on the app, and a no-op for any manager that
|
||||||
|
// doesn't honour them.
|
||||||
|
(function suppressPasswordManagerAutofill() {
|
||||||
|
const SKIP_CONTAINERS = ['#login-overlay', '#launch-pin-overlay', '#profile-pin-dialog'];
|
||||||
|
const isCredentialField = (el) => {
|
||||||
|
if (el.type === 'password') return true;
|
||||||
|
return SKIP_CONTAINERS.some(sel => typeof el.closest === 'function' && el.closest(sel));
|
||||||
|
};
|
||||||
|
const IGNORE_ATTRS = ['data-bwignore', 'data-1p-ignore', 'data-lpignore', 'data-form-type'];
|
||||||
|
const tag = (el) => {
|
||||||
|
if (el.dataset.pmTagged) return; // tagged once — never touch again
|
||||||
|
if (isCredentialField(el)) return; // leave real login fields for the manager
|
||||||
|
el.dataset.pmTagged = '1';
|
||||||
|
el.setAttribute('data-bwignore', 'true'); // Bitwarden
|
||||||
|
el.setAttribute('data-1p-ignore', ''); // 1Password
|
||||||
|
el.setAttribute('data-lpignore', 'true'); // LastPass
|
||||||
|
el.setAttribute('data-form-type', 'other'); // Dashlane
|
||||||
|
if (!el.hasAttribute('autocomplete')) el.setAttribute('autocomplete', 'off');
|
||||||
|
};
|
||||||
|
const sweep = () => {
|
||||||
|
document.querySelectorAll(
|
||||||
|
'input:not([data-pm-tagged]),textarea:not([data-pm-tagged]),select:not([data-pm-tagged])'
|
||||||
|
).forEach(tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debounce: a burst of DOM mutations triggers at most one sweep per idle slot.
|
||||||
|
// The `:not([data-pm-tagged])` selector makes the steady-state sweep a no-op
|
||||||
|
// (it only ever processes freshly-added inputs), and our own attribute writes
|
||||||
|
// don't re-arm the observer (it watches childList, not attributes).
|
||||||
|
let pending = false, observer = null, disabled = false;
|
||||||
|
const scheduleSweep = () => {
|
||||||
|
if (disabled || pending) return;
|
||||||
|
pending = true;
|
||||||
|
const run = () => { pending = false; if (!disabled) sweep(); };
|
||||||
|
if (typeof requestIdleCallback === 'function') requestIdleCallback(run, { timeout: 400 });
|
||||||
|
else setTimeout(run, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startObserving = () => {
|
||||||
|
if (observer) return;
|
||||||
|
observer = new MutationObserver(scheduleSweep);
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
};
|
||||||
|
const start = () => { sweep(); startObserving(); };
|
||||||
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
|
||||||
|
else start();
|
||||||
|
|
||||||
|
// Benchmark hook (not used by the app): toggle the suppression at runtime so a
|
||||||
|
// before/after can be measured without rebuilding. disable() strips the ignore
|
||||||
|
// hints + stops the observer, so password managers re-attach their autofill
|
||||||
|
// overlay — i.e. the pre-fix "before" behaviour. enable() re-tags + resumes.
|
||||||
|
window.__pmSuppress = {
|
||||||
|
disable() {
|
||||||
|
disabled = true;
|
||||||
|
if (observer) { observer.disconnect(); observer = null; }
|
||||||
|
document.querySelectorAll('[data-pm-tagged]').forEach((el) => {
|
||||||
|
IGNORE_ATTRS.forEach((a) => el.removeAttribute(a));
|
||||||
|
delete el.dataset.pmTagged;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enable() {
|
||||||
|
disabled = false;
|
||||||
|
sweep();
|
||||||
|
startObserving();
|
||||||
|
},
|
||||||
|
get isActive() { return !disabled; },
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
// ── Profile System ─────────────────────────────────────────────
|
// ── Profile System ─────────────────────────────────────────────
|
||||||
let currentProfile = null;
|
let currentProfile = null;
|
||||||
const PROFILE_CONTEXT_CHANGED_EVENT = 'ss:webui-profile-context-changed';
|
const PROFILE_CONTEXT_CHANGED_EVENT = 'ss:webui-profile-context-changed';
|
||||||
|
|
@ -2995,7 +3161,8 @@ async function loadPageData(pageId) {
|
||||||
const RECENTER_DELAY_MS = 1500;
|
const RECENTER_DELAY_MS = 1500;
|
||||||
let recenterTimer = 0;
|
let recenterTimer = 0;
|
||||||
|
|
||||||
const isReduced = () => document.body.classList.contains('reduce-effects');
|
const isReduced = () => document.body.classList.contains('reduce-effects')
|
||||||
|
|| document.body.classList.contains('max-performance');
|
||||||
|
|
||||||
const gridCenter = () => {
|
const gridCenter = () => {
|
||||||
const r = grid.getBoundingClientRect();
|
const r = grid.getBoundingClientRect();
|
||||||
|
|
|
||||||
|
|
@ -2352,8 +2352,8 @@
|
||||||
// Listen for page changes from script.js
|
// Listen for page changes from script.js
|
||||||
window.pageParticles = {
|
window.pageParticles = {
|
||||||
setPage(pageId) {
|
setPage(pageId) {
|
||||||
// Reduce Visual Effects performance mode halts the loop entirely.
|
// Reduce Visual Effects / Max Performance modes halt the loop entirely.
|
||||||
if (window._reduceEffectsActive) { stop(); return; }
|
if (window._reduceEffectsActive || window._maxPerfActive) { stop(); return; }
|
||||||
const presetName = PAGE_PRESETS[pageId] || 'none';
|
const presetName = PAGE_PRESETS[pageId] || 'none';
|
||||||
setPreset(presetName);
|
setPreset(presetName);
|
||||||
},
|
},
|
||||||
|
|
@ -2362,7 +2362,7 @@
|
||||||
|
|
||||||
// Auto-start for initial page (respect particles toggle)
|
// Auto-start for initial page (respect particles toggle)
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (window._particlesEnabled === false || window._reduceEffectsActive) return;
|
if (window._particlesEnabled === false || window._reduceEffectsActive || window._maxPerfActive) return;
|
||||||
const activePage = document.querySelector('.page.active');
|
const activePage = document.querySelector('.page.active');
|
||||||
if (activePage) {
|
if (activePage) {
|
||||||
const pageId = activePage.id.replace('-page', '');
|
const pageId = activePage.id.replace('-page', '');
|
||||||
|
|
|
||||||
|
|
@ -1572,6 +1572,16 @@ async function loadSettingsData() {
|
||||||
if (reduceCheckbox) reduceCheckbox.checked = reduceEffects;
|
if (reduceCheckbox) reduceCheckbox.checked = reduceEffects;
|
||||||
applyReduceEffects(reduceEffects);
|
applyReduceEffects(reduceEffects);
|
||||||
|
|
||||||
|
// Max Performance — same device-scoped resolution as reduce-effects:
|
||||||
|
// localStorage is the per-device truth, server value the cross-device default.
|
||||||
|
// Applied last so it can lock/override the dependent toggles above when on.
|
||||||
|
const serverMaxPerf = settings.ui_appearance?.max_performance === true; // default false
|
||||||
|
const localMaxPerf = localStorage.getItem('soulsync-max-performance'); // '1' | '0' | null
|
||||||
|
const maxPerf = localMaxPerf !== null ? (localMaxPerf === '1') : serverMaxPerf;
|
||||||
|
const maxPerfCheckbox = document.getElementById('max-performance-enabled');
|
||||||
|
if (maxPerfCheckbox) maxPerfCheckbox.checked = maxPerf;
|
||||||
|
applyMaxPerformance(maxPerf);
|
||||||
|
|
||||||
// Populate Logging information
|
// Populate Logging information
|
||||||
const logLevelSelect = document.getElementById('log-level-select');
|
const logLevelSelect = document.getElementById('log-level-select');
|
||||||
if (logLevelSelect) logLevelSelect.value = settings.logging?.level || 'INFO';
|
if (logLevelSelect) logLevelSelect.value = settings.logging?.level || 'INFO';
|
||||||
|
|
@ -3493,9 +3503,13 @@ async function saveSettings(quiet = false) {
|
||||||
accent_preset: document.getElementById('accent-preset')?.value || '#1db954',
|
accent_preset: document.getElementById('accent-preset')?.value || '#1db954',
|
||||||
accent_color: document.getElementById('accent-custom-color')?.value || '#1db954',
|
accent_color: document.getElementById('accent-custom-color')?.value || '#1db954',
|
||||||
sidebar_visualizer: document.getElementById('sidebar-visualizer-type')?.value || 'bars',
|
sidebar_visualizer: document.getElementById('sidebar-visualizer-type')?.value || 'bars',
|
||||||
particles_enabled: document.getElementById('particles-enabled')?.checked !== false,
|
// Read the runtime flags / localStorage, not the checkboxes: while Max
|
||||||
worker_orbs_enabled: document.getElementById('worker-orbs-enabled')?.checked !== false,
|
// Performance is on it locks those boxes visually-off, but the user's real
|
||||||
reduce_effects: document.getElementById('reduce-effects-enabled')?.checked === true
|
// saved prefs live in the flags — so saving must not clobber them.
|
||||||
|
particles_enabled: window._particlesEnabled !== false,
|
||||||
|
worker_orbs_enabled: window._workerOrbsEnabled !== false,
|
||||||
|
reduce_effects: window._reduceEffectsActive === true,
|
||||||
|
max_performance: window._maxPerfActive === true
|
||||||
},
|
},
|
||||||
youtube: {
|
youtube: {
|
||||||
cookies_browser: document.getElementById('youtube-cookies-browser').value,
|
cookies_browser: document.getElementById('youtube-cookies-browser').value,
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,9 @@ body {
|
||||||
/* In performance mode the aura loses its blur + animation, leaving two hard
|
/* In performance mode the aura loses its blur + animation, leaving two hard
|
||||||
static circles — hide them entirely instead. */
|
static circles — hide them entirely instead. */
|
||||||
body.reduce-effects .sidebar::before,
|
body.reduce-effects .sidebar::before,
|
||||||
body.reduce-effects .sidebar::after {
|
body.reduce-effects .sidebar::after,
|
||||||
|
body.max-performance .sidebar::before,
|
||||||
|
body.max-performance .sidebar::after {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,12 +502,14 @@ body:not(.reduce-effects) .nav-button.active:hover {
|
||||||
transparent border, so no layout shift). The expensive full-effects
|
transparent border, so no layout shift). The expensive full-effects
|
||||||
bits (gradient, transform/translateX, multi-layer box-shadow) stay
|
bits (gradient, transform/translateX, multi-layer box-shadow) stay
|
||||||
off so hovering doesn't trigger compositing/repaint churn. */
|
off so hovering doesn't trigger compositing/repaint churn. */
|
||||||
body.reduce-effects .nav-button:hover {
|
body.reduce-effects .nav-button:hover,
|
||||||
|
body.max-performance .nav-button:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
border-color: rgba(255, 255, 255, 0.08);
|
border-color: rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.reduce-effects .nav-button.active:hover {
|
body.reduce-effects .nav-button.active:hover,
|
||||||
|
body.max-performance .nav-button.active:hover {
|
||||||
background: rgba(var(--accent-rgb), 0.18);
|
background: rgba(var(--accent-rgb), 0.18);
|
||||||
border-color: rgba(var(--accent-rgb), 0.3);
|
border-color: rgba(var(--accent-rgb), 0.3);
|
||||||
}
|
}
|
||||||
|
|
@ -56088,6 +56092,13 @@ tr.tag-diff-same {
|
||||||
#settings-page .info-icon {
|
#settings-page .info-icon {
|
||||||
/* Single clean circle with a centred "i" (was the ⓘ glyph, which carried
|
/* Single clean circle with a centred "i" (was the ⓘ glyph, which carried
|
||||||
its own inner circle → a double-circle that read as off-centre). */
|
its own inner circle → a double-circle that read as off-centre). */
|
||||||
|
/* It's a role="button" with a text "i" inside — without these, hovering the
|
||||||
|
glyph gives the I-beam text caret on Windows (Linux happened to resolve a
|
||||||
|
pointer). Force the button hand + make the glyph non-selectable so the
|
||||||
|
cursor is the same on every platform. */
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -60406,6 +60417,38 @@ body.reduce-effects #page-particles-canvas {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Max Performance ── Nuclear low-power mode for software-rendered / no-GPU
|
||||||
|
setups (e.g. Docker, remote desktop), where there is no compositor and even
|
||||||
|
transform/opacity motion repaints on the CPU. Superset of Reduce Visual Effects:
|
||||||
|
kills the expensive GPU properties AND halts ALL animation + transitions — so
|
||||||
|
functional loading spinners go static. That's the deliberate trade-off for
|
||||||
|
minimum CPU; the user opts in explicitly. Worker orbs, particles, the cursor
|
||||||
|
glow and the API-monitor sparks are halted in JS (gated on window._maxPerfActive). */
|
||||||
|
body.max-performance *,
|
||||||
|
body.max-performance *::before,
|
||||||
|
body.max-performance *::after {
|
||||||
|
animation: none !important;
|
||||||
|
transition-duration: 0s !important;
|
||||||
|
transition-delay: 0s !important;
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
-webkit-backdrop-filter: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drop the always-on page-particles canvas from the compositor entirely. */
|
||||||
|
body.max-performance #page-particles-canvas {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Appearance toggles that Max Performance overrides: greyed + locked while it's on. */
|
||||||
|
.form-group.setting-overridden {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
.form-group.setting-overridden .checkbox-label {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
ACTIVE DOWNLOADS PAGE — Premium Glassmorphic
|
ACTIVE DOWNLOADS PAGE — Premium Glassmorphic
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
@ -63476,7 +63519,9 @@ body[data-artist-source="source"] #artist-detail-page #library-artist-enhance-bt
|
||||||
}
|
}
|
||||||
|
|
||||||
body.reduce-effects .dash-card::before,
|
body.reduce-effects .dash-card::before,
|
||||||
body.reduce-effects .dash-card::after {
|
body.reduce-effects .dash-card::after,
|
||||||
|
body.max-performance .dash-card::before,
|
||||||
|
body.max-performance .dash-card::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.dash-card:hover {
|
.dash-card:hover {
|
||||||
|
|
|
||||||
|
|
@ -1065,12 +1065,13 @@
|
||||||
// ── Page awareness ──
|
// ── Page awareness ──
|
||||||
|
|
||||||
function isEnabled() {
|
function isEnabled() {
|
||||||
// The worker-orbs toggle controls the orbs on its own — reduce-effects no
|
// Reduce-effects does NOT gate the orbs — they have their own toggle, so that
|
||||||
// longer force-kills them. The orb glow is canvas radial gradients, NOT a CSS
|
// setting controls them on its own. The orbs ARE killed by Max Performance: in a
|
||||||
// blur(28px) (that's the sidebar aura orbs + frosted glass, which reduce-effects
|
// Docker / headless / remote-desktop container there is no GPU, so the per-frame
|
||||||
// still kills), so the per-frame cost is moderate, not the blur-rasterize lag.
|
// radial-gradient canvas fill rasterizes on the CPU and can saturate a core,
|
||||||
// If real telemetry says otherwise, revert by re-adding `&& !window._reduceEffectsActive`.
|
// freezing the whole UI. Max Performance is the nuclear low-power switch for
|
||||||
return window._workerOrbsEnabled !== false;
|
// exactly those software-rendered setups, so the canvas loop must stay off there.
|
||||||
|
return window._workerOrbsEnabled !== false && !window._maxPerfActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Real telemetry → pulses ──
|
// ── Real telemetry → pulses ──
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue