Compare commits
No commits in common. "b3b28085087f43f26529a4cb2bfd947305e8aef1" and "083eca58cf8b22a45374b4bee040b434d308f57e" have entirely different histories.
b3b2808508
...
083eca58cf
@ -69,10 +69,8 @@ router.get('/structure', async (req: Request, res: Response) => {
|
|||||||
// Save playlists in batch (replacing all existing ones)
|
// Save playlists in batch (replacing all existing ones)
|
||||||
router.post('/batch', async (req, res) => {
|
router.post('/batch', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Replace all playlists atomically
|
await Playlist.deleteMany({}); // Clear existing playlists
|
||||||
await Playlist.deleteMany({});
|
const playlists = await Playlist.create(req.body);
|
||||||
const payload = Array.isArray(req.body) ? req.body : [];
|
|
||||||
const playlists = await Playlist.insertMany(payload, { ordered: false });
|
|
||||||
res.json(playlists);
|
res.json(playlists);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving playlists:', error);
|
console.error('Error saving playlists:', error);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Box, Button, Flex, Heading, Spinner, Text, useBreakpointValue, IconButton, Drawer, DrawerBody, DrawerHeader, DrawerOverlay, DrawerContent, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, VStack, useToast } from "@chakra-ui/react";
|
import { Box, Button, Flex, Heading, Spinner, Text, useBreakpointValue, IconButton, Drawer, DrawerBody, DrawerHeader, DrawerOverlay, DrawerContent, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, VStack } from "@chakra-ui/react";
|
||||||
import { ChevronLeftIcon, ChevronRightIcon, SettingsIcon, DownloadIcon } from "@chakra-ui/icons";
|
import { ChevronLeftIcon, ChevronRightIcon, SettingsIcon, DownloadIcon } from "@chakra-ui/icons";
|
||||||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||||
import { useNavigate, useLocation, Routes, Route } from "react-router-dom";
|
import { useNavigate, useLocation, Routes, Route } from "react-router-dom";
|
||||||
@ -76,7 +76,6 @@ const RekordboxReader: React.FC = () => {
|
|||||||
const [isDatabaseInitialized, setIsDatabaseInitialized] = useState(false);
|
const [isDatabaseInitialized, setIsDatabaseInitialized] = useState(false);
|
||||||
const [isSwitchingPlaylist, setIsSwitchingPlaylist] = useState(false);
|
const [isSwitchingPlaylist, setIsSwitchingPlaylist] = useState(false);
|
||||||
const { currentSong, playSong, closePlayer } = useMusicPlayer();
|
const { currentSong, playSong, closePlayer } = useMusicPlayer();
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
// Memoized song selection handler to prevent unnecessary re-renders
|
// Memoized song selection handler to prevent unnecessary re-renders
|
||||||
const handleSongSelect = useCallback((song: Song) => {
|
const handleSongSelect = useCallback((song: Song) => {
|
||||||
@ -232,66 +231,31 @@ const RekordboxReader: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddSongsToPlaylist = async (songIds: string[], playlistName: string) => {
|
const handleAddSongsToPlaylist = async (songIds: string[], playlistName: string) => {
|
||||||
// Fetch FULL playlists to avoid losing tracks (structure view strips them)
|
const updatedPlaylists = playlists.map(node => {
|
||||||
const fullTree = await api.getPlaylists();
|
if (node.name === playlistName && node.type === 'playlist') {
|
||||||
|
return {
|
||||||
const applyAdd = (nodes: PlaylistNode[]): PlaylistNode[] => {
|
...node,
|
||||||
return nodes.map(node => {
|
tracks: [...new Set([...(node.tracks || []), ...songIds])]
|
||||||
if (node.type === 'playlist' && node.name === playlistName) {
|
};
|
||||||
const current = Array.isArray(node.tracks) ? node.tracks : [];
|
|
||||||
const merged = Array.from(new Set([...current, ...songIds]));
|
|
||||||
return { ...node, tracks: merged };
|
|
||||||
}
|
}
|
||||||
if (node.type === 'folder' && node.children) {
|
if (node.type === 'folder' && node.children) {
|
||||||
return { ...node, children: applyAdd(node.children) };
|
return {
|
||||||
|
...node,
|
||||||
|
children: node.children.map(child => {
|
||||||
|
if (child.name === playlistName && child.type === 'playlist') {
|
||||||
|
return {
|
||||||
|
...child,
|
||||||
|
tracks: [...new Set([...(child.tracks || []), ...songIds])]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
};
|
const savedPlaylists = await api.savePlaylists(updatedPlaylists);
|
||||||
|
setPlaylists(savedPlaylists);
|
||||||
const updatedFullTree = applyAdd(fullTree);
|
|
||||||
await api.savePlaylists(updatedFullTree);
|
|
||||||
|
|
||||||
// Reload structure for UI counters
|
|
||||||
const structure = await api.getPlaylistStructure();
|
|
||||||
setPlaylists(structure);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle drop from song list into playlist (with duplicate check and user choice)
|
|
||||||
const handleDropSongsToPlaylist = async (playlistName: string, songIds: string[]) => {
|
|
||||||
// Find target playlist current tracks
|
|
||||||
const findNode = (nodes: PlaylistNode[]): PlaylistNode | null => {
|
|
||||||
for (const n of nodes) {
|
|
||||||
if (n.name === playlistName && n.type === 'playlist') return n;
|
|
||||||
if (n.type === 'folder' && n.children) {
|
|
||||||
const found = findNode(n.children);
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
const target = findNode(playlists);
|
|
||||||
if (!target) return;
|
|
||||||
const existing = new Set(target?.tracks || []);
|
|
||||||
const dupes = songIds.filter(id => existing.has(id));
|
|
||||||
|
|
||||||
let proceedMode: 'skip' | 'allow' = 'skip';
|
|
||||||
if (dupes.length > 0) {
|
|
||||||
// Simple confirm flow: OK = allow duplicates, Cancel = skip duplicates
|
|
||||||
const allow = window.confirm(`${dupes.length} duplicate${dupes.length>1?'s':''} detected in "${playlistName}". Press OK to add anyway (allow duplicates) or Cancel to skip duplicates.`);
|
|
||||||
proceedMode = allow ? 'allow' : 'skip';
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalIds = proceedMode === 'skip' ? songIds.filter(id => !existing.has(id)) : songIds;
|
|
||||||
if (finalIds.length === 0) return;
|
|
||||||
await handleAddSongsToPlaylist(finalIds, playlistName);
|
|
||||||
toast({
|
|
||||||
title: 'Songs Added',
|
|
||||||
description: `${finalIds.length} song${finalIds.length === 1 ? '' : 's'} added to "${playlistName}"`,
|
|
||||||
status: 'success',
|
|
||||||
duration: 3000,
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFromPlaylist = async (songIds: string[]) => {
|
const handleRemoveFromPlaylist = async (songIds: string[]) => {
|
||||||
@ -460,7 +424,6 @@ const RekordboxReader: React.FC = () => {
|
|||||||
onPlaylistDelete={handlePlaylistDelete}
|
onPlaylistDelete={handlePlaylistDelete}
|
||||||
onFolderCreate={handleCreateFolder}
|
onFolderCreate={handleCreateFolder}
|
||||||
onPlaylistMove={handleMovePlaylist}
|
onPlaylistMove={handleMovePlaylist}
|
||||||
onDropSongs={handleDropSongsToPlaylist}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -98,7 +98,7 @@ export const BackgroundJobProgress: React.FC<BackgroundJobProgressProps> = ({
|
|||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tick = async () => {
|
intervalRef.current = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
// Always reload job list to detect newly started jobs
|
// Always reload job list to detect newly started jobs
|
||||||
const jobsData = await api.getAllJobs();
|
const jobsData = await api.getAllJobs();
|
||||||
@ -116,23 +116,13 @@ export const BackgroundJobProgress: React.FC<BackgroundJobProgressProps> = ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore transient polling errors
|
// ignore transient polling errors
|
||||||
}
|
}
|
||||||
};
|
}, 2000);
|
||||||
|
|
||||||
// Adaptive interval: 2s if active jobs, else 10s
|
|
||||||
const schedule = async () => {
|
|
||||||
await tick();
|
|
||||||
const hasActive = (jobs || []).some(j => j.status === 'running');
|
|
||||||
const delay = hasActive ? 2000 : 10000;
|
|
||||||
intervalRef.current = setTimeout(schedule, delay) as any;
|
|
||||||
};
|
|
||||||
|
|
||||||
schedule();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stop polling
|
// Stop polling
|
||||||
const stopPolling = () => {
|
const stopPolling = () => {
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
clearTimeout(intervalRef.current as any);
|
clearInterval(intervalRef.current);
|
||||||
intervalRef.current = null;
|
intervalRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -49,8 +49,7 @@ const SongItem = memo<{
|
|||||||
onToggleSelection: (songId: string) => void;
|
onToggleSelection: (songId: string) => void;
|
||||||
showCheckbox: boolean;
|
showCheckbox: boolean;
|
||||||
onPlaySong?: (song: Song) => void;
|
onPlaySong?: (song: Song) => void;
|
||||||
onDragStart: (e: React.DragEvent, songIdsFallback: string[]) => void;
|
}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong }) => {
|
||||||
}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, onDragStart }) => {
|
|
||||||
// Memoize the formatted duration to prevent recalculation
|
// Memoize the formatted duration to prevent recalculation
|
||||||
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
|
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
@ -84,10 +83,6 @@ const SongItem = memo<{
|
|||||||
_hover={{ bg: isHighlighted ? "blue.800" : "gray.700" }}
|
_hover={{ bg: isHighlighted ? "blue.800" : "gray.700" }}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
transition="background-color 0.2s"
|
transition="background-color 0.2s"
|
||||||
draggable
|
|
||||||
onDragStart={(e) => {
|
|
||||||
onDragStart(e, [song.id]);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@ -151,8 +146,6 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
onPlaySong
|
onPlaySong
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedSongs, setSelectedSongs] = useState<Set<string>>(new Set());
|
const [selectedSongs, setSelectedSongs] = useState<Set<string>>(new Set());
|
||||||
const dragSelectionRef = useRef<string[] | null>(null);
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery);
|
const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery);
|
||||||
const { isOpen: isPlaylistModalOpen, onOpen: onPlaylistModalOpen, onClose: onPlaylistModalClose } = useDisclosure();
|
const { isOpen: isPlaylistModalOpen, onOpen: onPlaylistModalOpen, onClose: onPlaylistModalClose } = useDisclosure();
|
||||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||||
@ -234,22 +227,6 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
// Memoized search handler with debouncing
|
// Memoized search handler with debouncing
|
||||||
// Search handled inline via localSearchQuery effect
|
// Search handled inline via localSearchQuery effect
|
||||||
|
|
||||||
// Provide drag payload: if multiple selected, drag all; else drag the single item
|
|
||||||
const handleDragStart = useCallback((e: React.DragEvent, songIdsFallback: string[]) => {
|
|
||||||
const ids = selectedSongs.size > 0 ? Array.from(selectedSongs) : songIdsFallback;
|
|
||||||
dragSelectionRef.current = ids;
|
|
||||||
setIsDragging(true);
|
|
||||||
const payload = { type: 'songs', songIds: ids, count: ids.length };
|
|
||||||
e.dataTransfer.setData('application/json', JSON.stringify(payload));
|
|
||||||
e.dataTransfer.setData('text/plain', JSON.stringify(payload));
|
|
||||||
e.dataTransfer.effectAllowed = 'copyMove';
|
|
||||||
}, [selectedSongs]);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(() => {
|
|
||||||
setIsDragging(false);
|
|
||||||
dragSelectionRef.current = null;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Memoized song items to prevent unnecessary re-renders
|
// Memoized song items to prevent unnecessary re-renders
|
||||||
const songItems = useMemo(() => {
|
const songItems = useMemo(() => {
|
||||||
return songs.map(song => (
|
return songs.map(song => (
|
||||||
@ -262,10 +239,9 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
onToggleSelection={toggleSelection}
|
onToggleSelection={toggleSelection}
|
||||||
showCheckbox={selectedSongs.size > 0 || depth === 0}
|
showCheckbox={selectedSongs.size > 0 || depth === 0}
|
||||||
onPlaySong={onPlaySong}
|
onPlaySong={onPlaySong}
|
||||||
onDragStart={handleDragStart}
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}, [songs, selectedSongs, selectedSongId, toggleSelection, depth, onPlaySong, handleDragStart]); // Removed handleSongSelect since it's already memoized
|
}, [songs, selectedSongs, selectedSongId, toggleSelection, depth, onPlaySong]); // Removed handleSongSelect since it's already memoized
|
||||||
|
|
||||||
// Use total playlist duration if available, otherwise calculate from current songs
|
// Use total playlist duration if available, otherwise calculate from current songs
|
||||||
const totalDuration = useMemo(() => {
|
const totalDuration = useMemo(() => {
|
||||||
@ -333,12 +309,6 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" height="100%">
|
<Flex direction="column" height="100%">
|
||||||
{/* Global drag badge for selected count */}
|
|
||||||
{isDragging && (
|
|
||||||
<Box position="fixed" top={3} right={3} zIndex={2000} bg="blue.600" color="white" px={3} py={1} borderRadius="md" boxShadow="lg">
|
|
||||||
Dragging {dragSelectionRef.current?.length || 0} song{(dragSelectionRef.current?.length || 0) === 1 ? '' : 's'}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{/* Sticky Header */}
|
{/* Sticky Header */}
|
||||||
<Box
|
<Box
|
||||||
position="sticky"
|
position="sticky"
|
||||||
@ -440,7 +410,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
id="song-list-container"
|
id="song-list-container"
|
||||||
>
|
>
|
||||||
<Flex direction="column" gap={2}>
|
<Flex direction="column" gap={2}>
|
||||||
<Box onDragEnd={handleDragEnd}>{songItems}</Box>
|
{songItems}
|
||||||
|
|
||||||
{/* Loading indicator for infinite scroll or playlist switching */}
|
{/* Loading indicator for infinite scroll or playlist switching */}
|
||||||
{(loading || isSwitchingPlaylist) && (
|
{(loading || isSwitchingPlaylist) && (
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import {
|
|||||||
MenuList,
|
MenuList,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Text,
|
Text,
|
||||||
// HStack,
|
HStack,
|
||||||
Collapse,
|
Collapse,
|
||||||
MenuDivider,
|
MenuDivider,
|
||||||
MenuGroup,
|
MenuGroup,
|
||||||
@ -36,7 +36,6 @@ interface PlaylistManagerProps {
|
|||||||
onPlaylistDelete: (name: string) => void;
|
onPlaylistDelete: (name: string) => void;
|
||||||
onFolderCreate: (name: string) => void;
|
onFolderCreate: (name: string) => void;
|
||||||
onPlaylistMove: (playlistName: string, targetFolderName: string | null) => void;
|
onPlaylistMove: (playlistName: string, targetFolderName: string | null) => void;
|
||||||
onDropSongs?: (playlistName: string, songIds: string[]) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memoized button styles to prevent unnecessary re-renders
|
// Memoized button styles to prevent unnecessary re-renders
|
||||||
@ -92,7 +91,6 @@ interface PlaylistItemProps {
|
|||||||
onPlaylistDelete: (name: string) => void;
|
onPlaylistDelete: (name: string) => void;
|
||||||
onPlaylistMove: (playlistName: string, targetFolderName: string | null) => void;
|
onPlaylistMove: (playlistName: string, targetFolderName: string | null) => void;
|
||||||
allFolders: { name: string }[];
|
allFolders: { name: string }[];
|
||||||
onDropSongs?: (playlistName: string, songIds: string[]) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
||||||
@ -103,10 +101,8 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
onPlaylistDelete,
|
onPlaylistDelete,
|
||||||
onPlaylistMove,
|
onPlaylistMove,
|
||||||
allFolders,
|
allFolders,
|
||||||
onDropSongs,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
|
|
||||||
// Memoize click handlers to prevent recreation
|
// Memoize click handlers to prevent recreation
|
||||||
const handlePlaylistClick = useCallback(() => {
|
const handlePlaylistClick = useCallback(() => {
|
||||||
@ -141,30 +137,6 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
onDragOver={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const types = Array.from((e.dataTransfer.types || []) as any);
|
|
||||||
const hasOurType = types.includes('application/json') || types.includes('text/plain');
|
|
||||||
if (hasOurType) {
|
|
||||||
try { e.dataTransfer.dropEffect = 'copy'; } catch {}
|
|
||||||
setIsDragOver(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragLeave={() => setIsDragOver(false)}
|
|
||||||
onDrop={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
try {
|
|
||||||
const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain');
|
|
||||||
const parsed = json ? JSON.parse(json) : null;
|
|
||||||
if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) {
|
|
||||||
onDropSongs(node.name, parsed.songIds);
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
setIsDragOver(false);
|
|
||||||
}}
|
|
||||||
boxShadow={isDragOver ? 'inset 0 0 0 1px var(--chakra-colors-blue-400)' : 'none'}
|
|
||||||
>
|
>
|
||||||
{level > 0 && (
|
{level > 0 && (
|
||||||
<Box
|
<Box
|
||||||
@ -204,7 +176,6 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
onPlaylistDelete={onPlaylistDelete}
|
onPlaylistDelete={onPlaylistDelete}
|
||||||
onPlaylistMove={onPlaylistMove}
|
onPlaylistMove={onPlaylistMove}
|
||||||
allFolders={allFolders}
|
allFolders={allFolders}
|
||||||
onDropSongs={onDropSongs}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
@ -225,36 +196,6 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
borderRight="1px solid"
|
borderRight="1px solid"
|
||||||
borderRightColor="whiteAlpha.200"
|
borderRightColor="whiteAlpha.200"
|
||||||
position="relative"
|
position="relative"
|
||||||
border={selectedItem === node.name ? '1px solid' : undefined}
|
|
||||||
borderColor={selectedItem === node.name ? 'blue.800' : undefined}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const types = Array.from((e.dataTransfer.types || []) as any);
|
|
||||||
const hasOurType = types.includes('application/json') || types.includes('text/plain');
|
|
||||||
if (hasOurType) {
|
|
||||||
try { e.dataTransfer.dropEffect = 'copy'; } catch {}
|
|
||||||
setIsDragOver(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragLeave={() => setIsDragOver(false)}
|
|
||||||
onDrop={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
try {
|
|
||||||
let json = e.dataTransfer.getData('application/json');
|
|
||||||
if (!json) json = e.dataTransfer.getData('text/plain');
|
|
||||||
let parsed: any = null;
|
|
||||||
if (json && json.trim().length > 0) {
|
|
||||||
parsed = JSON.parse(json);
|
|
||||||
}
|
|
||||||
if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) {
|
|
||||||
onDropSongs(node.name, parsed.songIds);
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
setIsDragOver(false);
|
|
||||||
}}
|
|
||||||
boxShadow={isDragOver ? 'inset 0 0 0 1px var(--chakra-colors-blue-400)' : 'none'}
|
|
||||||
>
|
>
|
||||||
{level > 0 && (
|
{level > 0 && (
|
||||||
<Box
|
<Box
|
||||||
@ -347,7 +288,6 @@ export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
|
|||||||
onPlaylistDelete,
|
onPlaylistDelete,
|
||||||
onFolderCreate,
|
onFolderCreate,
|
||||||
onPlaylistMove,
|
onPlaylistMove,
|
||||||
onDropSongs,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { isOpen: isPlaylistModalOpen, onOpen: onPlaylistModalOpen, onClose: onPlaylistModalClose } = useDisclosure();
|
const { isOpen: isPlaylistModalOpen, onOpen: onPlaylistModalOpen, onClose: onPlaylistModalClose } = useDisclosure();
|
||||||
const { isOpen: isFolderModalOpen, onOpen: onFolderModalOpen, onClose: onFolderModalClose } = useDisclosure();
|
const { isOpen: isFolderModalOpen, onOpen: onFolderModalOpen, onClose: onFolderModalClose } = useDisclosure();
|
||||||
@ -438,7 +378,6 @@ export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
|
|||||||
onPlaylistDelete={onPlaylistDelete}
|
onPlaylistDelete={onPlaylistDelete}
|
||||||
onPlaylistMove={onPlaylistMove}
|
onPlaylistMove={onPlaylistMove}
|
||||||
allFolders={allFolders}
|
allFolders={allFolders}
|
||||||
onDropSongs={onDropSongs}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user