From 3e50ebcc658ac70d37436b9f08082c042a024bfd Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:01:44 +0300 Subject: [PATCH] Drag & Drop into/out of Spacedrive (#2849) * Better drag & drop into the os * Update drag.rs * Drag & Drop into Spacedrive from OS * Re-enable Supertokes & change Drag-rs pointer * Autoformat --- Cargo.lock | Bin 342350 -> 342819 bytes apps/desktop/src-tauri/Cargo.toml | 8 ++-- apps/desktop/src-tauri/src/drag.rs | 12 +++-- apps/desktop/src/App.tsx | 14 +++++- interface/app/$libraryId/Explorer/index.tsx | 17 ++++--- .../$libraryId/Explorer/useExplorerDnd.tsx | 17 +------ interface/app/$libraryId/Explorer/util.ts | 17 ++++++- interface/app/$libraryId/Layout/index.tsx | 4 ++ interface/hooks/index.ts | 1 + interface/hooks/useFileDropEventHandler.ts | 44 ++++++++++++++++++ interface/package.json | 4 +- interface/util/events.ts | 11 +++++ interface/util/index.tsx | 2 +- packages/ui/package.json | 2 +- pnpm-lock.yaml | Bin 1161975 -> 1162064 bytes 15 files changed, 117 insertions(+), 36 deletions(-) create mode 100644 interface/hooks/useFileDropEventHandler.ts diff --git a/Cargo.lock b/Cargo.lock index 74acb0b8230923f312656942cd18b0fb82d8917e..cd5c303657310f0778a515c9960903929ce2b090 100644 GIT binary patch delta 224 zcmX^2No4UikqzgcPWFGIIQjo2RqK?Z#B|-FV*8@hGFwAa^CY9>6ia0o!!pgxBFW4+ z#lpxe#lXPAFeTO8G|?i(%p}Re*u-%9M=4hE$sAA9r>~A=65ibNRH|nBgB6Uj)9)rR z@=d?O&X~B}D2ef_4M~0T-7LU;zZT9Ag1sJ(DyP9hW?x0S>pP wpaDx { + document.dispatchEvent(new FileDropEvent((data.payload as { paths: string[] }).paths)); + }); return () => { keybindListener.then((unlisten) => unlisten()); deeplinkListener.then((unlisten) => unlisten()); + fileDropListener.then((unlisten) => unlisten()); }; }, []); @@ -379,7 +388,8 @@ function AppInner() { new Promise((res) => { startTransition(() => { setTabs((tabs) => { - const { pathname, search } = selectedTab.router.state.location; + const { pathname, search } = + selectedTab.router.state.location; const newTab = createTab({ pathname, search }); const newTabs = [...tabs, newTab]; diff --git a/interface/app/$libraryId/Explorer/index.tsx b/interface/app/$libraryId/Explorer/index.tsx index 2bddff35d..1f74f0bd1 100644 --- a/interface/app/$libraryId/Explorer/index.tsx +++ b/interface/app/$libraryId/Explorer/index.tsx @@ -1,5 +1,5 @@ import { FolderNotchOpen } from '@phosphor-icons/react'; -import { CSSProperties, type PropsWithChildren, type ReactNode } from 'react'; +import { CSSProperties, useEffect, type PropsWithChildren, type ReactNode } from 'react'; import { explorerLayout, useExplorerLayoutStore, @@ -87,8 +87,6 @@ export default function Explorer(props: PropsWithChildren) { explorer.settingsStore.showHiddenFiles = !explorer.settingsStore.showHiddenFiles; }); - window.useDragAndDrop(); - useKeyRevealFinder(); useExplorerDnd(); @@ -118,13 +116,18 @@ export default function Explorer(props: PropsWithChildren) { contextMenu={props.contextMenu ? props.contextMenu() : } emptyNotice={ props.emptyNotice ?? ( - + ) } listViewOptions={{ hideHeaderBorder: true }} scrollPadding={{ top: topBar.topBarHeight, - bottom: showPathBar ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) : undefined + bottom: showPathBar + ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) + : undefined }} /> @@ -144,7 +147,9 @@ export default function Explorer(props: PropsWithChildren) { )} style={{ paddingTop: topBar.topBarHeight + 12, - bottom: showPathBar ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) : 0 + bottom: showPathBar + ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) + : 0 }} /> )} diff --git a/interface/app/$libraryId/Explorer/useExplorerDnd.tsx b/interface/app/$libraryId/Explorer/useExplorerDnd.tsx index 963d0bd0e..d563e6f2a 100644 --- a/interface/app/$libraryId/Explorer/useExplorerDnd.tsx +++ b/interface/app/$libraryId/Explorer/useExplorerDnd.tsx @@ -12,7 +12,7 @@ import { useAssignItemsToTag } from '../settings/library/tags/CreateDialog'; import { useExplorerContext } from './Context'; import { explorerStore } from './store'; import { explorerDroppableSchema } from './useExplorerDroppable'; -import { useExplorerSearchParams } from './util'; +import { getPathIdsPerLocation, useExplorerSearchParams } from './util'; export const getPaths = async (items: ExplorerItem[]) => { const paths = items.map(async (item) => { @@ -27,21 +27,6 @@ export const getPaths = async (items: ExplorerItem[]) => { 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 - ); -}; - export const useExplorerDnd = () => { const explorer = useExplorerContext(); diff --git a/interface/app/$libraryId/Explorer/util.ts b/interface/app/$libraryId/Explorer/util.ts index 7bcd80aa6..3e23100b9 100644 --- a/interface/app/$libraryId/Explorer/util.ts +++ b/interface/app/$libraryId/Explorer/util.ts @@ -1,5 +1,5 @@ import dayjs from 'dayjs'; -import { type ExplorerItem } from '@sd/client'; +import { getIndexedItemFilePath, type ExplorerItem } from '@sd/client'; import i18n from '~/app/I18n'; import { ExplorerParamsSchema } from '~/app/route-schemas'; import { useZodSearchParams } from '~/hooks'; @@ -200,3 +200,18 @@ export function fetchAccessToken(): string { .split(';')[0] || ''; return accessToken; } + +export 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 + ); +}; diff --git a/interface/app/$libraryId/Layout/index.tsx b/interface/app/$libraryId/Layout/index.tsx index 12044df66..a3f644792 100644 --- a/interface/app/$libraryId/Layout/index.tsx +++ b/interface/app/$libraryId/Layout/index.tsx @@ -16,6 +16,7 @@ import { LibraryIdParamsSchema } from '~/app/route-schemas'; import ErrorFallback, { BetterErrorBoundary } from '~/ErrorFallback'; import { useDeeplinkEventHandler, + useFileDropEventHandler, useKeybindEventHandler, useOperatingSystem, useRedirectToNewLocation, @@ -42,6 +43,9 @@ const Layout = () => { useKeybindEventHandler(library?.uuid); useDeeplinkEventHandler(); + useFileDropEventHandler(library?.uuid); + + window.useDragAndDrop(); const layoutRef = useRef(null); diff --git a/interface/hooks/index.ts b/interface/hooks/index.ts index fa9720835..600f39597 100644 --- a/interface/hooks/index.ts +++ b/interface/hooks/index.ts @@ -10,6 +10,7 @@ export * from './useIsDark'; export * from './useKeyDeleteFile'; export * from './useKeybind'; export * from './useKeybindEventHandler'; +export * from './useFileDropEventHandler'; export * from './useOperatingSystem'; export * from './useScrolled'; // export * from './useSearchStore'; diff --git a/interface/hooks/useFileDropEventHandler.ts b/interface/hooks/useFileDropEventHandler.ts new file mode 100644 index 000000000..f8a29e460 --- /dev/null +++ b/interface/hooks/useFileDropEventHandler.ts @@ -0,0 +1,44 @@ +import { useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router'; +import { libraryClient } from '@sd/client'; +import { getPathIdsPerLocation, useExplorerSearchParams } from '~/app/$libraryId/Explorer/util'; +import { isNonEmptyObject } from '~/util'; +import { FileDropEvent } from '~/util/events'; + +import { useQuickRescan } from './useQuickRescan'; + +export const useFileDropEventHandler = (libraryId?: string) => { + const navigate = useNavigate(); + const rescan = useQuickRescan(); + const regex = new RegExp( + '/[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/location/' + ); + const id = parseInt(useLocation().pathname.replace(regex, '')); + const [{ path }] = useExplorerSearchParams(); + + useEffect(() => { + const handler = async (e: FileDropEvent) => { + e.preventDefault(); + const paths = e.detail.paths; + + if (libraryId && path) { + libraryClient.mutation([ + 'ephemeralFiles.cutFiles', + { sources: paths, target_dir: path! } + ]); + } else if (libraryId) { + // Get Materialized Path using the location id + const locationId = id; + const location = await libraryClient.query(['locations.get', locationId]); + const locationPath = location!.path; + libraryClient.mutation([ + 'ephemeralFiles.cutFiles', + { sources: paths, target_dir: locationPath! } + ]); + } + }; + + document.addEventListener('filedrop', handler); + return () => document.removeEventListener('filedrop', handler); + }, [navigate, libraryId, rescan, id, path]); +}; diff --git a/interface/package.json b/interface/package.json index 13749384a..b8011684d 100644 --- a/interface/package.json +++ b/interface/package.json @@ -13,7 +13,7 @@ "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^1.7.17", "@icons-pack/react-simple-icons": "^9.1.0", - "@phosphor-icons/react": "^2.0.13", + "@phosphor-icons/react": "^2.1.0", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-progress": "^1.0.1", @@ -87,4 +87,4 @@ "vite": "^5.4.9", "vite-plugin-svgr": "^3.3.0" } -} \ No newline at end of file +} diff --git a/interface/util/events.ts b/interface/util/events.ts index 4b1e1a77c..691ac9567 100644 --- a/interface/util/events.ts +++ b/interface/util/events.ts @@ -2,6 +2,7 @@ declare global { interface GlobalEventHandlersEventMap { keybindexec: KeybindEvent; deeplink: DeeplinkEvent; + filedrop: FileDropEvent; } } @@ -24,3 +25,13 @@ export class DeeplinkEvent extends CustomEvent<{ url: string }> { }); } } + +export class FileDropEvent extends CustomEvent<{ paths: string[] }> { + constructor(paths: string[]) { + super('filedrop', { + detail: { + paths + } + }); + } +} diff --git a/interface/util/index.tsx b/interface/util/index.tsx index cf1391d35..4bb12e4b9 100644 --- a/interface/util/index.tsx +++ b/interface/util/index.tsx @@ -1,5 +1,5 @@ import cryptoRandomString from 'crypto-random-string'; -import { nonLibraryClient } from '@sd/client'; +import { ExplorerItem, getIndexedItemFilePath, nonLibraryClient } from '@sd/client'; // NOTE: `crypto` module is not available in RN so this can't be in client export const generatePassword = (length: number) => diff --git a/packages/ui/package.json b/packages/ui/package.json index ec7609f55..d6098abd9 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -21,7 +21,7 @@ "dependencies": { "@fontsource/ibm-plex-sans": "^5.1.0", "@headlessui/react": "^1.7.17", - "@phosphor-icons/react": "^2.0.13", + "@phosphor-icons/react": "^2.1.0", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5413162a3a3d7a40b291fae102c5127efe338539..8dad00fb613efccf95ba162b13bf1f8850a8d64b 100644 GIT binary patch delta 554 zcmezV)$PJJw+*rqldo$?PX1T@Vsd~v>tqEPw#{0Sdy^*L)DYkNulkD~RMvgE-A8VY z=9ty(F{>H3$E;>z=W0*i#stL7K+FQftlQJKvBl=|rk17VmFN}}Y#VeDmU z5mcUJY^fjaZfsc@5|t7ZTv23Pp>0%VWK>e-X%OaG=4fkMJKbg_zf${MRdyie*nU@) za~kJ#*FzivYypYM*@@}XCmiGub<|UE&d)1JOfFFfE-fg?FDg+;EK$fPDJdwn($_C9 zFW1W}$jvI&OU}>LFNP{e%P&&M%`Zw-$jnR2*PC9X&pCw_=CT@KG1r-5_i}Mf&vq7+ zobF&R$lkuofD?$hw(m0F=GfLA|C}3$d4QOAd;D|0X9q=5qD#+A&&W!ly0#q{u0SlX K-DaiWZb1O=U(G21 delta 315 zcmccc&F%YFw+*rqldG0XO#WT{YP!Hb9@fe7GHjbQCHE#xW||I9sH9x%*Wa z6qg!gT2_SRrj{l}1p8H#CI=)ZIs3SzSe67bTbSpTmgeXem!%nmM!2Q; z7=`7SB_*Z$XM`B~mgi3o^x{;VF4w^!Kiyx4lViHpHYVlCM^_2A$8Kc;VrC#_0b^^4mjI*@2j2d#Ebsdd}&E`ka%e7aDL*ZZ9