diff --git a/src/components/AlbumCard.tsx b/src/components/AlbumCard.tsx index 60617d5a..aee9a1f7 100644 --- a/src/components/AlbumCard.tsx +++ b/src/components/AlbumCard.tsx @@ -1,8 +1,9 @@ import React, { memo } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Play, HardDriveDownload, Check } from 'lucide-react'; -import { SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic'; -import { usePlayerStore } from '../store/playerStore'; +import { Play, ListPlus, HardDriveDownload, Check } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey, getAlbum } from '../api/subsonic'; +import { usePlayerStore, songToTrack } from '../store/playerStore'; import { useOfflineStore } from '../store/offlineStore'; import { useAuthStore } from '../store/authStore'; import CachedImage from './CachedImage'; @@ -19,8 +20,10 @@ interface AlbumCardProps { } function AlbumCard({ album, selected, selectionMode, onToggleSelect, showRating = false, selectedAlbums = [] }: AlbumCardProps) { + const { t } = useTranslation(); const navigate = useNavigate(); const openContextMenu = usePlayerStore(s => s.openContextMenu); + const enqueue = usePlayerStore(s => s.enqueue); const serverId = useAuthStore(s => s.activeServerId ?? ''); const isOffline = useOfflineStore(s => { const meta = s.albums[`${serverId}:${album.id}`]; @@ -94,9 +97,28 @@ function AlbumCard({ album, selected, selectionMode, onToggleSelect, showRating className="album-card-details-btn" onClick={e => { e.stopPropagation(); playAlbum(album.id); }} aria-label={`${album.name} abspielen`} + data-tooltip={t('hero.playAlbum')} + data-tooltip-pos="top" > + )} diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index 25af6088..149f69ed 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -1649,6 +1649,12 @@ export default function ContextMenu() {
handleAction(() => navigate(`/album/${album.id}`))}> {t('contextMenu.openAlbum')}
+
handleAction(async () => { + const albumData = await getAlbum(album.id); + enqueue(albumData.songs.map(songToTrack)); + })}> + {t('contextMenu.enqueueAlbum')} +
handleAction(() => navigate(`/artist/${album.artistId}`))}> {t('contextMenu.goToArtist')} @@ -1751,6 +1757,14 @@ export default function ContextMenu() { {t('contextMenu.selectedAlbums', { count: albums.length })}
+
handleAction(async () => { + // Parallel — Navidrome handles concurrent getAlbum requests fine. + const results = await Promise.all(albums.map(a => getAlbum(a.id))); + const allTracks = results.flatMap(r => r.songs.map(songToTrack)); + enqueue(allTracks); + })}> + {t('contextMenu.enqueueAlbums', { count: albums.length })} +
selectedIds.has(a.id)); const openContextMenu = usePlayerStore(state => state.openContextMenu); + const enqueue = usePlayerStore(state => state.enqueue); + + const handleEnqueueSelected = async () => { + if (selectedAlbums.length === 0) return; + try { + // Parallel — Navidrome handles concurrent getAlbum requests fine. + const results = await Promise.all(selectedAlbums.map(a => getAlbum(a.id).catch(() => null))); + const tracks = results.flatMap(r => r ? r.songs.map(songToTrack) : []); + if (tracks.length > 0) { + enqueue(tracks); + showToast(t('albums.enqueueQueued', { count: selectedAlbums.length }), 2500, 'info'); + } + } finally { + clearSelection(); + } + }; const cycleCompFilter = () => { setCompFilter(v => v === 'all' ? 'only' : v === 'only' ? 'hide' : 'all'); @@ -210,6 +227,10 @@ export default function Albums() {
{selectionMode && selectedIds.size > 0 ? ( <> +