diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx
index 16186a68..9791fd89 100644
--- a/src/components/ContextMenu.tsx
+++ b/src/components/ContextMenu.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
-import { Play, ListPlus, Radio, Heart, Download, ChevronRight, User, Disc3, ListMusic, Plus, Info, Sparkles, Star, Trash2 } from 'lucide-react';
+import { Play, ListPlus, Radio, Heart, Download, ChevronRight, User, Disc3, ListMusic, Plus, Info, Sparkles, Star, Trash2, HeartCrack } from 'lucide-react';
import LastfmIcon from './LastfmIcon';
import StarRating from './StarRating';
import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm';
@@ -1535,6 +1535,105 @@ export default function ContextMenu() {
);
})()}
+ {type === 'favorite-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')}
+
+ { setPlaylistSongIds([song.id]); setPlaylistSubmenuOpen(true); }}
+ onMouseLeave={() => setPlaylistSubmenuOpen(false)}
+ >
+
{t('contextMenu.addToPlaylist')}
+
+ {playlistSubmenuOpen && playlistSongIds[0] === song.id && (
+
{ setPlaylistSubmenuOpen(false); closeContextMenu(); }} />
+ )}
+
+
+ {song.albumId && (
+ handleAction(() => navigate(`/album/${song.albumId}`))}>
+ {t('contextMenu.openAlbum')}
+
+ )}
+ {song.artistId && (
+ handleAction(() => navigate(`/artist/${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')}
+
+ );
+ })()}
+ e.stopPropagation()}
+ >
+
+ { setKeyboardRating({ kind: 'song', id: song.id, value: r }); applySongRating(song.id, r); }}
+ ariaLabel={t('albumDetail.ratingLabel')}
+ />
+
+
+ handleAction(() => openSongInfo(song.id))}>
+ {t('contextMenu.songInfo')}
+
+
+ handleAction(() => {
+ setStarredOverride(song.id, false);
+ return unstar(song.id, 'song');
+ })}>
+ {t('contextMenu.unfavorite')}
+
+ >
+ );
+ })()}
+
{type === 'album' && (() => {
const album = item as SubsonicAlbum;
const albumRatingDisabled = entityRatingSupport === 'track_only';
diff --git a/src/locales/de.ts b/src/locales/de.ts
index 3e6cd223..1f30a391 100644
--- a/src/locales/de.ts
+++ b/src/locales/de.ts
@@ -210,6 +210,11 @@ export const deTranslation = {
playAll: 'Alle abspielen',
removeSong: 'Aus Favoriten entfernen',
stations: 'Radiosender',
+ showingFiltered: 'Zeige {{filtered}} von {{total}} ({{artist}})',
+ showingCount: 'Zeige {{filtered}} von {{total}}',
+ clearArtistFilter: 'Künstlerfilter zurücksetzen',
+ noFilterResults: 'Keine Ergebnisse mit den ausgewählten Filtern.',
+ allArtists: 'Alle Künstler',
},
randomLanding: {
title: 'Mix erstellen',
@@ -398,6 +403,10 @@ export const deTranslation = {
durationHoursMinutes: '{{hours}} Std. {{minutes}} Min.',
durationMinutesOnly: '{{minutes}} Min.',
updaterOpenGitHub: 'Auf GitHub öffnen',
+ filters: 'Filter',
+ more: 'mehr',
+ yearRange: 'Jahresbereich',
+ clearAll: 'Alles zurücksetzen',
},
settings: {
title: 'Einstellungen',
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 0d7ccbfe..e682206b 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -211,6 +211,11 @@ export const enTranslation = {
playAll: 'Play all',
removeSong: 'Remove from favorites',
stations: 'Radio Stations',
+ showingFiltered: 'Showing {{filtered}} of {{total}} ({{artist}})',
+ showingCount: 'Showing {{filtered}} of {{total}}',
+ clearArtistFilter: 'Clear artist filter',
+ noFilterResults: 'No results with selected filters.',
+ allArtists: 'All Artists',
},
randomLanding: {
title: 'Build a Mix',
@@ -399,6 +404,10 @@ export const enTranslation = {
durationHoursMinutes: '{{hours}}h {{minutes}}m',
durationMinutesOnly: '{{minutes}}m',
updaterOpenGitHub: 'Open on GitHub',
+ filters: 'Filters',
+ more: 'more',
+ yearRange: 'Year Range',
+ clearAll: 'Clear all',
},
settings: {
title: 'Settings',
diff --git a/src/locales/es.ts b/src/locales/es.ts
index 29eb8ab3..97eaaa7d 100644
--- a/src/locales/es.ts
+++ b/src/locales/es.ts
@@ -211,6 +211,11 @@ export const esTranslation = {
playAll: 'Reproducir todo',
removeSong: 'Quitar de favoritos',
stations: 'Estaciones de Radio',
+ showingFiltered: 'Mostrando {{filtered}} de {{total}} ({{artist}})',
+ showingCount: 'Mostrando {{filtered}} de {{total}}',
+ clearArtistFilter: 'Limpiar filtro de artista',
+ noFilterResults: 'No hay resultados con los filtros seleccionados.',
+ allArtists: 'Todos los Artistas',
},
randomLanding: {
title: 'Crear Mezcla',
@@ -400,6 +405,10 @@ export const esTranslation = {
durationHoursMinutes: '{{hours}}h {{minutes}}m',
durationMinutesOnly: '{{minutes}}m',
updaterOpenGitHub: 'Abrir en GitHub',
+ filters: 'Filtros',
+ more: 'más',
+ yearRange: 'Rango de años',
+ clearAll: 'Limpiar todo',
},
settings: {
title: 'Configuración',
diff --git a/src/locales/fr.ts b/src/locales/fr.ts
index b3270954..3c6d22bc 100644
--- a/src/locales/fr.ts
+++ b/src/locales/fr.ts
@@ -210,6 +210,11 @@ export const frTranslation = {
playAll: 'Tout lire',
removeSong: 'Retirer des favoris',
stations: 'Stations de radio',
+ showingFiltered: 'Affichage de {{filtered}} sur {{total}} ({{artist}})',
+ showingCount: 'Affichage de {{filtered}} sur {{total}}',
+ clearArtistFilter: 'Effacer le filtre artiste',
+ noFilterResults: 'Aucun résultat avec les filtres sélectionnés.',
+ allArtists: 'Tous les artistes',
},
randomLanding: {
title: 'Créer un mix',
@@ -398,6 +403,10 @@ export const frTranslation = {
durationHoursMinutes: '{{hours}} h {{minutes}} min',
durationMinutesOnly: '{{minutes}} min',
updaterOpenGitHub: 'Ouvrir sur GitHub',
+ filters: 'Filtres',
+ more: 'plus',
+ yearRange: 'Plage d\'années',
+ clearAll: 'Tout effacer',
},
settings: {
title: 'Paramètres',
diff --git a/src/locales/nb.ts b/src/locales/nb.ts
index 68235cf9..e1e7264a 100644
--- a/src/locales/nb.ts
+++ b/src/locales/nb.ts
@@ -209,7 +209,12 @@ export const nbTranslation = {
enqueueAll: 'Legg alle i kø',
playAll: 'Spill alle',
removeSong: 'Fjern fra favoritter',
- stations: 'Radiostasjoner',
+ stations: 'Radiostasjoner',
+ showingFiltered: 'Viser {{filtered}} av {{total}} ({{artist}})',
+ showingCount: 'Viser {{filtered}} av {{total}}',
+ clearArtistFilter: 'Tøm artistfilter',
+ noFilterResults: 'Ingen resultater med valgte filtre.',
+ allArtists: 'Alle artister',
},
randomLanding: {
title: 'Lag en miks',
@@ -398,6 +403,10 @@ export const nbTranslation = {
durationHoursMinutes: '{{hours}} t {{minutes}} min',
durationMinutesOnly: '{{minutes}} min',
updaterOpenGitHub: 'Åpne på GitHub',
+ filters: 'Filter',
+ more: 'mer',
+ yearRange: 'Årsspenn',
+ clearAll: 'Tøm alt',
},
settings: {
title: 'Innstillinger',
diff --git a/src/locales/nl.ts b/src/locales/nl.ts
index 1253d6bc..4c41f246 100644
--- a/src/locales/nl.ts
+++ b/src/locales/nl.ts
@@ -209,6 +209,11 @@ export const nlTranslation = {
playAll: 'Alles afspelen',
removeSong: 'Verwijderen uit favorieten',
stations: 'Radiostations',
+ showingFiltered: 'Toont {{filtered}} van {{total}} ({{artist}})',
+ showingCount: 'Toont {{filtered}} van {{total}}',
+ clearArtistFilter: 'Artiestfilter wissen',
+ noFilterResults: 'Geen resultaten met de geselecteerde filters.',
+ allArtists: 'Alle artiesten',
},
randomLanding: {
title: 'Mix samenstellen',
@@ -397,6 +402,10 @@ export const nlTranslation = {
durationHoursMinutes: '{{hours}} u {{minutes}} min',
durationMinutesOnly: '{{minutes}} min',
updaterOpenGitHub: 'Openen op GitHub',
+ filters: 'Filters',
+ more: 'meer',
+ yearRange: 'Jaarbereik',
+ clearAll: 'Alles wissen',
},
settings: {
title: 'Instellingen',
diff --git a/src/locales/ru.ts b/src/locales/ru.ts
index 9b71443f..73266994 100644
--- a/src/locales/ru.ts
+++ b/src/locales/ru.ts
@@ -214,6 +214,11 @@ export const ruTranslation = {
playAll: 'Воспроизвести всё',
removeSong: 'Убрать из избранного',
stations: 'Радиостанции',
+ showingFiltered: 'Показано {{filtered}} из {{total}} ({{artist}})',
+ showingCount: 'Показано {{filtered}} из {{total}}',
+ clearArtistFilter: 'Сбросить фильтр исполнителя',
+ noFilterResults: 'Нет результатов с выбранными фильтрами.',
+ allArtists: 'Все исполнители',
},
randomLanding: {
title: 'Собрать микс',
@@ -412,6 +417,10 @@ export const ruTranslation = {
durationHoursMinutes: '{{hours}}ч {{minutes}}мин',
durationMinutesOnly: '{{minutes}}мин',
updaterOpenGitHub: 'Открыть на GitHub',
+ filters: 'Фильтры',
+ more: 'еще',
+ yearRange: 'Диапазон лет',
+ clearAll: 'Очистить всё',
},
settings: {
title: 'Настройки',
diff --git a/src/locales/zh.ts b/src/locales/zh.ts
index 987de952..0d01a0f0 100644
--- a/src/locales/zh.ts
+++ b/src/locales/zh.ts
@@ -209,6 +209,11 @@ export const zhTranslation = {
playAll: '全部播放',
removeSong: '从收藏中移除',
stations: '广播电台',
+ showingFiltered: '显示 {{filtered}} / {{total}} ({{artist}})',
+ showingCount: '显示 {{filtered}} / {{total}}',
+ clearArtistFilter: '清除艺术家筛选',
+ noFilterResults: '所选筛选条件下无结果。',
+ allArtists: '全部艺术家',
},
randomLanding: {
title: '创建混音',
@@ -393,6 +398,10 @@ export const zhTranslation = {
durationHoursMinutes: '{{hours}}小时{{minutes}}分钟',
durationMinutesOnly: '{{minutes}}分钟',
updaterOpenGitHub: '在 GitHub 上打开',
+ filters: '筛选器',
+ more: '更多',
+ yearRange: '年份范围',
+ clearAll: '清除全部',
},
settings: {
title: '设置',
diff --git a/src/pages/Favorites.tsx b/src/pages/Favorites.tsx
index bf2de86d..20fdead2 100644
--- a/src/pages/Favorites.tsx
+++ b/src/pages/Favorites.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns';
import AlbumRow from '../components/AlbumRow';
import ArtistRow from '../components/ArtistRow';
@@ -10,7 +10,7 @@ import {
} from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import StarRating from '../components/StarRating';
-import { Cast, ChevronDown, ChevronLeft, ChevronRight, Check, Heart, ListPlus, Play, Star, X } from 'lucide-react';
+import { Cast, ChevronDown, ChevronLeft, ChevronRight, Check, Heart, ListPlus, Play, Star, X, SlidersHorizontal, ArrowUp, ArrowDown } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { unstar } from '../api/subsonic';
@@ -18,16 +18,25 @@ import { useDragDrop } from '../contexts/DragDropContext';
import { useAuthStore } from '../store/authStore';
import { useSelectionStore } from '../store/selectionStore';
import { AddToPlaylistSubmenu } from '../components/ContextMenu';
+import GenreFilterBar from '../components/GenreFilterBar';
const FAV_COLUMNS: readonly ColDef[] = [
{ key: 'num', i18nKey: null, minWidth: 60, defaultWidth: 60, required: true },
{ key: 'title', i18nKey: 'trackTitle', minWidth: 150, defaultWidth: 0, required: true, flex: true },
{ key: 'artist', i18nKey: 'trackArtist', minWidth: 80, defaultWidth: 180, required: false },
+ { key: 'album', i18nKey: 'trackAlbum', minWidth: 80, defaultWidth: 180, required: false },
{ key: 'rating', i18nKey: 'trackRating', minWidth: 80, defaultWidth: 120, required: false },
{ key: 'duration', i18nKey: 'trackDuration', minWidth: 72, defaultWidth: 92, required: false },
+ { key: 'format', i18nKey: 'trackFormat', minWidth: 60, defaultWidth: 80, required: false },
{ key: 'remove', i18nKey: null, minWidth: 36, defaultWidth: 36, required: true },
];
+const CURRENT_YEAR = new Date().getFullYear();
+const MIN_YEAR = 1950;
+
+// Columns that support 3-state sorting (asc → desc → reset)
+const SORTABLE_COLUMNS = new Set(['title', 'artist', 'album', 'rating', 'duration']);
+
export default function Favorites() {
const { t } = useTranslation();
const [albums, setAlbums] = useState([]);
@@ -36,6 +45,21 @@ export default function Favorites() {
const [radioStations, setRadioStations] = useState([]);
const [loading, setLoading] = useState(true);
+ // ── Sorting (3-state: asc → desc → reset) ────────────────────────────────
+ const [sortKey, setSortKey] = useState('natural');
+ const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
+ const [sortClickCount, setSortClickCount] = useState(0);
+
+ // ── Artist filtering ─────────────────────────────────────────────────────
+ const [selectedArtist, setSelectedArtist] = useState(null);
+
+ // ── Genre filtering ──────────────────────────────────────────────────────
+ const [selectedGenres, setSelectedGenres] = useState([]);
+
+ // ── Year range filtering ─────────────────────────────────────────────────
+ const [yearRange, setYearRange] = useState<[number, number]>([MIN_YEAR, CURRENT_YEAR]);
+ const [showFilters, setShowFilters] = useState(false);
+
// ── Column resize/visibility (must be before early return) ───────────────
const {
colVisible, visibleCols, gridStyle,
@@ -75,6 +99,36 @@ export default function Favorites() {
setSongs(prev => prev.filter(s => s.id !== id));
}
+ // ── Sorting logic ─────────────────────────────────────────────────────────
+ const handleSortClick = (key: string) => {
+ if (!SORTABLE_COLUMNS.has(key)) return;
+
+ if (sortKey === key) {
+ const nextCount = sortClickCount + 1;
+ if (nextCount >= 3) {
+ // Reset to natural order (favorite addition order)
+ setSortKey('natural');
+ setSortDir('asc');
+ setSortClickCount(0);
+ } else {
+ // Toggle direction
+ setSortDir(d => d === 'asc' ? 'desc' : 'asc');
+ setSortClickCount(nextCount);
+ }
+ } else {
+ // Start new sort on this column
+ setSortKey(key);
+ setSortDir('asc');
+ setSortClickCount(1);
+ }
+ };
+
+ const getSortIndicator = (key: string) => {
+ if (sortKey !== key) return null;
+ if (sortClickCount === 0) return null;
+ return sortDir === 'asc' ? : ;
+ };
+
function unfavoriteStation(id: string) {
setRadioStations(prev => prev.filter(s => s.id !== id));
try {
@@ -151,6 +205,68 @@ export default function Favorites() {
loadAll();
}, [musicLibraryFilterVersion]);
+ // ── Filter & sort logic ──────────────────────────────────────────────────
+ const filteredSongs = useMemo(() => {
+ return songs.filter(s => {
+ // Remove unfavorited
+ if (starredOverrides[s.id] === false) return false;
+
+ // Artist filter
+ if (selectedArtist) {
+ const artistMatch = s.artistId === selectedArtist ||
+ s.artist === selectedArtist ||
+ s.albumArtist === selectedArtist;
+ if (!artistMatch) return false;
+ }
+
+ // Genre filter
+ if (selectedGenres.length > 0) {
+ const songGenre = s.genre || '';
+ const hasMatchingGenre = selectedGenres.some(g =>
+ songGenre.toLowerCase().includes(g.toLowerCase())
+ );
+ if (!hasMatchingGenre) return false;
+ }
+
+ // Year range filter
+ if (s.year !== undefined) {
+ if (s.year < yearRange[0] || s.year > yearRange[1]) return false;
+ }
+
+ return true;
+ });
+ }, [songs, starredOverrides, selectedArtist, selectedGenres, yearRange]);
+
+ // ── Sort logic ───────────────────────────────────────────────────────────
+ const visibleSongs = useMemo(() => {
+ if (sortKey === 'natural' || sortClickCount === 0) {
+ return filteredSongs;
+ }
+
+ const sorted = [...filteredSongs];
+ const multiplier = sortDir === 'asc' ? 1 : -1;
+
+ return sorted.sort((a, b) => {
+ switch (sortKey) {
+ case 'title':
+ return multiplier * (a.title || '').localeCompare(b.title || '');
+ case 'artist':
+ return multiplier * ((a.artist || '').localeCompare(b.artist || ''));
+ case 'album':
+ return multiplier * ((a.album || '').localeCompare(b.album || ''));
+ case 'rating':
+ const ratingA = ratings[a.id] ?? userRatingOverrides[a.id] ?? a.userRating ?? 0;
+ const ratingB = ratings[b.id] ?? userRatingOverrides[b.id] ?? b.userRating ?? 0;
+ return multiplier * (ratingA - ratingB);
+ case 'duration':
+ return multiplier * ((a.duration || 0) - (b.duration || 0));
+ default:
+ return 0;
+ }
+ });
+ }, [filteredSongs, sortKey, sortDir, sortClickCount, ratings, userRatingOverrides]);
+
+
if (loading) {
return (
@@ -158,9 +274,8 @@ export default function Favorites() {
);
}
-
- const visibleSongs = songs.filter(s => starredOverrides[s.id] !== false);
- const hasAnyFavorites = albums.length > 0 || artists.length > 0 || visibleSongs.length > 0 || radioStations.length > 0;
+ // Check if user has any favorites (using original unfiltered lists)
+ const hasAnyFavorites = albums.length > 0 || artists.length > 0 || songs.length > 0 || radioStations.length > 0;
return (
@@ -194,30 +309,130 @@ export default function Favorites() {
/>
)}
- {visibleSongs.length > 0 && (
+ {(visibleSongs.length > 0 || selectedArtist || selectedGenres.length > 0 || yearRange[0] !== MIN_YEAR || yearRange[1] !== CURRENT_YEAR) && (
-
-
{t('favorites.songs')}
-
-
+ {/* ── Section Header with Stats & Filters ───────────────────────── */}
+
+ {/* Title Row with showing X of Y indicator */}
+
+
{t('favorites.songs')}
+
+ {selectedArtist
+ ? t('favorites.showingFiltered', { filtered: visibleSongs.length, total: songs.filter(s => starredOverrides[s.id] !== false).length, artist: selectedArtist })
+ : t('favorites.showingCount', { filtered: visibleSongs.length, total: songs.filter(s => starredOverrides[s.id] !== false).length })}
+
+
+
+ {/* Action Buttons */}
+
+
+
+
+ {/* Filter Toggle Button */}
+
+
+ {(selectedArtist || selectedGenres.length > 0 || yearRange[0] !== MIN_YEAR || yearRange[1] !== CURRENT_YEAR) && (
+
+ )}
+
+
+ {/* Filters Panel */}
+ {showFilters && (
+
+
+
+
+
+ {/* Year Range Filter */}
+
+
+ )}
+
+ {selectedArtist && (
+
+ )}
{
if (inSelectMode && e.target === e.currentTarget) useSelectionStore.getState().clearAll();
@@ -283,17 +498,34 @@ export default function Favorites() {
}
if (key === 'title') {
const hasNextCol = colIndex + 1 < visibleCols.length;
+ const canSort = SORTABLE_COLUMNS.has('title');
return (
-
+
handleSortClick('title')}
+ >
{label}
+ {canSort && getSortIndicator('title')}
{hasNextCol &&
startResize(e, colIndex + 1, -1)} />}
);
}
if (key === 'remove') return
;
+
const isCentered = key === 'duration' || key === 'rating';
+ const canSort = SORTABLE_COLUMNS.has(key);
+
return (
canSort && handleSortClick(key)}
>
{label}
+ {canSort && getSortIndicator(key)}
{!isLastCol &&
startResize(e, colIndex, 1)} />}
@@ -352,7 +588,7 @@ export default function Favorites() {
playTrack(track, visibleSongs.map(songToTrack));
}
}}
- onContextMenu={e => { e.preventDefault(); openContextMenu(e.clientX, e.clientY, track, 'song'); }}
+ onContextMenu={e => { e.preventDefault(); openContextMenu(e.clientX, e.clientY, track, 'favorite-song'); }}
role="row"
onMouseDown={e => {
if (e.button !== 0) return;
@@ -392,6 +628,34 @@ export default function Favorites() {
song.artistId && navigate(`/artist/${song.artistId}`)}>{song.artist}
);
+ case 'album': return (
+
+ {song.albumId ? (
+ {
+ e.stopPropagation();
+ navigate(`/album/${song.albumId}`);
+ }}
+ >
+ {song.album}
+
+ ) : (
+ {song.album}
+ )}
+
+ );
+ case 'format': return (
+
+ {(song.suffix || song.bitRate) && (
+
+ {song.suffix?.toUpperCase()}
+ {song.suffix && song.bitRate && ' · '}
+ {song.bitRate && `${song.bitRate} kbps`}
+
+ )}
+
+ );
case 'rating': return (
);
})}
+
+ {/* Empty state when filters return no results */}
+ {visibleSongs.length === 0 && (selectedArtist || selectedGenres.length > 0 || yearRange[0] !== MIN_YEAR || yearRange[1] !== CURRENT_YEAR) && (
+
+ {t('favorites.noFilterResults')}
+
+ )}
+
)}
>
diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts
index 6aaf6185..eb0c8bcc 100644
--- a/src/store/playerStore.ts
+++ b/src/store/playerStore.ts
@@ -174,12 +174,12 @@ interface PlayerState {
x: number;
y: number;
item: any;
- type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song' | 'playlist' | 'multi-album' | 'multi-artist' | 'multi-playlist' | null;
+ type: 'song' | 'favorite-song' | 'album' | 'artist' | 'queue-item' | 'album-song' | 'playlist' | 'multi-album' | 'multi-artist' | 'multi-playlist' | null;
queueIndex?: number;
playlistId?: string;
playlistSongIndex?: number;
};
- openContextMenu: (x: number, y: number, item: any, type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song' | 'playlist' | 'multi-album' | 'multi-artist' | 'multi-playlist', queueIndex?: number, playlistId?: string, playlistSongIndex?: number) => void;
+ openContextMenu: (x: number, y: number, item: any, type: 'song' | 'favorite-song' | 'album' | 'artist' | 'queue-item' | 'album-song' | 'playlist' | 'multi-album' | 'multi-artist' | 'multi-playlist', queueIndex?: number, playlistId?: string, playlistSongIndex?: number) => void;
closeContextMenu: () => void;
songInfoModal: { isOpen: boolean; songId: string | null };