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
+47 -17
View File
@@ -55,6 +55,31 @@ const CONTRIBUTORS = [
'songToTrack() — unified track construction across all sources',
],
},
{
github: 'nisarg-78',
since: '1.29.0',
contributions: [
'Click-to-seek in synced lyrics (PR #38)',
'Volume scroll wheel on volume slider (PR #38)',
'Lyrics line visual states: active / completed / upcoming (PR #38)',
],
},
{
github: 'JulianNymark',
since: '1.29.0',
contributions: [
'OGG/Vorbis container support via symphonia-format-ogg (PR #42)',
'Themed toast notifications for audio playback errors (PR #43)',
'Human-readable audio error messages (PR #44)',
],
},
] as const;
const SPECIAL_THANKS = [
{
github: 'netherguy4',
reason: 'Countless constructive feature ideas and thoughtful feedback',
},
] as const;
type Tab = 'playback' | 'library' | 'appearance' | 'shortcuts' | 'server' | 'about';
@@ -415,24 +440,7 @@ export default function Settings() {
</label>
</div>
<div className="divider" />
{/* Infinite Queue */}
<div className="settings-toggle-row">
<div>
<div style={{ fontWeight: 500 }}>
{t('settings.infiniteQueue')}
</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
{t('settings.infiniteQueueDesc')}
</div>
</div>
<label className="toggle-switch" aria-label={t('settings.infiniteQueue')}>
<input type="checkbox" checked={auth.infiniteQueueEnabled}
onChange={e => auth.setInfiniteQueueEnabled(e.target.checked)} id="infinite-queue-toggle" />
<span className="toggle-track" />
</label>
</div>
</div>
</section>
@@ -1129,6 +1137,28 @@ export default function Settings() {
</div>
)}
</div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'flex-start' }}>
<span style={{ color: 'var(--text-muted)', minWidth: 56, flexShrink: 0, paddingTop: 2, fontSize: 13 }}>{t('settings.aboutSpecialThanksLabel')}</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', flex: 1 }}>
{SPECIAL_THANKS.map(s => (
<div key={s.github} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<img
src={`https://github.com/${s.github}.png?size=32`}
width={22} height={22}
style={{ borderRadius: '50%', flexShrink: 0 }}
alt={s.github}
/>
<button
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, color: 'var(--accent)', fontWeight: 600, fontSize: 13 }}
onClick={() => openUrl(`https://github.com/${s.github}`)}
>
@{s.github}
</button>
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}> {s.reason}</span>
</div>
))}
</div>
</div>
</div>
<button