mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
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.
This commit is contained in:
committed by
GitHub
parent
d0a270d90a
commit
d08875dc70
@@ -0,0 +1,47 @@
|
|||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { setRating, star, unstar } from '../api/subsonicStarRating';
|
||||||
|
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||||
|
import { usePlayerStore } from '../store/playerStore';
|
||||||
|
|
||||||
|
export interface PlaylistStarRatingDeps {
|
||||||
|
ratings: Record<string, number>;
|
||||||
|
setRatings: React.Dispatch<React.SetStateAction<Record<string, number>>>;
|
||||||
|
starredSongs: Set<string>;
|
||||||
|
setStarredSongs: React.Dispatch<React.SetStateAction<Set<string>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlaylistStarRatingActions {
|
||||||
|
handleRate: (songId: string, rating: number) => void;
|
||||||
|
handleToggleStar: (song: SubsonicSong, e: React.MouseEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlaylistStarRating(deps: PlaylistStarRatingDeps): PlaylistStarRatingActions {
|
||||||
|
const { setRatings, starredSongs, setStarredSongs } = deps;
|
||||||
|
const starredOverrides = usePlayerStore(s => s.starredOverrides);
|
||||||
|
const setStarredOverride = usePlayerStore(s => s.setStarredOverride);
|
||||||
|
|
||||||
|
const handleRate = (songId: string, rating: number) => {
|
||||||
|
setRatings(prev => ({ ...prev, [songId]: rating }));
|
||||||
|
usePlayerStore.getState().setUserRatingOverride(songId, rating);
|
||||||
|
setRating(songId, rating).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleStar = (song: SubsonicSong, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const isStarred = song.id in starredOverrides ? starredOverrides[song.id] : starredSongs.has(song.id);
|
||||||
|
setStarredSongs(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
isStarred ? next.delete(song.id) : next.add(song.id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setStarredOverride(song.id, !isStarred);
|
||||||
|
(isStarred ? unstar(song.id, 'song') : star(song.id, 'song')).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleRate, handleToggleStar };
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import { getPlaylist, updatePlaylist, updatePlaylistMeta, uploadPlaylistCoverArt } from '../api/subsonicPlaylists';
|
import { getPlaylist, updatePlaylist } from '../api/subsonicPlaylists';
|
||||||
import { setRating, star, unstar } from '../api/subsonicStarRating';
|
|
||||||
import { search } from '../api/subsonicSearch';
|
|
||||||
import { filterSongsToActiveLibrary } from '../api/subsonicLibrary';
|
import { filterSongsToActiveLibrary } from '../api/subsonicLibrary';
|
||||||
import type { SubsonicPlaylist, SubsonicSong } from '../api/subsonicTypes';
|
import type { SubsonicPlaylist, SubsonicSong } from '../api/subsonicTypes';
|
||||||
import { songToTrack } from '../utils/songToTrack';
|
import { songToTrack } from '../utils/songToTrack';
|
||||||
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { ChevronDown, ChevronLeft, ChevronRight, Play, ListPlus, Trash2, Search, X, Loader2, Plus, GripVertical, Star, RefreshCw, Shuffle, Heart, HardDriveDownload, Check, Pencil, Globe, Lock, Camera, Download, FileUp, RotateCcw, Sparkles, Square, AudioLines } from 'lucide-react';
|
import { ChevronDown, ChevronLeft, ChevronRight, Play, ListPlus, Trash2, Search, X, Loader2, Plus, GripVertical, Star, RefreshCw, Shuffle, Heart, HardDriveDownload, Check, Pencil, Globe, Lock, Camera, Download, FileUp, RotateCcw, Sparkles, Square, AudioLines } from 'lucide-react';
|
||||||
import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns';
|
import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns';
|
||||||
@@ -22,7 +20,6 @@ import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
|
|||||||
import { useZipDownloadStore } from '../store/zipDownloadStore';
|
import { useZipDownloadStore } from '../store/zipDownloadStore';
|
||||||
import { useDragDrop } from '../contexts/DragDropContext';
|
import { useDragDrop } from '../contexts/DragDropContext';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { showToast } from '../utils/toast';
|
|
||||||
import StarRating from '../components/StarRating';
|
import StarRating from '../components/StarRating';
|
||||||
import {
|
import {
|
||||||
formatDuration,
|
formatDuration,
|
||||||
@@ -43,12 +40,16 @@ import PlaylistTracklist from '../components/playlist/PlaylistTracklist';
|
|||||||
import PlaylistFilterToolbar from '../components/playlist/PlaylistFilterToolbar';
|
import PlaylistFilterToolbar from '../components/playlist/PlaylistFilterToolbar';
|
||||||
import { getDisplayedSongs, type PlaylistSortKey, type PlaylistSortDir } from '../utils/playlistDisplayedSongs';
|
import { getDisplayedSongs, type PlaylistSortKey, type PlaylistSortDir } from '../utils/playlistDisplayedSongs';
|
||||||
import { runPlaylistZipDownload } from '../utils/runPlaylistZipDownload';
|
import { runPlaylistZipDownload } from '../utils/runPlaylistZipDownload';
|
||||||
|
import { runPlaylistSaveMeta } from '../utils/runPlaylistSaveMeta';
|
||||||
import { playPlaylistAll, shufflePlaylistAll, enqueuePlaylistAll } from '../utils/playlistBulkPlayActions';
|
import { playPlaylistAll, shufflePlaylistAll, enqueuePlaylistAll } from '../utils/playlistBulkPlayActions';
|
||||||
import { startPlaylistRowDrag } from '../utils/startPlaylistRowDrag';
|
import { startPlaylistRowDrag } from '../utils/startPlaylistRowDrag';
|
||||||
import { runPlaylistReorderDrop } from '../utils/runPlaylistReorderDrop';
|
import { runPlaylistReorderDrop } from '../utils/runPlaylistReorderDrop';
|
||||||
import { usePlaylistCovers } from '../hooks/usePlaylistCovers';
|
import { usePlaylistCovers } from '../hooks/usePlaylistCovers';
|
||||||
import { usePlaylistSelection } from '../hooks/usePlaylistSelection';
|
import { usePlaylistSelection } from '../hooks/usePlaylistSelection';
|
||||||
import { usePlaylistSuggestions } from '../hooks/usePlaylistSuggestions';
|
import { usePlaylistSuggestions } from '../hooks/usePlaylistSuggestions';
|
||||||
|
import { usePlaylistSongSearch } from '../hooks/usePlaylistSongSearch';
|
||||||
|
import { usePlaylistSongMutations } from '../hooks/usePlaylistSongMutations';
|
||||||
|
import { usePlaylistStarRating } from '../hooks/usePlaylistStarRating';
|
||||||
|
|
||||||
// ── Column configuration ──────────────────────────────────────────────────────
|
// ── Column configuration ──────────────────────────────────────────────────────
|
||||||
const PL_COLUMNS: readonly ColDef[] = [
|
const PL_COLUMNS: readonly ColDef[] = [
|
||||||
@@ -172,11 +173,10 @@ export default function PlaylistDetail() {
|
|||||||
// Song search
|
// Song search
|
||||||
const [searchOpen, setSearchOpen] = useState(false);
|
const [searchOpen, setSearchOpen] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [searchResults, setSearchResults] = useState<SubsonicSong[]>([]);
|
|
||||||
const [searching, setSearching] = useState(false);
|
|
||||||
const searchDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
const [selectedSearchIds, setSelectedSearchIds] = useState<Set<string>>(new Set());
|
const [selectedSearchIds, setSelectedSearchIds] = useState<Set<string>>(new Set());
|
||||||
const [searchPlPickerOpen, setSearchPlPickerOpen] = useState(false);
|
const [searchPlPickerOpen, setSearchPlPickerOpen] = useState(false);
|
||||||
|
const { searchResults, setSearchResults, searching } =
|
||||||
|
usePlaylistSongSearch(songs, searchOpen, searchQuery);
|
||||||
|
|
||||||
// Suggestions
|
// Suggestions
|
||||||
const { suggestions, setSuggestions, loadingSuggestions, loadSuggestions } =
|
const { suggestions, setSuggestions, loadingSuggestions, loadSuggestions } =
|
||||||
@@ -235,26 +235,10 @@ export default function PlaylistDetail() {
|
|||||||
coverFile: File | null; coverRemoved: boolean;
|
coverFile: File | null; coverRemoved: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
if (!id || !playlist) return;
|
if (!id || !playlist) return;
|
||||||
await updatePlaylistMeta(id, opts.name.trim() || playlist.name, opts.comment, opts.isPublic);
|
await runPlaylistSaveMeta(
|
||||||
setPlaylist(p => p
|
{ id, playlist, t, setPlaylist, setCustomCoverId, setEditingMeta },
|
||||||
? { ...p, name: opts.name.trim() || p.name, comment: opts.comment, public: opts.isPublic }
|
opts,
|
||||||
: p
|
|
||||||
);
|
);
|
||||||
if (opts.coverFile) {
|
|
||||||
try {
|
|
||||||
await uploadPlaylistCoverArt(id, opts.coverFile);
|
|
||||||
const { playlist: refreshed } = await getPlaylist(id);
|
|
||||||
setPlaylist(prev => prev ? { ...prev, coverArt: refreshed.coverArt } : prev);
|
|
||||||
if (refreshed.coverArt) setCustomCoverId(refreshed.coverArt);
|
|
||||||
showToast(t('playlists.coverUpdated'));
|
|
||||||
} catch (err) {
|
|
||||||
showToast(err instanceof Error ? err.message : t('playlists.coverUpdated'), 3000, 'error');
|
|
||||||
}
|
|
||||||
} else if (opts.coverRemoved) {
|
|
||||||
setCustomCoverId(null);
|
|
||||||
}
|
|
||||||
showToast(t('playlists.metaSaved'));
|
|
||||||
setEditingMeta(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── ZIP Download ──────────────────────────────────────────────
|
// ── ZIP Download ──────────────────────────────────────────────
|
||||||
@@ -275,28 +259,9 @@ export default function PlaylistDetail() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Remove ────────────────────────────────────────────────────
|
// ── Remove ────────────────────────────────────────────────────
|
||||||
const removeSong = (idx: number) => {
|
const { removeSong, addSong } = usePlaylistSongMutations({
|
||||||
const prevCount = songs.length;
|
songs, setSongs, savePlaylist, setSuggestions, setSearchResults, playlist, t,
|
||||||
const next = songs.filter((_, i) => i !== idx);
|
});
|
||||||
setSongs(next);
|
|
||||||
savePlaylist(next, prevCount);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Add ───────────────────────────────────────────────────────
|
|
||||||
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 }));
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Preview (30s mid-song sample via Rust audio engine) ────────
|
// ── Preview (30s mid-song sample via Rust audio engine) ────────
|
||||||
// Pause/resume of the main player + timer + cancel-on-supersede are all
|
// Pause/resume of the main player + timer + cancel-on-supersede are all
|
||||||
@@ -320,39 +285,9 @@ export default function PlaylistDetail() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ── Rating / Star ─────────────────────────────────────────────
|
// ── Rating / Star ─────────────────────────────────────────────
|
||||||
const handleRate = (songId: string, rating: number) => {
|
const { handleRate, handleToggleStar } = usePlaylistStarRating({
|
||||||
setRatings(prev => ({ ...prev, [songId]: rating }));
|
ratings, setRatings, starredSongs, setStarredSongs,
|
||||||
usePlayerStore.getState().setUserRatingOverride(songId, rating);
|
});
|
||||||
setRating(songId, rating).catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleStar = (song: SubsonicSong, e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const isStarred = song.id in starredOverrides ? starredOverrides[song.id] : starredSongs.has(song.id);
|
|
||||||
setStarredSongs(prev => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
isStarred ? next.delete(song.id) : next.add(song.id);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
setStarredOverride(song.id, !isStarred);
|
|
||||||
(isStarred ? unstar(song.id, 'song') : star(song.id, 'song')).catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Search ────────────────────────────────────────────────────
|
|
||||||
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]);
|
|
||||||
|
|
||||||
// ── psy-drop DnD reordering ───────────────────────────────────
|
// ── psy-drop DnD reordering ───────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type { TFunction } from 'i18next';
|
||||||
|
import { getPlaylist, updatePlaylistMeta, uploadPlaylistCoverArt } from '../api/subsonicPlaylists';
|
||||||
|
import type { SubsonicPlaylist } from '../api/subsonicTypes';
|
||||||
|
import { showToast } from './toast';
|
||||||
|
|
||||||
|
export interface RunPlaylistSaveMetaDeps {
|
||||||
|
id: string;
|
||||||
|
playlist: SubsonicPlaylist;
|
||||||
|
t: TFunction;
|
||||||
|
setPlaylist: (updater: (p: SubsonicPlaylist | null) => SubsonicPlaylist | null) => void;
|
||||||
|
setCustomCoverId: (id: string | null) => void;
|
||||||
|
setEditingMeta: (v: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runPlaylistSaveMeta(
|
||||||
|
deps: RunPlaylistSaveMetaDeps,
|
||||||
|
opts: {
|
||||||
|
name: string;
|
||||||
|
comment: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
coverFile: File | null;
|
||||||
|
coverRemoved: boolean;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
const { id, playlist, t, setPlaylist, setCustomCoverId, setEditingMeta } = deps;
|
||||||
|
await updatePlaylistMeta(id, opts.name.trim() || playlist.name, opts.comment, opts.isPublic);
|
||||||
|
setPlaylist(p => p
|
||||||
|
? { ...p, name: opts.name.trim() || p.name, comment: opts.comment, public: opts.isPublic }
|
||||||
|
: p
|
||||||
|
);
|
||||||
|
if (opts.coverFile) {
|
||||||
|
try {
|
||||||
|
await uploadPlaylistCoverArt(id, opts.coverFile);
|
||||||
|
const { playlist: refreshed } = await getPlaylist(id);
|
||||||
|
setPlaylist(prev => prev ? { ...prev, coverArt: refreshed.coverArt } : prev);
|
||||||
|
if (refreshed.coverArt) setCustomCoverId(refreshed.coverArt);
|
||||||
|
showToast(t('playlists.coverUpdated'));
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err instanceof Error ? err.message : t('playlists.coverUpdated'), 3000, 'error');
|
||||||
|
}
|
||||||
|
} else if (opts.coverRemoved) {
|
||||||
|
setCustomCoverId(null);
|
||||||
|
}
|
||||||
|
showToast(t('playlists.metaSaved'));
|
||||||
|
setEditingMeta(false);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user