import { useTranslation } from 'react-i18next'; import { Play, ListPlus, Radio, Heart, ChevronRight, ChevronsRight, User, Disc3, ListMusic, Info, Sparkles, Star, Trash2, HeartCrack, Share2, Orbit as OrbitIcon } from 'lucide-react'; import { useNavigateToAlbum } from '../../hooks/useNavigateToAlbum'; import { useNavigateToArtist } from '../../hooks/useNavigateToArtist'; import { resolveAlbum, resolveMediaServerId, resolvePlaylist } from '../../utils/offline/offlineMediaResolve'; import { queueSongStar } from '../../store/pendingStarSync'; import { lastfmLoveTrack, lastfmUnloveTrack } from '../../api/lastfm'; import type { Track } from '../../store/playerStoreTypes'; import { useAuthStore } from '../../store/authStore'; import { usePlaylistStore } from '../../store/playlistStore'; import { songToTrack } from '../../utils/playback/songToTrack'; import { showToast } from '../../utils/ui/toast'; import { suggestOrbitTrack, hostEnqueueToOrbit, evaluateOrbitSuggestGate, OrbitSuggestBlockedError } from '../../utils/orbit'; import LastfmIcon from '../LastfmIcon'; import StarRating from '../StarRating'; import { AddToPlaylistSubmenu } from './AddToPlaylistSubmenu'; import type { ContextMenuItemsProps } from './contextMenuItemTypes'; export default function SongContextItems(props: ContextMenuItemsProps) { const { type, item, queueIndex, playlistId, playlistSongIndex, shareKindOverride, playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu, starredOverrides, lastfmLovedCache, setLastfmLovedForSong, openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating, playlistSubmenuOpen, setPlaylistSubmenuOpen, cancelPlaylistSubmenuCloseTimer, onPlaylistSubmenuTriggerMouseLeave, playlistSongIds, setPlaylistSongIds, orbitRole, entityRatingSupport, audiomuseNavidromeEnabled, applySongRating, applyAlbumRating, applyArtistRating, handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred, offlinePolicy, } = props; const { t } = useTranslation(); const auth = useAuthStore(); const navigateToAlbum = useNavigateToAlbum(); const navigateToArtist = useNavigateToArtist(); return ( <> {(type === 'song' || type === 'album-song') && (() => { const song = item as Track; return ( <>
handleAction(() => playTrack(song, [song]))}> {t('contextMenu.playNow')}
handleAction(() => playNext([song]))}> {t('contextMenu.playNext')}
handleAction(() => enqueue([song]))}> {t('contextMenu.addToQueue')}
{orbitRole === 'guest' && (() => { const muted = evaluateOrbitSuggestGate().reason === 'muted'; return (
handleAction(() => { if (muted) { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); return; } suggestOrbitTrack(song.id) .then(() => showToast(t('orbit.ctxSuggestedToast'), 2200, 'info')) .catch(err => { if (err instanceof OrbitSuggestBlockedError && err.reason === 'muted') { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); } else { showToast(t('orbit.ctxSuggestFailed'), 3000, 'error'); } }); })} > {t('orbit.ctxAddToSession')}
); })()} {orbitRole === 'host' && (
handleAction(() => { hostEnqueueToOrbit(song.id) .then(() => showToast(t('orbit.ctxAddedHostToast'), 2200, 'info')) .catch(() => showToast(t('orbit.ctxAddHostFailed'), 3000, 'error')); })}> {t('orbit.ctxAddToSessionHost')}
)} {offlinePolicy.canAddToPlaylist && (
{ cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([song.id]); setPlaylistSubmenuOpen(true); }} onMouseLeave={onPlaylistSubmenuTriggerMouseLeave} > {t('contextMenu.addToPlaylist')} {playlistSubmenuOpen && playlistSongIds[0] === song.id && ( { setPlaylistSubmenuOpen(false); closeContextMenu(); }} /> )}
)} {type === 'album-song' && (
handleAction(async () => { const serverId = resolveMediaServerId(song.serverId); if (!serverId || !song.albumId) return; const albumData = await resolveAlbum(serverId, song.albumId); if (!albumData) return; const tracks = albumData.songs.map(songToTrack); enqueue(tracks); })}> {t('contextMenu.enqueueAlbum')}
)}
{song.albumId && (
handleAction(() => navigateToAlbum(song.albumId!))}> {t('contextMenu.openAlbum')}
)} {song.artistId && (
handleAction(() => navigateToArtist(song.artistId!))}> {t('contextMenu.goToArtist')}
)}
handleAction(() => startRadio(song.artistId ?? song.artist, song.artist, song))}> {t('contextMenu.startRadio')}
{audiomuseNavidromeEnabled && (
handleAction(() => startInstantMix(song))}> {t('contextMenu.instantMix')}
)} {offlinePolicy.canFavorite && (
handleAction(() => { queueSongStar(song.id, !isStarred(song.id, song.starred), song.serverId); })}> {isStarred(song.id, song.starred) ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
)} {auth.lastfmSessionKey && (() => { const loveKey = `${song.title}::${song.artist}`; const loved = lastfmLovedCache[loveKey] ?? false; return (
handleAction(() => { const newLoved = !loved; setLastfmLovedForSong(song.title, song.artist, newLoved); if (newLoved) lastfmLoveTrack(song, auth.lastfmSessionKey); else lastfmUnloveTrack(song, auth.lastfmSessionKey); })}> {loved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}
); })()} {offlinePolicy.canRate && (
e.stopPropagation()} > { setKeyboardRating({ kind: 'song', id: song.id, value: r }); applySongRating(song.id, r); }} ariaLabel={t('albumDetail.ratingLabel')} />
)}
handleAction(() => copyShareLink('track', song.id))}> {t('contextMenu.shareLink')}
handleAction(() => openSongInfo(song.id))}> {t('contextMenu.songInfo')}
{offlinePolicy.canEditPlaylist && playlistId && playlistSongIndex !== undefined && (
handleAction(async () => { const { updatePlaylist } = await import('../../api/subsonicPlaylists'); const { showToast } = await import('../../utils/ui/toast'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { const serverId = resolveMediaServerId(); if (!serverId) return; const resolved = await resolvePlaylist(serverId, playlistId); if (!resolved) return; const { songs } = resolved; const prevCount = songs.length; const updatedIds = songs.filter((_, i) => i !== playlistSongIndex).map(s => s.id); await updatePlaylist(playlistId, updatedIds, prevCount); touchPlaylist(playlistId); showToast(t('playlists.removeSuccess'), 3000, 'info'); } catch { showToast(t('playlists.removeError'), 4000, 'error'); } })}> {t('contextMenu.removeFromPlaylist')}
)} ); })()} {type === 'favorite-song' && (() => { const song = item as Track; return ( <>
handleAction(() => playTrack(song, [song]))}> {t('contextMenu.playNow')}
handleAction(() => playNext([song]))}> {t('contextMenu.playNext')}
handleAction(() => enqueue([song]))}> {t('contextMenu.addToQueue')}
{orbitRole === 'guest' && (() => { const muted = evaluateOrbitSuggestGate().reason === 'muted'; return (
handleAction(() => { if (muted) { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); return; } suggestOrbitTrack(song.id) .then(() => showToast(t('orbit.ctxSuggestedToast'), 2200, 'info')) .catch(err => { if (err instanceof OrbitSuggestBlockedError && err.reason === 'muted') { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); } else { showToast(t('orbit.ctxSuggestFailed'), 3000, 'error'); } }); })} > {t('orbit.ctxAddToSession')}
); })()} {orbitRole === 'host' && (
handleAction(() => { hostEnqueueToOrbit(song.id) .then(() => showToast(t('orbit.ctxAddedHostToast'), 2200, 'info')) .catch(() => showToast(t('orbit.ctxAddHostFailed'), 3000, 'error')); })}> {t('orbit.ctxAddToSessionHost')}
)} {offlinePolicy.canAddToPlaylist && (
{ cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([song.id]); setPlaylistSubmenuOpen(true); }} onMouseLeave={onPlaylistSubmenuTriggerMouseLeave} > {t('contextMenu.addToPlaylist')} {playlistSubmenuOpen && playlistSongIds[0] === song.id && ( { setPlaylistSubmenuOpen(false); closeContextMenu(); }} /> )}
)}
{song.albumId && (
handleAction(() => navigateToAlbum(song.albumId!))}> {t('contextMenu.openAlbum')}
)} {song.artistId && (
handleAction(() => navigateToArtist(song.artistId!))}> {t('contextMenu.goToArtist')}
)}
handleAction(() => startRadio(song.artistId ?? song.artist, song.artist, song))}> {t('contextMenu.startRadio')}
{audiomuseNavidromeEnabled && (
handleAction(() => startInstantMix(song))}> {t('contextMenu.instantMix')}
)} {auth.lastfmSessionKey && (() => { const loveKey = `${song.title}::${song.artist}`; const loved = lastfmLovedCache[loveKey] ?? false; return (
handleAction(() => { const newLoved = !loved; setLastfmLovedForSong(song.title, song.artist, newLoved); if (newLoved) lastfmLoveTrack(song, auth.lastfmSessionKey); else lastfmUnloveTrack(song, auth.lastfmSessionKey); })}> {loved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}
); })()} {offlinePolicy.canRate && (
e.stopPropagation()} > { setKeyboardRating({ kind: 'song', id: song.id, value: r }); applySongRating(song.id, r); }} ariaLabel={t('albumDetail.ratingLabel')} />
)}
handleAction(() => copyShareLink('track', song.id))}> {t('contextMenu.shareLink')}
handleAction(() => openSongInfo(song.id))}> {t('contextMenu.songInfo')}
{offlinePolicy.canFavorite && ( <>
handleAction(() => { queueSongStar(song.id, false, song.serverId); })}> {t('contextMenu.unfavorite')}
)} ); })()} ); }