mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
refactor(playlist): G.70 — extract three lifecycle hooks (route + bulk-picker dismiss + DnD reorder) (#637)
Three-cut cluster pulling the last cluster of useEffect bodies + the
DnD-reorder visual feedback out of PlaylistDetail.tsx. 452 → 423 LOC
(−29).
usePlaylistRouteEffects — bundles two route-driven effects:
contextMenu reset (clears contextMenuSongId whenever playerStore's
context menu closes) and openEditMeta-from-route (consumes the
`openEditMeta` location.state flag, opens the meta modal, and
clears the flag with a navigate-replace so back-nav doesn't
re-trigger). Subscribes to playerStore directly.
useBulkPlPickerOutsideClick — the global mousedown listener that
closes the bulk-add-to-playlist picker when clicking outside the
picker wrapper. No-op when closed.
usePlaylistDnDReorder — owns dropTargetIdx state, the
container.addEventListener('psy-drop', …) wiring that calls
runPlaylistReorderDrop, and the handleRowMouseEnter
drag-over-visual helper. Subscribes to DragDropContext directly.
PlaylistDetail drops the direct runPlaylistReorderDrop import and
the contextMenuOpen store subscription — both now live in the hooks
that need them. Pure code move otherwise.
This commit is contained in:
committed by
GitHub
parent
16ee1ea373
commit
e260669537
@@ -0,0 +1,15 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
export function useBulkPlPickerOutsideClick(
|
||||
showBulkPlPicker: boolean,
|
||||
setShowBulkPlPicker: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (!showBulkPlPicker) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (!(e.target as HTMLElement).closest('.bulk-pl-picker-wrap')) setShowBulkPlPicker(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [showBulkPlPicker, setShowBulkPlPicker]);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import { useDragDrop } from '../contexts/DragDropContext';
|
||||
import { runPlaylistReorderDrop } from '../utils/runPlaylistReorderDrop';
|
||||
|
||||
export interface PlaylistDnDReorderDeps {
|
||||
tracklistRef: React.RefObject<HTMLDivElement | null>;
|
||||
songs: SubsonicSong[];
|
||||
savePlaylist: (updatedSongs: SubsonicSong[], prevCount?: number) => Promise<void>;
|
||||
setSongs: React.Dispatch<React.SetStateAction<SubsonicSong[]>>;
|
||||
}
|
||||
|
||||
export interface PlaylistDnDReorderResult {
|
||||
dropTargetIdx: { idx: number; before: boolean } | null;
|
||||
setDropTargetIdx: React.Dispatch<React.SetStateAction<{ idx: number; before: boolean } | null>>;
|
||||
handleRowMouseEnter: (idx: number, e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
export function usePlaylistDnDReorder(deps: PlaylistDnDReorderDeps): PlaylistDnDReorderResult {
|
||||
const { tracklistRef, songs, savePlaylist, setSongs } = deps;
|
||||
const { isDragging } = useDragDrop();
|
||||
const [dropTargetIdx, setDropTargetIdx] = useState<{ idx: number; before: boolean } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = tracklistRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const onPsyDrop = (e: Event) => {
|
||||
runPlaylistReorderDrop({ e, songs, savePlaylist, setDropTargetIdx, setSongs });
|
||||
};
|
||||
|
||||
container.addEventListener('psy-drop', onPsyDrop);
|
||||
return () => container.removeEventListener('psy-drop', onPsyDrop);
|
||||
}, [songs, savePlaylist, tracklistRef, setSongs]);
|
||||
|
||||
const handleRowMouseEnter = (idx: number, e: React.MouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const before = e.clientY < rect.top + rect.height / 2;
|
||||
setDropTargetIdx({ idx, before });
|
||||
};
|
||||
|
||||
return { dropTargetIdx, setDropTargetIdx, handleRowMouseEnter };
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import type { Location, NavigateFunction } from 'react-router-dom';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
|
||||
export interface PlaylistRouteEffectsDeps {
|
||||
setContextMenuSongId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setEditingMeta: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
location: Location;
|
||||
navigate: NavigateFunction;
|
||||
}
|
||||
|
||||
export function usePlaylistRouteEffects(deps: PlaylistRouteEffectsDeps): void {
|
||||
const { setContextMenuSongId, setEditingMeta, location, navigate } = deps;
|
||||
const contextMenuOpen = usePlayerStore(s => s.contextMenu.isOpen);
|
||||
|
||||
useEffect(() => {
|
||||
if (!contextMenuOpen) setContextMenuSongId(null);
|
||||
}, [contextMenuOpen, setContextMenuSongId]);
|
||||
|
||||
useEffect(() => {
|
||||
const state = (location.state as { openEditMeta?: boolean } | null) ?? null;
|
||||
if (state?.openEditMeta) {
|
||||
setEditingMeta(true);
|
||||
navigate(location.pathname, { replace: true, state: null });
|
||||
}
|
||||
}, [location.state, location.pathname, navigate, setEditingMeta]);
|
||||
}
|
||||
Reference in New Issue
Block a user