From d33abf565ccac54f6c11b9f7d040f4c3934ee774 Mon Sep 17 00:00:00 2001 From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 5 May 2026 23:02:22 +0200 Subject: [PATCH] feat(library): "favorites only" filter on Albums, Artists, AdvancedSearch (#466) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- CHANGELOG.md | 8 ++++ src/components/StarFilterButton.tsx | 68 +++++++++++++++++++++++++++++ src/locales/de.ts | 3 ++ src/locales/en.ts | 3 ++ src/locales/es.ts | 3 ++ src/locales/fr.ts | 3 ++ src/locales/nb.ts | 3 ++ src/locales/nl.ts | 3 ++ src/locales/ru.ts | 3 ++ src/locales/zh.ts | 3 ++ src/pages/AdvancedSearch.tsx | 40 ++++++++++++----- src/pages/Albums.tsx | 17 ++++++-- src/pages/Artists.tsx | 11 ++++- src/pages/Settings.tsx | 1 + 14 files changed, 151 insertions(+), 18 deletions(-) create mode 100644 src/components/StarFilterButton.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 320b73d1..2b3db3f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * New optional toggle in Settings → Audio → Playback ("Preserve Play Next order"). When on, multiple "Play Next" insertions **queue up behind each other** instead of the latest one bumping earlier picks down. Default off — existing behaviour unchanged. * Side-benefit: single-song "Play Next" now goes through the unified `enqueueAt` path and gets undo + server-sync support that the album path already had. +### Library — "favorites only" filter on Albums, Artists and Advanced Search + +**By [@Psychotoxical](https://github.com/Psychotoxical), suggested by [@lilgringo](https://github.com/lilgringo), PR [#466](https://github.com/Psychotoxical/psysonic/pull/466)** + +* New star-toggle button in the toolbars of **Albums**, **Artists** and **Advanced Search** that flips the visible list to favourites-only. +* Filter state is ephemeral per page (not persisted) so users don't come back to a half-empty library and wonder where their content went. +* Reads star state live from in-memory overrides — toggling a favourite from a context menu updates the visible list immediately, no refetch. + ## Changed ### Dependencies — npm / Cargo refresh and rodio 0.22 diff --git a/src/components/StarFilterButton.tsx b/src/components/StarFilterButton.tsx new file mode 100644 index 00000000..5e9aed34 --- /dev/null +++ b/src/components/StarFilterButton.tsx @@ -0,0 +1,68 @@ +import { Star } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; + +interface Props { + active: boolean; + onChange: (next: boolean) => void; + /** 'default' = icon + label, regular padding (Albums toolbar). + * 'compact' = icon-only, 0.5rem padding (Artists view-mode buttons). + * 'small' = icon + label, 4px/14px padding + 12px text (AdvancedSearch tabs). */ + size?: 'default' | 'compact' | 'small'; +} + +export default function StarFilterButton({ active, onChange, size = 'default' }: Props) { + const { t } = useTranslation(); + const tooltip = active ? t('common.favoritesTooltipOn') : t('common.favoritesTooltipOff'); + const activeStyle = active ? { background: 'var(--accent)', color: 'var(--ctp-crust)' } : {}; + + if (size === 'compact') { + return ( + + ); + } + + if (size === 'small') { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/locales/de.ts b/src/locales/de.ts index 899f2253..17caed90 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -495,6 +495,9 @@ export const deTranslation = { filterSearchGenres: 'Genres suchen…', filterNoGenres: 'Keine Genres gefunden', filterClear: 'Zurücksetzen', + favorites: 'Favoriten', + favoritesTooltipOff: 'Nur Favoriten anzeigen', + favoritesTooltipOn: 'Alle anzeigen', play: 'Abspielen', bulkSelected: '{{count}} ausgewählt', clearSelection: 'Auswahl aufheben', diff --git a/src/locales/en.ts b/src/locales/en.ts index e470a649..383d6d27 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -497,6 +497,9 @@ export const enTranslation = { filterSearchGenres: 'Search genres…', filterNoGenres: 'No genres match', filterClear: 'Clear', + favorites: 'Favorites', + favoritesTooltipOff: 'Show only favorites', + favoritesTooltipOn: 'Show all', play: 'Play', bulkSelected: '{{count}} selected', clearSelection: 'Clear selection', diff --git a/src/locales/es.ts b/src/locales/es.ts index d0c201e0..bac368b9 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -496,6 +496,9 @@ export const esTranslation = { filterSearchGenres: 'Buscar géneros…', filterNoGenres: 'Ningún género coincide', filterClear: 'Limpiar', + favorites: 'Favoritos', + favoritesTooltipOff: 'Mostrar solo favoritos', + favoritesTooltipOn: 'Mostrar todo', play: 'Reproducir', bulkSelected: '{{count}} seleccionados', clearSelection: 'Limpiar selección', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index f74da8c9..ef1c3691 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -494,6 +494,9 @@ export const frTranslation = { filterSearchGenres: 'Rechercher des genres…', filterNoGenres: 'Aucun genre trouvé', filterClear: 'Effacer', + favorites: 'Favoris', + favoritesTooltipOff: 'Afficher uniquement les favoris', + favoritesTooltipOn: 'Tout afficher', play: 'Lire', bulkSelected: '{{count}} sélectionné(s)', clearSelection: 'Effacer la sélection', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index a5844b42..9648e204 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -494,6 +494,9 @@ export const nbTranslation = { filterSearchGenres: 'Søk i sjangre…', filterNoGenres: 'Ingen sjangre samsvarer', filterClear: 'Tøm', + favorites: 'Favoritter', + favoritesTooltipOff: 'Vis bare favoritter', + favoritesTooltipOn: 'Vis alle', play: 'Spill', bulkSelected: '{{count}} valgt', clearSelection: 'Fjern utvalg', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 5169a723..14ab7760 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -493,6 +493,9 @@ export const nlTranslation = { filterSearchGenres: 'Genres zoeken…', filterNoGenres: 'Geen genres gevonden', filterClear: 'Wissen', + favorites: 'Favorieten', + favoritesTooltipOff: 'Alleen favorieten tonen', + favoritesTooltipOn: 'Alles tonen', play: 'Afspelen', bulkSelected: '{{count}} geselecteerd', clearSelection: 'Selectie wissen', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index fc0f9a7f..57819c78 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -525,6 +525,9 @@ export const ruTranslation = { filterSearchGenres: 'Поиск жанров…', filterNoGenres: 'Нет совпадений', filterClear: 'Сбросить', + favorites: 'Избранное', + favoritesTooltipOff: 'Показывать только избранное', + favoritesTooltipOn: 'Показать всё', play: 'Воспроизвести', bulkSelected: 'Выбрано: {{count}}', clearSelection: 'Сбросить выбор', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 7cd915fd..a494cca3 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -488,6 +488,9 @@ export const zhTranslation = { filterSearchGenres: '搜索流派…', filterNoGenres: '未找到匹配流派', filterClear: '清除', + favorites: '收藏', + favoritesTooltipOff: '仅显示收藏', + favoritesTooltipOn: '显示全部', play: '播放', bulkSelected: '已选 {{count}} 首', clearSelection: '清除选择', diff --git a/src/pages/AdvancedSearch.tsx b/src/pages/AdvancedSearch.tsx index 27b84ce6..4be77da9 100644 --- a/src/pages/AdvancedSearch.tsx +++ b/src/pages/AdvancedSearch.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { SlidersVertical } from 'lucide-react'; import { @@ -10,7 +10,9 @@ import AlbumRow from '../components/AlbumRow'; import ArtistRow from '../components/ArtistRow'; import SongRow, { SongListHeader } from '../components/SongRow'; import CustomSelect from '../components/CustomSelect'; +import StarFilterButton from '../components/StarFilterButton'; import { useAuthStore } from '../store/authStore'; +import { usePlayerStore } from '../store/playerStore'; type ResultType = 'all' | 'artists' | 'albums' | 'songs'; @@ -37,10 +39,23 @@ export default function AdvancedSearch() { const [yearFrom, setYearFrom] = useState(''); const [yearTo, setYearTo] = useState(''); const [resultType, setResultType] = useState('all'); + const [starredOnly, setStarredOnly] = useState(false); const [genres, setGenres] = useState([]); const [results, setResults] = useState(null); - const total = results - ? results.artists.length + results.albums.length + results.songs.length + const starredOverrides = usePlayerStore(s => s.starredOverrides); + const filteredResults = useMemo(() => { + if (!results) return null; + if (!starredOnly) return results; + const isFav = (id: string, base: boolean | string | undefined) => + id in starredOverrides ? !!starredOverrides[id] : !!base; + return { + artists: results.artists.filter(a => isFav(a.id, a.starred)), + albums: results.albums.filter(a => isFav(a.id, a.starred)), + songs: results.songs.filter(s => isFav(s.id, s.starred)), + }; + }, [results, starredOnly, starredOverrides]); + const total = filteredResults + ? filteredResults.artists.length + filteredResults.albums.length + filteredResults.songs.length : 0; const [loading, setLoading] = useState(false); const [hasSearched, setHasSearched] = useState(false); @@ -260,7 +275,7 @@ export default function AdvancedSearch() { {/* Row 3: Result type + Search button */}
-
+
{typeOptions.map(opt => (