import { useMemo, useState } from 'react'; import type { SubsonicSong } from '../api/subsonicTypes'; export type AlbumSortKey = 'natural' | 'title' | 'artist' | 'album' | 'favorite' | 'rating' | 'duration' | 'playCount' | 'lastPlayed' | 'bpm'; interface UseAlbumDetailSortArgs { songs: SubsonicSong[] | undefined; filterText: string; starredSongs: Set; ratings: Record; userRatingOverrides: Record; } interface UseAlbumDetailSortResult { sortKey: AlbumSortKey; sortDir: 'asc' | 'desc'; handleSort: (key: AlbumSortKey) => void; displayedSongs: SubsonicSong[]; } /** * Sort + text-filter pipeline for the album track list. Click cycle on a * header column: asc → desc → off (back to `natural` order). Other * columns reset to asc on first click. * * Rating reads use the same priority as the row renderer * (`ratings[id] ?? userRatingOverrides[id] ?? song.userRating`) so the * sort matches the visible stars during optimistic updates. */ export function useAlbumDetailSort({ songs, filterText, starredSongs, ratings, userRatingOverrides, }: UseAlbumDetailSortArgs): UseAlbumDetailSortResult { const [sortKey, setSortKey] = useState('natural'); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); const [sortClickCount, setSortClickCount] = useState(0); const handleSort = (key: AlbumSortKey) => { if (key === 'natural') return; if (sortKey === key) { const nextCount = sortClickCount + 1; if (nextCount >= 3) { setSortKey('natural'); setSortDir('asc'); setSortClickCount(0); } else { setSortDir(d => d === 'asc' ? 'desc' : 'asc'); setSortClickCount(nextCount); } } else { setSortKey(key); setSortDir('asc'); setSortClickCount(1); } }; const displayedSongs = useMemo(() => { if (!songs) return []; const q = filterText.trim().toLowerCase(); if (!q && sortKey === 'natural') return songs; let result = [...songs]; if (q) result = result.filter(s => s.title.toLowerCase().includes(q) || (s.artist ?? '').toLowerCase().includes(q)); if (sortKey !== 'natural') { result.sort((a, b) => { let av: string | number; let bv: string | number; switch (sortKey) { case 'title': av = a.title; bv = b.title; break; case 'artist': av = a.artist ?? ''; bv = b.artist ?? ''; break; case 'album': av = a.album ?? ''; bv = b.album ?? ''; break; case 'favorite': av = starredSongs.has(a.id) ? 1 : 0; bv = starredSongs.has(b.id) ? 1 : 0; break; case 'rating': av = ratings[a.id] ?? userRatingOverrides[a.id] ?? a.userRating ?? 0; bv = ratings[b.id] ?? userRatingOverrides[b.id] ?? b.userRating ?? 0; break; case 'duration': av = a.duration ?? 0; bv = b.duration ?? 0; break; case 'playCount': av = a.playCount ?? 0; bv = b.playCount ?? 0; break; case 'lastPlayed': av = a.played ? Date.parse(a.played) || 0 : 0; bv = b.played ? Date.parse(b.played) || 0 : 0; break; case 'bpm': av = a.bpm ?? 0; bv = b.bpm ?? 0; break; default: av = a.title; bv = b.title; } if (typeof av === 'number' && typeof bv === 'number') { return sortDir === 'asc' ? av - bv : bv - av; } return sortDir === 'asc' ? (av as string).localeCompare(bv as string) : (bv as string).localeCompare(av as string); }); } return result; }, [songs, filterText, sortKey, sortDir, starredSongs, ratings, userRatingOverrides]); return { sortKey, sortDir, handleSort, displayedSongs }; }