diff --git a/src/renderer/features/playlists/mutations/sidebar-playlist-folder-move-mutation.ts b/src/renderer/features/playlists/mutations/sidebar-playlist-folder-move-mutation.ts new file mode 100644 index 000000000..2f40c254e --- /dev/null +++ b/src/renderer/features/playlists/mutations/sidebar-playlist-folder-move-mutation.ts @@ -0,0 +1,62 @@ +import { useIsMutating, useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; + +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { Playlist } from '/@/shared/types/domain-types'; + +export const sidebarPlaylistFolderMoveMutationKey = ['sidebar-playlist-folder-move']; + +export type SidebarPlaylistFolderMoveArgs = { + serverId: string; + updates: SidebarPlaylistFolderMoveUpdate[]; +}; + +export type SidebarPlaylistFolderMoveUpdate = { + newName: string; + playlist: Playlist; +}; + +export const useSidebarPlaylistFolderMove = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ serverId, updates }) => { + for (const { newName, playlist } of updates) { + if (newName === playlist.name) continue; + + await api.controller.updatePlaylist({ + apiClientProps: { serverId }, + body: { + comment: playlist.description || '', + name: newName, + ownerId: playlist.ownerId || '', + public: playlist.public || false, + queryBuilderRules: playlist.rules ?? undefined, + sync: playlist.sync ?? undefined, + }, + query: { id: playlist.id }, + }); + } + }, + mutationKey: sidebarPlaylistFolderMoveMutationKey, + onSuccess: (_data, { serverId, updates }) => { + queryClient.invalidateQueries({ + queryKey: queryKeys.playlists.list(serverId), + }); + + for (const { playlist } of updates) { + queryClient.invalidateQueries({ + queryKey: queryKeys.playlists.detail(serverId, playlist.id), + }); + queryClient.invalidateQueries({ + queryKey: queryKeys.playlists.songList(serverId, playlist.id), + }); + } + }, + }); +}; + +export const useIsMutatingSidebarPlaylistFolderMove = () => { + return useIsMutating({ mutationKey: sidebarPlaylistFolderMoveMutationKey }) > 0; +}; diff --git a/src/renderer/features/sidebar/components/playlist-folder-tree.tsx b/src/renderer/features/sidebar/components/playlist-folder-tree.tsx index 8c3cb9cee..8837641a6 100644 --- a/src/renderer/features/sidebar/components/playlist-folder-tree.tsx +++ b/src/renderer/features/sidebar/components/playlist-folder-tree.tsx @@ -17,7 +17,10 @@ import { useTranslation } from 'react-i18next'; import styles from './playlist-folder-tree.module.css'; -import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation'; +import { + SidebarPlaylistFolderMoveUpdate, + useSidebarPlaylistFolderMove, +} from '/@/renderer/features/playlists/mutations/sidebar-playlist-folder-move-mutation'; import { PlaylistRowButton } from '/@/renderer/features/sidebar/components/sidebar-playlist-list'; import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { @@ -209,76 +212,56 @@ export const remapPlaylistFolderToRoot = ( return `${folderName}${separator}${remainder}`; }; -const updatePlaylistName = async ( - updateMutation: ReturnType, - serverId: string, - playlist: Playlist, - newName: string, -) => { - if (newName === playlist.name) return; - - await updateMutation.mutateAsync({ - apiClientProps: { serverId }, - body: { - comment: playlist.description || '', - name: newName, - ownerId: playlist.ownerId || '', - public: playlist.public || false, - queryBuilderRules: playlist.rules ?? undefined, - sync: playlist.sync ?? undefined, - }, - query: { id: playlist.id }, - }); -}; - export const usePlaylistRootDrop = (allPlaylists: Playlist[]) => { const { t } = useTranslation(); const serverId = useCurrentServerId(); const separator = useSidebarPlaylistFolderSeparator(); - const updateMutation = useUpdatePlaylist({}); + const folderMoveMutation = useSidebarPlaylistFolderMove(); const handleDrop = useCallback( async (source: DragData) => { if (!serverId) return; - try { - if (source.type === DragTarget.SIDEBAR_PLAYLIST_FOLDER) { - const sourceFolderPath = - source.id[0] ?? - (source.metadata as undefined | { folderName?: string })?.folderName; - if (!sourceFolderPath) return; + const updates: SidebarPlaylistFolderMoveUpdate[] = []; - const affected = getPlaylistsInFolderTree( - allPlaylists, + if (source.type === DragTarget.SIDEBAR_PLAYLIST_FOLDER) { + const sourceFolderPath = + source.id[0] ?? + (source.metadata as undefined | { folderName?: string })?.folderName; + if (!sourceFolderPath) return; + + const affected = getPlaylistsInFolderTree( + allPlaylists, + sourceFolderPath, + separator, + ); + + for (const playlist of affected) { + const newName = remapPlaylistFolderToRoot( + playlist.name, sourceFolderPath, separator, ); + if (!newName) continue; - for (const playlist of affected) { - const newName = remapPlaylistFolderToRoot( - playlist.name, - sourceFolderPath, - separator, - ); - if (!newName) continue; - - await updatePlaylistName(updateMutation, serverId, playlist, newName); - } - - return; + updates.push({ newName, playlist }); } - + } else { const playlists = source.item as Playlist[] | undefined; if (!Array.isArray(playlists) || playlists.length === 0) return; for (const playlist of playlists) { - await updatePlaylistName( - updateMutation, - serverId, + updates.push({ + newName: remapPlaylistToRoot(playlist.name, separator), playlist, - remapPlaylistToRoot(playlist.name, separator), - ); + }); } + } + + if (updates.length === 0) return; + + try { + await folderMoveMutation.mutateAsync({ serverId, updates }); } catch (err: unknown) { toast.error({ message: err instanceof Error ? err.message : undefined, @@ -286,7 +269,7 @@ export const usePlaylistRootDrop = (allPlaylists: Playlist[]) => { }); } }, - [allPlaylists, separator, serverId, t, updateMutation], + [allPlaylists, folderMoveMutation, separator, serverId, t], ); const { isDraggedOver, ref } = useDragDrop({ @@ -518,59 +501,44 @@ const usePlaylistFolderDrop = (folderPath: string, allPlaylists: Playlist[]) => const { t } = useTranslation(); const serverId = useCurrentServerId(); const separator = useSidebarPlaylistFolderSeparator(); - const updateMutation = useUpdatePlaylist({}); + const folderMoveMutation = useSidebarPlaylistFolderMove(); const dragExpand = useContext(PlaylistFolderDragExpandContext); const handleDrop = useCallback( async (source: DragData) => { if (!serverId) return; - try { - if (source.type === DragTarget.SIDEBAR_PLAYLIST_FOLDER) { - // Folder drop: rename every playlist under the dragged folder tree. - const sourceFolderPath = - source.id[0] ?? - (source.metadata as undefined | { folderName?: string })?.folderName; - if ( - !sourceFolderPath || - !isValidFolderNest(sourceFolderPath, folderPath, separator) - ) { - return; - } - - const affected = getPlaylistsInFolderTree( - allPlaylists, - sourceFolderPath, - separator, - ); - - for (const playlist of affected) { - const newName = remapPlaylistFolderPath( - playlist.name, - sourceFolderPath, - folderPath, - separator, - ); - if (!newName || newName === playlist.name) continue; - - await updateMutation.mutateAsync({ - apiClientProps: { serverId }, - body: { - comment: playlist.description || '', - name: newName, - ownerId: playlist.ownerId || '', - public: playlist.public || false, - queryBuilderRules: playlist.rules ?? undefined, - sync: playlist.sync ?? undefined, - }, - query: { id: playlist.id }, - }); - } + const updates: SidebarPlaylistFolderMoveUpdate[] = []; + if (source.type === DragTarget.SIDEBAR_PLAYLIST_FOLDER) { + const sourceFolderPath = + source.id[0] ?? + (source.metadata as undefined | { folderName?: string })?.folderName; + if ( + !sourceFolderPath || + !isValidFolderNest(sourceFolderPath, folderPath, separator) + ) { return; } - // Playlist drop: move a single playlist into this folder using its leaf name only. + const affected = getPlaylistsInFolderTree( + allPlaylists, + sourceFolderPath, + separator, + ); + + for (const playlist of affected) { + const newName = remapPlaylistFolderPath( + playlist.name, + sourceFolderPath, + folderPath, + separator, + ); + if (!newName || newName === playlist.name) continue; + + updates.push({ newName, playlist }); + } + } else { const playlists = source.item as Playlist[] | undefined; if (!Array.isArray(playlists) || playlists.length === 0) return; @@ -579,19 +547,14 @@ const usePlaylistFolderDrop = (folderPath: string, allPlaylists: Playlist[]) => const newName = buildPlaylistNameInFolder(folderPath, leafName, separator); if (newName === playlist.name) continue; - await updateMutation.mutateAsync({ - apiClientProps: { serverId }, - body: { - comment: playlist.description || '', - name: newName, - ownerId: playlist.ownerId || '', - public: playlist.public || false, - queryBuilderRules: playlist.rules ?? undefined, - sync: playlist.sync ?? undefined, - }, - query: { id: playlist.id }, - }); + updates.push({ newName, playlist }); } + } + + if (updates.length === 0) return; + + try { + await folderMoveMutation.mutateAsync({ serverId, updates }); } catch (err: unknown) { toast.error({ message: err instanceof Error ? err.message : undefined, @@ -599,7 +562,7 @@ const usePlaylistFolderDrop = (folderPath: string, allPlaylists: Playlist[]) => }); } }, - [allPlaylists, folderPath, separator, serverId, t, updateMutation], + [allPlaylists, folderMoveMutation, folderPath, separator, serverId, t], ); const { isDraggedOver, isDragging, ref } = useDragDrop({ diff --git a/src/renderer/features/sidebar/components/sidebar-playlist-list.module.css b/src/renderer/features/sidebar/components/sidebar-playlist-list.module.css index 959674a45..d8d8bbed5 100644 --- a/src/renderer/features/sidebar/components/sidebar-playlist-list.module.css +++ b/src/renderer/features/sidebar/components/sidebar-playlist-list.module.css @@ -4,6 +4,10 @@ padding: var(--theme-spacing-sm) var(--theme-spacing-md); } +.panel { + position: relative; +} + .row { position: relative; display: flex; @@ -29,13 +33,12 @@ .compact-name { flex: 1; min-width: 0; - padding: 0; + padding: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .controls { position: absolute; top: 50%; @@ -45,7 +48,6 @@ transform: translateY(-50%); } - .controls-compact { position: absolute; top: 50%; @@ -56,13 +58,12 @@ transform: translateY(-50%); } - .row-dragged-over::after { position: absolute; inset: 0; z-index: 1; pointer-events: none; - content: ""; + content: ''; border: 2px solid var(--theme-colors-primary); border-radius: var(--theme-radius-md); opacity: 0.8; diff --git a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx index 2e229ddce..5a1294275 100644 --- a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx +++ b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx @@ -13,6 +13,7 @@ import { ContextMenuController } from '/@/renderer/features/context-menu/context import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api'; import { openCreatePlaylistModal } from '/@/renderer/features/playlists/components/create-playlist-form'; +import { useIsMutatingSidebarPlaylistFolderMove } from '/@/renderer/features/playlists/mutations/sidebar-playlist-folder-move-mutation'; import { LONG_PRESS_PLAY_BEHAVIOR, PlayTooltip, @@ -47,6 +48,7 @@ import { ButtonProps } from '/@/shared/components/button/button'; import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { Image } from '/@/shared/components/image/image'; +import { LoadingOverlay } from '/@/shared/components/loading-overlay/loading-overlay'; import { Text } from '/@/shared/components/text/text'; import { useLocalStorage } from '/@/shared/hooks/use-local-storage'; import { @@ -603,6 +605,7 @@ export const SidebarPlaylistList = () => { ); const showExpandAll = folderView !== 'navigation' && folderPaths.length > 0; + const isFolderMovePending = useIsMutatingSidebarPlaylistFolderMove(); return ( @@ -674,7 +677,8 @@ export const SidebarPlaylistList = () => { - + + { [navigation], ); + const isFolderMovePending = useIsMutatingSidebarPlaylistFolderMove(); + if (playlistItems?.items?.length === 0) { return null; } @@ -860,7 +866,8 @@ export const SidebarSharedPlaylistList = () => { - + +