diff --git a/CHANGELOG.md b/CHANGELOG.md index aeeae0d2..d225d02b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -149,6 +149,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added eight new dark themes covering the colour families people most commonly ask for: **Obsidian Black**, **Carbon Grey**, **Volcanic Dark**, **Forest Green**, **Violet Haze**, **Copper Oxide**, **Sakura Night**, **Obsidian Gold**. * Light polish on the existing **AMOLED Black Pure** surface variables so card surfaces no longer collapse onto a pure-black background that read as a single flat slab. +## Removed + +### Settings — Animations 3-state setting under Seekbar Style + +**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#495](https://github.com/Psychotoxical/psysonic/pull/495)** + +* The **Animations** 3-state setting (Full / Reduced / Static) under **Settings > Appearance > Seekbar Style** is gone. The newer perf-flag system and per-feature performance work cover the expensive animation paths more directly: marquee scrolling can be disabled via `perfFlags.disableMarqueeScroll` (Sidebar toggle), global CSS animations via the html-level `data-perf-disable-animations` switch, and seekbar performance is handled by the newer per-feature toggles. +* Anyone who had `'reduced'` or `'static'` selected silently lands on the normal animation path on first launch after upgrade — the persist layer strips the obsolete field, no user-facing prompt. + ## Fixed ### Hot cache, HTTP streaming replay, and queue source indicator diff --git a/src/components/MarqueeText.tsx b/src/components/MarqueeText.tsx index 6135407d..8ea87507 100644 --- a/src/components/MarqueeText.tsx +++ b/src/components/MarqueeText.tsx @@ -1,5 +1,4 @@ import React, { useRef, useState, useEffect, useCallback } from 'react'; -import { useAuthStore } from '../store/authStore'; import { usePerfProbeFlags } from '../utils/perfFlags'; interface Props { @@ -13,7 +12,6 @@ export default function MarqueeText({ text, className, style, onClick }: Props) const containerRef = useRef(null); const textRef = useRef(null); const [scrollAmount, setScrollAmount] = useState(0); - const animationMode = useAuthStore(s => s.animationMode); const perfFlags = usePerfProbeFlags(); const measure = useCallback(() => { @@ -34,9 +32,7 @@ export default function MarqueeText({ text, className, style, onClick }: Props) return () => ro.disconnect(); }, [text, measure]); - // In `static` animation mode the marquee never scrolls — overflowing text - // is truncated with an ellipsis (handled by CSS via data-anim-mode). - const shouldScroll = scrollAmount > 0 && animationMode !== 'static' && !perfFlags.disableMarqueeScroll; + const shouldScroll = scrollAmount > 0 && !perfFlags.disableMarqueeScroll; return (
s.waveformBins); const duration = usePlayerStore(s => s.currentTrack?.duration ?? 0); const seekbarStyle = useAuthStore(s => s.seekbarStyle); - const animationMode = useAuthStore(s => s.animationMode); // Ref so the subscription callback (closed over at mount) can read the // current style without stale-closure issues. const styleRef = useRef(seekbarStyle); styleRef.current = seekbarStyle; - const animationModeRef = useRef(animationMode); - animationModeRef.current = animationMode; useEffect(() => { if (!trackId) { @@ -1096,10 +1093,8 @@ export default function WaveformSeek({ trackId }: Props) { visualProgressRef.current = visualTargetProgressRef.current; } // Static styles always redraw on progress; animated styles let the rAF - // loop drive paints. In `static` animation mode we skip the rAF loop - // entirely, so animated styles also need to repaint here on every tick. - const drawNow = - !ANIMATED_STYLES.has(styleRef.current) || animationModeRef.current === 'static'; + // loop drive paints. + const drawNow = !ANIMATED_STYLES.has(styleRef.current); if (drawNow) { const canvas = canvasRef.current; if (!canvas) return; @@ -1137,28 +1132,14 @@ export default function WaveformSeek({ trackId }: Props) { duration, ]); - // rAF loop — animated styles only, and only in `full`/`reduced` mode. - // In `static` mode the loop is skipped entirely; the imperative progress - // subscription (~2 Hz audio heartbeat) handles repaints, so the seekbar - // updates a couple of times per second with no wave animation. + // rAF loop — animated styles only. useEffect(() => { if (!ANIMATED_STYLES.has(seekbarStyle)) return; - if (animationMode === 'static') { - // Repaint once on entry so the canvas reflects current progress - // without any wave morph and stays put until the next heartbeat. - const canvas = canvasRef.current; - if (canvas) { - animStateRef.current = makeAnimState(); - drawSeekbar(canvas, seekbarStyle, heightsRef.current, progressRef.current, bufferedRef.current, animStateRef.current); - } - return; - } const canvas = canvasRef.current; if (!canvas) return; animStateRef.current = makeAnimState(); let rafId: number | null = null; let pollId: number | null = null; - let skip = false; const stop = () => { if (rafId !== null) { cancelAnimationFrame(rafId); @@ -1177,22 +1158,13 @@ export default function WaveformSeek({ trackId }: Props) { }, 400); return; } - // 30 fps cap in `reduced` mode: skip every other rAF, advance - // animation time by a doubled delta so wave speed stays the same. - const isReduced = animationModeRef.current === 'reduced'; - if (isReduced && skip) { - skip = false; - rafId = requestAnimationFrame(tick); - return; - } - skip = isReduced; - animStateRef.current.time += isReduced ? 0.032 : 0.016; + animStateRef.current.time += 0.016; drawSeekbar(canvas, seekbarStyle, heightsRef.current, progressRef.current, bufferedRef.current, animStateRef.current); rafId = requestAnimationFrame(tick); }; tick(); return () => stop(); - }, [seekbarStyle, animationMode]); + }, [seekbarStyle]); // Smoothly advance progress between sparse transport ticks. // Preview pauses main sink in Rust while UI `isPlaying` may still be true. @@ -1267,8 +1239,7 @@ export default function WaveformSeek({ trackId }: Props) { : currentVisual + delta * smoothing; visualProgressRef.current = nextVisualProgress; progressRef.current = nextVisualProgress; - const needsDirectDraw = - !ANIMATED_STYLES.has(styleRef.current) || animationModeRef.current === 'static'; + const needsDirectDraw = !ANIMATED_STYLES.has(styleRef.current); if (needsDirectDraw && now - lastPaintAt >= INTERPOLATION_PAINT_MIN_MS) { const canvas = canvasRef.current; if (canvas) { diff --git a/src/locales/de.ts b/src/locales/de.ts index c3534395..00f386bd 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -979,13 +979,6 @@ export const deTranslation = { sidebarLyricsStyleAppleDesc: 'Aktive Zeile oben mit sanfter Animation.', seekbarStyle: 'Seekbar-Stil', seekbarStyleDesc: 'Aussehen der Wiedergabe-Seekbar auswählen', - animationMode: 'Animationen', - animationModeDesc: 'Visuelle Glätte gegen geringere GPU-/CPU-Last auf langsamerer Hardware tauschen.', - animationModeFull: 'Voll', - animationModeReduced: 'Reduziert', - animationModeStatic: 'Statisch', - animationModeReducedHint: '30 fps Limit auf der Seekbar-Welle; Titel im Player läuft mit halber Geschwindigkeit.', - animationModeStaticHint: 'Keine Wellenanimation; Titel im Player wird abgeschnitten statt gescrollt.', seekbarTruewave: 'Echte Wellenform', seekbarPseudowave: 'Pseudo-Wellenform', seekbarLinedot: 'Linie & Punkt', diff --git a/src/locales/en.ts b/src/locales/en.ts index 332109a1..de2933f1 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -985,13 +985,6 @@ export const enTranslation = { fsPortraitDim: 'Photo dimming', seekbarStyle: 'Seekbar Style', seekbarStyleDesc: 'Choose the look of the player seek bar', - animationMode: 'Animations', - animationModeDesc: 'Trade visual smoothness for lower GPU/CPU usage on slower hardware.', - animationModeFull: 'Full', - animationModeReduced: 'Reduced', - animationModeStatic: 'Static', - animationModeReducedHint: '30 fps cap on the seekbar wave; player title scrolls at half speed.', - animationModeStaticHint: 'No wave animation; player title is truncated instead of scrolling.', seekbarTruewave: 'Truewave', seekbarPseudowave: 'Pseudowave', seekbarLinedot: 'Line & Dot', diff --git a/src/locales/es.ts b/src/locales/es.ts index ffa80cbb..a20f9bc5 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -972,13 +972,6 @@ export const esTranslation = { sidebarLyricsStyleAppleDesc: 'La línea activa se ancla arriba con animación suave.', seekbarStyle: 'Estilo de Barra de Progreso', seekbarStyleDesc: 'Elige la apariencia de la barra de progreso del reproductor', - animationMode: 'Animaciones', - animationModeDesc: 'Cambia fluidez visual por menor uso de GPU/CPU en hardware más lento.', - animationModeFull: 'Completas', - animationModeReduced: 'Reducidas', - animationModeStatic: 'Estáticas', - animationModeReducedHint: 'Límite de 30 fps en la onda de la barra; el título del reproductor se desplaza a la mitad de velocidad.', - animationModeStaticHint: 'Sin animación de onda; el título del reproductor se trunca en lugar de desplazarse.', seekbarTruewave: 'Forma de Onda Real', seekbarPseudowave: 'Forma de Onda Pseudo', seekbarLinedot: 'Línea y Punto', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 1d90b136..2ec4396b 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -967,13 +967,6 @@ export const frTranslation = { sidebarLyricsStyleAppleDesc: "La ligne active reste en haut avec une animation fluide.", seekbarStyle: 'Style de la barre de lecture', seekbarStyleDesc: 'Choisir l\'apparence de la barre de progression', - animationMode: 'Animations', - animationModeDesc: 'Échanger la fluidité visuelle contre une utilisation GPU/CPU réduite sur du matériel plus lent.', - animationModeFull: 'Complètes', - animationModeReduced: 'Réduites', - animationModeStatic: 'Statiques', - animationModeReducedHint: 'Plafond de 30 fps sur l\'onde de la barre ; le titre du lecteur défile à moitié vitesse.', - animationModeStaticHint: 'Pas d\'animation d\'onde ; le titre du lecteur est tronqué au lieu de défiler.', seekbarTruewave: 'Forme d\'onde réelle', seekbarPseudowave: 'Forme d\'onde pseudo', seekbarLinedot: 'Ligne & point', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 8cd5e46a..4e46515f 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -966,13 +966,6 @@ export const nbTranslation = { sidebarLyricsStyleAppleDesc: 'Aktiv linje forankres øverst med jevn animasjon.', seekbarStyle: 'Søkefelt-stil', seekbarStyleDesc: 'Velg utseendet på avspillingssøkefeltet', - animationMode: 'Animasjoner', - animationModeDesc: 'Bytt visuell flyt mot lavere GPU-/CPU-bruk på tregere maskinvare.', - animationModeFull: 'Full', - animationModeReduced: 'Redusert', - animationModeStatic: 'Statisk', - animationModeReducedHint: '30 fps grense på søkefelt-bølgen; spillertittelen ruller med halv hastighet.', - animationModeStaticHint: 'Ingen bølgeanimasjon; spillertittelen forkortes i stedet for å rulle.', seekbarTruewave: 'Ekte bølgeform', seekbarPseudowave: 'Pseudo-bølgeform', seekbarLinedot: 'Linje & punkt', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 3658d22b..9314a0d7 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -966,13 +966,6 @@ export const nlTranslation = { sidebarLyricsStyleAppleDesc: 'Actieve regel verankerd bovenaan met vloeiende animatie.', seekbarStyle: 'Zoekbalkstijl', seekbarStyleDesc: 'Kies het uiterlijk van de afspeelbalk', - animationMode: 'Animaties', - animationModeDesc: 'Wissel visuele soepelheid in voor minder GPU-/CPU-gebruik op tragere hardware.', - animationModeFull: 'Volledig', - animationModeReduced: 'Gereduceerd', - animationModeStatic: 'Statisch', - animationModeReducedHint: '30 fps limiet op de zoekbalk-golf; spelertitel scrollt op halve snelheid.', - animationModeStaticHint: 'Geen golfanimatie; spelertitel wordt afgekapt in plaats van scrollen.', seekbarTruewave: 'Echte golfvorm', seekbarPseudowave: 'Pseudo-golfvorm', seekbarLinedot: 'Lijn & punt', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index c39ff36c..bc71d5c2 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1020,13 +1020,6 @@ export const ruTranslation = { sidebarLyricsStyleAppleDesc: 'Активная строка фиксируется сверху с плавной анимацией.', seekbarStyle: 'Стиль прогресс-бара', seekbarStyleDesc: 'Выбор внешнего вида полосы воспроизведения', - animationMode: 'Анимации', - animationModeDesc: 'Обменивайте визуальную плавность на меньшую нагрузку на GPU/CPU на слабом железе.', - animationModeFull: 'Полные', - animationModeReduced: 'Сниженные', - animationModeStatic: 'Статичные', - animationModeReducedHint: 'Ограничение 30 fps для волны прогресс-бара; заголовок плеера прокручивается с половинной скоростью.', - animationModeStaticHint: 'Без анимации волны; заголовок плеера обрезается вместо прокрутки.', seekbarTruewave: 'Реальная форма волны', seekbarPseudowave: 'Псевдо форма волны', seekbarLinedot: 'Линия и точка', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 931cb6bc..89b78eca 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -961,13 +961,6 @@ export const zhTranslation = { sidebarLyricsStyleAppleDesc: '活动行固定在顶部,带有平滑弹簧动画。', seekbarStyle: '进度条样式', seekbarStyleDesc: '选择播放进度条的外观', - animationMode: '动画', - animationModeDesc: '在较慢的硬件上以视觉流畅度换取更低的 GPU/CPU 占用。', - animationModeFull: '完整', - animationModeReduced: '减少', - animationModeStatic: '静态', - animationModeReducedHint: '进度条波形限制为 30 fps;播放器标题以半速滚动。', - animationModeStaticHint: '无波形动画;播放器标题被截断而不滚动。', seekbarTruewave: '真实波形', seekbarPseudowave: '伪波形', seekbarLinedot: '线条与点', diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 36fde534..7b97270f 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -3922,43 +3922,6 @@ export default function Settings() { /> ))}
-
-
-
- {t('settings.animationMode')} -
- - - -
-
-
- {auth.animationMode === 'reduced' - ? t('settings.animationModeReducedHint') - : auth.animationMode === 'static' - ? t('settings.animationModeStaticHint') - : t('settings.animationModeDesc')} -
-
-
diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 679e29ac..ccd278e0 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -25,7 +25,6 @@ export interface ServerProfile { export type SeekbarStyle = 'truewave' | 'pseudowave' | 'linedot' | 'bar' | 'thick' | 'segmented' | 'neon' | 'pulsewave' | 'particletrail' | 'liquidfill' | 'retrotape'; export type LoggingMode = 'off' | 'normal' | 'debug'; export type NormalizationEngine = 'off' | 'replaygain' | 'loudness'; -export type AnimationMode = 'full' | 'reduced' | 'static'; export type DiscordCoverSource = 'none' | 'apple' | 'server'; /** Integrated-loudness target presets (Settings + analysis). */ @@ -179,12 +178,6 @@ interface AuthState { lastSeenChangelogVersion: string; seekbarStyle: SeekbarStyle; - /** Animation budget across the UI: - * - `full` = native frame rate - * - `reduced`= 30 fps cap on animated seekbar; marquee at half frame rate - * - `static` = ~1 fps progress (no rAF interpolation); marquee disabled (truncate) - */ - animationMode: AnimationMode; /** Persisted UI toggle: is the Now Playing section in queue panel collapsed */ queueNowPlayingCollapsed: boolean; @@ -337,7 +330,6 @@ interface AuthState { setShowChangelogOnUpdate: (v: boolean) => void; setLastSeenChangelogVersion: (v: string) => void; setSeekbarStyle: (v: SeekbarStyle) => void; - setAnimationMode: (v: AnimationMode) => void; setQueueNowPlayingCollapsed: (v: boolean) => void; setEnableHiRes: (v: boolean) => void; setAudioOutputDevice: (v: string | null) => void; @@ -475,7 +467,6 @@ export const useAuthStore = create()( showChangelogOnUpdate: true, lastSeenChangelogVersion: '', seekbarStyle: 'truewave', - animationMode: 'full', queueNowPlayingCollapsed: false, enableHiRes: false, audioOutputDevice: null, @@ -640,7 +631,6 @@ export const useAuthStore = create()( setLastSeenChangelogVersion: (v) => set({ lastSeenChangelogVersion: v }), setSeekbarStyle: (v) => set({ seekbarStyle: v }), - setAnimationMode: (v) => set({ animationMode: v }), setQueueNowPlayingCollapsed: (v: boolean) => set({ queueNowPlayingCollapsed: v }), setEnableHiRes: (v) => set({ enableHiRes: v }), setAudioOutputDevice: (v) => set({ audioOutputDevice: v }), @@ -843,14 +833,14 @@ export const useAuthStore = create()( ? {} : { seekbarStyle: 'truewave' as SeekbarStyle }; - // `reducedAnimations: boolean` superseded by `animationMode: AnimationMode`. - // Map legacy true → 'reduced', false/missing → 'full'. Static is opt-in only. - let animationModeMigrated: { animationMode?: AnimationMode } = {}; - const legacyReduced = (state as { reducedAnimations?: unknown }).reducedAnimations; - const VALID_ANIM_MODES = new Set(['full', 'reduced', 'static']); - if (!VALID_ANIM_MODES.has(state.animationMode as string)) { - animationModeMigrated = { animationMode: legacyReduced === true ? 'reduced' : 'full' }; - } + // The `animationMode` 3-state setting was removed; users on `'reduced'` + // or `'static'` collapse onto the former `'full'` path automatically as + // soon as the field is gone from the store. Strip the persisted field + // so it doesn't sit in localStorage as cruft. + delete (state as { animationMode?: unknown }).animationMode; + // The earlier `reducedAnimations: boolean` predecessor likewise loses + // its meaning; clear it for the same reason. + delete (state as { reducedAnimations?: unknown }).reducedAnimations; const st = state as { loudnessTargetLufs?: unknown; @@ -891,7 +881,6 @@ export const useAuthStore = create()( ...lyricsSourcesMigrated, ...wheelSmoothOneTime, ...seekbarStyleMigrated, - ...animationModeMigrated, ...discordCoverSourceMigrated, }); }, diff --git a/src/styles/layout.css b/src/styles/layout.css index 68fbb3a6..9754a1dc 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -1516,23 +1516,12 @@ html[data-platform="windows"] .player-bar.floating { width: 100%; } -/* `static` animation mode: never scroll, truncate with ellipsis instead. */ -.marquee-wrap[data-anim-mode="static"] { - text-overflow: ellipsis; -} - .marquee-scroll { display: inline-block; animation: player-marquee 12s linear infinite; animation-delay: 2s; } -/* `reduced` animation mode: double the marquee cycle so visual change - per second is roughly halved. */ -.marquee-wrap[data-anim-mode="reduced"] .marquee-scroll { - animation-duration: 24s; -} - @keyframes player-marquee { 0%, 12% { transform: translateX(0); } 78%, 88% { transform: translateX(var(--marquee-amount)); }