feat: v1.29.0 — Radio fast-start, seek fix, OGG, error toasts, contributors

Incorporates PRs #38 (nisarg-78), #42 #43 #44 (JulianNymark) with fixes:

Audio / Playback
- Artist Radio starts immediately from fast getTopSongs (local library);
  getSimilarSongs2 (Last.fm) enriches queue in background — no more wait
- Fix seek audio glitch: EqualPowerFadeIn only resets to zero-gain on
  seeks to track start (<100 ms); all other seeks resume at unity gain
- OGG/Vorbis container support via symphonia-format-ogg (PR #42)
- Human-readable audio error messages in SizedDecoder (PR #44)
- Audio playback errors shown as themed toast notifications (PR #43)

Queue / Radio
- Infinite Queue: proactive load of 5 tracks (was 25) when ≤2 remain
- Radio: proactive reload at ≤2 remaining tracks, independent of
  Infinite Queue setting — radio no longer stops at last track
- Fix: clicking Start Radio multiple times no longer stacks duplicates
- Fix: Start Radio on artist keeps current song playing
- Manual tracks always appear before Radio, Radio before Auto-added
- Queue separators: "— Radio —" and "— Auto —" dividers
- Fix: radio proactive load now works even when songs lack artistId
  (uses currentRadioArtistId module var as fallback)

UI / UX
- Click synced lyrics lines to seek (PR #38)
- Volume scroll wheel on volume slider (PR #38)
- Lyrics: active / completed / upcoming visual states (PR #38)
- Shared toast utility (src/utils/toast.ts) replaces inline toast fn
- Auto-updater: relaunch_after_update Rust command exits first to
  release single-instance lock before spawning new process

About / Credits
- nisarg-78 and JulianNymark added to contributors (since v1.29.0)
- netherguy4 added as Special Thanks for feature ideas and feedback
- i18n: aboutSpecialThanksLabel in all 5 languages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-02 22:17:55 +02:00
parent 95283d792b
commit 7263d93d42
20 changed files with 6264 additions and 3900 deletions
+12 -1
View File
@@ -30,6 +30,8 @@ export default function LyricsPane({ currentTrack }: Props) {
const hasSynced = syncedLines !== null && syncedLines.length > 0;
const currentTime = usePlayerStore(s => hasSynced ? s.currentTime : 0);
const seek = usePlayerStore(s => s.seek);
const duration = usePlayerStore(s => s.currentTrack?.duration ?? 0);
const lineRefs = useRef<(HTMLDivElement | null)[]>([]);
const prevActive = useRef(-1);
@@ -103,6 +105,13 @@ export default function LyricsPane({ currentTrack }: Props) {
);
}
const getLyricLineClass = (i: number, active: number) => {
const base = 'lyrics-line';
if (i > active) return base;
if (i < active) return `${base} completed`;
return `${base} active`;
};
return (
<div className="lyrics-pane">
{loading && <p className="lyrics-status">{t('player.lyricsLoading')}</p>}
@@ -113,7 +122,9 @@ export default function LyricsPane({ currentTrack }: Props) {
<div
key={i}
ref={el => { lineRefs.current[i] = el; }}
className={`lyrics-line${i === activeIdx ? ' active' : ''}`}
className={getLyricLineClass(i, activeIdx)}
onClick={() => { if (duration > 0) seek(line.time / duration); }}
style={{ cursor: 'pointer' }}
>
{line.text || '\u00A0'}
</div>