mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-10 06:12:43 +02:00
add loading state to playlist folder move
This commit is contained in:
@@ -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<void, AxiosError, SidebarPlaylistFolderMoveArgs>({
|
||||
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;
|
||||
};
|
||||
@@ -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<typeof useUpdatePlaylist>,
|
||||
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<HTMLButtonElement>({
|
||||
@@ -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<HTMLButtonElement>({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<Accordion.Item value="playlists">
|
||||
@@ -674,7 +677,8 @@ export const SidebarPlaylistList = () => {
|
||||
</Group>
|
||||
</Group>
|
||||
</PlaylistRootAccordionControl>
|
||||
<Accordion.Panel>
|
||||
<Accordion.Panel className={styles.panel}>
|
||||
<LoadingOverlay pos="absolute" visible={isFolderMovePending} />
|
||||
<PlaylistFolderDragExpandProvider expandedSet={expandedSet} setMany={setMany}>
|
||||
<PlaylistFolderViews
|
||||
{...folderViewState}
|
||||
@@ -837,6 +841,8 @@ export const SidebarSharedPlaylistList = () => {
|
||||
[navigation],
|
||||
);
|
||||
|
||||
const isFolderMovePending = useIsMutatingSidebarPlaylistFolderMove();
|
||||
|
||||
if (playlistItems?.items?.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -860,7 +866,8 @@ export const SidebarSharedPlaylistList = () => {
|
||||
</Text>
|
||||
</Group>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Accordion.Panel className={styles.panel}>
|
||||
<LoadingOverlay pos="absolute" visible={isFolderMovePending} />
|
||||
<PlaylistFolderDragExpandProvider expandedSet={expandedSet} setMany={setMany}>
|
||||
<PlaylistFolderViews
|
||||
{...folderViewState}
|
||||
|
||||
Reference in New Issue
Block a user