feat: Waveform seekbar, MilkDrop visualizer, EQ bars, in-app browser, and UX improvements (v1.3.0)

- PlayerBar redesigned: canvas waveform seekbar (500 bars, blue→mauve gradient + glow) replaces thin slider; new flex layout; queue toggle moved to content header (consistent with sidebar pattern)
- MilkDrop visualizer in Ambient Stage via Butterchurn; hidden audio analysis, preset shuffle, smooth blend transitions
- Tracklist: animated EQ bars for playing track, play icon when paused, align-items: center fix
- Artist pages: Last.fm + Wikipedia open in native Tauri WebviewWindow (in-app browser)
- Hero/Discover deduplication: single fetch of 20 random albums split between sections
- Update checker: runs every 10 minutes during runtime; version shown without v-prefix
- Settings version: now read from package.json instead of hardcoded
- Help page: new Random Mix section, updated Playback + Library sections, improved accordion styling
- Bump version to 1.3.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-03-15 21:24:00 +01:00
parent 9bdd433a4b
commit c91fdd7e1d
28 changed files with 800 additions and 272 deletions
+5 -3
View File
@@ -8,6 +8,7 @@ export default function Home() {
const [starred, setStarred] = useState<SubsonicAlbum[]>([]);
const [recent, setRecent] = useState<SubsonicAlbum[]>([]);
const [random, setRandom] = useState<SubsonicAlbum[]>([]);
const [heroAlbums, setHeroAlbums] = useState<SubsonicAlbum[]>([]);
const [mostPlayed, setMostPlayed] = useState<SubsonicAlbum[]>([]);
const [loading, setLoading] = useState(true);
@@ -15,12 +16,13 @@ export default function Home() {
Promise.all([
getAlbumList('starred', 12).catch(() => []),
getAlbumList('newest', 12).catch(() => []),
getAlbumList('random', 12).catch(() => []),
getAlbumList('random', 20).catch(() => []), // fetch 20 — split between Hero and Discover
getAlbumList('frequent', 12).catch(() => []),
]).then(([s, n, r, f]) => {
setStarred(s);
setRecent(n);
setRandom(r);
setHeroAlbums(r.slice(0, 8));
setRandom(r.slice(8));
setMostPlayed(f);
setLoading(false);
}).catch(() => setLoading(false));
@@ -47,7 +49,7 @@ export default function Home() {
return (
<div className="animate-fade-in">
<Hero />
<Hero albums={heroAlbums} />
<div className="content-body" style={{ display: 'flex', flexDirection: 'column', gap: '3rem' }}>
{loading ? (