feat: v1.9.0 — new themes, keybindings, font picker, home play behavior

Themes:
- Add Neon Drift (midnight blue / electric cyan synthwave)
- Add Cupertino Light + Cupertino Dark (macOS Ventura-inspired, frosted glass)
- Add Betriebssysteme group (Cupertino, Aero Glass, Luna Teal)
- Rename all trademarked theme IDs to original names (WnAmp, Navy Jukebox,
  Cobalt Media, Onyx Cinema, Aero Glass, Luna Teal)
- Reorder ThemePicker: Psysonic Themes first, then Mediaplayer, Betriebssysteme

Keybindings:
- New keybindingsStore with 10 bindable actions (play/pause, next, prev,
  volume, seek ±10s, queue, fullscreen, native fullscreen)
- Settings UI for rebinding; defaults: Space=play-pause, F11=native-fullscreen

Font picker:
- New fontStore; 10 UI fonts selectable in Settings → Appearance
- Applied via data-font attribute on <html>

Home page:
- AlbumCard: Details button → Play button via playAlbum() utility
- Hero: Play Album starts playback with 700ms fade-out instead of navigating
- playAlbum.ts: fade, store-only volume restore, playTrack handoff

Now Playing:
- 3-column hero layout; EQ bars truly centred (flex:1 on both outer columns)
- Background brightness 0.25→0.55, overlay 0.55→0.38 (art now visible)
- Better text contrast for track times, card links, section titles

Linux audio:
- Set PIPEWIRE_LATENCY + PULSE_LATENCY_MSEC before stream creation
  to reduce ALSA snd_pcm_recover underrun frequency on PipeWire

Remove butterchurn visualizer (VisualizerCanvas, butterchurn.d.ts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-03-21 14:22:02 +01:00
parent 0b1ed8cc5a
commit 57b70e6154
30 changed files with 2760 additions and 702 deletions
+39
View File
@@ -0,0 +1,39 @@
import { getAlbum } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { songToTrack } from '../store/playerStore';
function fadeOut(setVolume: (v: number) => void, from: number, durationMs: number): Promise<void> {
return new Promise(resolve => {
const steps = 16;
const stepMs = durationMs / steps;
let step = 0;
const id = setInterval(() => {
step++;
setVolume(Math.max(0, from * (1 - step / steps)));
if (step >= steps) {
clearInterval(id);
resolve();
}
}, stepMs);
});
}
export async function playAlbum(albumId: string): Promise<void> {
const albumData = await getAlbum(albumId);
const tracks = albumData.songs.map(songToTrack);
if (!tracks.length) return;
const store = usePlayerStore.getState();
const { isPlaying, volume } = store;
if (isPlaying) {
await fadeOut(store.setVolume, volume, 700);
// Restore volume only in the Zustand store — do NOT call audio_set_volume here,
// otherwise the old track glitches back to full volume before playTrack stops it.
// playTrack reads state.volume and passes it to audio_play, so the new track
// starts at the correct volume without the Rust engine ever hearing a restore.
usePlayerStore.setState({ volume });
}
usePlayerStore.getState().playTrack(tracks[0], tracks);
}