mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
45e0e1206f
* 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.
125 lines
6.6 KiB
TypeScript
125 lines
6.6 KiB
TypeScript
import { useTranslation } from 'react-i18next';
|
|
import { Play, Radio, Heart, ChevronRight, User, Disc3, ListMusic, Info, Sparkles, Star, Trash2, Share2 } from 'lucide-react';
|
|
import { star, unstar } from '../../api/subsonicStarRating';
|
|
import { lastfmLoveTrack, lastfmUnloveTrack } from '../../api/lastfm';
|
|
import type { Track } from '../../store/playerStoreTypes';
|
|
import { useAuthStore } from '../../store/authStore';
|
|
import LastfmIcon from '../LastfmIcon';
|
|
import StarRating from '../StarRating';
|
|
import { AddToPlaylistSubmenu } from './AddToPlaylistSubmenu';
|
|
import type { ContextMenuItemsProps } from './contextMenuItemTypes';
|
|
|
|
export default function QueueItemContextItems(props: ContextMenuItemsProps) {
|
|
const {
|
|
type, item, queueIndex, playlistId, playlistSongIndex, shareKindOverride,
|
|
playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu,
|
|
starredOverrides, setStarredOverride, lastfmLovedCache, setLastfmLovedForSong,
|
|
openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating,
|
|
playlistSubmenuOpen, setPlaylistSubmenuOpen, cancelPlaylistSubmenuCloseTimer, onPlaylistSubmenuTriggerMouseLeave,
|
|
playlistSongIds, setPlaylistSongIds,
|
|
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
|
applySongRating, applyAlbumRating, applyArtistRating,
|
|
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
|
navigateLibrary,
|
|
} = props;
|
|
const { t } = useTranslation();
|
|
const auth = useAuthStore();
|
|
|
|
return (
|
|
<>
|
|
{type === 'queue-item' && (() => {
|
|
const song = item as Track;
|
|
return (
|
|
<>
|
|
<div className="context-menu-item" onClick={() => handleAction(() => playTrack(song, queue, undefined, undefined, queueIndex))}>
|
|
<Play size={14} /> {t('contextMenu.playNow')}
|
|
</div>
|
|
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => handleAction(() => {
|
|
if (queueIndex !== undefined) removeTrack(queueIndex);
|
|
})}>
|
|
<Trash2 size={14} /> {t('contextMenu.removeFromQueue')}
|
|
</div>
|
|
<div
|
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === song.id ? 'active' : ''}`}
|
|
data-playlist-trigger-id={song.id}
|
|
onMouseEnter={() => { cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([song.id]); setPlaylistSubmenuOpen(true); }}
|
|
onMouseLeave={onPlaylistSubmenuTriggerMouseLeave}
|
|
>
|
|
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
|
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
|
{playlistSubmenuOpen && playlistSongIds[0] === song.id && (
|
|
<AddToPlaylistSubmenu songIds={[song.id]} triggerId={song.id} onDone={() => { setPlaylistSubmenuOpen(false); closeContextMenu(); }} />
|
|
)}
|
|
</div>
|
|
<div className="context-menu-divider" />
|
|
{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(() => navigateLibrary(`/artist/${song.artistId}`))}>
|
|
<User size={14} /> {t('contextMenu.goToArtist')}
|
|
</div>
|
|
)}
|
|
<div className="context-menu-item" onClick={() => handleAction(() => startRadio(song.artistId ?? song.artist, song.artist, song))}>
|
|
<Radio size={14} /> {t('contextMenu.startRadio')}
|
|
</div>
|
|
{audiomuseNavidromeEnabled && (
|
|
<div className="context-menu-item" onClick={() => handleAction(() => startInstantMix(song))}>
|
|
<Sparkles size={14} /> {t('contextMenu.instantMix')}
|
|
</div>
|
|
)}
|
|
<div className="context-menu-item" onClick={() => handleAction(() => {
|
|
const starred = isStarred(song.id, song.starred);
|
|
setStarredOverride(song.id, !starred);
|
|
return starred ? unstar(song.id, 'song') : star(song.id, 'song');
|
|
})}>
|
|
<Heart size={14} fill={isStarred(song.id, song.starred) ? 'currentColor' : 'none'} />
|
|
{isStarred(song.id, song.starred) ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
|
|
</div>
|
|
{auth.lastfmSessionKey && (() => {
|
|
const loveKey = `${song.title}::${song.artist}`;
|
|
const loved = lastfmLovedCache[loveKey] ?? false;
|
|
return (
|
|
<div className="context-menu-item" onClick={() => handleAction(() => {
|
|
const newLoved = !loved;
|
|
setLastfmLovedForSong(song.title, song.artist, newLoved);
|
|
if (newLoved) lastfmLoveTrack(song, auth.lastfmSessionKey);
|
|
else lastfmUnloveTrack(song, auth.lastfmSessionKey);
|
|
})}>
|
|
<LastfmIcon size={14} />
|
|
{loved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}
|
|
</div>
|
|
);
|
|
})()}
|
|
<div
|
|
className="context-menu-rating-row"
|
|
data-rating-kind="song"
|
|
data-rating-id={song.id}
|
|
data-rating-disabled="false"
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
<Star size={14} className="context-menu-rating-icon" aria-hidden />
|
|
<StarRating
|
|
value={keyboardRating?.kind === 'song' && keyboardRating.id === song.id
|
|
? keyboardRating.value
|
|
: userRatingOverrides[song.id] ?? song.userRating ?? 0}
|
|
onChange={r => { setKeyboardRating({ kind: 'song', id: song.id, value: r }); applySongRating(song.id, r); }}
|
|
ariaLabel={t('albumDetail.ratingLabel')}
|
|
/>
|
|
</div>
|
|
<div className="context-menu-divider" />
|
|
<div className="context-menu-item" onClick={() => handleAction(() => copyShareLink('track', song.id))}>
|
|
<Share2 size={14} /> {t('contextMenu.shareLink')}
|
|
</div>
|
|
<div className="context-menu-item" onClick={() => handleAction(() => openSongInfo(song.id))}>
|
|
<Info size={14} /> {t('contextMenu.songInfo')}
|
|
</div>
|
|
</>
|
|
);
|
|
})()}
|
|
</>
|
|
);
|
|
}
|