mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
d08875dc70
Four-cut cluster. 572 → 471 LOC (−101). Each handler on the page
collapses to a one-line delegate; pure code move otherwise.
runPlaylistSaveMeta — the meta save flow (updatePlaylistMeta then
optional uploadPlaylistCoverArt + getPlaylist refresh + cover toast,
or coverRemoved → null, then metaSaved toast + closes the modal).
Takes the deps separately from the opts object so the call-site
just passes through the modal's payload.
usePlaylistSongSearch — searchOpen + searchQuery driven debounced
search against subsonic. Owns the 350 ms timeout, the filter-out
of songs already in the playlist, and the searching state. Returns
{ searchResults, setSearchResults, searching } so addSong on the
page can still drop a just-added song out of the result list.
usePlaylistSongMutations — addSong / removeSong. removeSong is
trivial (filter + setSongs + savePlaylist with prevCount).
addSong preserves the .main-content scrollTop save / requestAnimationFrame
restore trick + drops the song out of both suggestions and search
results + fires the add toast with playlist name interpolation.
usePlaylistStarRating — handleRate (local override + playerStore
userRatingOverride + setRating API) + handleToggleStar (e.stopPropagation,
local set + playerStore starredOverride + star/unstar API). Reads
starredOverrides / setStarredOverride from playerStore directly so
the page doesn't have to thread them through.
PlaylistDetail drops these direct imports (now consumed in the
hooks/utility): updatePlaylistMeta, uploadPlaylistCoverArt, search,
setRating, star, unstar, showToast, useRef. Pure code move.
38 lines
1.4 KiB
TypeScript
38 lines
1.4 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import type React from 'react';
|
|
import { search } from '../api/subsonicSearch';
|
|
import type { SubsonicSong } from '../api/subsonicTypes';
|
|
|
|
export interface PlaylistSongSearchResult {
|
|
searchResults: SubsonicSong[];
|
|
setSearchResults: React.Dispatch<React.SetStateAction<SubsonicSong[]>>;
|
|
searching: boolean;
|
|
}
|
|
|
|
export function usePlaylistSongSearch(
|
|
songs: SubsonicSong[],
|
|
searchOpen: boolean,
|
|
searchQuery: string,
|
|
): PlaylistSongSearchResult {
|
|
const [searchResults, setSearchResults] = useState<SubsonicSong[]>([]);
|
|
const [searching, setSearching] = useState(false);
|
|
const searchDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!searchOpen || !searchQuery.trim()) { setSearchResults([]); return; }
|
|
if (searchDebounce.current) clearTimeout(searchDebounce.current);
|
|
searchDebounce.current = setTimeout(async () => {
|
|
setSearching(true);
|
|
try {
|
|
const res = await search(searchQuery, { songCount: 20, artistCount: 0, albumCount: 0 });
|
|
const existingIds = new Set(songs.map(s => s.id));
|
|
setSearchResults(res.songs.filter(s => !existingIds.has(s.id)));
|
|
} catch {}
|
|
setSearching(false);
|
|
}, 350);
|
|
return () => { if (searchDebounce.current) clearTimeout(searchDebounce.current); };
|
|
}, [searchQuery, searchOpen, songs]);
|
|
|
|
return { searchResults, setSearchResults, searching };
|
|
}
|