mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-12 07:12:58 +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 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 { PlaylistRowButton } from '/@/renderer/features/sidebar/components/sidebar-playlist-list';
|
||||||
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
||||||
import {
|
import {
|
||||||
@@ -209,76 +212,56 @@ export const remapPlaylistFolderToRoot = (
|
|||||||
return `${folderName}${separator}${remainder}`;
|
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[]) => {
|
export const usePlaylistRootDrop = (allPlaylists: Playlist[]) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
const separator = useSidebarPlaylistFolderSeparator();
|
const separator = useSidebarPlaylistFolderSeparator();
|
||||||
const updateMutation = useUpdatePlaylist({});
|
const folderMoveMutation = useSidebarPlaylistFolderMove();
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
async (source: DragData) => {
|
async (source: DragData) => {
|
||||||
if (!serverId) return;
|
if (!serverId) return;
|
||||||
|
|
||||||
try {
|
const updates: SidebarPlaylistFolderMoveUpdate[] = [];
|
||||||
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(
|
if (source.type === DragTarget.SIDEBAR_PLAYLIST_FOLDER) {
|
||||||
allPlaylists,
|
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,
|
sourceFolderPath,
|
||||||
separator,
|
separator,
|
||||||
);
|
);
|
||||||
|
if (!newName) continue;
|
||||||
|
|
||||||
for (const playlist of affected) {
|
updates.push({ newName, playlist });
|
||||||
const newName = remapPlaylistFolderToRoot(
|
|
||||||
playlist.name,
|
|
||||||
sourceFolderPath,
|
|
||||||
separator,
|
|
||||||
);
|
|
||||||
if (!newName) continue;
|
|
||||||
|
|
||||||
await updatePlaylistName(updateMutation, serverId, playlist, newName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
const playlists = source.item as Playlist[] | undefined;
|
const playlists = source.item as Playlist[] | undefined;
|
||||||
if (!Array.isArray(playlists) || playlists.length === 0) return;
|
if (!Array.isArray(playlists) || playlists.length === 0) return;
|
||||||
|
|
||||||
for (const playlist of playlists) {
|
for (const playlist of playlists) {
|
||||||
await updatePlaylistName(
|
updates.push({
|
||||||
updateMutation,
|
newName: remapPlaylistToRoot(playlist.name, separator),
|
||||||
serverId,
|
|
||||||
playlist,
|
playlist,
|
||||||
remapPlaylistToRoot(playlist.name, separator),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await folderMoveMutation.mutateAsync({ serverId, updates });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
toast.error({
|
toast.error({
|
||||||
message: err instanceof Error ? err.message : undefined,
|
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>({
|
const { isDraggedOver, ref } = useDragDrop<HTMLButtonElement>({
|
||||||
@@ -518,59 +501,44 @@ const usePlaylistFolderDrop = (folderPath: string, allPlaylists: Playlist[]) =>
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
const separator = useSidebarPlaylistFolderSeparator();
|
const separator = useSidebarPlaylistFolderSeparator();
|
||||||
const updateMutation = useUpdatePlaylist({});
|
const folderMoveMutation = useSidebarPlaylistFolderMove();
|
||||||
const dragExpand = useContext(PlaylistFolderDragExpandContext);
|
const dragExpand = useContext(PlaylistFolderDragExpandContext);
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
async (source: DragData) => {
|
async (source: DragData) => {
|
||||||
if (!serverId) return;
|
if (!serverId) return;
|
||||||
|
|
||||||
try {
|
const updates: SidebarPlaylistFolderMoveUpdate[] = [];
|
||||||
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 },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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;
|
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;
|
const playlists = source.item as Playlist[] | undefined;
|
||||||
if (!Array.isArray(playlists) || playlists.length === 0) return;
|
if (!Array.isArray(playlists) || playlists.length === 0) return;
|
||||||
|
|
||||||
@@ -579,19 +547,14 @@ const usePlaylistFolderDrop = (folderPath: string, allPlaylists: Playlist[]) =>
|
|||||||
const newName = buildPlaylistNameInFolder(folderPath, leafName, separator);
|
const newName = buildPlaylistNameInFolder(folderPath, leafName, separator);
|
||||||
if (newName === playlist.name) continue;
|
if (newName === playlist.name) continue;
|
||||||
|
|
||||||
await updateMutation.mutateAsync({
|
updates.push({ newName, playlist });
|
||||||
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 },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await folderMoveMutation.mutateAsync({ serverId, updates });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
toast.error({
|
toast.error({
|
||||||
message: err instanceof Error ? err.message : undefined,
|
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>({
|
const { isDraggedOver, isDragging, ref } = useDragDrop<HTMLButtonElement>({
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -29,13 +33,12 @@
|
|||||||
.compact-name {
|
.compact-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -45,7 +48,6 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.controls-compact {
|
.controls-compact {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -56,13 +58,12 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.row-dragged-over::after {
|
.row-dragged-over::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
content: "";
|
content: '';
|
||||||
border: 2px solid var(--theme-colors-primary);
|
border: 2px solid var(--theme-colors-primary);
|
||||||
border-radius: var(--theme-radius-md);
|
border-radius: var(--theme-radius-md);
|
||||||
opacity: 0.8;
|
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 { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||||
import { openCreatePlaylistModal } from '/@/renderer/features/playlists/components/create-playlist-form';
|
import { openCreatePlaylistModal } from '/@/renderer/features/playlists/components/create-playlist-form';
|
||||||
|
import { useIsMutatingSidebarPlaylistFolderMove } from '/@/renderer/features/playlists/mutations/sidebar-playlist-folder-move-mutation';
|
||||||
import {
|
import {
|
||||||
LONG_PRESS_PLAY_BEHAVIOR,
|
LONG_PRESS_PLAY_BEHAVIOR,
|
||||||
PlayTooltip,
|
PlayTooltip,
|
||||||
@@ -47,6 +48,7 @@ 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';
|
||||||
import { Image } from '/@/shared/components/image/image';
|
import { Image } from '/@/shared/components/image/image';
|
||||||
|
import { LoadingOverlay } from '/@/shared/components/loading-overlay/loading-overlay';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||||
import {
|
import {
|
||||||
@@ -603,6 +605,7 @@ export const SidebarPlaylistList = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const showExpandAll = folderView !== 'navigation' && folderPaths.length > 0;
|
const showExpandAll = folderView !== 'navigation' && folderPaths.length > 0;
|
||||||
|
const isFolderMovePending = useIsMutatingSidebarPlaylistFolderMove();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion.Item value="playlists">
|
<Accordion.Item value="playlists">
|
||||||
@@ -674,7 +677,8 @@ export const SidebarPlaylistList = () => {
|
|||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</PlaylistRootAccordionControl>
|
</PlaylistRootAccordionControl>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel className={styles.panel}>
|
||||||
|
<LoadingOverlay pos="absolute" visible={isFolderMovePending} />
|
||||||
<PlaylistFolderDragExpandProvider expandedSet={expandedSet} setMany={setMany}>
|
<PlaylistFolderDragExpandProvider expandedSet={expandedSet} setMany={setMany}>
|
||||||
<PlaylistFolderViews
|
<PlaylistFolderViews
|
||||||
{...folderViewState}
|
{...folderViewState}
|
||||||
@@ -837,6 +841,8 @@ export const SidebarSharedPlaylistList = () => {
|
|||||||
[navigation],
|
[navigation],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFolderMovePending = useIsMutatingSidebarPlaylistFolderMove();
|
||||||
|
|
||||||
if (playlistItems?.items?.length === 0) {
|
if (playlistItems?.items?.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -860,7 +866,8 @@ export const SidebarSharedPlaylistList = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel className={styles.panel}>
|
||||||
|
<LoadingOverlay pos="absolute" visible={isFolderMovePending} />
|
||||||
<PlaylistFolderDragExpandProvider expandedSet={expandedSet} setMany={setMany}>
|
<PlaylistFolderDragExpandProvider expandedSet={expandedSet} setMany={setMany}>
|
||||||
<PlaylistFolderViews
|
<PlaylistFolderViews
|
||||||
{...folderViewState}
|
{...folderViewState}
|
||||||
|
|||||||
Reference in New Issue
Block a user