mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
perf(tracklist): drop now-playing pulse + EQ-bar animations (#488)
* perf(tracklist): replace animated EQ-bar + active-row pulse with static icon The currently-playing track in any tracklist (AlbumDetail, ArtistDetail, PlaylistDetail, Favorites, RandomMix) was rendered with two animations on top of an already-busy DOM: - `.track-row.active` ran `track-pulse` 3s opacity 1 → 0.6 → 1 infinite on the entire row subtree (title button, several Lucide icons, star rating SVGs, hover affordances). Opacity is a compositor property, but on WebKitGTK without compositing — Linux + NVIDIA proprietary + WEBKIT_DISABLE_COMPOSITING_MODE=1 — every animated row falls back to a full software repaint of the subtree per frame. - The "now playing" indicator in the track-number cell was three `<span class="eq-bar">` siblings with `transform: scaleY()` keyframes (`eq-bounce`), each on its own delay/duration. Same composite story: three software-repainted layers per frame, every frame. On AlbumDetail (long tracklist + cover-art header background + the already-running WaveformSeek progress rAF in the player bar) the combined cost held the WebProcess at ~80 % CPU with 1.2 GB RSS for the entire duration of playback. CPU dropped immediately on pause/stop; on Composers / Settings (no track rows) the symptom never appeared. Profiler confirmed continuous Layout & Rendering work synchronised with isPlaying, not the canvas itself. Replace both animations with static visuals: - `.track-row.active` keeps the `--accent-dim` background, drops the pulse animation entirely. - The "now playing" indicator becomes a single Lucide `AudioLines` icon (four vertical bars of different heights — reads as an EQ icon, no animation, one SVG per active row instead of three animated spans). `.eq-bars` className now only sets the accent colour. Cleanup: dead `@keyframes track-pulse`, `@keyframes eq-bounce`, `.eq-bars.paused` rule, plus a duplicate `.eq-bar` block in theme.css (with a `wave` keyframe that was being shadowed by components.css and had no other consumers). No behaviour change beyond removing the animation; the row is still visibly the active one and the icon still marks the playing track. * docs: CHANGELOG entry for tracklist animation perf fix (PR #488)
This commit is contained in:
committed by
GitHub
parent
59744601d4
commit
5b37ab70f1
@@ -179,6 +179,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
* The **Playlists** icon in the collapsed sidebar was **off-centre** and had a **wider hover background** than every other item, because it still rendered through the expanded-mode wrapper (with `padding-right` and a `flex: 1` main link to fit the expand-toggle). Collapsed mode now reuses the **standard nav-link path** — same hitbox, same alignment as Artists, Albums, Favorites, etc.
|
||||
|
||||
### Tracklist — drop now-playing pulse + EQ-bar animations
|
||||
|
||||
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#488](https://github.com/Psychotoxical/psysonic/pull/488)**
|
||||
|
||||
* The currently-playing track in any tracklist (**AlbumDetail**, **ArtistDetail**, **PlaylistDetail**, **Favorites**, **RandomMix**) ran an **`opacity` pulse** on the entire row plus three **`transform` keyframe** EQ-bar siblings — both compositor properties, but on **WebKitGTK without compositing** (Linux + NVIDIA proprietary + `WEBKIT_DISABLE_COMPOSITING_MODE=1`) every animated row falls back to a full **software repaint** of the subtree per frame. On AlbumDetail the combined cost held the WebProcess at **~80 % CPU** for the duration of playback; CPU dropped immediately on pause/stop.
|
||||
* `.track-row.active` keeps the **accent-tinted background** but no longer pulses. The "now playing" indicator becomes a single Lucide **`AudioLines` icon** (one SVG per active row instead of three animated spans). Cleanup: dead `track-pulse` + `eq-bounce` keyframes and a duplicate, shadowed `.eq-bar` block in `theme.css`.
|
||||
|
||||
## [1.45.0] - 2026-05-04
|
||||
|
||||
## Added
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Play, ChevronRight, Heart, ChevronDown, Check, RotateCcw, Square } from 'lucide-react';
|
||||
import { Play, ChevronRight, Heart, ChevronDown, Check, RotateCcw, Square, AudioLines } from 'lucide-react';
|
||||
import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns';
|
||||
import { SubsonicSong } from '../api/subsonic';
|
||||
import { Track, usePlayerStore, songToTrack } from '../store/playerStore';
|
||||
@@ -136,7 +136,7 @@ const TrackRow = React.memo(function TrackRow({
|
||||
/>
|
||||
{isActive && isPlaying ? (
|
||||
<span className="track-num-eq">
|
||||
<div className="eq-bars"><span className="eq-bar" /><span className="eq-bar" /><span className="eq-bar" /></div>
|
||||
<AudioLines className="eq-bars" size={14} />
|
||||
</span>
|
||||
) : (
|
||||
<span className="track-num-number">{song.track ?? '—'}</span>
|
||||
@@ -541,7 +541,7 @@ export default function AlbumTrackList({
|
||||
<div className="tracklist-mobile-main">
|
||||
{isActive && isPlaying ? (
|
||||
<span className="tracklist-mobile-eq">
|
||||
<div className="eq-bars"><span className="eq-bar" /><span className="eq-bar" /><span className="eq-bar" /></div>
|
||||
<AudioLines className="eq-bars" size={14} />
|
||||
</span>
|
||||
) : (
|
||||
<span className="tracklist-mobile-num">{song.track ?? ''}</span>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getArtist, getArtistInfo, getTopSongs, getSimilarSongs2, getAlbum, sear
|
||||
import AlbumCard from '../components/AlbumCard';
|
||||
import CachedImage from '../components/CachedImage';
|
||||
import CoverLightbox from '../components/CoverLightbox';
|
||||
import { ArrowLeft, Users, ExternalLink, Heart, Play, Square, Shuffle, Radio, HardDriveDownload, Check, Camera, Loader2, ChevronDown, ChevronRight, ChevronUp, Share2 } from 'lucide-react';
|
||||
import { ArrowLeft, Users, ExternalLink, Heart, Play, Square, Shuffle, Radio, HardDriveDownload, Check, Camera, Loader2, ChevronDown, ChevronRight, ChevronUp, Share2, AudioLines } from 'lucide-react';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
@@ -794,7 +794,7 @@ export default function ArtistDetail() {
|
||||
>
|
||||
<div className={`track-num${currentTrack?.id === song.id ? ' track-num-active' : ''}`}>
|
||||
{currentTrack?.id === song.id && isPlaying ? (
|
||||
<span className="track-num-eq"><div className="eq-bars"><span className="eq-bar" /><span className="eq-bar" /><span className="eq-bar" /></div></span>
|
||||
<span className="track-num-eq"><AudioLines className="eq-bars" size={14} /></span>
|
||||
) : (
|
||||
<span className="track-num-number">{idx + 1}</span>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { usePlayerStore, songToTrack } from '../store/playerStore';
|
||||
import { usePreviewStore } from '../store/previewStore';
|
||||
import StarRating from '../components/StarRating';
|
||||
import { Cast, ChevronDown, ChevronLeft, ChevronRight, Check, Heart, ListPlus, Play, Square, Star, Users, X, SlidersHorizontal, ArrowUp, ArrowDown, RotateCcw } from 'lucide-react';
|
||||
import { Cast, ChevronDown, ChevronLeft, ChevronRight, Check, Heart, ListPlus, Play, Square, Star, Users, X, SlidersHorizontal, ArrowUp, ArrowDown, RotateCcw, AudioLines } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { unstar } from '../api/subsonic';
|
||||
@@ -675,7 +675,7 @@ export default function Favorites() {
|
||||
<div key="num" className={`track-num${currentTrack?.id === song.id ? ' track-num-active' : ''}`}>
|
||||
<span className={`bulk-check${isSelected ? ' checked' : ''}${inSelectMode ? ' bulk-check-visible' : ''}`} onClick={e => { e.stopPropagation(); toggleSelect(song.id, i, e.shiftKey); }} />
|
||||
{currentTrack?.id === song.id && isPlaying ? (
|
||||
<span className="track-num-eq"><div className="eq-bars"><span className="eq-bar" /><span className="eq-bar" /><span className="eq-bar" /></div></span>
|
||||
<span className="track-num-eq"><AudioLines className="eq-bars" size={14} /></span>
|
||||
) : (
|
||||
<span className="track-num-number">{i + 1}</span>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { ChevronDown, ChevronLeft, ChevronRight, Play, ListPlus, Trash2, Search, X, Loader2, Plus, GripVertical, Star, RefreshCw, Shuffle, Heart, HardDriveDownload, Check, Pencil, Globe, Lock, Camera, Download, FileUp, RotateCcw, Sparkles, Square } from 'lucide-react';
|
||||
import { ChevronDown, ChevronLeft, ChevronRight, Play, ListPlus, Trash2, Search, X, Loader2, Plus, GripVertical, Star, RefreshCw, Shuffle, Heart, HardDriveDownload, Check, Pencil, Globe, Lock, Camera, Download, FileUp, RotateCcw, Sparkles, Square, AudioLines } from 'lucide-react';
|
||||
import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns';
|
||||
import { AddToPlaylistSubmenu } from '../components/ContextMenu';
|
||||
import {
|
||||
@@ -1697,7 +1697,7 @@ export default function PlaylistDetail() {
|
||||
<div key="num" className={`track-num${currentTrack?.id === song.id ? ' track-num-active' : ''}`}>
|
||||
<span className={`bulk-check${selectedIds.has(song.id) ? ' checked' : ''}${inSelectMode ? ' bulk-check-visible' : ''}`} onClick={e => { e.stopPropagation(); toggleSelect(song.id, i, e.shiftKey); }} />
|
||||
{currentTrack?.id === song.id && isPlaying ? (
|
||||
<span className="track-num-eq"><div className="eq-bars"><span className="eq-bar" /><span className="eq-bar" /><span className="eq-bar" /></div></span>
|
||||
<span className="track-num-eq"><AudioLines className="eq-bars" size={14} /></span>
|
||||
) : (
|
||||
<span className="track-num-number">{i + 1}</span>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getGenres, SubsonicSong, SubsonicGenre, star, unstar } from '../api/sub
|
||||
import { usePlayerStore, songToTrack } from '../store/playerStore';
|
||||
import { usePreviewStore } from '../store/previewStore';
|
||||
import { useAuthStore, RANDOM_MIX_SIZE_OPTIONS } from '../store/authStore';
|
||||
import { Play, RefreshCw, ChevronDown, ChevronRight, ChevronUp, Heart, Square } from 'lucide-react';
|
||||
import { Play, RefreshCw, ChevronDown, ChevronRight, ChevronUp, Heart, Square, AudioLines } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDragDrop } from '../contexts/DragDropContext';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
@@ -491,7 +491,7 @@ export default function RandomMix() {
|
||||
>
|
||||
<div className={`track-num${isCurrentTrack ? ' track-num-active' : ''}`}>
|
||||
{isCurrentTrack && isPlaying ? (
|
||||
<span className="track-num-eq"><div className="eq-bars"><span className="eq-bar" /><span className="eq-bar" /><span className="eq-bar" /></div></span>
|
||||
<span className="track-num-eq"><AudioLines className="eq-bars" size={14} /></span>
|
||||
) : (
|
||||
<span className="track-num-number">{idx + 1}</span>
|
||||
)}
|
||||
@@ -623,7 +623,7 @@ export default function RandomMix() {
|
||||
>
|
||||
<div className={`track-num${isCurrentTrack ? ' track-num-active' : ''}`}>
|
||||
{isCurrentTrack && isPlaying ? (
|
||||
<span className="track-num-eq"><div className="eq-bars"><span className="eq-bar" /><span className="eq-bar" /><span className="eq-bar" /></div></span>
|
||||
<span className="track-num-eq"><AudioLines className="eq-bars" size={14} /></span>
|
||||
) : (
|
||||
<span className="track-num-number">{idx + 1}</span>
|
||||
)}
|
||||
|
||||
@@ -2007,24 +2007,10 @@ html[data-track-previews-randommix="off"] [data-preview-loc="randomMix"] .pl
|
||||
|
||||
.track-row.active {
|
||||
background: var(--accent-dim);
|
||||
animation: track-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.track-row.active:hover {
|
||||
background: var(--accent-dim);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes track-pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.track-row>* {
|
||||
@@ -2111,56 +2097,14 @@ html[data-track-previews-randommix="off"] [data-preview-loc="randomMix"] .pl
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Equalizer bars — shown for the currently playing track */
|
||||
/* "Now playing" indicator — Lucide AudioLines icon coloured with the active
|
||||
* accent. Was an animated three-bar transform setup; the per-frame opacity +
|
||||
* scaleY repaints fell back to software composite on WebKitGTK without
|
||||
* compositing (NVIDIA proprietary + WEBKIT_DISABLE_COMPOSITING_MODE=1) and
|
||||
* pegged the WebProcess at ~80% CPU on AlbumDetail with a long tracklist. */
|
||||
.eq-bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
width: 14px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.eq-bar {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
border-radius: 1px;
|
||||
transform-origin: bottom;
|
||||
transform: scaleY(0.25);
|
||||
animation: eq-bounce 1.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.eq-bar:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
animation-duration: 1.0s;
|
||||
}
|
||||
|
||||
.eq-bar:nth-child(2) {
|
||||
animation-delay: 0.18s;
|
||||
animation-duration: 1.3s;
|
||||
}
|
||||
|
||||
.eq-bar:nth-child(3) {
|
||||
animation-delay: 0.35s;
|
||||
animation-duration: 0.9s;
|
||||
}
|
||||
|
||||
/* Pause EQ bars animation when playback is paused */
|
||||
.eq-bars.paused .eq-bar {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@keyframes eq-bounce {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scaleY(0.25);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
color: var(--accent);
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* CD / Disc separator */
|
||||
|
||||
@@ -4075,18 +4075,6 @@ select.input.input:focus {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scaleY(0.4);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn var(--transition-slow) both;
|
||||
}
|
||||
@@ -6528,31 +6516,6 @@ input[type="range"]:hover::-webkit-slider-thumb {
|
||||
--font-display: 'Unbounded Variable', sans-serif;
|
||||
}
|
||||
|
||||
/* ─── Equalizer bars animation ─── */
|
||||
.eq-bar {
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
.eq-bar:nth-child(1) {
|
||||
animation: wave 0.8s ease-in-out 0.0s infinite;
|
||||
}
|
||||
|
||||
.eq-bar:nth-child(2) {
|
||||
animation: wave 0.8s ease-in-out 0.15s infinite;
|
||||
}
|
||||
|
||||
.eq-bar:nth-child(3) {
|
||||
animation: wave 0.8s ease-in-out 0.3s infinite;
|
||||
}
|
||||
|
||||
.eq-bar:nth-child(4) {
|
||||
animation: wave 0.8s ease-in-out 0.45s infinite;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
Middle Earth — Epic Fantasy / Cinematic
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user