From 57b70e6154294a890c375ca0046382cd757b9243 Mon Sep 17 00:00:00 2001 From: Psychotoxical Date: Sat, 21 Mar 2026 14:22:02 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20v1.9.0=20=E2=80=94=20new=20themes,=20ke?= =?UTF-8?q?ybindings,=20font=20picker,=20home=20play=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- .gitignore | 6 + CHANGELOG.md | 54 + CLAUDE.md | 34 +- README.md | 10 +- package-lock.json | 63 +- package.json | 4 +- packages/aur/PKGBUILD | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/audio.rs | 15 +- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 52 +- src/api/subsonic.ts | 1 + src/components/AlbumCard.tsx | 7 +- src/components/FullscreenPlayer.tsx | 53 +- src/components/Hero.tsx | 3 +- src/components/QueuePanel.tsx | 27 +- src/components/ThemePicker.tsx | 37 +- src/components/VisualizerCanvas.tsx | 130 --- src/i18n.ts | 112 +- src/pages/NowPlaying.tsx | 595 +++++------ src/pages/Settings.tsx | 140 ++- src/store/fontStore.ts | 19 + src/store/keybindingsStore.ts | 80 ++ src/store/themeStore.ts | 2 +- src/styles/components.css | 406 ++++++- src/styles/layout.css | 5 +- src/styles/theme.css | 1540 ++++++++++++++++++++++++++- src/types/butterchurn.d.ts | 20 - src/utils/playAlbum.ts | 39 + 30 files changed, 2760 insertions(+), 702 deletions(-) delete mode 100644 src/components/VisualizerCanvas.tsx create mode 100644 src/store/fontStore.ts create mode 100644 src/store/keybindingsStore.ts delete mode 100644 src/types/butterchurn.d.ts create mode 100644 src/utils/playAlbum.ts diff --git a/.gitignore b/.gitignore index 1a5289ab..afce1c1b 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,9 @@ src-tauri/target/ # Documentation CLAUDE.md + +# Claude Code memory (local only) +memory/ + +# Local scratchpad / notes (not committed) +tmp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b3c6724..24062cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,60 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.9.0] - 2026-03-21 + +### Added + +#### Three New Themes +- **Neon Drift**: Deep midnight-blue background (`#12132c`) with electric cyan accent (`#00f2ff`) — subtle synthwave/cyberpunk aesthetic. Glowing player track name, cyan-glow nav active state, neon-lit primary buttons, glowing range slider thumb. +- **Cupertino Light**: macOS Ventura-inspired light theme. Clean white base, Apple-grey sidebar (`#f2f2f7`), Apple blue accent (`#0071e3`). Frosted-glass sidebar and player bar with `backdrop-filter: blur`. Solid blue pill nav active (white text, no left border). +- **Cupertino Dark**: macOS Ventura-inspired dark theme. Space Grey base (`#1e1e1f`), dark frosted sidebar, vibrant blue accent (`#007aff`). Same pill nav active as Cupertino Light. Solid blue Play/Pause button with glow. + +#### New Theme Group: Betriebssysteme +- OS-aesthetic themes are now consolidated into one group: **Cupertino Light**, **Cupertino Dark**, **Aero Glass**, **Luna Teal**. +- **Psysonic Themes** and **Psysonic Themes — Mediaplayer** moved to the top of the theme picker. + +#### Configurable Keybindings +- New `keybindingsStore` with 10 bindable actions: Play/Pause, Next, Previous, Volume Up/Down, Seek ±10 s, Toggle Queue, Fullscreen Player, Native Fullscreen. +- Rebind any action in **Settings → Keybindings** — click the key badge, press any key, saved immediately to `localStorage`. +- Defaults: `Space` = Play/Pause, `F11` = Native Fullscreen. All other actions unbound by default. + +#### Font Picker +- 10 UI fonts selectable in **Settings → Appearance**: Inter, Outfit, DM Sans, Nunito, Rubik, Space Grotesk, Figtree, Manrope, Plus Jakarta Sans, Lexend. +- Persisted in `localStorage` (`psysonic_font`), applied via `data-font` attribute on ``. + +#### Home Page — Instant Play +- **Album cards**: "Details" button replaced with a **Play** button — clicking plays the album immediately with a smooth 700 ms fade-out of the current track. +- **Hero**: "Play Album" button now starts playback directly (with fade-out) instead of navigating to the album detail page. +- Fade-out implemented via `playAlbum.ts` utility: fades volume to 0 over 700 ms, restores volume in the store (no Rust side-effect) before handing off to `playTrack`. + +#### Now Playing Page — Layout & Readability +- **3-column hero layout**: album cover + info (left, `flex: 1`) — EQ bars (centre, fixed width) — tag cloud (right, `flex: 1`). EQ bars are now truly centred regardless of content length on either side. +- **Background**: increased brightness from `0.25` to `0.55`, reduced overlay opacity from `0.55` to `0.38` — background art is now visible instead of near-black. +- **Text contrast**: track times, card links (artist/album), and section title opacity all increased for better readability on the blurred background. + +### Changed + +#### Theme Renames — Trademark-Safe Names +All media-player and OS-themed theme IDs and labels have been renamed to avoid potential trademark conflicts: + +| Old Name | New Name | +|---|---| +| Classic Winamp | WnAmp | +| Musicmatch Jukebox | Navy Jukebox | +| WMP8 Classic | Cobalt Media | +| PowerDVD Classic | Onyx Cinema | +| Win7 Aero | Aero Glass | +| WinXP Luna | Luna Teal | + +> **Note**: If you had one of these themes selected, your preference will reset to Mocha on first launch. Re-select your preferred theme in Settings. + +### Fixed + +- **Linux — ALSA underruns**: `PIPEWIRE_LATENCY` (`4096/48000` ≈ 85 ms) and `PULSE_LATENCY_MSEC` (`85`) are now set before audio stream creation, reducing the frequency of ALSA `snd_pcm_recover` underrun events on PipeWire systems. Existing user-set values are respected. + +--- + ## [1.8.0] - 2026-03-21 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 9138c4a4..94cb2a14 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,10 @@ There are no test scripts. TypeScript compilation (`tsc`) is part of the build. | `src/store/authStore.ts` | Multi-server support via `ServerProfile[]` + `activeServerId`. `getBaseUrl()` / `getActiveServer()` used by subsonic.ts. Also stores Last.fm session key, username, scrobbling toggle. Persisted via **`localStorage`** (synchronous — do not change to async storage). | | `src/store/playerStore.ts` | Playback state, queue, scrobbling at 50% via Last.fm, server queue sync (debounced 1.5s). On `playTrack`: calls `reportNowPlaying` (Navidrome) + `lastfmUpdateNowPlaying` (Last.fm) independently. Persists `currentTrack`, `queue`, `queueIndex`, `currentTime` for cold-start resume. | | `src-tauri/src/audio.rs` | Rust audio engine: `audio_play`, `audio_pause`, `audio_resume`, `audio_stop`, `audio_seek`, `audio_set_volume` commands. Emits `audio:playing`, `audio:progress` (500ms), `audio:ended`, `audio:error` events. | -| `src/store/themeStore.ts` | Theme selection (21 themes), applied as `data-theme` on `` | +| `src/store/themeStore.ts` | Theme selection (30 themes), applied as `data-theme` on `` | +| `src/store/fontStore.ts` | Font selection (10 fonts), applied as `data-font` on ``. Persisted in `psysonic_font`. | +| `src/store/keybindingsStore.ts` | Configurable keybindings — maps `KeyAction` to `e.code` strings. Persisted in `psysonic_keybindings`. | +| `src/utils/playAlbum.ts` | `playAlbum(albumId)` — fetches album, fades out current track (700 ms), restores volume in store only (no Rust invoke), calls `playTrack`. Used by `AlbumCard` and `Hero` play buttons. | | `src-tauri/src/lib.rs` | Tray menu, media key global shortcuts (disabled on Linux), `exit_app` command | | `src/App.tsx` | Root routing, `RequireAuth` guard, `TauriEventBridge` (media keys → store actions), `` mount | | `src/i18n.ts` | All translations (en + de + fr + nl) inline. Language persisted in `localStorage('psysonic_language')`. | @@ -107,12 +110,25 @@ Use `getActiveServer()` to get the current server, `getBaseUrl()` to get its URL Add a function to `src/api/subsonic.ts` using the `api()` helper. The helper automatically injects auth params and unwraps `subsonic-response`. ### Themes -21 themes are available, selectable in Settings via `ThemePicker` (grouped). `themeStore` persists the choice and sets `data-theme` on ``. All component CSS uses semantic tokens (`--accent`, `--text-primary`, etc.) — only the player button gradient and a few decorative elements reference `--ctp-*` palette vars directly, so every theme must define the full `--ctp-*` set. +30 themes are available, selectable in Settings via `ThemePicker` (grouped). `themeStore` persists the choice and sets `data-theme` on ``. All component CSS uses semantic tokens (`--accent`, `--text-primary`, etc.) — only the player button gradient and a few decorative elements reference `--ctp-*` palette vars directly, so every theme must define the full `--ctp-*` set. -`--volume-accent` overrides the volume slider colour independently of `--accent` (used by Classic Winamp for orange volume, yellow accent elsewhere). +`--volume-accent` overrides the volume slider colour independently of `--accent` (used by WnAmp for orange volume, yellow accent elsewhere). | Theme | Group | Style | Accent | |---|---|---|---| +| `poison` | Psysonic Themes | dark charcoal, phosphor green LCD glow | Green `#1bd655` | +| `nucleo` | Psysonic Themes | warm brass/cream light | Brass `#7a5218` | +| `psychowave` | Psysonic Themes | deep violet synthwave | Purple `#a06ae0` | +| `vintage-tube-radio` | Psysonic Themes | warm brown, VFD orange | Orange `#FF6F00` | +| `neon-drift` | Psysonic Themes | midnight blue, electric cyan glow | Cyan `#00f2ff` | +| `wnamp` | Psysonic — Mediaplayer | cool gray-blue dark, LCD glow, Courier New | Yellow `#d4cc46`, volume `#de9b35` | +| `navy-jukebox` | Psysonic — Mediaplayer | silver/blue light | Blue `#0070a0` | +| `cobalt-media` | Psysonic — Mediaplayer | cobalt blue dark | Lime `#45ff00` | +| `onyx-cinema` | Psysonic — Mediaplayer | near-black cinematic | Cyan `#00aaff` | +| `cupertino-light` | Betriebssysteme | macOS light, frosted glass | Apple Blue `#0071e3` | +| `cupertino-dark` | Betriebssysteme | macOS Space Grey, frosted glass | Vibrant Blue `#007aff` | +| `aero-glass` | Betriebssysteme | Win7 Aero glass blue | Blue `#1878e8` | +| `luna-teal` | Betriebssysteme | WinXP Luna, green gel buttons | Green `#3c9d29` | | `mocha` | Catppuccin | dark | Mauve | | `macchiato` | Catppuccin | medium-dark | Mauve | | `frappe` | Catppuccin | medium | Mauve | @@ -127,13 +143,9 @@ Add a function to `src/api/subsonic.ts` using the `api()` helper. The helper | `gruvbox-light-hard` | Retro | Gruvbox light hard | Orange `#af3a03` | | `gruvbox-light-medium` | Retro | Gruvbox light medium | Orange `#af3a03` | | `gruvbox-light-soft` | Retro | Gruvbox light soft | Orange `#af3a03` | -| `tokyo-night` | Tokyo Night | dark blue | Purple `#7aa2f7` | -| `tokyo-night-storm` | Tokyo Night | stormy blue-gray | Purple `#7aa2f7` | -| `tokyo-night-light` | Tokyo Night | light | Purple `#7aa2f7` | -| `classic-winamp` | Psysonic | cool gray-blue dark, LCD glow, Courier New | Yellow `#d4cc46`, volume `#de9b35` | -| `poison` | Psysonic | dark charcoal, phosphor green LCD glow | Green `#1bd655` | -| `nucleo` | Psysonic | warm brass/cream light | Gold `#c8a860` | -| `psychowave` | Psysonic | deep violet synthwave | Purple `#a06ae0` | +| `tokyo-night` | Tokyo Night | dark blue | Blue `#7aa2f7` | +| `tokyo-night-storm` | Tokyo Night | stormy blue-gray | Blue `#7aa2f7` | +| `tokyo-night-light` | Tokyo Night | light | Blue `#7aa2f7` | **Light-theme gotcha**: The Hero and Fullscreen Player sit on top of album-art backgrounds with dark overlays. Their text colors are hardcoded white (not `var(--text-primary)`) so they stay readable in light themes (Latte, Nord Snowstorm). @@ -210,4 +222,4 @@ The workflow is split into three jobs: `create-release` (creates the GitHub Rele - **Last.fm API key**: Stored in `.env` as `VITE_LASTFM_API_KEY` / `VITE_LASTFM_API_SECRET`. Bundled into the JS at build time (Vite). Not in git. For desktop apps this is acceptable — Last.fm's own docs acknowledge client-side keys can't be truly hidden. - **NowPlayingDropdown refresh**: `spinning` state is separate from `loading` — button is always clickable. Spin lasts min 600 ms via `setTimeout`. Background poll (`loading`) does not block the button. - **CoverLightbox**: Shared component (`src/components/CoverLightbox.tsx`). Props: `{ src, alt, onClose }`. ESC + overlay click to close. Used in `AlbumHeader` (album cover) and `ArtistDetail` (artist avatar, wrapped in `.artist-detail-avatar-btn`). -- **Version**: 1.8.0 +- **Version**: 1.9.0 diff --git a/README.md b/README.md index afd19bee..db31e806 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Designed specifically for users hosting their own music via Navidrome or other S ## ✨ Features -- 🎨 **Gorgeous UI**: 21 deeply integrated themes — Catppuccin, Nord, Retro (Gruvbox), Tokyo Night, and Psysonic originals (Classic Winamp, Poison, Nucleo, Psychowave) — with smooth glassmorphism effects and micro-animations. +- 🎨 **Gorgeous UI**: 30 deeply integrated themes across 7 groups — Catppuccin, Nord, Retro (Gruvbox), Tokyo Night, Betriebssysteme (Cupertino, Aero Glass, Luna Teal), and Psysonic originals (WnAmp, Poison, Nucleo, Neon Drift…) — with smooth glassmorphism effects and micro-animations. - ⚡ **Blazing Fast**: Built with Rust & Tauri — native audio engine (rodio), minimal RAM usage compared to typical Electron apps. - 🌍 **Internationalization (i18n)**: Fully translated into English, German, French, and Dutch. - 📻 **Live "Now Playing"**: See what other users on your server are currently listening to in real-time. @@ -31,8 +31,9 @@ Designed specifically for users hosting their own music via Navidrome or other S - 📀 **Album Downloads**: Support for downloading entire albums directly to your local machine. - 💿 **Album & Artist Views**: Beautiful grid displays and detailed artist pages with related albums and color-coded initial avatars for fast browsing. - 〰️ **Waveform Seekbar**: Canvas-based waveform with a blue-to-mauve gradient and glow effect — click or drag anywhere to seek. -- 🌊 **MilkDrop Visualizer**: Full-screen Butterchurn/MilkDrop visualizer in the Ambient Stage with hundreds of presets and smooth transitions. - 🎛️ **Queue Management**: Drag & drop reordering, shuffle, playlist saving/loading, and server-side queue synchronization. +- ⌨️ **Configurable Keybindings**: Rebind any playback action (play/pause, next, seek, volume…) directly in Settings. +- 🔤 **Font Picker**: 10 UI fonts to choose from in Settings → Appearance. - 🎼 **Random Mix**: Generate a random playlist from your entire library. Filter by keyword or pick a Super Genre (Metal, Rock, Electronic, Jazz…) for a focused mix with progressive loading. - 🔄 **Update Notifications**: Built-in update checker (on startup + every 10 minutes) that notifies you when a new version is available on GitHub. - 🖥️ **Cross-Platform**: Available natively for Windows, macOS, and Linux (including Wayland support). @@ -45,7 +46,6 @@ Designed specifically for users hosting their own music via Navidrome or other S - [x] Crossfade between tracks - [x] Replay Gain (track + album mode) - [x] Gapless playback *(experimental)* -- [x] MilkDrop/Butterchurn visualizer - [x] Waveform seekbar - [x] Last.fm scrobbling, Now Playing & love/unlove *(beta)* - [x] Similar Artists via Last.fm, filtered to library @@ -53,9 +53,11 @@ Designed specifically for users hosting their own music via Navidrome or other S - [x] Multi-server support - [x] IndexedDB image caching - [x] Random Mix with keyword filter & Super Genre mix -- [x] 21 themes: Catppuccin, Nord, Retro (Gruvbox), Tokyo Night, Psysonic originals (Classic Winamp, Poison, Nucleo, Psychowave) +- [x] 30 themes across 7 groups: Catppuccin, Nord, Retro (Gruvbox), Tokyo Night, Betriebssysteme, Psysonic originals, Psysonic Mediaplayer - [x] Internationalization (English, German, French, Dutch) - [x] AUR package (Arch / CachyOS) +- [x] Configurable keybindings +- [x] Font picker (10 UI fonts) ### 🚧 In Progress - [ ] **Last.fm integration** — stabilising, moving out of beta diff --git a/package-lock.json b/package-lock.json index 27024d92..22d43232 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "psysonic", - "version": "1.4.5", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "psysonic", - "version": "1.4.5", + "version": "1.8.0", "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.6.0", @@ -17,8 +17,6 @@ "@tauri-apps/plugin-store": "^2", "@tauri-apps/plugin-window-state": "^2.4.1", "axios": "^1.7.7", - "butterchurn": "^2.6.7", - "butterchurn-presets": "^2.4.7", "i18next": "^25.8.16", "lucide-react": "^0.462.0", "md5": "^2.3.0", @@ -1667,16 +1665,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", - "license": "MIT", - "dependencies": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", @@ -1724,27 +1712,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/butterchurn": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/butterchurn/-/butterchurn-2.6.7.tgz", - "integrity": "sha512-BJiRA8L0L2+84uoG2SSfkp0kclBuN+vQKf217pK7pMlwEO2ZEg3MtO2/o+l8Qpr8Nbejg8tmL1ZHD1jmhiaaqg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "ecma-proposal-math-extensions": "0.0.2" - } - }, - "node_modules/butterchurn-presets": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/butterchurn-presets/-/butterchurn-presets-2.4.7.tgz", - "integrity": "sha512-4MdM8ripz/VfH1BCldrIKdAc/1ryJFBDvqlyow6Ivo1frwj0H3duzvSMFC7/wIjAjxb1QpwVHVqGqS9uAFKhpg==", - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.26.0", - "ecma-proposal-math-extensions": "0.0.2", - "lodash": "^4.17.4" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1807,14 +1774,6 @@ "dev": true, "license": "MIT" }, - "node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true, - "license": "MIT" - }, "node_modules/crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", @@ -1872,12 +1831,6 @@ "node": ">= 0.4" } }, - "node_modules/ecma-proposal-math-extensions": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/ecma-proposal-math-extensions/-/ecma-proposal-math-extensions-0.0.2.tgz", - "integrity": "sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q==", - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.307", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", @@ -2236,12 +2189,6 @@ "node": ">=6" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2496,12 +2443,6 @@ "react-dom": ">=16.8" } }, - "node_modules/regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "license": "MIT" - }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", diff --git a/package.json b/package.json index 0f6e6660..a3a72f96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psysonic", - "version": "1.8.0", + "version": "1.9.0", "private": true, "scripts": { "dev": "vite", @@ -20,8 +20,6 @@ "@tauri-apps/plugin-store": "^2", "@tauri-apps/plugin-window-state": "^2.4.1", "axios": "^1.7.7", - "butterchurn": "^2.6.7", - "butterchurn-presets": "^2.4.7", "i18next": "^25.8.16", "lucide-react": "^0.462.0", "md5": "^2.3.0", diff --git a/packages/aur/PKGBUILD b/packages/aur/PKGBUILD index 6e5597d8..becd8538 100644 --- a/packages/aur/PKGBUILD +++ b/packages/aur/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: stelle pkgname=psysonic -pkgver=1.8.0 +pkgver=1.9.0 pkgrel=1 pkgdesc="Desktop music player for Subsonic API-compatible servers (Navidrome, Gonic, etc.)" arch=('x86_64') diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6269ee04..dd9c96ed 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3139,7 +3139,7 @@ dependencies = [ [[package]] name = "psysonic" -version = "1.8.0" +version = "1.9.0" dependencies = [ "biquad", "md5", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index eac0075b..1eafeed2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psysonic" -version = "1.8.0" +version = "1.9.0" description = "Psysonic Desktop Music Player" authors = [] license = "" diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index f8c7037d..423a556b 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -389,7 +389,7 @@ pub(crate) struct PreloadedTrack { /// Info about the track that has been appended (chained) to the current Sink /// but whose source has not yet started playing (gapless mode only). -struct ChainedInfo { +pub(crate) struct ChainedInfo { /// The URL that was chained — used by audio_play to detect a pre-chain hit. url: String, duration_secs: f64, @@ -446,6 +446,19 @@ impl AudioCurrent { pub fn create_engine() -> (AudioEngine, std::thread::JoinHandle<()>) { let (tx, rx) = std::sync::mpsc::sync_channel::(0); + // Request a larger audio buffer from PipeWire/PulseAudio to reduce ALSA underruns. + // Only set if the user hasn't already configured these themselves. + // PIPEWIRE_LATENCY: 4096 frames / 48000 Hz ≈ 85 ms — enough to absorb scheduler jitter. + #[cfg(target_os = "linux")] + { + if std::env::var("PIPEWIRE_LATENCY").is_err() { + std::env::set_var("PIPEWIRE_LATENCY", "4096/48000"); + } + if std::env::var("PULSE_LATENCY_MSEC").is_err() { + std::env::set_var("PULSE_LATENCY_MSEC", "85"); + } + } + let thread = std::thread::Builder::new() .name("psysonic-audio-stream".into()) .spawn(move || match rodio::OutputStream::try_default() { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 55588902..fa02cc49 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Psysonic", - "version": "1.8.0", + "version": "1.9.0", "identifier": "dev.psysonic.player", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index 930c6a9c..b07621a3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,7 +38,9 @@ import { useConnectionStatus } from './hooks/useConnectionStatus'; import { useAuthStore } from './store/authStore'; import { usePlayerStore, initAudioListeners } from './store/playerStore'; import { useThemeStore } from './store/themeStore'; +import { useFontStore } from './store/fontStore'; import { useEqStore } from './store/eqStore'; +import { useKeybindingsStore } from './store/keybindingsStore'; function RequireAuth({ children }: { children: React.ReactNode }) { const { isLoggedIn, servers, activeServerId } = useAuthStore(); @@ -207,24 +209,49 @@ function TauriEventBridge() { const previous = usePlayerStore(s => s.previous); const { minimizeToTray } = useAuthStore(); - // Spacebar → play/pause, F11 → window fullscreen + // Configurable keybindings useEffect(() => { const onKey = (e: KeyboardEvent) => { - if (e.code === 'F11') { - e.preventDefault(); - const win = getCurrentWindow(); - win.isFullscreen().then(fs => win.setFullscreen(!fs)); - return; - } - if (e.code !== 'Space') return; const tag = (e.target as HTMLElement)?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; + + const { bindings } = useKeybindingsStore.getState(); + const { togglePlay, next, previous, setVolume, seek, toggleQueue, toggleFullscreen } = usePlayerStore.getState(); + + const action = (Object.entries(bindings) as [string, string | null][]) + .find(([, code]) => code === e.code)?.[0]; + + if (!action) return; e.preventDefault(); - togglePlay(); + + switch (action) { + case 'play-pause': togglePlay(); break; + case 'next': next(); break; + case 'prev': previous(); break; + case 'volume-up': setVolume(Math.min(1, usePlayerStore.getState().volume + 0.05)); break; + case 'volume-down': setVolume(Math.max(0, usePlayerStore.getState().volume - 0.05)); break; + case 'seek-forward': { + const s = usePlayerStore.getState(); + seek(Math.min(s.currentTrack?.duration ?? 0, s.currentTime + 10)); + break; + } + case 'seek-backward': { + const s = usePlayerStore.getState(); + seek(Math.max(0, s.currentTime - 10)); + break; + } + case 'toggle-queue': toggleQueue(); break; + case 'fullscreen-player': toggleFullscreen(); break; + case 'native-fullscreen': { + const win = getCurrentWindow(); + win.isFullscreen().then(fs => win.setFullscreen(!fs)); + break; + } + } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); - }, [togglePlay]); + }, []); useEffect(() => { const unlisten: Array<() => void> = []; @@ -255,11 +282,16 @@ function TauriEventBridge() { export default function App() { const theme = useThemeStore(s => s.theme); + const font = useFontStore(s => s.font); useEffect(() => { document.documentElement.setAttribute('data-theme', theme); }, [theme]); + useEffect(() => { + document.documentElement.setAttribute('data-font', font); + }, [font]); + useEffect(() => { return initAudioListeners(); }, []); diff --git a/src/api/subsonic.ts b/src/api/subsonic.ts index df537bce..8b63e573 100644 --- a/src/api/subsonic.ts +++ b/src/api/subsonic.ts @@ -124,6 +124,7 @@ export interface SubsonicArtistInfo { smallImageUrl?: string; mediumImageUrl?: string; largeImageUrl?: string; + similarArtist?: Array<{ id: string; name: string; albumCount?: number }>; } // ─── API Methods ────────────────────────────────────────────── diff --git a/src/components/AlbumCard.tsx b/src/components/AlbumCard.tsx index f270092c..b29dde9a 100644 --- a/src/components/AlbumCard.tsx +++ b/src/components/AlbumCard.tsx @@ -4,6 +4,7 @@ import { Play } from 'lucide-react'; import { SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic'; import { usePlayerStore } from '../store/playerStore'; import CachedImage from './CachedImage'; +import { playAlbum } from '../utils/playAlbum'; interface AlbumCardProps { album: SubsonicAlbum; @@ -50,10 +51,10 @@ export default function AlbumCard({ album }: AlbumCardProps) {
diff --git a/src/components/FullscreenPlayer.tsx b/src/components/FullscreenPlayer.tsx index 3cbae117..8765212e 100644 --- a/src/components/FullscreenPlayer.tsx +++ b/src/components/FullscreenPlayer.tsx @@ -1,13 +1,12 @@ import React, { useCallback, useEffect, useState, useRef, memo } from 'react'; import { Play, Pause, SkipBack, SkipForward, - ChevronDown, Repeat, Repeat1, Square, Music, AudioWaveform, Shuffle + ChevronDown, Repeat, Repeat1, Square, Music } from 'lucide-react'; import { usePlayerStore } from '../store/playerStore'; import { buildCoverArtUrl, coverArtCacheKey, getArtistInfo } from '../api/subsonic'; import CachedImage, { useCachedUrl } from './CachedImage'; import { useTranslation } from 'react-i18next'; -import VisualizerCanvas from './VisualizerCanvas'; function formatTime(seconds: number): string { if (!seconds || isNaN(seconds)) return '0:00'; @@ -148,10 +147,6 @@ export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) { const stop = usePlayerStore(s => s.stop); const toggleRepeat = usePlayerStore(s => s.toggleRepeat); - const [vizActive, setVizActive] = useState(false); - const [nextPresetTrigger, setNextPresetTrigger] = useState(0); - const [presetName, setPresetName] = useState(''); - const duration = currentTrack?.duration ?? 0; const coverUrl = currentTrack?.coverArt ? buildCoverArtUrl(currentTrack.coverArt, 800) : ''; const coverKey = currentTrack?.coverArt ? coverArtCacheKey(currentTrack.coverArt, 800) : ''; @@ -182,55 +177,15 @@ export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) { return (
- {/* Layer 1 — blurred artist image OR visualizer */} - {vizActive && currentTrack ? ( - - ) : ( - <> - -