diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ae764b..dcdfa373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ 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.15.0] - 2026-03-23 + +### Added + +- **Queue — Genre · Format · Bitrate Strip**: The meta box above the queue now shows a full-width frosted strip with Genre, audio format, and bitrate (e.g. `Electronic · FLAC · 1411 kbps`). Genre is sourced directly from track metadata and is now propagated through all 11 track construction sites across the codebase. +- **Lyrics — Accent Color Highlight**: The active synced lyrics line is now highlighted in the theme accent color instead of bold+larger text. Eliminates layout jumps caused by the font-weight change pushing lines to wrap. + +### Fixed + +- **Sidebar — Collapse Button**: The collapse button now correctly sits on the right border of the sidebar, straddling the dividing line between sidebar and main content, and is always visible. + +### Changed + +- **Queue — Tech Info**: Codec/bitrate badge replaced by the new full-width Genre · Format · Bitrate strip at the top of the meta box. + +--- + ## [1.14.0] - 2026-03-22 ### Critical Fixes diff --git a/CLAUDE.md b/CLAUDE.md index 8b02e0d1..7be07fa5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -246,4 +246,4 @@ The workflow is split into three jobs: `create-release` (creates the GitHub Rele - **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`). - **Home page**: Section order: recent → discover → artist discovery (pill-buttons, no images) → starred → mostPlayed. Artist discovery uses `getArtists()` full list + client-side Fisher-Yates shuffle (16 random), rendered as `artist-ext-link` pill-buttons (same as ArtistDetail "Similar Artists") — no image loading, no performance impact. - **CoverLightbox + EQ popup**: Both use `createPortal(…, document.body)` to escape `backdrop-filter` CSS containing-block issues on the player bar and other ancestors. -- **Version**: 1.14.0 +- **Version**: 1.15.0 diff --git a/package.json b/package.json index e3ea2b3a..41a2a1c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psysonic", - "version": "1.14.0", + "version": "1.15.0", "private": true, "scripts": { "dev": "vite", diff --git a/packages/aur/PKGBUILD b/packages/aur/PKGBUILD index ee32502c..5c6638ec 100644 --- a/packages/aur/PKGBUILD +++ b/packages/aur/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Psychotoxic pkgname=psysonic -pkgver=1.14.0 +pkgver=1.15.0 pkgrel=1 pkgdesc="Desktop music player for Subsonic API-compatible servers (Navidrome, Gonic, etc.)" arch=('x86_64') diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a554895f..e9b5c308 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psysonic" -version = "1.14.0" +version = "1.15.0" description = "Psysonic Desktop Music Player" authors = [] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f8fb7b2a..53e2ae92 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.14.0", + "version": "1.15.0", "identifier": "dev.psysonic.player", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index ee777d2e..0be986ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -135,9 +135,9 @@ function AppShell() { } as React.CSSProperties} onContextMenu={e => e.preventDefault()} > - setIsSidebarCollapsed(!isSidebarCollapsed)} + setIsSidebarCollapsed(!isSidebarCollapsed)} />
@@ -147,12 +147,12 @@ function AppShell() {
diff --git a/src/components/AlbumTrackList.tsx b/src/components/AlbumTrackList.tsx index b51108c6..2cac63e4 100644 --- a/src/components/AlbumTrackList.tsx +++ b/src/components/AlbumTrackList.tsx @@ -86,7 +86,7 @@ export default function AlbumTrackList({ albumId: song.albumId, artistId: song.artistId, duration: song.duration, coverArt: song.coverArt, track: song.track, year: song.year, bitRate: song.bitRate, suffix: song.suffix, userRating: song.userRating, - starred: song.starred, + starred: song.starred, genre: song.genre, }); return ( diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index 8ba902f4..24cc05fe 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -69,7 +69,7 @@ export default function ContextMenu() { const radioTracks = [...top, ...similar].map(s => ({ id: s.id, title: s.title, artist: s.artist, album: s.album, albumId: s.albumId, artistId: s.artistId, duration: s.duration, coverArt: s.coverArt, track: s.track, - year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating, + year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating, genre: s.genre, })); playTrack(radioTracks[0], radioTracks); } @@ -135,7 +135,7 @@ export default function ContextMenu() { const tracks = albumData.songs.map(s => ({ id: s.id, title: s.title, artist: s.artist, album: s.album, albumId: s.albumId, artistId: s.artistId, duration: s.duration, coverArt: s.coverArt, track: s.track, - year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating, + year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating, genre: s.genre, })); enqueue(tracks); })}> diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 1cee050d..89ded66a 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -153,7 +153,7 @@ export default function Hero({ albums: albumsProp }: HeroProps = {}) { const tracks = albumData.songs.map(s => ({ id: s.id, title: s.title, artist: s.artist, album: s.album, albumId: s.albumId, artistId: s.artistId, duration: s.duration, coverArt: s.coverArt, track: s.track, - year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating, + year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating, genre: s.genre, })); usePlayerStore.getState().enqueue(tracks); } catch (_) { } diff --git a/src/components/LiveSearch.tsx b/src/components/LiveSearch.tsx index 2ec20a80..6512edf4 100644 --- a/src/components/LiveSearch.tsx +++ b/src/components/LiveSearch.tsx @@ -58,7 +58,7 @@ export default function LiveSearch() { ...(results.artists.map(a => ({ id: a.id, action: () => { navigate(`/artist/${a.id}`); setOpen(false); setQuery(''); } }))), ...(results.albums.map(a => ({ id: a.id, action: () => { navigate(`/album/${a.id}`); setOpen(false); setQuery(''); } }))), ...(results.songs.map(s => ({ id: s.id, action: () => { - playTrack({ id: s.id, title: s.title, artist: s.artist, album: s.album, albumId: s.albumId, artistId: s.artistId, duration: s.duration, coverArt: s.coverArt, year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating }); + playTrack({ id: s.id, title: s.title, artist: s.artist, album: s.album, albumId: s.albumId, artistId: s.artistId, duration: s.duration, coverArt: s.coverArt, year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating, genre: s.genre }); setOpen(false); setQuery(''); }}))), ] : []; @@ -176,7 +176,7 @@ export default function LiveSearch() { return (
{currentTrack && ( -
- {(currentTrack.bitRate || currentTrack.suffix) && ( +
+ {(currentTrack.genre || currentTrack.suffix || currentTrack.bitRate) && (
- {currentTrack.suffix?.toUpperCase() ?? ''} - {currentTrack.bitRate ? ` · ${currentTrack.bitRate}` : ''} + {[ + currentTrack.genre, + currentTrack.suffix?.toUpperCase(), + currentTrack.bitRate ? `${currentTrack.bitRate} kbps` : undefined, + ].filter(Boolean).join(' · ')}
)} -
- {currentTrack.coverArt ? ( - - ) : ( -
- )} -
-
-

{currentTrack.title}

-
currentTrack.artistId && navigate(`/artist/${currentTrack.artistId}`)} - >{currentTrack.artist}
-
currentTrack.albumId && navigate(`/album/${currentTrack.albumId}`)} - >{currentTrack.album}
- {currentTrack.year && ( -
{currentTrack.year}
- )} - {renderStars(currentTrack.userRating)} +
+
+ {currentTrack.coverArt ? ( + + ) : ( +
+ )} +
+
+

{currentTrack.title}

+
currentTrack.artistId && navigate(`/artist/${currentTrack.artistId}`)} + >{currentTrack.artist}
+
currentTrack.albumId && navigate(`/album/${currentTrack.albumId}`)} + >{currentTrack.album}
+ {currentTrack.year && ( +
{currentTrack.year}
+ )} + {renderStars(currentTrack.userRating)} +
)} @@ -524,7 +529,7 @@ export default function QueuePanel() { const tracks: Track[] = data.songs.map(s => ({ id: s.id, title: s.title, artist: s.artist, album: s.album, albumId: s.albumId, artistId: s.artistId, duration: s.duration, coverArt: s.coverArt, track: s.track, - year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating, + year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating, genre: s.genre, })); if (tracks.length > 0) { clearQueue(); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index dc562052..af076484 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -61,7 +61,7 @@ function UpdateToast({ isCollapsed, latestVersion }: { isCollapsed: boolean; lat export default function Sidebar({ isCollapsed = false, - toggleCollapse + toggleCollapse, }: { isCollapsed?: boolean; toggleCollapse?: () => void; diff --git a/src/pages/Favorites.tsx b/src/pages/Favorites.tsx index c1cada0a..93cbb8a5 100644 --- a/src/pages/Favorites.tsx +++ b/src/pages/Favorites.tsx @@ -67,7 +67,7 @@ export default function Favorites() { const tracks = songs.map(s => ({ id: s.id, title: s.title, artist: s.artist, album: s.album, albumId: s.albumId, artistId: s.artistId, duration: s.duration, coverArt: s.coverArt, - track: s.track, year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating, + track: s.track, year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating, genre: s.genre, })); enqueue(tracks); }} @@ -87,7 +87,7 @@ export default function Favorites() { const track = { id: song.id, title: song.title, artist: song.artist, album: song.album, albumId: song.albumId, artistId: song.artistId, duration: song.duration, coverArt: song.coverArt, - track: song.track, year: song.year, bitRate: song.bitRate, suffix: song.suffix, userRating: song.userRating, starred: song.starred, + track: song.track, year: song.year, bitRate: song.bitRate, suffix: song.suffix, userRating: song.userRating, starred: song.starred, genre: song.genre, }; return (
0) { clearQueue(); playTrack(tracks[0], tracks); } } catch (e) { console.error('Failed to play playlist', e); } diff --git a/src/pages/RandomMix.tsx b/src/pages/RandomMix.tsx index b38f6ed1..3bf0c1bd 100644 --- a/src/pages/RandomMix.tsx +++ b/src/pages/RandomMix.tsx @@ -342,10 +342,10 @@ export default function RandomMix() { {genreMixSongs.map(song => (
playTrack(song, genreMixSongs)} role="row" draggable - onContextMenu={e => { e.preventDefault(); setContextMenuSongId(song.id); openContextMenu(e.clientX, e.clientY, { id: song.id, title: song.title, artist: song.artist, album: song.album, albumId: song.albumId, artistId: song.artistId, duration: song.duration, coverArt: song.coverArt, track: song.track, year: song.year, bitRate: song.bitRate, suffix: song.suffix, userRating: song.userRating, starred: song.starred }, 'song'); }} + onContextMenu={e => { e.preventDefault(); setContextMenuSongId(song.id); openContextMenu(e.clientX, e.clientY, { id: song.id, title: song.title, artist: song.artist, album: song.album, albumId: song.albumId, artistId: song.artistId, duration: song.duration, coverArt: song.coverArt, track: song.track, year: song.year, bitRate: song.bitRate, suffix: song.suffix, userRating: song.userRating, starred: song.starred, genre: song.genre }, 'song'); }} onDragStart={e => { e.dataTransfer.effectAllowed = 'copy'; - e.dataTransfer.setData('text/plain', JSON.stringify({ type: 'song', track: { id: song.id, title: song.title, artist: song.artist, album: song.album, albumId: song.albumId, artistId: song.artistId, duration: song.duration, coverArt: song.coverArt, track: song.track, year: song.year, bitRate: song.bitRate, suffix: song.suffix, userRating: song.userRating } })); + e.dataTransfer.setData('text/plain', JSON.stringify({ type: 'song', track: { id: song.id, title: song.title, artist: song.artist, album: song.album, albumId: song.albumId, artistId: song.artistId, duration: song.duration, coverArt: song.coverArt, track: song.track, year: song.year, bitRate: song.bitRate, suffix: song.suffix, userRating: song.userRating, genre: song.genre } })); }} >