diff --git a/CHANGELOG.md b/CHANGELOG.md index 35a1fd37..d88b3aa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/components/AlbumTrackList.tsx b/src/components/AlbumTrackList.tsx index 43e0a62a..8a0d8e8f 100644 --- a/src/components/AlbumTrackList.tsx +++ b/src/components/AlbumTrackList.tsx @@ -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 ? ( -
+
) : ( {song.track ?? '—'} @@ -541,7 +541,7 @@ export default function AlbumTrackList({
{isActive && isPlaying ? ( -
+
) : ( {song.track ?? ''} diff --git a/src/pages/ArtistDetail.tsx b/src/pages/ArtistDetail.tsx index 828848a9..efdbfd28 100644 --- a/src/pages/ArtistDetail.tsx +++ b/src/pages/ArtistDetail.tsx @@ -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() { >
{currentTrack?.id === song.id && isPlaying ? ( -
+ ) : ( {idx + 1} )} diff --git a/src/pages/Favorites.tsx b/src/pages/Favorites.tsx index eca972a7..a08e4959 100644 --- a/src/pages/Favorites.tsx +++ b/src/pages/Favorites.tsx @@ -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() {
{ e.stopPropagation(); toggleSelect(song.id, i, e.shiftKey); }} /> {currentTrack?.id === song.id && isPlaying ? ( -
+ ) : ( {i + 1} )} diff --git a/src/pages/PlaylistDetail.tsx b/src/pages/PlaylistDetail.tsx index b6af500e..a76873f0 100644 --- a/src/pages/PlaylistDetail.tsx +++ b/src/pages/PlaylistDetail.tsx @@ -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() {
{ e.stopPropagation(); toggleSelect(song.id, i, e.shiftKey); }} /> {currentTrack?.id === song.id && isPlaying ? ( -
+ ) : ( {i + 1} )} diff --git a/src/pages/RandomMix.tsx b/src/pages/RandomMix.tsx index 05aee1b7..59db7779 100644 --- a/src/pages/RandomMix.tsx +++ b/src/pages/RandomMix.tsx @@ -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() { >
{isCurrentTrack && isPlaying ? ( -
+ ) : ( {idx + 1} )} @@ -623,7 +623,7 @@ export default function RandomMix() { >
{isCurrentTrack && isPlaying ? ( -
+ ) : ( {idx + 1} )} diff --git a/src/styles/components.css b/src/styles/components.css index 30bcc98e..1586e2fe 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -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 */ diff --git a/src/styles/theme.css b/src/styles/theme.css index be45b26a..7392ed4e 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -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 ───────────────────────────────────────────────────────────── */