diff --git a/apps/web/src/pages/library/Library.tsx b/apps/web/src/pages/library/Library.tsx index 554d72a2..5ac82834 100644 --- a/apps/web/src/pages/library/Library.tsx +++ b/apps/web/src/pages/library/Library.tsx @@ -1097,6 +1097,7 @@ export default function Library() { const [openFavoriteMenuId, setOpenFavoriteMenuId] = useState(null); const [favoriteMenuAnchor, setFavoriteMenuAnchor] = useState(null); const [playlistFilter, setPlaylistFilter] = useState(""); + const [favoritesFilter, setFavoritesFilter] = useState(""); const [searchInput, setSearchInput] = useState(""); const setLibrarySearchOpen = useSetAtom(librarySearchOpenAtom); const [searchQuery, setSearchQuery] = useState(undefined); @@ -1128,15 +1129,23 @@ export default function Library() { // getStarred2 takes no query, so the search box filters this tab here instead // of server-side — otherwise typing would leave favorites untouched and look // broken next to the three tabs that do respond. - const favorites: NavidromeSong[] = useMemo(() => { + const matches = (s: NavidromeSong, q: string) => + [s.title, s.artist, s.album].some((f) => (f ?? "").toLowerCase().includes(q)); + + // What the tab holds before the quick filter. Kept apart so the filter box + // stays on screen when it matches nothing — otherwise it would unmount with + // the list and leave no way to clear the term. + const favoritesAll: NavidromeSong[] = useMemo(() => { const all = favoritesQuery.data ?? []; const q = searchQuery?.toLowerCase(); - if (!q) return all; - return all.filter((s) => - [s.title, s.artist, s.album].some((f) => (f ?? "").toLowerCase().includes(q)), - ); + return q ? all.filter((s) => matches(s, q)) : all; }, [favoritesQuery.data, searchQuery]); + const favorites: NavidromeSong[] = useMemo(() => { + const q = favoritesFilter.trim().toLowerCase(); + return q ? favoritesAll.filter((s) => matches(s, q)) : favoritesAll; + }, [favoritesAll, favoritesFilter]); + const tracksSentinelRef = useInfiniteScrollSentinel(tracksQuery.hasNextPage, tracksQuery.isFetchingNextPage, tracksQuery.fetchNextPage); const albumsSentinelRef = useInfiniteScrollSentinel(albumsQuery.hasNextPage, albumsQuery.isFetchingNextPage, albumsQuery.fetchNextPage); @@ -1519,7 +1528,7 @@ export default function Library() { {favoritesFailed && favorites.length === 0 && } - {!favoritesFailed && !favoritesQuery.isLoading && creds && favorites.length === 0 && ( + {!favoritesFailed && !favoritesQuery.isLoading && creds && favoritesAll.length === 0 && (
@@ -1543,9 +1552,10 @@ export default function Library() { )} - {creds && favorites.length > 0 && ( + {creds && favoritesAll.length > 0 && ( <> - +
+ playFavorites()}> Play @@ -1576,6 +1586,22 @@ export default function Library() { )} + +
+ + {favorites.length === 0 && ( + + +
+ No favorites match "{favoritesFilter.trim()}" + Try a different search term +
+
+ )} {favorites.map((song, idx) => { diff --git a/desktop/src/pages/library/Library.tsx b/desktop/src/pages/library/Library.tsx index 1c5b7f16..2f0d44a2 100644 --- a/desktop/src/pages/library/Library.tsx +++ b/desktop/src/pages/library/Library.tsx @@ -1125,6 +1125,7 @@ export default function Library() { const [openFavoriteMenuId, setOpenFavoriteMenuId] = useState(null); const [favoriteMenuAnchor, setFavoriteMenuAnchor] = useState(null); const [playlistFilter, setPlaylistFilter] = useState(""); + const [favoritesFilter, setFavoritesFilter] = useState(""); const [searchInput, setSearchInput] = useState(""); const setLibrarySearchOpen = useSetAtom(librarySearchOpenAtom); const [searchQuery, setSearchQuery] = useState(undefined); @@ -1156,15 +1157,23 @@ export default function Library() { // getStarred2 takes no query, so the search box filters this tab here instead // of server-side — otherwise typing would leave favorites untouched and look // broken next to the three tabs that do respond. - const favorites: NavidromeSong[] = useMemo(() => { + const matches = (s: NavidromeSong, q: string) => + [s.title, s.artist, s.album].some((f) => (f ?? "").toLowerCase().includes(q)); + + // What the tab holds before the quick filter. Kept apart so the filter box + // stays on screen when it matches nothing — otherwise it would unmount with + // the list and leave no way to clear the term. + const favoritesAll: NavidromeSong[] = useMemo(() => { const all = favoritesQuery.data ?? []; const q = searchQuery?.toLowerCase(); - if (!q) return all; - return all.filter((s) => - [s.title, s.artist, s.album].some((f) => (f ?? "").toLowerCase().includes(q)), - ); + return q ? all.filter((s) => matches(s, q)) : all; }, [favoritesQuery.data, searchQuery]); + const favorites: NavidromeSong[] = useMemo(() => { + const q = favoritesFilter.trim().toLowerCase(); + return q ? favoritesAll.filter((s) => matches(s, q)) : favoritesAll; + }, [favoritesAll, favoritesFilter]); + const tracksSentinelRef = useInfiniteScrollSentinel(tracksQuery.hasNextPage, tracksQuery.isFetchingNextPage, tracksQuery.fetchNextPage); const albumsSentinelRef = useInfiniteScrollSentinel(albumsQuery.hasNextPage, albumsQuery.isFetchingNextPage, albumsQuery.fetchNextPage); @@ -1547,7 +1556,7 @@ export default function Library() { {favoritesFailed && favorites.length === 0 && } - {!favoritesFailed && !favoritesQuery.isLoading && creds && favorites.length === 0 && ( + {!favoritesFailed && !favoritesQuery.isLoading && creds && favoritesAll.length === 0 && (
@@ -1571,9 +1580,10 @@ export default function Library() { )} - {creds && favorites.length > 0 && ( + {creds && favoritesAll.length > 0 && ( <> - +
+ playFavorites()}> Play @@ -1605,6 +1615,22 @@ export default function Library() { )} + +
+ + {favorites.length === 0 && ( + + +
+ No favorites match "{favoritesFilter.trim()}" + Try a different search term +
+
+ )} {favorites.map((song, idx) => {