Files
Psychotoxical-psysonic/src/hooks/usePlaylistSongMutations.ts
T
Frank Stellmacher d08875dc70 refactor(playlist): G.68 — extract runPlaylistSaveMeta + 3 hooks (song search / mutations / star+rating) (#635)
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.
2026-05-13 13:46:59 +02:00

48 lines
1.8 KiB
TypeScript

import type React from 'react';
import type { TFunction } from 'i18next';
import type { SubsonicPlaylist, SubsonicSong } from '../api/subsonicTypes';
import { showToast } from '../utils/toast';
export interface PlaylistSongMutationsDeps {
songs: SubsonicSong[];
setSongs: React.Dispatch<React.SetStateAction<SubsonicSong[]>>;
savePlaylist: (updatedSongs: SubsonicSong[], prevCount?: number) => Promise<void>;
setSuggestions: React.Dispatch<React.SetStateAction<SubsonicSong[]>>;
setSearchResults: React.Dispatch<React.SetStateAction<SubsonicSong[]>>;
playlist: SubsonicPlaylist | null;
t: TFunction;
}
export interface PlaylistSongMutations {
removeSong: (idx: number) => void;
addSong: (song: SubsonicSong) => void;
}
export function usePlaylistSongMutations(deps: PlaylistSongMutationsDeps): PlaylistSongMutations {
const { songs, setSongs, savePlaylist, setSuggestions, setSearchResults, playlist, t } = deps;
const removeSong = (idx: number) => {
const prevCount = songs.length;
const next = songs.filter((_, i) => i !== idx);
setSongs(next);
savePlaylist(next, prevCount);
};
const addSong = (song: SubsonicSong) => {
if (songs.some(s => s.id === song.id)) return;
const scrollHost = document.querySelector('.main-content') as HTMLElement | null;
const savedScroll = scrollHost?.scrollTop ?? 0;
const next = [...songs, song];
setSongs(next);
savePlaylist(next);
setSuggestions(prev => prev.filter(s => s.id !== song.id));
setSearchResults(prev => prev.filter(s => s.id !== song.id));
if (scrollHost) {
requestAnimationFrame(() => { scrollHost.scrollTop = savedScroll; });
}
showToast(t('playlists.addSuccess', { count: 1, playlist: playlist?.name }));
};
return { removeSong, addSong };
}