mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-18 21:36:56 -04:00
210 lines
5.5 KiB
TypeScript
210 lines
5.5 KiB
TypeScript
import { useDndMonitor } from '@dnd-kit/core';
|
|
import {
|
|
ExplorerItem,
|
|
getIndexedItemFilePath,
|
|
getItemFilePath,
|
|
libraryClient,
|
|
useLibraryMutation
|
|
} from '@sd/client';
|
|
import { isNonEmptyObject } from '~/util';
|
|
|
|
import { useAssignItemsToTag } from '../settings/library/tags/CreateDialog';
|
|
import { useExplorerContext } from './Context';
|
|
import { explorerStore } from './store';
|
|
import { explorerDroppableSchema } from './useExplorerDroppable';
|
|
import { useExplorerSearchParams } from './util';
|
|
|
|
export const getPaths = async (items: ExplorerItem[]) => {
|
|
const paths = items.map(async (item) => {
|
|
const filePath = getItemFilePath(item);
|
|
if (!filePath) return;
|
|
|
|
return 'path' in filePath
|
|
? filePath.path
|
|
: await libraryClient.query(['files.getPath', filePath.id]);
|
|
});
|
|
|
|
return (await Promise.all(paths)).filter((path): path is string => Boolean(path));
|
|
};
|
|
|
|
const getPathIdsPerLocation = (items: ExplorerItem[]) => {
|
|
return items.reduce(
|
|
(items, item) => {
|
|
const path = getIndexedItemFilePath(item);
|
|
if (!path || path.location_id === null) return items;
|
|
|
|
return {
|
|
...items,
|
|
[path.location_id]: [...(items[path.location_id] ?? []), path.id]
|
|
};
|
|
},
|
|
{} as Record<number, number[]>
|
|
);
|
|
};
|
|
|
|
export const useExplorerDnd = () => {
|
|
const explorer = useExplorerContext();
|
|
|
|
const [{ path }] = useExplorerSearchParams();
|
|
|
|
const cutFiles = useLibraryMutation('files.cutFiles');
|
|
const cutEphemeralFiles = useLibraryMutation('ephemeralFiles.cutFiles');
|
|
const assignItemsToTag = useAssignItemsToTag();
|
|
|
|
useDndMonitor({
|
|
onDragStart: () => {
|
|
if (explorer.selectedItems.size === 0) return;
|
|
explorerStore.drag = {
|
|
type: 'dragging',
|
|
items: [...explorer.selectedItems],
|
|
sourcePath: path ?? '/',
|
|
sourceLocationId:
|
|
explorer.parent?.type === 'Location' ? explorer.parent.location.id : undefined,
|
|
sourceTagId: explorer.parent?.type === 'Tag' ? explorer.parent.tag.id : undefined
|
|
};
|
|
},
|
|
onDragEnd: async ({ over }) => {
|
|
const drag = explorerStore.drag;
|
|
explorerStore.drag = null;
|
|
|
|
if (!over || !drag || drag.type === 'touched') return;
|
|
|
|
const drop = explorerDroppableSchema.parse(over.data.current);
|
|
|
|
switch (drop.type) {
|
|
case 'location': {
|
|
if (!drop.data) {
|
|
cutEphemeralFiles.mutate({
|
|
sources: await getPaths(drag.items),
|
|
target_dir: drop.path
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
const paths = getPathIdsPerLocation(drag.items);
|
|
if (isNonEmptyObject(paths)) {
|
|
const locationId = drop.data.id;
|
|
|
|
Object.entries(paths).map(([sourceLocationId, paths]) => {
|
|
cutFiles.mutate({
|
|
source_location_id: Number(sourceLocationId),
|
|
sources_file_path_ids: paths,
|
|
target_location_id: locationId,
|
|
target_location_relative_directory_path: drop.path
|
|
});
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
cutEphemeralFiles.mutate({
|
|
sources: await getPaths(drag.items),
|
|
target_dir: drop.data.path + drop.path
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
case 'explorer-item': {
|
|
switch (drop.data.type) {
|
|
case 'Path':
|
|
case 'Object': {
|
|
const { item } = drop.data;
|
|
|
|
const filePath = 'file_paths' in item ? item.file_paths[0] : item;
|
|
if (!filePath) return;
|
|
|
|
const paths = getPathIdsPerLocation(drag.items);
|
|
if (isNonEmptyObject(paths)) {
|
|
const locationId = filePath.location_id;
|
|
const path = filePath.materialized_path + filePath.name + '/';
|
|
|
|
Object.entries(paths).map(([sourceLocationId, paths]) => {
|
|
cutFiles.mutate({
|
|
source_location_id: Number(sourceLocationId),
|
|
sources_file_path_ids: paths,
|
|
target_location_id: locationId,
|
|
target_location_relative_directory_path: path
|
|
});
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
const path = await libraryClient.query(['files.getPath', filePath.id]);
|
|
if (!path) return;
|
|
|
|
cutEphemeralFiles.mutate({
|
|
sources: await getPaths(drag.items),
|
|
target_dir: path
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
case 'Location':
|
|
case 'NonIndexedPath': {
|
|
cutEphemeralFiles.mutate({
|
|
sources: await getPaths(drag.items),
|
|
target_dir: drop.data.item.path
|
|
});
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'tag': {
|
|
const items = drag.items.flatMap((item) => {
|
|
if (item.type !== 'Object' && item.type !== 'Path') return [];
|
|
return [item];
|
|
});
|
|
await assignItemsToTag(drop.data.id, items);
|
|
}
|
|
}
|
|
},
|
|
onDragCancel: () => (explorerStore.drag = null)
|
|
});
|
|
};
|
|
|
|
// interface DndNoticeProps extends UseDialogProps {
|
|
// count: number;
|
|
// path: string;
|
|
// onConfirm: (val: { dismissNotice: boolean }) => void;
|
|
// }
|
|
|
|
// const DndNotice = (props: DndNoticeProps) => {
|
|
// const form = useZodForm();
|
|
// const [dismissNotice, setDismissNotice] = useState(false);
|
|
|
|
// const { t } = useLocale();
|
|
|
|
// return (
|
|
// <Dialog
|
|
// form={form}
|
|
// onSubmit={form.handleSubmit(() => props.onConfirm({ dismissNotice: dismissNotice }))}
|
|
// dialog={useDialog(props)}
|
|
// title={t('move_files')}
|
|
// icon={<Icon name="MoveLocation" size={28} />}
|
|
// description={
|
|
// <span className="break-all">
|
|
// Are you sure you want to move {props.count} file{props.count > 1 ? 's' : ''} to{' '}
|
|
// {props.path}?
|
|
// </span>
|
|
// }
|
|
// ctaDanger
|
|
// ctaLabel={t('continue')}
|
|
// closeLabel={t('cancel')}
|
|
// buttonsSideContent={
|
|
// <RadixCheckbox
|
|
// label={t('dont_show_again')}
|
|
// name="ephemeral-alert-notice"
|
|
// checked={dismissNotice}
|
|
// onCheckedChange={(val) => typeof val === 'boolean' && setDismissNotice(val)}
|
|
// />
|
|
// }
|
|
// />
|
|
// );
|
|
// };
|