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:
cucadmuh
2026-05-15 16:08:41 +03:00
committed by GitHub
parent a9b50b9244
commit 45e0e1206f
58 changed files with 701 additions and 178 deletions
+18 -1
View File
@@ -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>
</>
+12 -9
View File
@@ -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);
+4 -9
View File
@@ -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 (
+9 -10
View File
@@ -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' }}
+25 -12
View File
@@ -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(() => {
+12 -5
View File
@@ -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}
/>
+7 -11
View File
@@ -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>;
}
+2 -2
View File
@@ -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';