feat: Rust audio engine, genre mix, queue shuffle, and UX improvements (v1.2.0)

### Audio Engine
- Replace Howler.js with native Rust/rodio backend (src-tauri/src/audio.rs)
- audio_play/pause/resume/stop/seek/set_volume Tauri commands
- Generation counter cancels stale in-flight downloads on skip
- Wall-clock position tracking; audio:ended after 2 ticks near end

### Playback Persistence
- Persist currentTrack, queue, queueIndex, currentTime to localStorage
- Cold-start resume: play + seek to saved position on app restart
- Position priority: server queue > localStorage

### Random Mix
- Genre blacklist: excludeAudiobooks toggle + custom chip-based blacklist
- Clickable genre chips in tracklist to blacklist on the fly
- Super Genre Mix: 9 genres, progressive rendering, Load 10 more
- Promise.allSettled + 45s timeout for large Metal/Rock libraries
- Two-column filter panel at top of page

### Queue & UI
- Shuffle button in queue header (Fisher-Yates, keeps current track at 0)
- LiveSearch arrow key / Enter / Escape keyboard navigation
- Multi-line tooltip support (data-tooltip-wrap attribute)
- Update link uses Tauri shell open() to launch system browser
- Sidebar min-width 180px → 200px (fixes Psysonic title clipping)
- Tooltip z-index fix: main-content z-index:1 over queue panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-03-15 12:06:03 +01:00
parent 9b1f3b019f
commit 744775d3a1
20 changed files with 2037 additions and 312 deletions
+46 -4
View File
@@ -145,6 +145,19 @@ const enTranslation = {
favoriteAdd: 'Add to Favorites',
favoriteRemove: 'Remove from Favorites',
play: 'Play',
trackGenre: 'Genre',
excludeAudiobooks: 'Exclude audiobooks & radio plays',
excludeAudiobooksDesc: 'Filters genres: Hörbuch, Hörspiel, Audiobook, Spoken Word, …',
genreBlocked: 'Genre blocked',
genreAddedToBlacklist: 'Added to blacklist',
genreAlreadyBlocked: 'Already blocked',
blacklistToggle: 'Genre Blacklist',
genreMixTitle: 'Genre Mix',
genreMixDesc: 'Select a genre to get a curated random mix',
genreMixLoadMore: 'Load 10 more',
genreMixNoGenres: 'No genres found on server.',
filterPanelTitle: 'Filters',
genreClickHint: 'Click a genre tag\nto add it to your blacklist.\nExcluded from future mixes.',
},
playlists: {
title: 'Playlists',
@@ -261,8 +274,15 @@ const enTranslation = {
aboutLicenseText: 'MIT — free to use, modify, and distribute.',
aboutRepo: 'Source Code on GitHub',
aboutVersion: 'Version',
aboutBuiltWith: 'Built with Tauri · React · TypeScript · Howler.js',
aboutAiCredit: 'Developed with the support of Claude Code by Anthropic'
aboutBuiltWith: 'Built with Tauri · React · TypeScript · Rust/rodio',
aboutAiCredit: 'Developed with the support of Claude Code by Anthropic',
randomMixTitle: 'Random Mix',
randomMixBlacklistTitle: 'Custom Genre Blacklist',
randomMixBlacklistDesc: 'Songs whose genre matches an entry in this list are excluded from the Random Mix (when "Exclude audiobooks" is active).',
randomMixBlacklistPlaceholder: 'Add genre…',
randomMixBlacklistAdd: 'Add',
randomMixBlacklistEmpty: 'No custom genres added yet.',
randomMixHardcodedTitle: 'Built-in exclusions (active when checkbox is on)',
},
help: {
title: 'Help',
@@ -328,6 +348,7 @@ const enTranslation = {
delete: 'Delete',
deleteConfirm: 'Delete playlist "{{name}}"?',
clear: 'Clear',
shuffle: 'Shuffle queue',
hide: 'Hide',
close: 'Close',
nextTracks: 'Next Tracks',
@@ -506,6 +527,19 @@ const deTranslation = {
favoriteAdd: 'Zu Favoriten hinzufügen',
favoriteRemove: 'Aus Favoriten entfernen',
play: 'Abspielen',
trackGenre: 'Genre',
excludeAudiobooks: 'Hörbücher & Hörspiele ausschließen',
excludeAudiobooksDesc: 'Filtert Genres: Hörbuch, Hörspiel, Audiobook, Spoken Word, …',
genreBlocked: 'Genre gesperrt',
genreAddedToBlacklist: 'Zur Blacklist hinzugefügt',
genreAlreadyBlocked: 'Bereits gesperrt',
blacklistToggle: 'Genre-Blacklist',
genreMixTitle: 'Genre-Mix',
genreMixDesc: 'Genre auswählen für einen passenden Zufallsmix',
genreMixLoadMore: '10 weitere laden',
genreMixNoGenres: 'Keine Genres auf dem Server gefunden.',
filterPanelTitle: 'Filter',
genreClickHint: 'Genre-Tag anklicken,\num es zur Blacklist hinzuzufügen.\nBeim nächsten Mix ausgeschlossen.',
},
playlists: {
title: 'Playlists',
@@ -622,8 +656,15 @@ const deTranslation = {
aboutLicenseText: 'MIT — kostenlos nutzbar, veränderbar und weiterzugeben.',
aboutRepo: 'Quellcode auf GitHub',
aboutVersion: 'Version',
aboutBuiltWith: 'Gebaut mit Tauri · React · TypeScript · Howler.js',
aboutAiCredit: 'Mit freundlicher Unterstützung von Claude Code by Anthropic'
aboutBuiltWith: 'Gebaut mit Tauri · React · TypeScript · Rust/rodio',
aboutAiCredit: 'Mit freundlicher Unterstützung von Claude Code by Anthropic',
randomMixTitle: 'Zufallsmix',
randomMixBlacklistTitle: 'Eigene Genre-Blacklist',
randomMixBlacklistDesc: 'Songs, deren Genre einem Eintrag in dieser Liste entspricht, werden aus dem Zufallsmix ausgeschlossen (wenn „Hörbücher ausschließen" aktiv ist).',
randomMixBlacklistPlaceholder: 'Genre hinzufügen…',
randomMixBlacklistAdd: 'Hinzufügen',
randomMixBlacklistEmpty: 'Noch keine eigenen Genres hinzugefügt.',
randomMixHardcodedTitle: 'Eingebaute Ausschlüsse (aktiv wenn Checkbox an)',
},
help: {
title: 'Hilfe',
@@ -689,6 +730,7 @@ const deTranslation = {
delete: 'Löschen',
deleteConfirm: 'Playlist "{{name}}" löschen?',
clear: 'Leeren',
shuffle: 'Queue mischen',
hide: 'Verbergen',
close: 'Schließen',
nextTracks: 'Nächste Titel',