mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
a4b1b29dd6
Four-cut cluster opening the Favorites refactor. 1018 → 643 LOC (−375). TopFavoriteArtists — TopFavoriteArtistsRow (the horizontal-scroll section with chevron nav buttons and resize-driven scroll-state) plus the private TopFavoriteArtistCard with the cached avatar image and selected-outline styling. Exports the TopFavoriteArtist row-data shape. RadioFavorites — RadioStationRow (same horizontal-scroll pattern as the artists row) plus the private RadioFavCard with cover or Cast-icon fallback, live-radio badge overlay when active, and an unfavorite heart button. useFavoritesData — owns the four data states (albums, artists, songs, radioStations) + loading + the load-on-mount effect (calls getStarred + reads radio favorites from localStorage + fetches matching stations). Computes topFavoriteArtists memo (counts favorited songs by artist, top 12). Exports unfavoriteStation (removes from state + persists to localStorage). useFavoritesSongFiltering — owns the filtering pipeline (drops unfavorited, applies artist / genre / year-range filters) and the three-state sort (asc → desc → reset). Returns filteredSongs / visibleSongs plus handleSortClick / getSortIndicator. Hook file uses .tsx because getSortIndicator returns ArrowUp / ArrowDown JSX. Favorites drops the inline definitions plus the now-unused direct imports (getInternetRadioStations, getStarred, buildCoverArtUrl / coverArtCacheKey, useAuthStore, Users / ArrowUp / ArrowDown icons). Pure code move otherwise.
96 lines
3.4 KiB
TypeScript
96 lines
3.4 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { getInternetRadioStations } from '../api/subsonicRadio';
|
|
import { getStarred } from '../api/subsonicStarRating';
|
|
import type {
|
|
InternetRadioStation, SubsonicAlbum, SubsonicArtist, SubsonicSong,
|
|
} from '../api/subsonicTypes';
|
|
import { useAuthStore } from '../store/authStore';
|
|
import { usePlayerStore } from '../store/playerStore';
|
|
import type { TopFavoriteArtist } from '../components/favorites/TopFavoriteArtists';
|
|
|
|
export interface FavoritesDataResult {
|
|
albums: SubsonicAlbum[];
|
|
artists: SubsonicArtist[];
|
|
songs: SubsonicSong[];
|
|
setSongs: React.Dispatch<React.SetStateAction<SubsonicSong[]>>;
|
|
radioStations: InternetRadioStation[];
|
|
setRadioStations: React.Dispatch<React.SetStateAction<InternetRadioStation[]>>;
|
|
loading: boolean;
|
|
topFavoriteArtists: TopFavoriteArtist[];
|
|
unfavoriteStation: (id: string) => void;
|
|
}
|
|
|
|
export function useFavoritesData(): FavoritesDataResult {
|
|
const [albums, setAlbums] = useState<SubsonicAlbum[]>([]);
|
|
const [artists, setArtists] = useState<SubsonicArtist[]>([]);
|
|
const [songs, setSongs] = useState<SubsonicSong[]>([]);
|
|
const [radioStations, setRadioStations] = useState<InternetRadioStation[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const musicLibraryFilterVersion = useAuthStore(s => s.musicLibraryFilterVersion);
|
|
const starredOverrides = usePlayerStore(s => s.starredOverrides);
|
|
|
|
useEffect(() => {
|
|
const loadAll = async () => {
|
|
const [starredResult] = await Promise.allSettled([
|
|
getStarred(),
|
|
]);
|
|
if (starredResult.status === 'fulfilled') {
|
|
setAlbums(starredResult.value.albums);
|
|
setArtists(starredResult.value.artists);
|
|
setSongs(starredResult.value.songs);
|
|
}
|
|
|
|
// Radio favorites: read IDs from localStorage, fetch all stations, filter
|
|
try {
|
|
const favIds = new Set<string>(JSON.parse(localStorage.getItem('psysonic_radio_favorites') ?? '[]'));
|
|
if (favIds.size > 0) {
|
|
const all = await getInternetRadioStations();
|
|
setRadioStations(all.filter(s => favIds.has(s.id)));
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
setLoading(false);
|
|
};
|
|
loadAll();
|
|
}, [musicLibraryFilterVersion]);
|
|
|
|
// ── Top Favorite Artists aggregated from favorited songs ─────────────
|
|
const topFavoriteArtists = useMemo<TopFavoriteArtist[]>(() => {
|
|
const counts = new Map<string, TopFavoriteArtist>();
|
|
for (const s of songs) {
|
|
if (starredOverrides[s.id] === false) continue;
|
|
const key = s.artistId || s.artist;
|
|
if (!key) continue;
|
|
const existing = counts.get(key);
|
|
if (existing) {
|
|
existing.count += 1;
|
|
} else {
|
|
counts.set(key, {
|
|
id: key,
|
|
name: s.artist || key,
|
|
count: 1,
|
|
coverArtId: s.artistId || '',
|
|
});
|
|
}
|
|
}
|
|
return Array.from(counts.values())
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 12);
|
|
}, [songs, starredOverrides]);
|
|
|
|
function unfavoriteStation(id: string) {
|
|
setRadioStations(prev => prev.filter(s => s.id !== id));
|
|
try {
|
|
const next = new Set<string>(JSON.parse(localStorage.getItem('psysonic_radio_favorites') ?? '[]'));
|
|
next.delete(id);
|
|
localStorage.setItem('psysonic_radio_favorites', JSON.stringify([...next]));
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
return {
|
|
albums, artists, songs, setSongs, radioStations, setRadioStations,
|
|
loading, topFavoriteArtists, unfavoriteStation,
|
|
};
|
|
}
|