mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
fix(playback): pin queue playback to source server when browsing another library (#717)
* fix(playback): pin queue streams, cover art, and library links to queue server When the active server changes while a queue from another server is playing, keep streams and UI on queueServerId; switch back for artist/album links and queue or player-bar context menus. * fix(playback): switch to queue server when opening Now Playing Ensure active server matches queueServerId before Subsonic fetches on the Now Playing page, mobile player route, and queue info panel; scope caches by server id. * docs(credits): mention Now Playing in PR #717 contribution line * fix(playback): route scrobble and queue sync to queue server Address PR review: apiForServer for scrobble/now-playing/savePlayQueue, clear queueServerId on server removal, mini-player queueServerId sync, block cross-server enqueue with toast, and regression tests.
This commit is contained in:
@@ -15,6 +15,8 @@ import {
|
||||
} from '../utils/componentHelpers/contextMenuActions';
|
||||
import { useContextMenuKeyboardNav } from '../hooks/useContextMenuKeyboardNav';
|
||||
import { useContextMenuRating } from '../hooks/useContextMenuRating';
|
||||
import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ContextMenuItems from './contextMenu/ContextMenuItems';
|
||||
|
||||
export { AddToPlaylistSubmenu };
|
||||
@@ -22,6 +24,8 @@ export { AddToPlaylistSubmenu };
|
||||
|
||||
export default function ContextMenu() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const navigatePlaybackLibrary = usePlaybackLibraryNavigate();
|
||||
const orbitRole = useOrbitStore(s => s.role);
|
||||
const { contextMenu, closeContextMenu, playTrack, enqueue, playNext, queue, currentTrack, removeTrack, lastfmLovedCache, setLastfmLovedForSong, starredOverrides, setStarredOverride, openSongInfo, userRatingOverrides, setUserRatingOverride } = usePlayerStore(
|
||||
useShallow(s => ({
|
||||
@@ -125,7 +129,18 @@ export default function ContextMenu() {
|
||||
}, [contextMenu.isOpen, closeContextMenu, cancelPlaylistSubmenuCloseTimer]);
|
||||
|
||||
|
||||
const { type, item, queueIndex, playlistId, playlistSongIndex, shareKindOverride } = contextMenu;
|
||||
const {
|
||||
type,
|
||||
item,
|
||||
queueIndex,
|
||||
playlistId,
|
||||
playlistSongIndex,
|
||||
shareKindOverride,
|
||||
pinToPlaybackServer = false,
|
||||
} = contextMenu;
|
||||
const navigateLibrary = pinToPlaybackServer
|
||||
? navigatePlaybackLibrary
|
||||
: (path: string) => { navigate(path); };
|
||||
|
||||
const isStarred = (id: string, itemStarred?: string) =>
|
||||
id in starredOverrides ? starredOverrides[id] : !!itemStarred;
|
||||
@@ -216,6 +231,8 @@ export default function ContextMenu() {
|
||||
downloadAlbum={downloadAlbum}
|
||||
copyShareLink={copyShareLink}
|
||||
isStarred={isStarred}
|
||||
pinToPlaybackServer={pinToPlaybackServer}
|
||||
navigateLibrary={navigateLibrary}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { star, unstar } from '../api/subsonicStarRating';
|
||||
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
|
||||
import { usePlaybackCoverArt } from '../hooks/usePlaybackCoverArt';
|
||||
import { playbackCoverArtForId } from '../utils/playback/playbackServer';
|
||||
import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
||||
import {
|
||||
SkipBack, SkipForward,
|
||||
@@ -56,12 +57,13 @@ export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) {
|
||||
|
||||
const duration = currentTrack?.duration ?? 0;
|
||||
|
||||
// buildCoverArtUrl generates a new salt on every call — must be memoized.
|
||||
// 300px for the small art box; 500px for the right-side portrait fallback.
|
||||
const artUrl = useMemo(() => currentTrack?.coverArt ? buildCoverArtUrl(currentTrack.coverArt, 300) : '', [currentTrack?.coverArt]);
|
||||
const artKey = useMemo(() => currentTrack?.coverArt ? coverArtCacheKey(currentTrack.coverArt, 300) : '', [currentTrack?.coverArt]);
|
||||
const coverUrl = useMemo(() => currentTrack?.coverArt ? buildCoverArtUrl(currentTrack.coverArt, 500) : '', [currentTrack?.coverArt]);
|
||||
const coverKey = useMemo(() => currentTrack?.coverArt ? coverArtCacheKey(currentTrack.coverArt, 500) : '', [currentTrack?.coverArt]);
|
||||
const artCover = usePlaybackCoverArt(currentTrack?.coverArt, 300);
|
||||
const artUrl = artCover.src;
|
||||
const artKey = artCover.cacheKey;
|
||||
const portraitCover = usePlaybackCoverArt(currentTrack?.coverArt, 500);
|
||||
const coverUrl = portraitCover.src;
|
||||
const coverKey = portraitCover.cacheKey;
|
||||
// `false` = no fetchUrl fallback — prevents double crossfade (fetchUrl → blobUrl).
|
||||
const resolvedCoverUrl = useCachedUrl(coverUrl, coverKey, false);
|
||||
|
||||
@@ -86,12 +88,13 @@ export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) {
|
||||
const idx = s.queueIndex;
|
||||
return (idx >= 0 && idx + 1 < q.length) ? (q[idx + 1]?.coverArt ?? null) : null;
|
||||
});
|
||||
const queueServerId = usePlayerStore(s => s.queueServerId);
|
||||
const activeServerId = useAuthStore(s => s.activeServerId);
|
||||
useEffect(() => {
|
||||
if (!nextCoverArt) return;
|
||||
const url = buildCoverArtUrl(nextCoverArt, 300);
|
||||
const key = coverArtCacheKey(nextCoverArt, 300);
|
||||
const { src: url, cacheKey: key } = playbackCoverArtForId(nextCoverArt, 300);
|
||||
getCachedBlob(url, key).catch(() => {});
|
||||
}, [nextCoverArt]);
|
||||
}, [nextCoverArt, queueServerId, activeServerId]);
|
||||
|
||||
// Lyrics settings popover state
|
||||
const [lyricsMenuOpen, setLyricsMenuOpen] = useState(false);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
|
||||
import { usePlaybackCoverArt } from '../hooks/usePlaybackCoverArt';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { registerQueueDragHitTest } from '../contexts/DragDropContext';
|
||||
import MiniContextMenu from './MiniContextMenu';
|
||||
import type { MiniSyncPayload, MiniControlAction, MiniTrackInfo } from '../utils/miniPlayerBridge';
|
||||
@@ -63,6 +64,7 @@ export default function MiniPlayer() {
|
||||
useMiniSync({
|
||||
onSync: (payload) => {
|
||||
setState(payload);
|
||||
usePlayerStore.setState({ queueServerId: payload.queueServerId ?? null });
|
||||
if (payload.track?.duration) setDuration(payload.track.duration);
|
||||
if (typeof payload.volume === 'number') setVolumeState(payload.volume);
|
||||
},
|
||||
@@ -138,14 +140,7 @@ export default function MiniPlayer() {
|
||||
}, [queueOpen, state.queueIndex]);
|
||||
|
||||
const { track, isPlaying } = state;
|
||||
const miniCoverSrc = useMemo(
|
||||
() => (track?.coverArt ? buildCoverArtUrl(track.coverArt, 300) : ''),
|
||||
[track?.coverArt],
|
||||
);
|
||||
const miniCoverKey = useMemo(
|
||||
() => (track?.coverArt ? coverArtCacheKey(track.coverArt, 300) : ''),
|
||||
[track?.coverArt],
|
||||
);
|
||||
const { src: miniCoverSrc, cacheKey: miniCoverKey } = usePlaybackCoverArt(track?.coverArt, 300);
|
||||
const progress = duration > 0 ? Math.min(100, (currentTime / duration) * 100) : 0;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { star, unstar } from '../api/subsonicStarRating';
|
||||
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
|
||||
import { usePlaybackCoverArt } from '../hooks/usePlaybackCoverArt';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
|
||||
import React, { useState, useCallback, useMemo, useRef, useEffect, useSyncExternalStore, CSSProperties } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate';
|
||||
import { useEnsurePlaybackServerOnMount } from '../hooks/useEnsurePlaybackServerOnMount';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ChevronDown, Play, Pause, SkipBack, SkipForward,
|
||||
@@ -152,6 +154,8 @@ function LyricsDrawer({ onClose, currentTrack }: { onClose: () => void; currentT
|
||||
export default function MobilePlayerView() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const navigatePlaybackLibrary = usePlaybackLibraryNavigate();
|
||||
useEnsurePlaybackServerOnMount();
|
||||
|
||||
// Lock body scroll while full-screen player is mounted
|
||||
useEffect(() => {
|
||||
@@ -185,12 +189,7 @@ export default function MobilePlayerView() {
|
||||
|
||||
const duration = currentTrack?.duration ?? 0;
|
||||
|
||||
// Cover art
|
||||
const coverFetchUrl = useMemo(
|
||||
() => currentTrack?.coverArt ? buildCoverArtUrl(currentTrack.coverArt, 800) : '',
|
||||
[currentTrack?.coverArt]
|
||||
);
|
||||
const coverKey = currentTrack?.coverArt ? coverArtCacheKey(currentTrack.coverArt, 800) : '';
|
||||
const { src: coverFetchUrl, cacheKey: coverKey } = usePlaybackCoverArt(currentTrack?.coverArt, 800);
|
||||
const resolvedCover = useCachedUrl(coverFetchUrl, coverKey);
|
||||
|
||||
// Dynamic background color extracted from cover art
|
||||
@@ -332,7 +331,7 @@ export default function MobilePlayerView() {
|
||||
<OpenArtistRefInline
|
||||
refs={currentTrack.artists}
|
||||
fallbackName={currentTrack.artist}
|
||||
onGoArtist={id => navigate(`/artist/${id}`)}
|
||||
onGoArtist={id => { void navigatePlaybackLibrary(`/artist/${id}`); }}
|
||||
as="none"
|
||||
linkTag="span"
|
||||
linkClassName="mp-artist-link"
|
||||
@@ -341,12 +340,12 @@ export default function MobilePlayerView() {
|
||||
<span
|
||||
role={currentTrack.artistId ? 'link' : undefined}
|
||||
tabIndex={currentTrack.artistId ? 0 : undefined}
|
||||
onClick={() => currentTrack.artistId && navigate(`/artist/${currentTrack.artistId}`)}
|
||||
onClick={() => currentTrack.artistId && void navigatePlaybackLibrary(`/artist/${currentTrack.artistId}`)}
|
||||
onKeyDown={e => {
|
||||
if (!currentTrack.artistId) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
navigate(`/artist/${currentTrack.artistId}`);
|
||||
void navigatePlaybackLibrary(`/artist/${currentTrack.artistId}`);
|
||||
}
|
||||
}}
|
||||
style={{ cursor: currentTrack.artistId ? 'pointer' : 'default' }}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Info } from 'lucide-react';
|
||||
import { open as shellOpen } from '@tauri-apps/plugin-shell';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useEnsurePlaybackServerOnMount } from '../hooks/useEnsurePlaybackServerOnMount';
|
||||
import { fetchBandsintownEvents, type BandsintownEvent } from '../api/bandsintown';
|
||||
import CachedImage from './CachedImage';
|
||||
import OverlayScrollArea from './OverlayScrollArea';
|
||||
@@ -72,21 +73,31 @@ function buildContributorRows(
|
||||
return out;
|
||||
}
|
||||
|
||||
function queuePanelCacheKey(serverId: string, id: string): string {
|
||||
return serverId ? `${serverId}:${id}` : id;
|
||||
}
|
||||
|
||||
export default function NowPlayingInfo() {
|
||||
const { t } = useTranslation();
|
||||
const currentTrack = usePlayerStore(s => s.currentTrack);
|
||||
const enableBandsintown = useAuthStore(s => s.enableBandsintown);
|
||||
const setEnableBandsintown = useAuthStore(s => s.setEnableBandsintown);
|
||||
const subsonicReady = useEnsurePlaybackServerOnMount();
|
||||
const subsonicServerId = useAuthStore(s => s.activeServerId ?? '');
|
||||
|
||||
const artistName = currentTrack?.artist || '';
|
||||
const artistId = currentTrack?.artistId || '';
|
||||
const songId = currentTrack?.id || '';
|
||||
|
||||
const [artistInfo, setArtistInfo] = useState<SubsonicArtistInfo | null>(
|
||||
artistId ? artistInfoCache.get(artistId) ?? null : null,
|
||||
artistId && subsonicServerId
|
||||
? artistInfoCache.get(queuePanelCacheKey(subsonicServerId, artistId)) ?? null
|
||||
: null,
|
||||
);
|
||||
const [songDetail, setSongDetail] = useState<SubsonicSong | null>(
|
||||
songId ? songDetailCache.get(songId) ?? null : null,
|
||||
songId && subsonicServerId
|
||||
? songDetailCache.get(queuePanelCacheKey(subsonicServerId, songId)) ?? null
|
||||
: null,
|
||||
);
|
||||
const [tourEvents, setTourEvents] = useState<BandsintownEvent[]>([]);
|
||||
const [tourLoading, setTourLoading] = useState(false);
|
||||
@@ -101,27 +112,29 @@ export default function NowPlayingInfo() {
|
||||
|
||||
// Artist bio + image
|
||||
useEffect(() => {
|
||||
if (!artistId) { setArtistInfo(null); return; }
|
||||
const cached = artistInfoCache.get(artistId);
|
||||
if (!subsonicReady || !subsonicServerId || !artistId) { setArtistInfo(null); return; }
|
||||
const cacheKey = queuePanelCacheKey(subsonicServerId, artistId);
|
||||
const cached = artistInfoCache.get(cacheKey);
|
||||
if (cached !== undefined) { setArtistInfo(cached); return; }
|
||||
let cancelled = false;
|
||||
getArtistInfo(artistId)
|
||||
.then(info => { if (!cancelled) { artistInfoCache.set(artistId, info ?? null); setArtistInfo(info ?? null); } })
|
||||
.catch(() => { if (!cancelled) { artistInfoCache.set(artistId, null); setArtistInfo(null); } });
|
||||
.then(info => { if (!cancelled) { artistInfoCache.set(cacheKey, info ?? null); setArtistInfo(info ?? null); } })
|
||||
.catch(() => { if (!cancelled) { artistInfoCache.set(cacheKey, null); setArtistInfo(null); } });
|
||||
return () => { cancelled = true; };
|
||||
}, [artistId]);
|
||||
}, [subsonicReady, subsonicServerId, artistId]);
|
||||
|
||||
// Song detail (for OpenSubsonic contributors[])
|
||||
useEffect(() => {
|
||||
if (!songId) { setSongDetail(null); return; }
|
||||
const cached = songDetailCache.get(songId);
|
||||
if (!subsonicReady || !subsonicServerId || !songId) { setSongDetail(null); return; }
|
||||
const cacheKey = queuePanelCacheKey(subsonicServerId, songId);
|
||||
const cached = songDetailCache.get(cacheKey);
|
||||
if (cached !== undefined) { setSongDetail(cached); return; }
|
||||
let cancelled = false;
|
||||
getSong(songId)
|
||||
.then(song => { if (!cancelled) { songDetailCache.set(songId, song ?? null); setSongDetail(song ?? null); } })
|
||||
.catch(() => { if (!cancelled) { songDetailCache.set(songId, null); setSongDetail(null); } });
|
||||
.then(song => { if (!cancelled) { songDetailCache.set(cacheKey, song ?? null); setSongDetail(song ?? null); } })
|
||||
.catch(() => { if (!cancelled) { songDetailCache.set(cacheKey, null); setSongDetail(null); } });
|
||||
return () => { cancelled = true; };
|
||||
}, [songId]);
|
||||
}, [subsonicReady, subsonicServerId, songId]);
|
||||
|
||||
// Bandsintown — only when opt-in toggle is on
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { star, unstar } from '../api/subsonicStarRating';
|
||||
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
|
||||
import { usePlaybackCoverArt } from '../hooks/usePlaybackCoverArt';
|
||||
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
@@ -17,7 +18,7 @@ import WaveformSeek from './WaveformSeek';
|
||||
import Equalizer from './Equalizer';
|
||||
import StarRating from './StarRating';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate';
|
||||
import { useLyricsStore } from '../store/lyricsStore';
|
||||
import MarqueeText from './MarqueeText';
|
||||
import LastfmIcon from './LastfmIcon';
|
||||
@@ -40,7 +41,7 @@ import { useUtilityOverflowMenu } from '../hooks/useUtilityOverflowMenu';
|
||||
|
||||
export default function PlayerBar() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const navigatePlaybackLibrary = usePlaybackLibraryNavigate();
|
||||
const [eqOpen, setEqOpen] = useState(false);
|
||||
const [showVolPct, setShowVolPct] = useState(false);
|
||||
const [localShowRemaining, setLocalShowRemaining] = useState(() => useThemeStore.getState().showRemainingTime);
|
||||
@@ -156,8 +157,14 @@ export default function PlayerBar() {
|
||||
? currentTrack.artists
|
||||
: undefined;
|
||||
|
||||
const coverSrc = useMemo(() => displayCoverArt ? buildCoverArtUrl(displayCoverArt, 128) : '', [displayCoverArt]);
|
||||
const coverKey = displayCoverArt ? coverArtCacheKey(displayCoverArt, 128) : '';
|
||||
const previewCover = useMemo(() => {
|
||||
if (!showPreviewMeta || !previewingTrack?.coverArt) return { src: '', cacheKey: '' };
|
||||
const id = previewingTrack.coverArt;
|
||||
return { src: buildCoverArtUrl(id, 128), cacheKey: coverArtCacheKey(id, 128) };
|
||||
}, [showPreviewMeta, previewingTrack?.coverArt]);
|
||||
const queueCover = usePlaybackCoverArt(showPreviewMeta ? undefined : displayCoverArt, 128);
|
||||
const coverSrc = showPreviewMeta ? previewCover.src : queueCover.src;
|
||||
const coverKey = showPreviewMeta ? previewCover.cacheKey : queueCover.cacheKey;
|
||||
|
||||
const handleVolume = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setVolume(parseFloat(e.target.value));
|
||||
@@ -220,7 +227,7 @@ export default function PlayerBar() {
|
||||
userRatingOverrides={userRatingOverrides}
|
||||
setUserRatingOverride={setUserRatingOverride}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
navigate={navigate}
|
||||
navigate={navigatePlaybackLibrary}
|
||||
openContextMenu={openContextMenu}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getPlaylist, updatePlaylist } from '../api/subsonicPlaylists';
|
||||
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
|
||||
import { songToTrack } from '../utils/playback/songToTrack';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import { useState, useRef, useMemo } from 'react';
|
||||
@@ -11,7 +10,7 @@ import HostApprovalQueue from './HostApprovalQueue';
|
||||
import { usePlaylistStore } from '../store/playlistStore';
|
||||
import { useCachedUrl } from './CachedImage';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { encodeSharePayload } from '../utils/share/shareLink';
|
||||
import { copyTextToClipboard } from '../utils/server/serverMagicString';
|
||||
@@ -34,6 +33,7 @@ import { QueueToolbar } from './queuePanel/QueueToolbar';
|
||||
import { QueueList } from './queuePanel/QueueList';
|
||||
import { QueueTabBar } from './queuePanel/QueueTabBar';
|
||||
import { useQueueAutoScroll } from '../hooks/useQueueAutoScroll';
|
||||
import { usePlaybackCoverArt } from '../hooks/usePlaybackCoverArt';
|
||||
|
||||
export default function QueuePanel() {
|
||||
const orbitRole = useOrbitStore(s => s.role);
|
||||
@@ -49,7 +49,7 @@ export default function QueuePanel() {
|
||||
|
||||
function QueuePanelHostOrSolo() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const navigatePlaybackLibrary = usePlaybackLibraryNavigate();
|
||||
const orbitRole = useOrbitStore(s => s.role);
|
||||
const orbitState = useOrbitStore(s => s.state);
|
||||
/** trackId → addedBy (host username or guest username) — only populated while
|
||||
@@ -78,13 +78,9 @@ function QueuePanelHostOrSolo() {
|
||||
const queueIndex = usePlayerStore(s => s.queueIndex);
|
||||
const currentTrack = usePlayerStore(s => s.currentTrack);
|
||||
const userRatingOverrides = usePlayerStore(s => s.userRatingOverrides);
|
||||
const currentCoverFetchUrl = useMemo(
|
||||
() => currentTrack?.coverArt ? buildCoverArtUrl(currentTrack.coverArt, 128) : '',
|
||||
[currentTrack?.coverArt]
|
||||
);
|
||||
const currentCoverCacheKey = useMemo(
|
||||
() => currentTrack?.coverArt ? coverArtCacheKey(currentTrack.coverArt, 128) : '',
|
||||
[currentTrack?.coverArt]
|
||||
const { src: currentCoverFetchUrl, cacheKey: currentCoverCacheKey } = usePlaybackCoverArt(
|
||||
currentTrack?.coverArt,
|
||||
128,
|
||||
);
|
||||
const currentCoverSrc = useCachedUrl(currentCoverFetchUrl, currentCoverCacheKey);
|
||||
const isQueueVisible = usePlayerStore(s => s.isQueueVisible);
|
||||
@@ -265,7 +261,7 @@ function QueuePanelHostOrSolo() {
|
||||
currentCoverSrc={currentCoverSrc}
|
||||
userRatingOverrides={userRatingOverrides}
|
||||
orbitAttributionLabel={orbitAttributionLabel}
|
||||
navigate={navigate}
|
||||
navigate={navigatePlaybackLibrary}
|
||||
playbackSource={playbackSource}
|
||||
normalizationEngine={normalizationEngine}
|
||||
normalizationEngineLive={normalizationEngineLive}
|
||||
|
||||
@@ -22,10 +22,12 @@ export default function AlbumContextItems(props: ContextMenuItemsProps) {
|
||||
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
||||
applySongRating, applyAlbumRating, applyArtistRating,
|
||||
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
||||
pinToPlaybackServer, navigateLibrary,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const auth = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
const goLibrary = pinToPlaybackServer ? navigateLibrary : (path: string) => { navigate(path); };
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -34,7 +36,7 @@ export default function AlbumContextItems(props: ContextMenuItemsProps) {
|
||||
const albumRatingDisabled = entityRatingSupport === 'track_only';
|
||||
return (
|
||||
<>
|
||||
<div className="context-menu-item" onClick={() => handleAction(() => navigate(`/album/${album.id}`))}>
|
||||
<div className="context-menu-item" onClick={() => handleAction(() => goLibrary(`/album/${album.id}`))}>
|
||||
<Play size={14} /> {t('contextMenu.openAlbum')}
|
||||
</div>
|
||||
<div className="context-menu-item" onClick={() => handleAction(async () => {
|
||||
@@ -52,7 +54,7 @@ export default function AlbumContextItems(props: ContextMenuItemsProps) {
|
||||
<ListPlus size={14} /> {t('contextMenu.enqueueAlbum')}
|
||||
</div>
|
||||
<div className="context-menu-divider" />
|
||||
<div className="context-menu-item" onClick={() => handleAction(() => navigate(`/artist/${album.artistId}`))}>
|
||||
<div className="context-menu-item" onClick={() => handleAction(() => goLibrary(`/artist/${album.artistId}`))}>
|
||||
<User size={14} /> {t('contextMenu.goToArtist')}
|
||||
</div>
|
||||
<div className="context-menu-item" onClick={() => handleAction(() => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Play, Radio, Heart, ChevronRight, User, Disc3, ListMusic, Info, Sparkles, Star, Trash2, Share2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { star, unstar } from '../../api/subsonicStarRating';
|
||||
import { lastfmLoveTrack, lastfmUnloveTrack } from '../../api/lastfm';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
@@ -21,10 +20,10 @@ export default function QueueItemContextItems(props: ContextMenuItemsProps) {
|
||||
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
||||
applySongRating, applyAlbumRating, applyArtistRating,
|
||||
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
||||
navigateLibrary,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const auth = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -54,12 +53,12 @@ export default function QueueItemContextItems(props: ContextMenuItemsProps) {
|
||||
</div>
|
||||
<div className="context-menu-divider" />
|
||||
{song.albumId && (
|
||||
<div className="context-menu-item" onClick={() => handleAction(() => navigate(`/album/${song.albumId}`))}>
|
||||
<div className="context-menu-item" onClick={() => handleAction(() => navigateLibrary(`/album/${song.albumId}`))}>
|
||||
<Disc3 size={14} /> {t('contextMenu.openAlbum')}
|
||||
</div>
|
||||
)}
|
||||
{song.artistId && (
|
||||
<div className="context-menu-item" onClick={() => handleAction(() => navigate(`/artist/${song.artistId}`))}>
|
||||
<div className="context-menu-item" onClick={() => handleAction(() => navigateLibrary(`/artist/${song.artistId}`))}>
|
||||
<User size={14} /> {t('contextMenu.goToArtist')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -51,4 +51,7 @@ export interface ContextMenuItemsProps {
|
||||
downloadAlbum: (albumName: string, albumId: string) => Promise<void>;
|
||||
copyShareLink: (kind: EntityShareKind, id: string) => void;
|
||||
isStarred: (id: string, itemStarred?: string) => boolean;
|
||||
/** When true, album/artist links switch to the queue server before routing. */
|
||||
pinToPlaybackServer: boolean;
|
||||
navigateLibrary: (path: string) => void | Promise<void>;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ interface Props {
|
||||
userRatingOverrides: Record<string, number>;
|
||||
setUserRatingOverride: (id: string, r: number) => void;
|
||||
toggleFullscreen: () => void;
|
||||
navigate: (to: string) => void;
|
||||
navigate: (to: string) => void | Promise<void>;
|
||||
openContextMenu: PlayerState['openContextMenu'];
|
||||
t: TFunction;
|
||||
}
|
||||
@@ -115,7 +115,7 @@ export function PlayerTrackInfo({
|
||||
songCount: 0,
|
||||
duration: 0,
|
||||
};
|
||||
openContextMenu(e.clientX, e.clientY, album, 'album');
|
||||
openContextMenu(e.clientX, e.clientY, album, 'album', undefined, undefined, undefined, undefined, true);
|
||||
}
|
||||
: undefined}
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
currentCoverSrc: string;
|
||||
userRatingOverrides: Record<string, number>;
|
||||
orbitAttributionLabel: (trackId: string) => string | null;
|
||||
navigate: (to: string) => void;
|
||||
navigate: (to: string) => void | Promise<void>;
|
||||
playbackSource: PlaybackSourceKind | null;
|
||||
normalizationEngine: NormalizationEngine;
|
||||
normalizationEngineLive: 'off' | 'replaygain' | 'loudness';
|
||||
|
||||
Reference in New Issue
Block a user