Quick Actions tiles: live amplification (animation == gauge) + GPU cleanup
The three bento tiles had signature background animations that were pure decoration. Each now SURGES while its subsystem is actually working, driven by the live socket events — idle keeps the exact calm look they always had: - Auto-Sync: the EQ bars dance fast + brighter, the playhead sweeps quicker and the pulse dot races while a sync/discovery pipeline is running (sync:progress / discovery:progress) - Tools: the gear spins up 4x and brightens while a tool, scan, db-update or repair job is running (tool:* / scan:media / repair:progress, with a shape- tolerant "actually running" check so the 1s idle pushes don't light it) - Automations: the flow nodes + line signals pulse at 2.5x while an automation is firing (automation:progress) Tiles carry .is-live while the last matching event is <6s old; a 2s interval handles decay (no rAF, no per-frame JS). GPU pass on the same tiles, same visuals: - hero playhead animated `left` (layout + paint every frame, 9s loop) -> a full-width strip whose 1.5px line is a static background, transform-only - flow-node pulse animated background + box-shadow x3 nodes -> bright state painted once on a pseudo, opacity breathes; added to reduced-motion kills
This commit is contained in:
parent
318dd28748
commit
ace4b15d2e
2 changed files with 95 additions and 22 deletions
|
|
@ -469,7 +469,7 @@ function initializeWebSocket() {
|
|||
socket.on('enrichment:repair', (data) => updateRepairStatusFromData(data));
|
||||
socket.on('enrichment:soulid', (data) => updateSoulIDStatusFromData(data));
|
||||
socket.on('enrichment:listening-stats', () => { }); // Status only, no UI update needed
|
||||
socket.on('repair:progress', (data) => updateRepairJobProgressFromData(data));
|
||||
socket.on('repair:progress', (data) => { qaSignal('tools'); updateRepairJobProgressFromData(data); });
|
||||
|
||||
// Forward enrichment status to the dashboard worker-orbs so the hub fires
|
||||
// a pulse on each real item matched / error (additional listener — does not
|
||||
|
|
@ -487,15 +487,15 @@ function initializeWebSocket() {
|
|||
// 'tool:stream' is intentionally NOT wired: stream state is per-listener
|
||||
// (session cookie), so the global broadcast could only carry the DEFAULT
|
||||
// session's eternal "stopped" — the player polls /api/stream/status instead.
|
||||
socket.on('tool:quality-scanner', (data) => updateQualityScanProgressFromData(data));
|
||||
socket.on('tool:duplicate-cleaner', (data) => updateDuplicateCleanProgressFromData(data));
|
||||
socket.on('tool:db-update', (data) => updateDbProgressFromData(data));
|
||||
socket.on('tool:metadata', (data) => updateMetadataStatusFromData(data));
|
||||
socket.on('tool:quality-scanner', (data) => { if (_qaToolBusy(data)) qaSignal('tools'); updateQualityScanProgressFromData(data); });
|
||||
socket.on('tool:duplicate-cleaner', (data) => { if (_qaToolBusy(data)) qaSignal('tools'); updateDuplicateCleanProgressFromData(data); });
|
||||
socket.on('tool:db-update', (data) => { if (_qaToolBusy(data)) qaSignal('tools'); updateDbProgressFromData(data); });
|
||||
socket.on('tool:metadata', (data) => { if (_qaToolBusy(data)) qaSignal('tools'); updateMetadataStatusFromData(data); });
|
||||
socket.on('tool:logs', (data) => updateLogsFromData(data));
|
||||
|
||||
// Phase 5 event listeners (sync/discovery progress + scans)
|
||||
socket.on('sync:progress', (data) => updateSyncProgressFromData(data));
|
||||
socket.on('discovery:progress', (data) => updateDiscoveryProgressFromData(data));
|
||||
socket.on('sync:progress', (data) => { qaSignal('sync'); updateSyncProgressFromData(data); });
|
||||
socket.on('discovery:progress', (data) => { qaSignal('sync'); updateDiscoveryProgressFromData(data); });
|
||||
socket.on('scan:watchlist', (data) => {
|
||||
updateWatchlistScanFromData(data);
|
||||
const watchlistBtn = document.querySelector('.nav-button[data-page="watchlist"]');
|
||||
|
|
@ -503,12 +503,44 @@ function initializeWebSocket() {
|
|||
watchlistBtn.classList.toggle('nav-watchlist-scanning', data.status === 'scanning');
|
||||
}
|
||||
});
|
||||
socket.on('scan:media', (data) => updateMediaScanFromData(data));
|
||||
socket.on('scan:media', (data) => { if (_qaToolBusy(data)) qaSignal('tools'); updateMediaScanFromData(data); });
|
||||
socket.on('wishlist:stats', (data) => updateWishlistStatsFromData(data));
|
||||
// Phase 6: Automation progress
|
||||
socket.on('automation:progress', (data) => updateAutomationProgressFromData(data));
|
||||
socket.on('automation:progress', (data) => { qaSignal('auto'); updateAutomationProgressFromData(data); });
|
||||
}
|
||||
|
||||
// ── Quick Actions tiles: animation == gauge ──
|
||||
// Each tile's signature background animation SURGES while its subsystem is
|
||||
// actually working: the sync EQ dances while a playlist pipeline runs, the
|
||||
// gear spins up while a tool/scan/repair job runs, the automation flow pulses
|
||||
// while an automation fires. Socket handlers ping a channel; tiles carry
|
||||
// .is-live while the last ping is fresh. Idle keeps the original calm look.
|
||||
const _qaLastSignal = { sync: 0, tools: 0, auto: 0 };
|
||||
|
||||
function qaSignal(channel) {
|
||||
_qaLastSignal[channel] = Date.now();
|
||||
}
|
||||
|
||||
// Recognise "actually running" across the tool payload shapes
|
||||
// ({status:'running'}, {status:{is_scanning:true}}, {running:true}, ...).
|
||||
function _qaToolBusy(d) {
|
||||
if (!d) return false;
|
||||
const s = d.status;
|
||||
if (s && typeof s === 'object') {
|
||||
return !!(s.is_scanning || s.status === 'running' || s.status === 'scanning');
|
||||
}
|
||||
return s === 'running' || s === 'scanning' || d.is_scanning === true || d.running === true;
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
const map = { sync: '.qa-tile--sync', tools: '.qa-tile--tools', auto: '.qa-tile--auto' };
|
||||
for (const ch in map) {
|
||||
const tile = document.querySelector(map[ch]);
|
||||
if (tile) tile.classList.toggle('is-live', now - _qaLastSignal[ch] < 6000);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
function handleServiceStatusUpdate(data) {
|
||||
// Cache for library status card
|
||||
_lastStatusPayload = data;
|
||||
|
|
|
|||
|
|
@ -62200,28 +62200,34 @@ body.reduce-effects .dash-card::after {
|
|||
/* Hero playhead — slow vertical accent line that drifts across the
|
||||
equalizer like a now-playing scrubber. Fades in/out at the edges so
|
||||
the loop reset isn't visible. */
|
||||
/* GPU note: this used to animate `left`, which re-lays-out and repaints every
|
||||
* frame for the whole 9s loop. The pseudo is now a full-width strip whose
|
||||
* 1.5px line is a STATIC background (background-size positioned at center),
|
||||
* and only transform/opacity animate — compositor-only, identical look. */
|
||||
.qa-tile--hero .qa-tile__bg::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
height: 65%;
|
||||
width: 1.5px;
|
||||
background: linear-gradient(180deg,
|
||||
transparent 0%,
|
||||
rgba(var(--accent-rgb), 0.7) 40%,
|
||||
rgba(var(--accent-rgb), 0.7) 85%,
|
||||
transparent 100%);
|
||||
background:
|
||||
linear-gradient(180deg,
|
||||
transparent 0%,
|
||||
rgba(var(--accent-rgb), 0.7) 40%,
|
||||
rgba(var(--accent-rgb), 0.7) 85%,
|
||||
transparent 100%)
|
||||
50% 0 / 1.5px 100% no-repeat;
|
||||
filter: drop-shadow(0 0 4px rgba(var(--accent-rgb), 0.35));
|
||||
opacity: 0;
|
||||
transform: translateX(-54%);
|
||||
animation: qa-playhead 9s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes qa-playhead {
|
||||
0% { left: -4%; opacity: 0; }
|
||||
0% { transform: translateX(-54%); opacity: 0; }
|
||||
10% { opacity: 0.5; }
|
||||
85% { opacity: 0.5; }
|
||||
100% { left: 102%; opacity: 0; }
|
||||
100% { transform: translateX(52%); opacity: 0; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
|
@ -62273,6 +62279,7 @@ body.reduce-effects .dash-card::after {
|
|||
}
|
||||
|
||||
.qa-flow-node {
|
||||
position: relative;
|
||||
width: clamp(16px, 1.4cqw + 8px, 24px);
|
||||
aspect-ratio: 1;
|
||||
border: 1.5px solid rgba(var(--accent-rgb), 0.6);
|
||||
|
|
@ -62281,9 +62288,23 @@ body.reduce-effects .dash-card::after {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qa-flow-node:nth-of-type(1) { animation: qa-flow-pulse 3.2s ease-in-out infinite; }
|
||||
.qa-flow-node:nth-of-type(2) { animation: qa-flow-pulse 3.2s ease-in-out infinite; animation-delay: -1.07s; }
|
||||
.qa-flow-node:nth-of-type(3) { animation: qa-flow-pulse 3.2s ease-in-out infinite; animation-delay: -2.13s; }
|
||||
/* GPU note: the node pulse used to animate background + box-shadow (repaint
|
||||
* every frame, forever, x3 nodes). The bright state is painted once on a
|
||||
* pseudo and only its opacity breathes. */
|
||||
.qa-flow-node::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1.5px;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--accent-rgb), 0.4);
|
||||
box-shadow: 0 0 0 5px rgba(var(--accent-rgb), 0.18);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.qa-flow-node:nth-of-type(1)::after { animation: qa-flow-pulse 3.2s ease-in-out infinite; }
|
||||
.qa-flow-node:nth-of-type(2)::after { animation: qa-flow-pulse 3.2s ease-in-out infinite; animation-delay: -1.07s; }
|
||||
.qa-flow-node:nth-of-type(3)::after { animation: qa-flow-pulse 3.2s ease-in-out infinite; animation-delay: -2.13s; }
|
||||
|
||||
.qa-flow-line {
|
||||
position: relative;
|
||||
|
|
@ -62323,17 +62344,37 @@ body.reduce-effects .dash-card::after {
|
|||
}
|
||||
|
||||
@keyframes qa-flow-pulse {
|
||||
0%, 100% { background: rgba(var(--accent-rgb), 0.1); box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0); }
|
||||
50% { background: rgba(var(--accent-rgb), 0.5); box-shadow: 0 0 0 5px rgba(var(--accent-rgb), 0.18); }
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.qa-tile--auto:hover .qa-tile__flow,
|
||||
.qa-tile--auto:focus-visible .qa-tile__flow { opacity: 0.85; }
|
||||
|
||||
/* ── Live amplification (animation == gauge) ──
|
||||
* Each tile's signature animation SURGES while its subsystem is actually
|
||||
* working — sync pipeline running, a tool/scan/repair job running, an
|
||||
* automation firing. core.js toggles .is-live from the live socket events;
|
||||
* idle keeps the exact calm look these tiles always had. Purely additive. */
|
||||
.qa-tile--sync.is-live .qa-tile__eq { opacity: 0.65; }
|
||||
.qa-tile--sync.is-live .qa-tile__eq span { animation-duration: 1.1s; }
|
||||
.qa-tile--sync.is-live .qa-tile__bg::after { animation-duration: 4s; }
|
||||
.qa-tile--sync.is-live .qa-tile__pulse::after { animation-duration: 0.9s; }
|
||||
|
||||
.qa-tile--tools.is-live .qa-tile__gear {
|
||||
color: rgba(var(--accent-rgb), 0.16);
|
||||
animation-duration: 7s;
|
||||
}
|
||||
|
||||
.qa-tile--auto.is-live .qa-tile__flow { opacity: 0.85; }
|
||||
.qa-tile--auto.is-live .qa-flow-node::after { animation-duration: 1.3s; }
|
||||
.qa-tile--auto.is-live .qa-flow-line::before { animation-duration: 1.3s; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.qa-tile__eq span,
|
||||
.qa-tile__gear,
|
||||
.qa-flow-node,
|
||||
.qa-flow-node::after,
|
||||
.qa-tile__pulse::after { animation: none !important; }
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue