From ed76090a542aa71b767ecce82a3987d53baf8477 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Sun, 26 Apr 2026 02:45:51 +0200 Subject: [PATCH] fix(seekbar): split waveform style into truewave (analyzed) + pseudowave (deterministic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The waveform-loudness-cache merge replaced the existing deterministic per-track-ID waveform with a bins-based one driven by the analysis cache. The bins-based variant is the better default but the old deterministic look is still valuable when no analysis is available (brand-new track, cache-miss, etc.) and several users prefer it. Split into two explicit options in the seekbar style picker: - 'truewave' (default, replaces old 'waveform') — bins from the analysis cache, with morph-on-arrival animation and flat-line fallback while empty. - 'pseudowave' — pseudo-random heights derived deterministically from the track ID. No analysis dependency, no morph, instant render. Existing persisted seekbarStyle: 'waveform' is migrated to 'truewave' in onRehydrateStorage so users keep the visual they have today. The useEffect that builds heightsRef now lists seekbarStyle in its deps so switching between the two is live. i18n labels added in all 8 locales. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/WaveformSeek.tsx | 18 +++++++++++++++--- src/locales/de.ts | 3 ++- src/locales/en.ts | 3 ++- src/locales/es.ts | 3 ++- src/locales/fr.ts | 3 ++- src/locales/nb.ts | 3 ++- src/locales/nl.ts | 3 ++- src/locales/ru.ts | 3 ++- src/locales/zh.ts | 3 ++- src/pages/Settings.tsx | 2 +- src/store/authStore.ts | 12 ++++++++++-- 11 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/components/WaveformSeek.tsx b/src/components/WaveformSeek.tsx index 274b5b62..e3d9636a 100644 --- a/src/components/WaveformSeek.tsx +++ b/src/components/WaveformSeek.tsx @@ -750,7 +750,8 @@ export function drawSeekbar( ) { const anim = animState ?? makeAnimState(); switch (style) { - case 'waveform': drawWaveform(canvas, heights, progress, buffered); break; + case 'truewave': drawWaveform(canvas, heights, progress, buffered); break; + case 'pseudowave': drawWaveform(canvas, heights, progress, buffered); break; case 'linedot': drawLineDot(canvas, progress, buffered); break; case 'bar': drawBar(canvas, progress, buffered); break; case 'thick': drawThick(canvas, progress, buffered); break; @@ -782,7 +783,7 @@ export function SeekbarPreview({ const canvas = canvasRef.current; if (!canvas) return; let heights: Float32Array | null = null; - if (style === 'waveform') { + if (style === 'truewave' || style === 'pseudowave') { heights = makeHeights('seekbar-preview-demo'); } const animState = makeAnimState(); @@ -896,6 +897,17 @@ export default function WaveformSeek({ trackId }: Props) { heightsRef.current = null; return; } + // Pseudowave is the deterministic per-track-ID variant — no analysis needed, + // no morph animation, no flat-fallback. It just sits there looking like a + // waveform. + if (seekbarStyle === 'pseudowave') { + heightsRef.current = makeHeights(trackId); + const canvas = canvasRef.current; + if (canvas && !ANIMATED_STYLES.has(seekbarStyle)) { + drawSeekbar(canvas, seekbarStyle, heightsRef.current, progressRef.current, bufferedRef.current, animStateRef.current); + } + return; + } if (waveformBins && waveformBins.length > 0) { const h = binsToHeights(waveformBins); const prev = heightsRef.current; @@ -959,7 +971,7 @@ export default function WaveformSeek({ trackId }: Props) { } // No analysis bins yet: render 500 flat bars immediately. heightsRef.current = makeFlatWaveHeights(); - }, [trackId, waveformBins]); + }, [trackId, waveformBins, seekbarStyle]); // Imperative subscription — no React re-renders from progress changes. // Static styles draw here; animated styles only update refs. diff --git a/src/locales/de.ts b/src/locales/de.ts index ee5d6c94..ec64c99e 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -888,7 +888,8 @@ export const deTranslation = { sidebarLyricsStyleAppleDesc: 'Aktive Zeile oben mit sanfter Animation.', seekbarStyle: 'Seekbar-Stil', seekbarStyleDesc: 'Aussehen der Wiedergabe-Seekbar auswählen', - seekbarWaveform: 'Wellenform', + seekbarTruewave: 'Echte Wellenform', + seekbarPseudowave: 'Pseudo-Wellenform', seekbarLinedot: 'Linie & Punkt', seekbarBar: 'Balken', seekbarThick: 'Dicker Balken', diff --git a/src/locales/en.ts b/src/locales/en.ts index ae79cfe7..1ab2e6ad 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -894,7 +894,8 @@ export const enTranslation = { fsPortraitDim: 'Photo dimming', seekbarStyle: 'Seekbar Style', seekbarStyleDesc: 'Choose the look of the player seek bar', - seekbarWaveform: 'Waveform', + seekbarTruewave: 'Truewave', + seekbarPseudowave: 'Pseudowave', seekbarLinedot: 'Line & Dot', seekbarBar: 'Bar', seekbarThick: 'Thick Bar', diff --git a/src/locales/es.ts b/src/locales/es.ts index 848ac685..0b4ed21f 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -881,7 +881,8 @@ 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', - seekbarWaveform: 'Forma de Onda', + seekbarTruewave: 'Forma de Onda Real', + seekbarPseudowave: 'Forma de Onda Pseudo', seekbarLinedot: 'Línea y Punto', seekbarBar: 'Barra', seekbarThick: 'Barra Gruesa', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 96b25972..58800d30 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -876,7 +876,8 @@ 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', - seekbarWaveform: 'Forme d\'onde', + seekbarTruewave: 'Forme d\'onde réelle', + seekbarPseudowave: 'Forme d\'onde pseudo', seekbarLinedot: 'Ligne & point', seekbarBar: 'Barre', seekbarThick: 'Barre épaisse', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 6f0dc8c5..1cf9c7d0 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -875,7 +875,8 @@ export const nbTranslation = { sidebarLyricsStyleAppleDesc: 'Aktiv linje forankres øverst med jevn animasjon.', seekbarStyle: 'Søkefelt-stil', seekbarStyleDesc: 'Velg utseendet på avspillingssøkefeltet', - seekbarWaveform: 'Bølgeform', + seekbarTruewave: 'Ekte bølgeform', + seekbarPseudowave: 'Pseudo-bølgeform', seekbarLinedot: 'Linje & punkt', seekbarBar: 'Linje', seekbarThick: 'Tykk linje', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 22796f55..f8a6ff51 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -875,7 +875,8 @@ export const nlTranslation = { sidebarLyricsStyleAppleDesc: 'Actieve regel verankerd bovenaan met vloeiende animatie.', seekbarStyle: 'Zoekbalkstijl', seekbarStyleDesc: 'Kies het uiterlijk van de afspeelbalk', - seekbarWaveform: 'Golfvorm', + seekbarTruewave: 'Echte golfvorm', + seekbarPseudowave: 'Pseudo-golfvorm', seekbarLinedot: 'Lijn & punt', seekbarBar: 'Balk', seekbarThick: 'Dikke balk', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 24c4883c..ba7c2187 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -927,7 +927,8 @@ export const ruTranslation = { sidebarLyricsStyleAppleDesc: 'Активная строка фиксируется сверху с плавной анимацией.', seekbarStyle: 'Стиль прогресс-бара', seekbarStyleDesc: 'Выбор внешнего вида полосы воспроизведения', - seekbarWaveform: 'Форма волны', + seekbarTruewave: 'Реальная форма волны', + seekbarPseudowave: 'Псевдо форма волны', seekbarLinedot: 'Линия и точка', seekbarBar: 'Полоса', seekbarThick: 'Толстая полоса', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 3da16846..f9feee91 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -870,7 +870,8 @@ export const zhTranslation = { sidebarLyricsStyleAppleDesc: '活动行固定在顶部,带有平滑弹簧动画。', seekbarStyle: '进度条样式', seekbarStyleDesc: '选择播放进度条的外观', - seekbarWaveform: '波形', + seekbarTruewave: '真实波形', + seekbarPseudowave: '伪波形', seekbarLinedot: '线条与点', seekbarBar: '条形', seekbarThick: '粗条形', diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 3d7f5f51..6b5b56e7 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -3676,7 +3676,7 @@ export default function Settings() { {t('settings.seekbarStyleDesc')}
- {(['waveform', 'linedot', 'bar', 'thick', 'segmented', 'neon', 'pulsewave', 'particletrail', 'liquidfill', 'retrotape'] as SeekbarStyle[]).map(style => ( + {(['truewave', 'pseudowave', 'linedot', 'bar', 'thick', 'segmented', 'neon', 'pulsewave', 'particletrail', 'liquidfill', 'retrotape'] as SeekbarStyle[]).map(style => ( ()( fsPortraitDim: 28, showChangelogOnUpdate: true, lastSeenChangelogVersion: '', - seekbarStyle: 'waveform', + seekbarStyle: 'truewave', enableHiRes: false, audioOutputDevice: null, hotCacheEnabled: false, @@ -729,6 +729,13 @@ export const useAuthStore = create()( } catch { /* ignore */ } } + // One-time: 'waveform' style was renamed to 'truewave' (with 'pseudowave' + // added as the deterministic legacy variant). Existing persisted value + // 'waveform' should land on the new bins-based default. + const seekbarStyleMigrated = (state.seekbarStyle as string) === 'waveform' + ? { seekbarStyle: 'truewave' as SeekbarStyle } + : {}; + const st = state as { loudnessTargetLufs?: unknown; loudnessPreAnalysisAttenuationDb?: unknown }; const targetSan = sanitizeLoudnessLufsPreset(st.loudnessTargetLufs, -12); const preSan = clampLoudnessPreAnalysisAttenuationDb(st.loudnessPreAnalysisAttenuationDb); @@ -745,6 +752,7 @@ export const useAuthStore = create()( ...conflictingLegacyState, ...lyricsSourcesMigrated, ...wheelSmoothOneTime, + ...seekbarStyleMigrated, }); }, }