perf(webui): faster navigation, smoother scroll, no spurious settings save
Navigation & sidebar feedback: - Show legacy pages optimistically on pointerdown + CSS :active so the sidebar reacts instantly instead of waiting for the click/router cycle. - Defer heavy per-page init via requestIdleCallback so a page becomes scrollable before its init work runs. Scroll smoothness: - Cache particle canvas dimensions (no forced reflow per navigation). - Pause particle + worker-orb canvas redraws during active scroll so the scroll gets the full frame budget. - content-visibility:auto on discover shelves and search/wishlist/library list items to skip off-screen layout. Dashboard: - Run the independent initial loads in parallel (Promise.all) instead of six sequential awaits, collapsing the reflow cascade. Settings: - Wire input listeners once instead of rescanning the ~960-node subtree on every visit. - Suppress auto-save while the form is programmatically populated on load, fixing a spurious full save (4 POSTs + backend service re-init) that fired on every Settings visit. Reduce Visual Effects = full performance mode: - Also halts particles, worker orbs and all filters; hides the static sidebar aura circles that looked broken without their blur/animation. Global search bar hidden on settings/help/issues/import pages.
This commit is contained in:
parent
25222bd8f7
commit
05c15833fa
9 changed files with 219 additions and 56 deletions
|
|
@ -381,7 +381,7 @@ function initializeWebSocket() {
|
||||||
}
|
}
|
||||||
|
|
||||||
socket = io({
|
socket = io({
|
||||||
transports: ['polling', 'websocket'],
|
transports: ['websocket', 'polling'],
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
reconnectionAttempts: Infinity,
|
reconnectionAttempts: Infinity,
|
||||||
reconnectionDelay: 1000,
|
reconnectionDelay: 1000,
|
||||||
|
|
|
||||||
|
|
@ -5784,12 +5784,12 @@ function _gsUpdateVisibility() {
|
||||||
const bar = document.getElementById('gsearch-bar');
|
const bar = document.getElementById('gsearch-bar');
|
||||||
const aura = document.getElementById('gsearch-aura');
|
const aura = document.getElementById('gsearch-aura');
|
||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
// Hide on the Search page where the unified search already exists. Accept the
|
// Hide on pages where global search doesn't belong.
|
||||||
// legacy 'downloads' id for callers that predate the page rename.
|
const _gsHidePages = new Set(['search', 'downloads', 'settings', 'help', 'issues', 'import']);
|
||||||
const onSearchPage = typeof currentPage !== 'undefined' && (currentPage === 'search' || currentPage === 'downloads');
|
const onHidePage = typeof currentPage !== 'undefined' && _gsHidePages.has(currentPage);
|
||||||
bar.style.display = onSearchPage ? 'none' : '';
|
bar.style.display = onHidePage ? 'none' : '';
|
||||||
if (aura) aura.classList.toggle('hidden', onSearchPage);
|
if (aura) aura.classList.toggle('hidden', onHidePage);
|
||||||
if (onSearchPage && _gsState.active) _gsDeactivate();
|
if (onHidePage && _gsState.active) _gsDeactivate();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _gsDeactivate() {
|
function _gsDeactivate() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
// INITIALIZATION
|
// INITIALIZATION
|
||||||
// ===============================
|
// ===============================
|
||||||
let navigationEpoch = 0;
|
let navigationEpoch = 0;
|
||||||
|
let _optimisticNavPageId = null;
|
||||||
|
|
||||||
|
// Schedule heavy per-page init during browser idle time so navigation paints and
|
||||||
|
// the page becomes scrollable first. timeout caps the delay so content still loads
|
||||||
|
// promptly. Falls back to a macrotask in browsers without requestIdleCallback.
|
||||||
|
function _scheduleHeavyInit(fn) {
|
||||||
|
if (typeof requestIdleCallback === 'function') {
|
||||||
|
requestIdleCallback(fn, { timeout: 200 });
|
||||||
|
} else {
|
||||||
|
setTimeout(fn, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function notifyPageWillChange(nextPageId) {
|
function notifyPageWillChange(nextPageId) {
|
||||||
const fromPageId = typeof currentPage === 'string' ? currentPage : null;
|
const fromPageId = typeof currentPage === 'string' ? currentPage : null;
|
||||||
|
|
@ -175,13 +187,35 @@ function applyReduceEffects(enabled) {
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.remove('reduce-effects');
|
document.body.classList.remove('reduce-effects');
|
||||||
}
|
}
|
||||||
|
window._reduceEffectsActive = enabled;
|
||||||
localStorage.setItem('soulsync-reduce-effects', enabled ? '1' : '0');
|
localStorage.setItem('soulsync-reduce-effects', enabled ? '1' : '0');
|
||||||
|
|
||||||
|
// Reduce Visual Effects is a full performance switch: also halt the canvas
|
||||||
|
// animation loops (particles + worker orbs), not just CSS effects.
|
||||||
|
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 only what the user's own toggles still allow.
|
||||||
|
const activePage = document.querySelector('.page.active');
|
||||||
|
const activeId = activePage ? activePage.id.replace('-page', '') : null;
|
||||||
|
if (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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bootstrap accent and reduce-effects from localStorage instantly (prevents flash)
|
// Bootstrap accent and reduce-effects from localStorage instantly (prevents flash)
|
||||||
(function () {
|
(function () {
|
||||||
if (localStorage.getItem('soulsync-reduce-effects') === '1') {
|
if (localStorage.getItem('soulsync-reduce-effects') === '1') {
|
||||||
document.body.classList.add('reduce-effects');
|
document.body.classList.add('reduce-effects');
|
||||||
|
window._reduceEffectsActive = true;
|
||||||
}
|
}
|
||||||
const saved = localStorage.getItem('soulsync-accent');
|
const saved = localStorage.getItem('soulsync-accent');
|
||||||
if (saved) applyAccentColor(saved);
|
if (saved) applyAccentColor(saved);
|
||||||
|
|
@ -2314,6 +2348,15 @@ function navigateToPage(pageId, options = {}) {
|
||||||
if (route?.kind === 'react') {
|
if (route?.kind === 'react') {
|
||||||
showReactHost(pageId);
|
showReactHost(pageId);
|
||||||
setActivePageChrome(pageId);
|
setActivePageChrome(pageId);
|
||||||
|
} else if (route?.kind === 'legacy' && pageId !== 'artist-detail') {
|
||||||
|
// Show legacy page immediately — don't wait for TanStack Router's async cycle
|
||||||
|
showLegacyPage(pageId);
|
||||||
|
setActivePageChrome(pageId);
|
||||||
|
_optimisticNavPageId = pageId;
|
||||||
|
// Defer data loading until the browser is idle, so the page paints AND
|
||||||
|
// becomes scrollable before heavy sync init (settings form wiring, etc.)
|
||||||
|
// runs. Falls back to a macrotask where requestIdleCallback is missing.
|
||||||
|
_scheduleHeavyInit(() => loadPageData(pageId));
|
||||||
}
|
}
|
||||||
return router.navigateToPage(pageId, {
|
return router.navigateToPage(pageId, {
|
||||||
replace: options.replace === true,
|
replace: options.replace === true,
|
||||||
|
|
@ -2413,12 +2456,20 @@ async function loadPageData(pageId) {
|
||||||
initExplorer();
|
initExplorer();
|
||||||
break;
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
initializeSettings();
|
// Suppress auto-save while the form is being populated, so opening
|
||||||
switchSettingsTab('connections');
|
// Settings no longer fires a spurious full save (4 POSTs + backend
|
||||||
await loadSettingsData();
|
// service re-init) on every visit.
|
||||||
await loadQualityProfile();
|
window._suppressSettingsAutoSave = true;
|
||||||
loadApiKeys();
|
try {
|
||||||
loadBlacklistCount();
|
initializeSettings();
|
||||||
|
switchSettingsTab('connections');
|
||||||
|
await loadSettingsData();
|
||||||
|
await loadQualityProfile();
|
||||||
|
loadApiKeys();
|
||||||
|
loadBlacklistCount();
|
||||||
|
} finally {
|
||||||
|
window._suppressSettingsAutoSave = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'hydrabase':
|
case 'hydrabase':
|
||||||
// Check connection status and pre-fill saved credentials
|
// Check connection status and pre-fill saved credentials
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,32 @@
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache viewport dims so initLayer/setPreset (runs on every page nav) doesn't
|
||||||
|
// force a synchronous layout flush by reading clientWidth/clientHeight each time.
|
||||||
|
let _vpW = 0, _vpH = 0;
|
||||||
|
function measureViewport() {
|
||||||
|
_vpW = canvas.clientWidth;
|
||||||
|
_vpH = canvas.clientHeight;
|
||||||
|
}
|
||||||
function resize() {
|
function resize() {
|
||||||
const dpr = 1; // keep 1:1 for performance
|
if (_vpW === 0) measureViewport(); // first run only
|
||||||
canvas.width = canvas.clientWidth * dpr;
|
if (canvas.width !== _vpW || canvas.height !== _vpH) {
|
||||||
canvas.height = canvas.clientHeight * dpr;
|
canvas.width = _vpW;
|
||||||
|
canvas.height = _vpH;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
let _resizeRaf = 0;
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
// Debounce to a frame; re-measure (reads layout) then apply.
|
||||||
|
if (_resizeRaf) return;
|
||||||
|
_resizeRaf = requestAnimationFrame(() => {
|
||||||
|
_resizeRaf = 0;
|
||||||
|
measureViewport();
|
||||||
|
resize();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
measureViewport();
|
||||||
resize();
|
resize();
|
||||||
|
|
||||||
// ── Preset Definitions ──
|
// ── Preset Definitions ──
|
||||||
|
|
@ -2061,9 +2080,31 @@
|
||||||
return { preset, particles: parts, extras: layerExtras, name: presetName };
|
return { preset, particles: parts, extras: layerExtras, name: presetName };
|
||||||
}
|
}
|
||||||
|
|
||||||
function loop() {
|
let _lastFrameTime = 0;
|
||||||
|
const _targetFps = 30;
|
||||||
|
const _frameInterval = 1000 / _targetFps;
|
||||||
|
|
||||||
|
// Pause particle drawing during active scroll (timestamp threshold, ms).
|
||||||
|
let _scrollPauseUntil = 0;
|
||||||
|
(function attachScrollPause() {
|
||||||
|
const scroller = document.querySelector('.main-content') || window;
|
||||||
|
scroller.addEventListener('scroll', () => {
|
||||||
|
// requestAnimationFrame timestamps share the performance.now() clock.
|
||||||
|
_scrollPauseUntil = performance.now() + 180;
|
||||||
|
}, { passive: true });
|
||||||
|
})();
|
||||||
|
|
||||||
|
function loop(timestamp) {
|
||||||
animFrame = requestAnimationFrame(loop);
|
animFrame = requestAnimationFrame(loop);
|
||||||
|
|
||||||
|
// Give the scroll its full frame budget: while the user is actively
|
||||||
|
// scrolling, skip the full-viewport canvas redraw (particles freeze on
|
||||||
|
// their last frame, then resume on scroll-idle). Eliminates scroll jank.
|
||||||
|
if (timestamp < _scrollPauseUntil) return;
|
||||||
|
|
||||||
|
if (timestamp - _lastFrameTime < _frameInterval) return;
|
||||||
|
_lastFrameTime = timestamp - ((timestamp - _lastFrameTime) % _frameInterval);
|
||||||
|
|
||||||
const w = canvas.width, h = canvas.height;
|
const w = canvas.width, h = canvas.height;
|
||||||
if (w === 0 || h === 0) { resize(); return; }
|
if (w === 0 || h === 0) { resize(); return; }
|
||||||
|
|
||||||
|
|
@ -2286,6 +2327,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.
|
||||||
|
if (window._reduceEffectsActive) { stop(); return; }
|
||||||
const presetName = PAGE_PRESETS[pageId] || 'none';
|
const presetName = PAGE_PRESETS[pageId] || 'none';
|
||||||
setPreset(presetName);
|
setPreset(presetName);
|
||||||
},
|
},
|
||||||
|
|
@ -2294,7 +2337,7 @@
|
||||||
|
|
||||||
// Auto-start for initial page (respect particles toggle)
|
// Auto-start for initial page (respect particles toggle)
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (window._particlesEnabled === false) return;
|
if (window._particlesEnabled === false || window._reduceEffectsActive) 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', '');
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ async function copyAddress(address, cryptoName) {
|
||||||
let settingsAutoSaveTimer = null;
|
let settingsAutoSaveTimer = null;
|
||||||
|
|
||||||
function debouncedAutoSaveSettings() {
|
function debouncedAutoSaveSettings() {
|
||||||
|
// Ignore changes made while the page is programmatically populating its
|
||||||
|
// fields on load — those aren't user edits and must not trigger a full
|
||||||
|
// save (which re-initializes every backend service client).
|
||||||
|
if (window._suppressSettingsAutoSave) return;
|
||||||
if (settingsAutoSaveTimer) clearTimeout(settingsAutoSaveTimer);
|
if (settingsAutoSaveTimer) clearTimeout(settingsAutoSaveTimer);
|
||||||
settingsAutoSaveTimer = setTimeout(() => saveSettings(true), 2000);
|
settingsAutoSaveTimer = setTimeout(() => saveSettings(true), 2000);
|
||||||
}
|
}
|
||||||
|
|
@ -138,9 +142,14 @@ function handleMetadataSourceChange(event) {
|
||||||
sanitizeMetadataSourceSelection({ quiet: false });
|
sanitizeMetadataSourceSelection({ quiet: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _settingsInitialized = false;
|
||||||
function initializeSettings() {
|
function initializeSettings() {
|
||||||
// This function is called when the settings page is loaded.
|
// This function is called when the settings page is loaded.
|
||||||
// It attaches event listeners to all interactive elements on the page.
|
// It attaches event listeners to all interactive elements on the page.
|
||||||
|
// Listeners are stable for the page lifetime, so wiring them once avoids
|
||||||
|
// re-scanning the ~960-node settings subtree on every revisit (scroll jank).
|
||||||
|
if (_settingsInitialized) return;
|
||||||
|
_settingsInitialized = true;
|
||||||
|
|
||||||
// Accent color listeners (live preview + custom picker toggle)
|
// Accent color listeners (live preview + custom picker toggle)
|
||||||
initAccentColorListeners();
|
initAccentColorListeners();
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,11 @@ function setActivePageChrome(pageId) {
|
||||||
downloadSidebar.classList.add('hidden');
|
downloadSidebar.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (window.pageParticles && window._particlesEnabled !== false) window.pageParticles.setPage(pageId);
|
// Defer to next frame so the page switch paints before particle/orb reinitialization
|
||||||
if (window.workerOrbs) window.workerOrbs.setPage(pageId);
|
requestAnimationFrame(() => {
|
||||||
|
if (window.pageParticles && window._particlesEnabled !== false) window.pageParticles.setPage(pageId);
|
||||||
|
if (window.workerOrbs) window.workerOrbs.setPage(pageId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showReactHost(pageId) {
|
function showReactHost(pageId) {
|
||||||
|
|
@ -83,6 +86,14 @@ function activateLegacyPath(pathname) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the page was already shown optimistically (forward nav), skip re-activation
|
||||||
|
// to avoid a duplicate data load. Back-button nav always has _optimisticNavPageId = null.
|
||||||
|
if (typeof _optimisticNavPageId !== 'undefined' && _optimisticNavPageId === targetPage) {
|
||||||
|
_optimisticNavPageId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_optimisticNavPageId = null;
|
||||||
|
|
||||||
notifyPageWillChange(targetPage);
|
notifyPageWillChange(targetPage);
|
||||||
activatePage(targetPage, { forceReload: true });
|
activatePage(targetPage, { forceReload: true });
|
||||||
}
|
}
|
||||||
|
|
@ -241,4 +252,15 @@ function _isModifiedLinkClick(event) {
|
||||||
|
|
||||||
window.addEventListener('popstate', syncActivePageFromLocation);
|
window.addEventListener('popstate', syncActivePageFromLocation);
|
||||||
document.addEventListener('click', _handleShellLinkClick, true);
|
document.addEventListener('click', _handleShellLinkClick, true);
|
||||||
|
|
||||||
|
// Fire nav on pointerdown (fires on press, 100-200ms before click) for instant sidebar response.
|
||||||
|
// navigateToPage's early-return guard (pageId === currentPage) prevents double-navigation on click.
|
||||||
|
document.addEventListener('pointerdown', (event) => {
|
||||||
|
if (event.button !== 0 || _isModifiedLinkClick(event)) return;
|
||||||
|
const btn = event.target?.closest?.('.nav-button[data-page]');
|
||||||
|
if (!btn) return;
|
||||||
|
const pageId = btn.getAttribute('data-page');
|
||||||
|
if (pageId) void navigateToPage(pageId);
|
||||||
|
}, true);
|
||||||
|
|
||||||
window.dispatchEvent(new CustomEvent(SHELL_BRIDGE_READY_EVENT));
|
window.dispatchEvent(new CustomEvent(SHELL_BRIDGE_READY_EVENT));
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,13 @@ body {
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* In performance mode the aura loses its blur + animation, leaving two hard
|
||||||
|
static circles — hide them entirely instead. */
|
||||||
|
body.reduce-effects .sidebar::before,
|
||||||
|
body.reduce-effects .sidebar::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes sidebar-orb-1 {
|
@keyframes sidebar-orb-1 {
|
||||||
0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.5; }
|
0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.5; }
|
||||||
25% { transform: translate(30px, 80px) scale(1.2); opacity: 1; }
|
25% { transform: translate(30px, 80px) scale(1.2); opacity: 1; }
|
||||||
|
|
@ -388,7 +395,7 @@ body {
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: background 0.1s ease, border-color 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease, color 0.1s ease;
|
||||||
font-family: 'SF Pro Text', -apple-system, sans-serif;
|
font-family: 'SF Pro Text', -apple-system, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
@ -408,14 +415,14 @@ body {
|
||||||
border-radius: 0 3px 3px 0;
|
border-radius: 0 3px 3px 0;
|
||||||
background: linear-gradient(180deg, rgb(var(--accent-light-rgb)), rgb(var(--accent-rgb)));
|
background: linear-gradient(180deg, rgb(var(--accent-light-rgb)), rgb(var(--accent-rgb)));
|
||||||
box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.6);
|
box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.6);
|
||||||
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: height 0.12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-button.active::before {
|
.nav-button.active::before {
|
||||||
height: 60%;
|
height: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-button:hover {
|
body:not(.reduce-effects) .nav-button:hover {
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(255, 255, 255, 0.06) 0%,
|
rgba(255, 255, 255, 0.06) 0%,
|
||||||
rgba(255, 255, 255, 0.03) 100%);
|
rgba(255, 255, 255, 0.03) 100%);
|
||||||
|
|
@ -426,20 +433,32 @@ body {
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-button:active:not(.active) {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(var(--accent-rgb), 0.14) 0%,
|
||||||
|
rgba(var(--accent-rgb), 0.10) 50%,
|
||||||
|
rgba(var(--accent-rgb), 0.06) 100%);
|
||||||
|
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 16px rgba(var(--accent-rgb), 0.2),
|
||||||
|
0 2px 8px rgba(0, 0, 0, 0.2),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-button.active {
|
.nav-button.active {
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(var(--accent-rgb), 0.14) 0%,
|
rgba(var(--accent-rgb), 0.14) 0%,
|
||||||
rgba(var(--accent-rgb), 0.10) 50%,
|
rgba(var(--accent-rgb), 0.10) 50%,
|
||||||
rgba(var(--accent-rgb), 0.06) 100%);
|
rgba(var(--accent-rgb), 0.06) 100%);
|
||||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||||
transform: translateX(6px);
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 4px 16px rgba(var(--accent-rgb), 0.2),
|
0 4px 16px rgba(var(--accent-rgb), 0.2),
|
||||||
0 2px 8px rgba(0, 0, 0, 0.2),
|
0 2px 8px rgba(0, 0, 0, 0.2),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-button.active:hover {
|
body:not(.reduce-effects) .nav-button.active:hover {
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(var(--accent-rgb), 0.20) 0%,
|
rgba(var(--accent-rgb), 0.20) 0%,
|
||||||
rgba(var(--accent-rgb), 0.16) 50%,
|
rgba(var(--accent-rgb), 0.16) 50%,
|
||||||
|
|
@ -466,7 +485,7 @@ body {
|
||||||
rgba(255, 255, 255, 0.03) 100%);
|
rgba(255, 255, 255, 0.03) 100%);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: background 0.1s ease, border-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 2px 4px rgba(0, 0, 0, 0.15),
|
0 2px 4px rgba(0, 0, 0, 0.15),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
|
@ -476,7 +495,7 @@ body {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: color 0.1s ease, opacity 0.1s ease;
|
||||||
color: rgba(var(--accent-rgb), 0.7);
|
color: rgba(var(--accent-rgb), 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4925,6 +4944,8 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
||||||
border: 1px solid rgba(70, 70, 76, 0.5);
|
border: 1px solid rgba(70, 70, 76, 0.5);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 88px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -20370,6 +20391,8 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
||||||
/* Wishlist Delete Buttons */
|
/* Wishlist Delete Buttons */
|
||||||
.wishlist-track-item {
|
.wishlist-track-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wishlist-delete-btn {
|
.wishlist-delete-btn {
|
||||||
|
|
@ -26406,6 +26429,8 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 240px;
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
@ -33576,6 +33601,10 @@ div.artist-hero-badge {
|
||||||
.discover-section {
|
.discover-section {
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
/* Skip layout/paint of off-screen shelves so entering Discover and scrolling
|
||||||
|
stays cheap. 'auto' lets the browser remember real size after first render. */
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 340px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discover-section-header {
|
.discover-section-header {
|
||||||
|
|
@ -58643,17 +58672,23 @@ tr:hover .enhanced-track-actions-group { opacity: 1; }
|
||||||
}
|
}
|
||||||
.blacklist-entry-remove:hover { background: rgba(239, 83, 80, 0.12); color: #ef5350; }
|
.blacklist-entry-remove:hover { background: rgba(239, 83, 80, 0.12); color: #ef5350; }
|
||||||
|
|
||||||
/* ── Reduce Visual Effects ── Disables GPU-heavy properties globally */
|
/* ── Reduce Visual Effects ── Full performance mode: disables GPU-heavy
|
||||||
|
properties and halts decorative animation globally. */
|
||||||
body.reduce-effects *,
|
body.reduce-effects *,
|
||||||
body.reduce-effects *::before,
|
body.reduce-effects *::before,
|
||||||
body.reduce-effects *::after {
|
body.reduce-effects *::after {
|
||||||
animation-duration: 0s !important;
|
animation: none !important;
|
||||||
animation-delay: 0s !important;
|
|
||||||
transition-duration: 0s !important;
|
transition-duration: 0s !important;
|
||||||
transition-delay: 0s !important;
|
transition-delay: 0s !important;
|
||||||
backdrop-filter: none !important;
|
backdrop-filter: none !important;
|
||||||
-webkit-backdrop-filter: none !important;
|
-webkit-backdrop-filter: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kill the background particle canvas entirely in performance mode */
|
||||||
|
body.reduce-effects #page-particles-canvas {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
|
|
|
||||||
|
|
@ -7495,35 +7495,27 @@ async function initializeToolsPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDashboardData() {
|
async function loadDashboardData() {
|
||||||
// Initial load of wishlist count
|
// Start periodic refreshers up front (independent of the initial loads).
|
||||||
await updateWishlistCount();
|
|
||||||
|
|
||||||
// Start periodic refresh of wishlist count (every 10 seconds)
|
|
||||||
stopWishlistCountPolling(); // Ensure no duplicates
|
stopWishlistCountPolling(); // Ensure no duplicates
|
||||||
wishlistCountInterval = setInterval(updateWishlistCount, 10000);
|
wishlistCountInterval = setInterval(updateWishlistCount, 10000);
|
||||||
|
setInterval(fetchAndUpdateSystemStats, 10000); // dashboard-specific (service status polled globally)
|
||||||
// Initial load of service status, system statistics, and library status
|
setInterval(fetchAndUpdateActivityFeed, 2000); // responsive activity feed
|
||||||
await fetchAndUpdateServiceStatus();
|
|
||||||
await fetchAndUpdateSystemStats();
|
|
||||||
await fetchAndUpdateDbStats();
|
|
||||||
|
|
||||||
// Service status is already polled globally (line 311)
|
|
||||||
// System stats polling kept here (dashboard-specific)
|
|
||||||
setInterval(fetchAndUpdateSystemStats, 10000);
|
|
||||||
|
|
||||||
// Initial load of activity feed
|
|
||||||
await fetchAndUpdateActivityFeed();
|
|
||||||
|
|
||||||
// Start periodic refresh of activity feed (every 2 seconds for responsiveness)
|
|
||||||
setInterval(fetchAndUpdateActivityFeed, 2000);
|
|
||||||
|
|
||||||
// Start periodic toast checking (every 3 seconds)
|
|
||||||
setInterval(checkForActivityToasts, 3000);
|
setInterval(checkForActivityToasts, 3000);
|
||||||
|
|
||||||
// Check for any active download processes that need rehydration
|
// Fire all independent initial loads in parallel instead of sequentially.
|
||||||
await checkForActiveProcesses();
|
// Sequential awaits meant 6 back-to-back round-trips, each triggering its own
|
||||||
|
// reflow — the layout kept shifting for ~1-2s, which made the page feel
|
||||||
|
// unscrollable. Concurrent loads collapse that into a single settle.
|
||||||
|
await Promise.all([
|
||||||
|
updateWishlistCount(),
|
||||||
|
fetchAndUpdateServiceStatus(),
|
||||||
|
fetchAndUpdateSystemStats(),
|
||||||
|
fetchAndUpdateDbStats(),
|
||||||
|
fetchAndUpdateActivityFeed(),
|
||||||
|
checkForActiveProcesses(),
|
||||||
|
]);
|
||||||
|
|
||||||
// Populate the Active Downloads dashboard section with any existing downloads
|
// Render existing downloads once active processes are known.
|
||||||
updateDashboardDownloads();
|
updateDashboardDownloads();
|
||||||
|
|
||||||
// Automatic wishlist processing now runs server-side
|
// Automatic wishlist processing now runs server-side
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,14 @@
|
||||||
|
|
||||||
let frameCount = 0;
|
let frameCount = 0;
|
||||||
|
|
||||||
|
let _scrollPauseUntil = 0;
|
||||||
|
(function attachScrollPause() {
|
||||||
|
const scroller = document.querySelector('.main-content') || window;
|
||||||
|
scroller.addEventListener('scroll', () => {
|
||||||
|
_scrollPauseUntil = performance.now() + 180;
|
||||||
|
}, { passive: true });
|
||||||
|
})();
|
||||||
|
|
||||||
function startLoop() {
|
function startLoop() {
|
||||||
if (animFrame) return;
|
if (animFrame) return;
|
||||||
tick();
|
tick();
|
||||||
|
|
@ -347,6 +355,9 @@
|
||||||
animFrame = requestAnimationFrame(tick);
|
animFrame = requestAnimationFrame(tick);
|
||||||
if (!canvas || !ctx) return;
|
if (!canvas || !ctx) return;
|
||||||
|
|
||||||
|
// Yield the frame to active scrolling (orbs freeze, resume on idle).
|
||||||
|
if (performance.now() < _scrollPauseUntil) return;
|
||||||
|
|
||||||
frameCount++;
|
frameCount++;
|
||||||
const time = frameCount / 60;
|
const time = frameCount / 60;
|
||||||
const w = canvas.width;
|
const w = canvas.width;
|
||||||
|
|
@ -582,7 +593,7 @@
|
||||||
// ── Page awareness ──
|
// ── Page awareness ──
|
||||||
|
|
||||||
function isEnabled() {
|
function isEnabled() {
|
||||||
return window._workerOrbsEnabled !== false;
|
return window._workerOrbsEnabled !== false && !window._reduceEffectsActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPage(pageId) {
|
function setPage(pageId) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue