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 ? (
<>
+