refactor(settings): remove redundant Animations 3-state setting (#495)

* refactor(settings): remove redundant Animations 3-state setting under Seekbar Style

The `animationMode` setting (Full / Reduced / Static) duplicated work
the perf-flag system and OS-level reduced-motion preference already
covered:

- `perfFlags.disableMarqueeScroll` already kills marquee scrolling on
  demand, replacing what `static` mode used to gate.
- The `data-perf-disable-animations` html-level switch already strips
  every `*` animation, replacing what `static` mode used to do globally.
- `@media (prefers-reduced-motion: reduce)` honours the OS setting for
  every user that asked for it via system preferences.
- The 30 fps cap that `reduced` mode applied to the seekbar wave was
  better served by per-feature perf toggles cucadmuh added later.

Removed:
- `AnimationMode` type, `animationMode` field + setter from auth store.
- Settings UI block (3 buttons + hint text) under Appearance > Seekbar
  Style.
- `animationMode === 'static'` short-circuit in WaveformSeek's rAF
  effect; `isReduced` skip-every-other-frame logic; `static`-checks in
  `drawNow` / `needsDirectDraw`.
- `animationMode !== 'static'` guard and `data-anim-mode` attribute in
  MarqueeText.
- `[data-anim-mode="static"]` and `[data-anim-mode="reduced"]` rules in
  layout.css.
- Seven i18n keys (animationMode + 6 variants) across all eight
  locales.

Migration: the persist layer strips `animationMode` (and the legacy
`reducedAnimations` boolean predecessor) so anyone who had `'reduced'`
or `'static'` selected silently lands on the former `'full'` path on
first launch after upgrade. No user-facing prompt — the missing setting
just stops existing.

cucadmuh's PR #472 (FPS overlay), #476 (preview-freeze main seekbar,
sleep-recovery hooks, card-hover removal) and #486 (interpolation
anchor reset on resume) are all preserved untouched — they live in
separate effects / files and were not driven by `animationMode`.

* docs(changelog): add Removed section for animationMode setting (PR #495)

* docs(changelog): refine animationMode removal rationale (drop prefers-reduced-motion overstatement)
This commit is contained in:
Frank Stellmacher
2026-05-07 12:53:19 +02:00
committed by GitHub
parent ba73649360
commit ddb1f29af9
14 changed files with 24 additions and 164 deletions
+1 -6
View File
@@ -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<HTMLDivElement>(null);
const textRef = useRef<HTMLSpanElement>(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 (
<div
@@ -44,7 +40,6 @@ export default function MarqueeText({ text, className, style, onClick }: Props)
className={`marquee-wrap${className ? ` ${className}` : ''}`}
style={style}
onClick={onClick}
data-anim-mode={animationMode}
>
<span
ref={textRef}
+6 -35
View File
@@ -967,14 +967,11 @@ export default function WaveformSeek({ trackId }: Props) {
const waveformBins = usePlayerStore(s => 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) {