mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(playlists): G.81 — extract PlaylistsHeader + PlaylistCard + action utilities (cluster) (#648)
Three-cut cluster closing out the Playlists refactor. 516 → 273 LOC
(−243).
runPlaylistsActions — runPlaylistDelete (two-click confirm with
tooltip re-trigger), runPlaylistDeleteSelected (filters by
deletable, refreshes store, fires per-row error toasts), and
runPlaylistMergeSelected (collects unique songs across selected
playlists into the target, updatePlaylist + touchPlaylist + total
count toast). Each takes a deps object so all state-setters /
callbacks are explicit.
PlaylistsHeader — title row + creation controls (inline name
input with Enter / Escape handling, "New playlist" button,
"New smart" button gated on isNavidromeServer) + bulk delete
button + selection-mode toggle. The selection-mode title swaps
between t('playlists.title') and t('playlists.selectionCount').
PlaylistCard — full single-card render: cover area (smart-playlist
2×2 collage / cover image / fallback ListMusic icon + pending
clock badge), hover-only edit + delete buttons (delete with
two-click confirm), selection check overlay, play overlay button
with spinner state, and the info row (smart-playlist sparkle +
display name + song count + duration). Subscribes to
playerStore.openContextMenu directly.
Playlists drops the inline definitions + the now-unused direct
imports (deletePlaylist / updatePlaylist, buildCoverArtUrl /
coverArtCacheKey, CachedImage, StarRating, the cover image
helpers, most lucide icons, useMemo). Pure code move otherwise.
This commit is contained in:
committed by
GitHub
parent
6e4ebca938
commit
7482030a6b
@@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CheckSquare2, Plus, Trash2 } from 'lucide-react';
|
||||
import type { SubsonicPlaylist } from '../../api/subsonicTypes';
|
||||
import {
|
||||
defaultSmartFilters, type SmartFilters,
|
||||
} from '../../utils/playlistsSmart';
|
||||
|
||||
interface Props {
|
||||
selectionMode: boolean;
|
||||
selectedIds: Set<string>;
|
||||
selectedPlaylists: SubsonicPlaylist[];
|
||||
isPlaylistDeletable: (pl: SubsonicPlaylist) => boolean;
|
||||
toggleSelectionMode: () => void;
|
||||
handleDeleteSelected: () => void;
|
||||
creating: boolean;
|
||||
setCreating: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setCreatingSmart: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
newName: string;
|
||||
setNewName: React.Dispatch<React.SetStateAction<string>>;
|
||||
nameInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
handleCreate: () => Promise<void>;
|
||||
isNavidromeServer: boolean;
|
||||
setEditingSmartId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setSmartFilters: React.Dispatch<React.SetStateAction<SmartFilters>>;
|
||||
setGenreQuery: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export default function PlaylistsHeader({
|
||||
selectionMode, selectedIds, selectedPlaylists, isPlaylistDeletable,
|
||||
toggleSelectionMode, handleDeleteSelected,
|
||||
creating, setCreating, setCreatingSmart,
|
||||
newName, setNewName, nameInputRef, handleCreate,
|
||||
isNavidromeServer, setEditingSmartId, setSmartFilters, setGenreQuery,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="playlists-header">
|
||||
<h1 className="page-title" style={{ marginBottom: 0 }}>
|
||||
{selectionMode && selectedIds.size > 0
|
||||
? t('playlists.selectionCount', { count: selectedIds.size })
|
||||
: t('playlists.title')}
|
||||
</h1>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
{!(selectionMode && selectedIds.size > 0) && (<>
|
||||
{creating ? (
|
||||
<>
|
||||
<input
|
||||
ref={nameInputRef}
|
||||
className="input"
|
||||
style={{ width: 220 }}
|
||||
placeholder={t('playlists.createName')}
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleCreate();
|
||||
if (e.key === 'Escape') { setCreating(false); setNewName(''); }
|
||||
}}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={handleCreate}>
|
||||
{t('playlists.create')}
|
||||
</button>
|
||||
<button className="btn btn-surface" onClick={() => { setCreating(false); setNewName(''); }}>
|
||||
{t('playlists.cancel')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button className="btn btn-primary" onClick={() => { setCreatingSmart(false); setCreating(true); }}>
|
||||
<Plus size={15} /> {t('playlists.newPlaylist')}
|
||||
</button>
|
||||
)}
|
||||
{!creating && isNavidromeServer && (
|
||||
<button className="btn btn-surface" onClick={() => {
|
||||
setCreating(false);
|
||||
setEditingSmartId(null);
|
||||
setSmartFilters(defaultSmartFilters);
|
||||
setGenreQuery('');
|
||||
setCreatingSmart(v => !v);
|
||||
}}>
|
||||
<Plus size={15} /> {t('smartPlaylists.create')}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{selectionMode && selectedIds.size > 0 && (() => {
|
||||
const deletableCount = selectedPlaylists.filter(isPlaylistDeletable).length;
|
||||
return (
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={deletableCount === 0}
|
||||
data-tooltip={deletableCount === selectedIds.size
|
||||
? undefined
|
||||
: t('playlists.deleteSelectedPartial', { n: deletableCount, total: selectedIds.size })}
|
||||
data-tooltip-pos="bottom"
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
{t('playlists.deleteSelected')}
|
||||
</button>
|
||||
);
|
||||
})()}
|
||||
<button
|
||||
className={`btn btn-surface${selectionMode ? ' btn-sort-active' : ''}`}
|
||||
onClick={toggleSelectionMode}
|
||||
data-tooltip={selectionMode ? t('playlists.cancelSelect') : t('playlists.startSelect')}
|
||||
data-tooltip-pos="bottom"
|
||||
style={selectionMode ? { background: 'var(--accent)', color: 'var(--ctp-crust)' } : {}}
|
||||
>
|
||||
<CheckSquare2 size={15} />
|
||||
{selectionMode ? t('playlists.cancelSelect') : t('playlists.select')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user