feat(library): "favorites only" filter on Albums, Artists, AdvancedSearch (#466)

* feat(ui): StarFilterButton component + common i18n keys

Reusable toggle button for "favorites only" filtering. Three size
variants for different toolbar contexts:
- default: icon + label (Albums-style)
- compact: icon-only with 0.5rem padding (Artists view-mode buttons)
- small:   icon + label at 12px / 4×14 padding (AdvancedSearch tabs)

Adds common.favorites + favoritesTooltipOff/On in all 8 locales.

* feat(library): "favorites only" filter on Albums, Artists, AdvancedSearch

Client-side filter using the existing useMemo pipelines on each page.
Reads starred state from item.starred + playerStore.starredOverrides
(O(1) Map lookup, picks up live star toggles without refetch).

- Albums: toolbar button (default size) next to compilation filter.
- Artists: toolbar button (compact / icon-only) before the Images toggle.
- AdvancedSearch: toolbar button (small) next to the result-type tabs;
  filters all three result categories (artists / albums / songs) and
  updates the count badges accordingly.

Filter state is ephemeral per-page (not persisted) so users don't get
surprised by hidden items after a restart. Zero extra server calls.

* docs(contributors): credit + changelog entry for #466
This commit is contained in:
Frank Stellmacher
2026-05-05 23:02:22 +02:00
committed by GitHub
parent 0fab2849e5
commit d33abf565c
14 changed files with 151 additions and 18 deletions
+13 -4
View File
@@ -2,6 +2,7 @@ import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react'
import AlbumCard from '../components/AlbumCard';
import GenreFilterBar from '../components/GenreFilterBar';
import YearFilterButton from '../components/YearFilterButton';
import StarFilterButton from '../components/StarFilterButton';
import SortDropdown from '../components/SortDropdown';
import { getAlbumList, getAlbumsByGenre, getAlbum, SubsonicAlbum, buildDownloadUrl } from '../api/subsonic';
import { songToTrack } from '../store/playerStore';
@@ -50,6 +51,7 @@ export default function Albums() {
const [yearFrom, setYearFrom] = useState('');
const [yearTo, setYearTo] = useState('');
const [compFilter, setCompFilter] = useState<CompFilter>('all');
const [starredOnly, setStarredOnly] = useState(false);
const observerTarget = useRef<HTMLDivElement>(null);
// ── Multi-selection ──────────────────────────────────────────────────────
@@ -74,11 +76,16 @@ export default function Albums() {
setSelectedIds(new Set());
};
const starredOverrides = usePlayerStore(s => s.starredOverrides);
const visibleAlbums = useMemo(() => {
if (compFilter === 'all') return albums;
if (compFilter === 'only') return albums.filter(a => a.isCompilation);
return albums.filter(a => !a.isCompilation);
}, [albums, compFilter]);
let out = albums;
if (compFilter === 'only') out = out.filter(a => a.isCompilation);
else if (compFilter === 'hide') out = out.filter(a => !a.isCompilation);
if (starredOnly) {
out = out.filter(a => a.id in starredOverrides ? starredOverrides[a.id] : !!a.starred);
}
return out;
}, [albums, compFilter, starredOnly, starredOverrides]);
const selectedAlbums = visibleAlbums.filter(a => selectedIds.has(a.id));
const openContextMenu = usePlayerStore(state => state.openContextMenu);
@@ -261,6 +268,8 @@ export default function Albums() {
<GenreFilterBar selected={selectedGenres} onSelectionChange={setSelectedGenres} />
<StarFilterButton active={starredOnly} onChange={setStarredOnly} />
<button
className={`btn btn-surface${compFilter !== 'all' ? ' btn-sort-active' : ''}`}
onClick={cycleCompFilter}