import React, { useEffect, useRef, useState } from 'react'; import { Play, ListPlus, Radio, Star, Download, ChevronRight, User, Disc3, Heart } from 'lucide-react'; import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm'; import { usePlayerStore, Track, songToTrack } from '../store/playerStore'; import { SubsonicAlbum, SubsonicArtist, star, unstar, getSimilarSongs2, getTopSongs, buildDownloadUrl, getAlbum } from '../api/subsonic'; import { useNavigate } from 'react-router-dom'; import { useAuthStore } from '../store/authStore'; import { useDownloadModalStore } from '../store/downloadModalStore'; import { open } from '@tauri-apps/plugin-shell'; import { writeFile } from '@tauri-apps/plugin-fs'; import { join } from '@tauri-apps/api/path'; import { useTranslation } from 'react-i18next'; function sanitizeFilename(name: string): string { return name .replace(/[/\\?%*:|"<>]/g, '-') .replace(/\.{2,}/g, '.') .replace(/^[\s.]+|[\s.]+$/g, '') .substring(0, 200) || 'download'; } export default function ContextMenu() { const { t } = useTranslation(); const { contextMenu, closeContextMenu, playTrack, enqueue, queue, currentTrack, removeTrack, lastfmLovedCache, setLastfmLovedForSong, starredOverrides, setStarredOverride } = usePlayerStore(); const auth = useAuthStore(); const requestDownloadFolder = useDownloadModalStore(s => s.requestFolder); const navigate = useNavigate(); const menuRef = useRef(null); // Adjusted coordinates to keep menu on screen const [coords, setCoords] = useState({ x: 0, y: 0 }); useEffect(() => { if (contextMenu.isOpen) { setCoords({ x: contextMenu.x, y: contextMenu.y }); } }, [contextMenu.isOpen, contextMenu.x, contextMenu.y]); useEffect(() => { if (contextMenu.isOpen && menuRef.current) { const rect = menuRef.current.getBoundingClientRect(); const winW = window.innerWidth; const winH = window.innerHeight; let finalX = contextMenu.x; let finalY = contextMenu.y; if (finalX + rect.width > winW) finalX = winW - rect.width - 10; if (finalY + rect.height > winH) finalY = winH - rect.height - 10; setCoords({ x: finalX, y: finalY }); } }, [contextMenu.isOpen, contextMenu.x, contextMenu.y]); if (!contextMenu.isOpen || !contextMenu.item) return null; const { type, item, queueIndex } = contextMenu; const isStarred = (id: string, itemStarred?: string) => id in starredOverrides ? starredOverrides[id] : !!itemStarred; const handleAction = async (action: () => void | Promise) => { closeContextMenu(); await action(); }; const startRadio = async (artistId: string, artistName: string) => { try { const similar = await getSimilarSongs2(artistId); if (similar.length > 0) { const top = await getTopSongs(artistName); const radioTracks = [...top, ...similar].map(songToTrack); playTrack(radioTracks[0], radioTracks); } } catch (e) { console.error('Failed to start radio', e); } }; const downloadAlbum = async (albumName: string, albumId: string) => { try { const folder = auth.downloadFolder || await requestDownloadFolder(); if (!folder) return; const url = buildDownloadUrl(albumId); const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); const blob = await response.blob(); const buffer = await blob.arrayBuffer(); const path = await join(folder, `${sanitizeFilename(albumName)}.zip`); await writeFile(path, new Uint8Array(buffer)); } catch (e) { console.error('Download failed:', e); } }; return ( <> {/* Transparent backdrop — catches all outside clicks cleanly, preventing freeze */}
closeContextMenu()} />
{(type === 'song' || type === 'album-song') && (() => { const song = item as Track; return ( <>
handleAction(() => playTrack(song, [song]))}> {t('contextMenu.playNow')}
handleAction(() => { if (!currentTrack) { playTrack(song, [song]); return; } const currentIdx = usePlayerStore.getState().queueIndex; const newQueue = [...queue]; newQueue.splice(currentIdx + 1, 0, song); usePlayerStore.setState({ queue: newQueue }); })}> {t('contextMenu.playNext')}
handleAction(() => enqueue([song]))}> {t('contextMenu.addToQueue')}
{type === 'album-song' && (
handleAction(async () => { const albumData = await getAlbum(song.albumId); const tracks = albumData.songs.map(songToTrack); enqueue(tracks); })}> {t('contextMenu.enqueueAlbum')}
)}
{song.albumId && (
handleAction(() => navigate(`/album/${song.albumId}`))}> {t('contextMenu.openAlbum')}
)}
handleAction(() => startRadio(song.artist, song.artist))}> {t('contextMenu.startRadio')}
handleAction(() => { const starred = isStarred(song.id, song.starred); setStarredOverride(song.id, !starred); return starred ? unstar(song.id, 'song') : star(song.id, 'song'); })}> {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')}
); })()} ); })()} {type === 'album' && (() => { const album = item as SubsonicAlbum; return ( <>
handleAction(() => navigate(`/album/${album.id}`))}> {t('contextMenu.openAlbum')}
handleAction(() => navigate(`/artist/${album.artistId}`))}> {t('contextMenu.goToArtist')}
handleAction(() => { const starred = isStarred(album.id, album.starred); setStarredOverride(album.id, !starred); return starred ? unstar(album.id, 'album') : star(album.id, 'album'); })}> {isStarred(album.id, album.starred) ? t('contextMenu.unfavoriteAlbum') : t('contextMenu.favoriteAlbum')}
handleAction(() => downloadAlbum(album.name, album.id))}> {t('contextMenu.download')}
); })()} {type === 'artist' && (() => { const artist = item as SubsonicArtist; return ( <>
handleAction(() => startRadio(artist.id, artist.name))}> {t('contextMenu.startRadio')}
handleAction(() => { const starred = isStarred(artist.id, artist.starred); setStarredOverride(artist.id, !starred); return starred ? unstar(artist.id, 'artist') : star(artist.id, 'artist'); })}> {isStarred(artist.id, artist.starred) ? t('contextMenu.unfavoriteArtist') : t('contextMenu.favoriteArtist')}
); })()} {type === 'queue-item' && (() => { const song = item as Track; return ( <>
handleAction(() => playTrack(song, queue))}> {t('contextMenu.playNow')}
handleAction(() => { if (queueIndex !== undefined) removeTrack(queueIndex); })}> {t('contextMenu.removeFromQueue')}
{song.albumId && (
handleAction(() => navigate(`/album/${song.albumId}`))}> {t('contextMenu.openAlbum')}
)}
handleAction(() => { const starred = isStarred(song.id, song.starred); setStarredOverride(song.id, !starred); return starred ? unstar(song.id, 'song') : star(song.id, 'song'); })}> {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')}
); })()}
handleAction(() => startRadio(song.artist, song.artist))}> {t('contextMenu.startRadio')}
); })()}
); }