feat: v1.26.0 — Bulk Select, Song Info, Favorite Button, Recently Played

### Added
- Favorite/Star button in player bar (requested by @halfkey)
- Bulk multi-select for album tracklist and playlist detail (add to playlist, remove from playlist)
- Song Info modal via right-click context menu (metadata: format, bitrate, sample rate, bit depth, channels, file size, path, replay gain)
- Recently Played section on Home page
- "Show activity in Now Playing" opt-in toggle in Settings → Behavior

### Fixed
- Queue cover art not updating on track change (useCachedUrl/CachedImage reset on cacheKey change)
- FullscreenPlayer background flickering (FsBg preloads image before layer transition)
- Playlist card delete confirmation visual (size expansion + pulse animation)
- Gruvbox Light Soft: back button and badge invisible against light background

### Changed
- buildStreamUrl: removed unused suffix parameter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-01 20:57:28 +02:00
parent 7d1c66071e
commit 434ee0ecf1
26 changed files with 1074 additions and 221 deletions
+4
View File
@@ -34,6 +34,7 @@ interface AuthState {
crossfadeSecs: number;
gaplessEnabled: boolean;
minimizeToTray: boolean;
nowPlayingEnabled: boolean;
showChangelogOnUpdate: boolean;
lastSeenChangelogVersion: string;
@@ -66,6 +67,7 @@ interface AuthState {
setCrossfadeSecs: (v: number) => void;
setGaplessEnabled: (v: boolean) => void;
setMinimizeToTray: (v: boolean) => void;
setNowPlayingEnabled: (v: boolean) => void;
setShowChangelogOnUpdate: (v: boolean) => void;
setLastSeenChangelogVersion: (v: string) => void;
logout: () => void;
@@ -99,6 +101,7 @@ export const useAuthStore = create<AuthState>()(
crossfadeSecs: 3,
gaplessEnabled: false,
minimizeToTray: false,
nowPlayingEnabled: false,
showChangelogOnUpdate: true,
lastSeenChangelogVersion: '',
isLoggedIn: false,
@@ -164,6 +167,7 @@ export const useAuthStore = create<AuthState>()(
setCrossfadeSecs: (v) => set({ crossfadeSecs: v }),
setGaplessEnabled: (v) => set({ gaplessEnabled: v }),
setMinimizeToTray: (v) => set({ minimizeToTray: v }),
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
setShowChangelogOnUpdate: (v) => set({ showChangelogOnUpdate: v }),
setLastSeenChangelogVersion: (v) => set({ lastSeenChangelogVersion: v }),
+16 -4
View File
@@ -26,6 +26,8 @@ export interface Track {
replayGainPeak?: number;
starred?: string;
genre?: string;
samplingRate?: number;
bitDepth?: number;
}
export function songToTrack(song: SubsonicSong): Track {
@@ -48,6 +50,8 @@ export function songToTrack(song: SubsonicSong): Track {
replayGainPeak: song.replayGain?.trackPeak,
starred: song.starred,
genre: song.genre,
samplingRate: song.samplingRate,
bitDepth: song.bitDepth,
};
}
@@ -112,6 +116,10 @@ interface PlayerState {
};
openContextMenu: (x: number, y: number, item: any, type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song', queueIndex?: number) => void;
closeContextMenu: () => void;
songInfoModal: { isOpen: boolean; songId: string | null };
openSongInfo: (songId: string) => void;
closeSongInfo: () => void;
}
// ─── Module-level playback primitives ─────────────────────────────────────────
@@ -294,8 +302,8 @@ function handleAudioTrackSwitched(duration: number) {
});
// Report Now Playing to Navidrome + Last.fm
reportNowPlaying(nextTrack.id);
const { scrobblingEnabled, lastfmSessionKey } = useAuthStore.getState();
const { nowPlayingEnabled, scrobblingEnabled, lastfmSessionKey } = useAuthStore.getState();
if (nowPlayingEnabled) reportNowPlaying(nextTrack.id);
if (lastfmSessionKey) {
if (scrobblingEnabled) lastfmUpdateNowPlaying(nextTrack, lastfmSessionKey);
lastfmGetTrackLoved(nextTrack.title, nextTrack.artist, lastfmSessionKey).then(loved => {
@@ -439,6 +447,10 @@ export const usePlayerStore = create<PlayerState>()(
contextMenu: { ...state.contextMenu, isOpen: false },
})),
songInfoModal: { isOpen: false, songId: null },
openSongInfo: (songId) => set({ songInfoModal: { isOpen: true, songId } }),
closeSongInfo: () => set({ songInfoModal: { isOpen: false, songId: null } }),
toggleQueue: () => set(state => ({ isQueueVisible: !state.isQueueVisible })),
setQueueVisible: (v: boolean) => set({ isQueueVisible: v }),
toggleFullscreen: () => set(state => ({ isFullscreenOpen: !state.isFullscreenOpen })),
@@ -558,8 +570,8 @@ export const usePlayerStore = create<PlayerState>()(
});
// Report Now Playing to Navidrome (for Live/getNowPlaying) + Last.fm
reportNowPlaying(track.id);
const { scrobblingEnabled: lfmEnabled, lastfmSessionKey: lfmKey } = useAuthStore.getState();
const { nowPlayingEnabled: npEnabled, scrobblingEnabled: lfmEnabled, lastfmSessionKey: lfmKey } = useAuthStore.getState();
if (npEnabled) reportNowPlaying(track.id);
if (lfmKey) {
if (lfmEnabled) lastfmUpdateNowPlaying(track, lfmKey);
lastfmGetTrackLoved(track.title, track.artist, lfmKey).then(loved => {