prevent item drop on smart playlists

This commit is contained in:
jeffvli
2026-05-19 01:30:27 -07:00
parent 37367a6741
commit c8675ab600
4 changed files with 120 additions and 20 deletions
@@ -7,8 +7,10 @@ import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon'; import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item'; import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
import { import {
SidebarPlaylistAddDragContext,
SidebarPlaylistList, SidebarPlaylistList,
SidebarSharedPlaylistList, SidebarSharedPlaylistList,
useSidebarPlaylistAddDragMonitor,
} from '/@/renderer/features/sidebar/components/sidebar-playlist-list'; } from '/@/renderer/features/sidebar/components/sidebar-playlist-list';
import { import {
SidebarItemType, SidebarItemType,
@@ -20,6 +22,17 @@ import { Group } from '/@/shared/components/group/group';
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area'; import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
const MobileSidebarPlaylistSection = () => {
const isAddDragActive = useSidebarPlaylistAddDragMonitor();
return (
<SidebarPlaylistAddDragContext.Provider value={isAddDragActive}>
<SidebarPlaylistList />
<SidebarSharedPlaylistList />
</SidebarPlaylistAddDragContext.Provider>
);
};
export const MobileSidebar = () => { export const MobileSidebar = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const sidebarPlaylistList = useSidebarPlaylistList(); const sidebarPlaylistList = useSidebarPlaylistList();
@@ -93,12 +106,7 @@ export const MobileSidebar = () => {
})} })}
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
{sidebarPlaylistList && ( {sidebarPlaylistList && <MobileSidebarPlaylistSection />}
<>
<SidebarPlaylistList />
<SidebarSharedPlaylistList />
</>
)}
</Accordion> </Accordion>
</ScrollArea> </ScrollArea>
</div> </div>
@@ -1,7 +1,8 @@
import { openContextModal } from '@mantine/modals'; import { openContextModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import clsx from 'clsx'; import clsx from 'clsx';
import { memo, MouseEvent, useCallback, useMemo, useState } from 'react'; import { motion } from 'motion/react';
import { createContext, memo, MouseEvent, useCallback, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, Link } from 'react-router'; import { generatePath, Link } from 'react-router';
@@ -27,6 +28,7 @@ import {
usePlaylistNavigationState, usePlaylistNavigationState,
} from '/@/renderer/features/sidebar/components/playlist-folder-tree'; } from '/@/renderer/features/sidebar/components/playlist-folder-tree';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { useDragMonitor } from '/@/renderer/hooks/use-drag-monitor';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { import {
useCurrentServer, useCurrentServer,
@@ -39,6 +41,8 @@ import {
import { formatDurationString } from '/@/renderer/utils'; import { formatDurationString } from '/@/renderer/utils';
import { Accordion } from '/@/shared/components/accordion/accordion'; import { Accordion } from '/@/shared/components/accordion/accordion';
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
import { animationProps } from '/@/shared/components/animations/animation-props';
import { animationVariants } from '/@/shared/components/animations/animation-variants';
import { ButtonProps } from '/@/shared/components/button/button'; import { ButtonProps } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
@@ -52,14 +56,50 @@ import {
Song, Song,
SortOrder, SortOrder,
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { DragOperation, DragTarget } from '/@/shared/types/drag-and-drop'; import { DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
const MotionLink = motion.create(Link);
const playlistRowDimVariants = animationVariants.combine(animationVariants.fadeIn, {
hidden: { opacity: 0.5 },
});
const getPlaylistOrderKey = (serverId: string | undefined, scope: 'owned' | 'shared') => { const getPlaylistOrderKey = (serverId: string | undefined, scope: 'owned' | 'shared') => {
const sid = serverId || 'local'; const sid = serverId || 'local';
return `playlist_order:${sid}:${scope}`; return `playlist_order:${sid}:${scope}`;
}; };
export const SidebarPlaylistAddDragContext = createContext(false);
const isAddToPlaylistDragSource = (source: DragData) => {
return (
source.itemType !== undefined &&
source.type !== DragTarget.PLAYLIST &&
(source.operation?.includes(DragOperation.ADD) ?? false)
);
};
export const useSidebarPlaylistAddDragMonitor = () => {
const [isAddDragActive, setIsAddDragActive] = useState(false);
const handleAddDragStart = useCallback(() => {
setIsAddDragActive(true);
}, []);
const handleAddDragDrop = useCallback(() => {
setIsAddDragActive(false);
}, []);
useDragMonitor({
canMonitor: isAddToPlaylistDragSource,
onDragStart: handleAddDragStart,
onDrop: handleAddDragDrop,
});
return isAddDragActive;
};
export interface PlaylistRowButtonProps extends Omit<ButtonProps, 'onContextMenu' | 'onPlay'> { export interface PlaylistRowButtonProps extends Omit<ButtonProps, 'onContextMenu' | 'onPlay'> {
item: Playlist; item: Playlist;
name: string; name: string;
@@ -80,6 +120,8 @@ export const PlaylistRowButton = memo(
const isCompact = sidebarPlaylistMode === 'compact'; const isCompact = sidebarPlaylistMode === 'compact';
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const isSmartPlaylist = Boolean(item.rules);
const isAddDragActive = useContext(SidebarPlaylistAddDragContext);
const { isDraggedOver, isDragging, ref } = useDragDrop<HTMLAnchorElement>({ const { isDraggedOver, isDragging, ref } = useDragDrop<HTMLAnchorElement>({
drag: { drag: {
@@ -97,6 +139,7 @@ export const PlaylistRowButton = memo(
canDrop: (args) => { canDrop: (args) => {
// Allow dropping items into a playlist (ADD) // Allow dropping items into a playlist (ADD)
const canAdd = const canAdd =
!isSmartPlaylist &&
args.source.itemType !== undefined && args.source.itemType !== undefined &&
args.source.type !== DragTarget.PLAYLIST && args.source.type !== DragTarget.PLAYLIST &&
(args.source.operation?.includes(DragOperation.ADD) ?? false); (args.source.operation?.includes(DragOperation.ADD) ?? false);
@@ -118,6 +161,7 @@ export const PlaylistRowButton = memo(
}; };
}, },
onDrag: () => { onDrag: () => {
console.log('started drag');
return; return;
}, },
onDragLeave: () => { onDragLeave: () => {
@@ -150,6 +194,10 @@ export const PlaylistRowButton = memo(
return; return;
} }
if (isSmartPlaylist) {
return;
}
const modalProps: { const modalProps: {
albumId?: string[]; albumId?: string[];
artistId?: string[]; artistId?: string[];
@@ -222,13 +270,18 @@ export const PlaylistRowButton = memo(
type: 'table', type: 'table',
}); });
const isDimmed = isDragging || (isSmartPlaylist && isAddDragActive);
return ( return (
<Link <MotionLink
{...animationProps.fadeIn}
animate={isDimmed ? 'hidden' : 'show'}
className={clsx(styles.row, { className={clsx(styles.row, {
[styles.rowCompact]: isCompact, [styles.rowCompact]: isCompact,
[styles.rowDraggedOver]: isDraggedOver, [styles.rowDraggedOver]: isDraggedOver && !isSmartPlaylist,
[styles.rowHover]: isHovered, [styles.rowHover]: isHovered,
})} })}
initial={false}
onContextMenu={(e: MouseEvent<HTMLAnchorElement>) => { onContextMenu={(e: MouseEvent<HTMLAnchorElement>) => {
e.preventDefault(); e.preventDefault();
onContextMenu(e, item); onContextMenu(e, item);
@@ -236,10 +289,8 @@ export const PlaylistRowButton = memo(
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
ref={ref} ref={ref}
style={{
opacity: isDragging ? 0.5 : 1,
}}
to={url} to={url}
variants={playlistRowDimVariants}
> >
{isCompact ? ( {isCompact ? (
<> <>
@@ -297,7 +348,7 @@ export const PlaylistRowButton = memo(
{isHovered && <RowControls id={to} onPlay={handlePlay} />} {isHovered && <RowControls id={to} onPlay={handlePlay} />}
</> </>
)} )}
</Link> </MotionLink>
); );
}, },
); );
@@ -16,8 +16,10 @@ import { SidebarCollectionList } from '/@/renderer/features/sidebar/components/s
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon'; import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item'; import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
import { import {
SidebarPlaylistAddDragContext,
SidebarPlaylistList, SidebarPlaylistList,
SidebarSharedPlaylistList, SidebarSharedPlaylistList,
useSidebarPlaylistAddDragMonitor,
} from '/@/renderer/features/sidebar/components/sidebar-playlist-list'; } from '/@/renderer/features/sidebar/components/sidebar-playlist-list';
import { import {
useAppStore, useAppStore,
@@ -45,6 +47,17 @@ import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { ExplicitStatus, LibraryItem } from '/@/shared/types/domain-types'; import { ExplicitStatus, LibraryItem } from '/@/shared/types/domain-types';
import { Platform } from '/@/shared/types/types'; import { Platform } from '/@/shared/types/types';
const SidebarPlaylistSection = () => {
const isAddDragActive = useSidebarPlaylistAddDragMonitor();
return (
<SidebarPlaylistAddDragContext.Provider value={isAddDragActive}>
<SidebarPlaylistList />
<SidebarSharedPlaylistList />
</SidebarPlaylistAddDragContext.Provider>
);
};
export const Sidebar = () => { export const Sidebar = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -140,12 +153,7 @@ export const Sidebar = () => {
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
<SidebarCollectionList /> <SidebarCollectionList />
{sidebarPlaylistList && ( {sidebarPlaylistList && <SidebarPlaylistSection />}
<>
<SidebarPlaylistList />
<SidebarSharedPlaylistList />
</>
)}
</Accordion> </Accordion>
</ScrollArea> </ScrollArea>
<AnimatePresence initial={false} mode="popLayout"> <AnimatePresence initial={false} mode="popLayout">
+33
View File
@@ -0,0 +1,33 @@
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { useEffect } from 'react';
import { DragData } from '/@/shared/types/drag-and-drop';
interface UseDragMonitorProps {
canMonitor?: (source: DragData) => boolean;
isEnabled?: boolean;
onDragStart?: (source: DragData) => void;
onDrop?: () => void;
}
export const useDragMonitor = ({
canMonitor,
isEnabled = true,
onDragStart,
onDrop,
}: UseDragMonitorProps) => {
useEffect(() => {
if (!isEnabled) return;
return monitorForElements({
onDragStart: ({ source }) => {
const data = source.data as unknown as DragData;
if (canMonitor && !canMonitor(data)) return;
onDragStart?.(data);
},
onDrop: () => {
onDrop?.();
},
});
}, [canMonitor, isEnabled, onDragStart, onDrop]);
};