From afeeb32ea9df3c48d39f3e08b5a874615afe47ba Mon Sep 17 00:00:00 2001 From: nikec <43032218+niikeec@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:59:27 +0100 Subject: [PATCH] [ENG-1353] explorer dnd (#1737) * locations dnd * fix icon * reduce navigate timeout * fix types * another * fix drag overlay count * Update pnpm-lock.yaml * merge * ephemeral support and other improvements * merge * Tag dnd * merge * type * merge * remove offset * update dnd logic to not depend on drag source * handle allowed types if parent isn't available * saved searches dnd navigation * well * rendering * Update pnpm-lock.yaml * types * remove width * Temporary solution * merge * @dnd-kit/utilities * Update pnpm-lock.yaml * explorer path dnd * remove unused drag hook * fix dnd on LayeredFileIcon --------- Co-authored-by: Brendan Allan --- interface/app/$libraryId/Explorer/Context.tsx | 2 +- .../Explorer/ContextMenu/SharedItems.tsx | 4 +- .../app/$libraryId/Explorer/DragOverlay.tsx | 98 ++ .../$libraryId/Explorer/ExplorerDraggable.tsx | 28 + .../$libraryId/Explorer/ExplorerDroppable.tsx | 36 + .../app/$libraryId/Explorer/ExplorerPath.tsx | 164 ++++ .../Explorer/FilePath/LayeredFileIcon.tsx | 42 +- .../$libraryId/Explorer/FilePath/Original.tsx | 4 +- .../Explorer/FilePath/RenameTextBox.tsx | 79 +- .../$libraryId/Explorer/FilePath/Thumb.tsx | 55 +- .../$libraryId/Explorer/Inspector/index.tsx | 4 +- .../{ViewContext.ts => View/Context.ts} | 8 - .../Explorer/View/DragScrollable.tsx | 33 + .../$libraryId/Explorer/View/EmptyNotice.tsx | 41 + .../$libraryId/Explorer/View/ExplorerPath.tsx | 255 ------ .../$libraryId/Explorer/View/Grid/Item.tsx | 86 ++ .../$libraryId/Explorer/View/Grid/context.tsx | 18 + .../View/{GridList.tsx => Grid/index.tsx} | 206 ++--- .../app/$libraryId/Explorer/View/GridView.tsx | 89 -- .../Explorer/View/GridView/Item/Context.tsx | 13 + .../Explorer/View/GridView/Item/index.tsx | 128 +++ .../Explorer/View/GridView/index.tsx | 12 + .../Explorer/View/ListView/Item.tsx | 84 ++ .../Explorer/View/ListView/TableRow.tsx | 57 ++ .../Explorer/View/ListView/context.tsx | 16 + .../Explorer/View/ListView/index.tsx | 860 ++++++++---------- .../{util/ranges.tsx => useRanges.tsx} | 0 .../ListView/{util/table.tsx => useTable.tsx} | 73 +- .../$libraryId/Explorer/View/MediaView.tsx | 67 -- .../Explorer/View/MediaView/Item.tsx | 67 ++ .../Explorer/View/MediaView/index.tsx | 20 + .../Explorer/View/RenamableItemText.tsx | 52 +- .../app/$libraryId/Explorer/View/ViewItem.tsx | 15 +- .../app/$libraryId/Explorer/View/index.tsx | 351 ++++--- .../Explorer/View/useDragScrollable.tsx | 63 ++ .../app/$libraryId/Explorer/View/util.ts | 17 - interface/app/$libraryId/Explorer/index.tsx | 66 +- interface/app/$libraryId/Explorer/store.ts | 22 +- .../$libraryId/Explorer/useExplorerDnd.tsx | 211 +++++ .../Explorer/useExplorerDraggable.tsx | 50 + .../Explorer/useExplorerDroppable.tsx | 211 +++++ .../app/$libraryId/Layout/DndContext.tsx | 24 + .../Layout/Sidebar/Devices/index.tsx | 41 + .../Layout/Sidebar/EphemeralSection.tsx | 59 +- .../Layout/Sidebar/LibrarySection.tsx | 228 +---- .../ContextMenu.tsx} | 11 +- .../Layout/Sidebar/Locations/index.tsx | 87 ++ .../Layout/Sidebar/SavedSearches/index.tsx | 103 +++ .../ContextMenu.tsx} | 9 +- .../$libraryId/Layout/Sidebar/Tags/index.tsx | 67 ++ interface/app/$libraryId/Layout/index.tsx | 49 +- interface/app/$libraryId/TopBar/Layout.tsx | 10 +- .../$libraryId/TopBar/NavigationButtons.tsx | 24 +- interface/app/$libraryId/TopBar/index.tsx | 4 +- interface/app/$libraryId/ephemeral.tsx | 2 +- interface/app/$libraryId/location/$id.tsx | 2 +- interface/app/$libraryId/saved-search/$id.tsx | 2 +- .../settings/library/tags/CreateDialog.tsx | 2 +- interface/app/$libraryId/tag/$id.tsx | 2 +- interface/hooks/index.ts | 17 +- interface/hooks/useDismissibleNoticeStore.tsx | 3 +- interface/hooks/useDragSelect.tsx | 39 - interface/package.json | 3 +- packages/assets/icons/MoveLocation.png | Bin 0 -> 81629 bytes packages/assets/icons/MoveLocation_Light.png | Bin 0 -> 65758 bytes packages/assets/icons/index.ts | 4 + packages/ui/src/Dialog.tsx | 5 +- pnpm-lock.yaml | Bin 902316 -> 903324 bytes 68 files changed, 2746 insertions(+), 1758 deletions(-) create mode 100644 interface/app/$libraryId/Explorer/DragOverlay.tsx create mode 100644 interface/app/$libraryId/Explorer/ExplorerDraggable.tsx create mode 100644 interface/app/$libraryId/Explorer/ExplorerDroppable.tsx create mode 100644 interface/app/$libraryId/Explorer/ExplorerPath.tsx rename interface/app/$libraryId/Explorer/{ViewContext.ts => View/Context.ts} (65%) create mode 100644 interface/app/$libraryId/Explorer/View/DragScrollable.tsx create mode 100644 interface/app/$libraryId/Explorer/View/EmptyNotice.tsx delete mode 100644 interface/app/$libraryId/Explorer/View/ExplorerPath.tsx create mode 100644 interface/app/$libraryId/Explorer/View/Grid/Item.tsx create mode 100644 interface/app/$libraryId/Explorer/View/Grid/context.tsx rename interface/app/$libraryId/Explorer/View/{GridList.tsx => Grid/index.tsx} (77%) delete mode 100644 interface/app/$libraryId/Explorer/View/GridView.tsx create mode 100644 interface/app/$libraryId/Explorer/View/GridView/Item/Context.tsx create mode 100644 interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx create mode 100644 interface/app/$libraryId/Explorer/View/GridView/index.tsx create mode 100644 interface/app/$libraryId/Explorer/View/ListView/Item.tsx create mode 100644 interface/app/$libraryId/Explorer/View/ListView/TableRow.tsx create mode 100644 interface/app/$libraryId/Explorer/View/ListView/context.tsx rename interface/app/$libraryId/Explorer/View/ListView/{util/ranges.tsx => useRanges.tsx} (100%) rename interface/app/$libraryId/Explorer/View/ListView/{util/table.tsx => useTable.tsx} (77%) delete mode 100644 interface/app/$libraryId/Explorer/View/MediaView.tsx create mode 100644 interface/app/$libraryId/Explorer/View/MediaView/Item.tsx create mode 100644 interface/app/$libraryId/Explorer/View/MediaView/index.tsx create mode 100644 interface/app/$libraryId/Explorer/View/useDragScrollable.tsx delete mode 100644 interface/app/$libraryId/Explorer/View/util.ts create mode 100644 interface/app/$libraryId/Explorer/useExplorerDnd.tsx create mode 100644 interface/app/$libraryId/Explorer/useExplorerDraggable.tsx create mode 100644 interface/app/$libraryId/Explorer/useExplorerDroppable.tsx create mode 100644 interface/app/$libraryId/Layout/DndContext.tsx create mode 100644 interface/app/$libraryId/Layout/Sidebar/Devices/index.tsx rename interface/app/$libraryId/Layout/Sidebar/{LocationsContextMenu.tsx => Locations/ContextMenu.tsx} (91%) create mode 100644 interface/app/$libraryId/Layout/Sidebar/Locations/index.tsx create mode 100644 interface/app/$libraryId/Layout/Sidebar/SavedSearches/index.tsx rename interface/app/$libraryId/Layout/Sidebar/{TagsContextMenu.tsx => Tags/ContextMenu.tsx} (88%) create mode 100644 interface/app/$libraryId/Layout/Sidebar/Tags/index.tsx delete mode 100644 interface/hooks/useDragSelect.tsx create mode 100644 packages/assets/icons/MoveLocation.png create mode 100644 packages/assets/icons/MoveLocation_Light.png diff --git a/interface/app/$libraryId/Explorer/Context.tsx b/interface/app/$libraryId/Explorer/Context.tsx index ab9b87c3f..7ca71f576 100644 --- a/interface/app/$libraryId/Explorer/Context.tsx +++ b/interface/app/$libraryId/Explorer/Context.tsx @@ -26,4 +26,4 @@ export const ExplorerContextProvider = >({ children }: PropsWithChildren<{ explorer: TExplorer; -}>) => {children}; +}>) => {children}; diff --git a/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx b/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx index cf15ddddc..03701a59c 100644 --- a/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx +++ b/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx @@ -15,7 +15,6 @@ import { getQuickPreviewStore } from '../QuickPreview/store'; import { RevealInNativeExplorerBase } from '../RevealInNativeExplorer'; import { getExplorerStore, useExplorerStore } from '../store'; import { useViewItemDoubleClick } from '../View/ViewItem'; -import { useExplorerViewContext } from '../ViewContext'; import { Conditional, ConditionalItem } from './ConditionalItem'; import { useContextMenuContext } from './context'; import OpenWith from './OpenWith'; @@ -101,7 +100,6 @@ export const Rename = new ConditionalItem({ return {}; }, Component: () => { - const explorerView = useExplorerViewContext(); const keybind = useKeybindFactory(); const os = useOperatingSystem(true); @@ -109,7 +107,7 @@ export const Rename = new ConditionalItem({ explorerView.setIsRenaming(true)} + onClick={() => (getExplorerStore().isRenaming = true)} /> ); } diff --git a/interface/app/$libraryId/Explorer/DragOverlay.tsx b/interface/app/$libraryId/Explorer/DragOverlay.tsx new file mode 100644 index 000000000..d38be45b1 --- /dev/null +++ b/interface/app/$libraryId/Explorer/DragOverlay.tsx @@ -0,0 +1,98 @@ +import type { ClientRect, Modifier } from '@dnd-kit/core'; +import { DragOverlay as DragOverlayPrimitive } from '@dnd-kit/core'; +import { getEventCoordinates } from '@dnd-kit/utilities'; +import clsx from 'clsx'; +import { memo, useEffect, useRef } from 'react'; +import { ExplorerItem } from '@sd/client'; +import { useIsDark } from '~/hooks'; + +import { FileThumb } from './FilePath/Thumb'; +import { useExplorerStore } from './store'; +import { RenamableItemText } from './View/RenamableItemText'; + +const useSnapToCursorModifier = () => { + const explorerStore = useExplorerStore(); + + const initialRect = useRef(null); + + const modifier: Modifier = ({ activatorEvent, activeNodeRect, transform }) => { + if (!activeNodeRect || !activatorEvent) return transform; + + const activatorCoordinates = getEventCoordinates(activatorEvent); + if (!activatorCoordinates) return transform; + + const rect = initialRect.current ?? activeNodeRect; + + if (!initialRect.current) initialRect.current = activeNodeRect; + + // Default offset so during drag the cursor doesn't overlap the overlay + // which can cause issues with mouse events on other elements + const offset = 12; + + const offsetX = activatorCoordinates.x - rect.left; + const offsetY = activatorCoordinates.y - rect.top; + + return { + ...transform, + x: transform.x + offsetX + offset, + y: transform.y + offsetY + offset + }; + }; + + useEffect(() => { + if (!explorerStore.drag) initialRect.current = null; + }, [explorerStore.drag]); + + return modifier; +}; + +export const DragOverlay = memo(() => { + const isDark = useIsDark(); + + const modifier = useSnapToCursorModifier(); + + const { drag } = useExplorerStore(); + + return ( + + {!drag || drag.type === 'touched' ? null : ( +
+ {drag.items.length > 1 && ( +
+ {drag.items.length} +
+ )} + + {(drag.items.slice(0, 8) as ExplorerItem[]).map((item, i, items) => ( +
7 && [ + i + 1 === items.length && 'opacity-10', + i + 2 === items.length && 'opacity-50', + i + 3 === items.length && 'opacity-90' + ] + )} + > + + +
+ ))} +
+ )} +
+ ); +}); diff --git a/interface/app/$libraryId/Explorer/ExplorerDraggable.tsx b/interface/app/$libraryId/Explorer/ExplorerDraggable.tsx new file mode 100644 index 000000000..f6172b0f9 --- /dev/null +++ b/interface/app/$libraryId/Explorer/ExplorerDraggable.tsx @@ -0,0 +1,28 @@ +import { HTMLAttributes } from 'react'; + +import { useExplorerDraggable, UseExplorerDraggableProps } from './useExplorerDraggable'; + +/** + * Wrapper for explorer draggable items until dnd-kit solvers their re-rendering issues + * https://github.com/clauderic/dnd-kit/issues/1194#issuecomment-1696704815 + */ +export const ExplorerDraggable = ({ + draggable, + ...props +}: Omit, 'draggable'> & { + draggable: UseExplorerDraggableProps; +}) => { + const { attributes, listeners, style, setDraggableRef } = useExplorerDraggable(draggable); + + return ( +
+ {props.children} +
+ ); +}; diff --git a/interface/app/$libraryId/Explorer/ExplorerDroppable.tsx b/interface/app/$libraryId/Explorer/ExplorerDroppable.tsx new file mode 100644 index 000000000..f08459c02 --- /dev/null +++ b/interface/app/$libraryId/Explorer/ExplorerDroppable.tsx @@ -0,0 +1,36 @@ +import clsx from 'clsx'; +import { createContext, HTMLAttributes, useContext, useMemo } from 'react'; + +import { useExplorerDroppable, UseExplorerDroppableProps } from './useExplorerDroppable'; + +const ExplorerDroppableContext = createContext<{ isDroppable: boolean } | null>(null); + +export const useExplorerDroppableContext = () => { + const ctx = useContext(ExplorerDroppableContext); + + if (ctx === null) throw new Error('ExplorerDroppableContext.Provider not found!'); + + return ctx; +}; + +/** + * Wrapper for explorer droppable items until dnd-kit solvers their re-rendering issues + * https://github.com/clauderic/dnd-kit/issues/1194#issuecomment-1696704815 + */ +export const ExplorerDroppable = ({ + droppable, + children, + ...props +}: HTMLAttributes & { droppable: UseExplorerDroppableProps }) => { + const { isDroppable, className, setDroppableRef } = useExplorerDroppable(droppable); + + const context = useMemo(() => ({ isDroppable }), [isDroppable]); + + return ( + +
+ {children} +
+
+ ); +}; diff --git a/interface/app/$libraryId/Explorer/ExplorerPath.tsx b/interface/app/$libraryId/Explorer/ExplorerPath.tsx new file mode 100644 index 000000000..33e020431 --- /dev/null +++ b/interface/app/$libraryId/Explorer/ExplorerPath.tsx @@ -0,0 +1,164 @@ +import { CaretRight } from '@phosphor-icons/react'; +import clsx from 'clsx'; +import { memo, useMemo } from 'react'; +import { useNavigate } from 'react-router'; +import { createSearchParams } from 'react-router-dom'; +import { getExplorerItemData, getIndexedItemFilePath, useLibraryQuery } from '@sd/client'; +import { Icon } from '~/components'; +import { useIsDark, useOperatingSystem } from '~/hooks'; + +import { useExplorerContext } from './Context'; +import { FileThumb } from './FilePath/Thumb'; +import { useExplorerDroppable } from './useExplorerDroppable'; +import { useExplorerSearchParams } from './util'; + +export const PATH_BAR_HEIGHT = 32; + +export const ExplorerPath = memo(() => { + const os = useOperatingSystem(); + const navigate = useNavigate(); + + const [{ path: searchPath }] = useExplorerSearchParams(); + const { parent: explorerParent, selectedItems } = useExplorerContext(); + + const pathSlash = os === 'windows' ? '\\' : '/'; + + const location = explorerParent?.type === 'Location' ? explorerParent.location : undefined; + + const selectedItem = useMemo( + () => (selectedItems.size === 1 ? [...selectedItems][0] : undefined), + [selectedItems] + ); + + const indexedFilePath = selectedItem && getIndexedItemFilePath(selectedItem); + + const queryPath = !!indexedFilePath && (!searchPath || !location); + + const { data: filePathname } = useLibraryQuery(['files.getPath', indexedFilePath?.id ?? -1], { + enabled: queryPath + }); + + const paths = useMemo(() => { + // Remove file name from the path + const _filePathname = filePathname?.slice(0, filePathname.lastIndexOf(pathSlash)); + + const pathname = _filePathname ?? [location?.path, searchPath].filter(Boolean).join(''); + + const paths = [...(pathname.match(new RegExp(`[^${pathSlash}]+`, 'g')) ?? [])]; + + let locationPath = location?.path; + + if (!locationPath && indexedFilePath?.materialized_path) { + if (indexedFilePath.materialized_path === '/') locationPath = pathname; + else { + // Remove last slash from materialized_path + const materializedPath = indexedFilePath.materialized_path.slice(0, -1); + + // Extract location path from pathname + locationPath = pathname.slice(0, pathname.indexOf(materializedPath)); + } + } + + const locationIndex = (locationPath ?? '').split(pathSlash).filter(Boolean).length - 1; + + return paths.map((path, i) => { + const isLocation = locationIndex !== -1 && i >= locationIndex; + + const _paths = [ + ...paths.slice(!isLocation ? 0 : locationIndex + 1, i), + i === locationIndex ? '' : path + ]; + + let pathname = `${pathSlash}${_paths.join(pathSlash)}`; + + // Add slash to the end of the pathname if it's a location + if (isLocation && i > locationIndex) pathname += pathSlash; + + return { + name: path, + pathname, + locationId: isLocation ? indexedFilePath?.location_id ?? location?.id : undefined + }; + }); + }, [location, indexedFilePath, filePathname, pathSlash, searchPath]); + + const handleOnClick = ({ pathname, locationId }: (typeof paths)[number]) => { + if (locationId === undefined) { + // TODO: Handle ephemeral volumes + navigate({ + pathname: '../ephemeral/0-0', + search: `${createSearchParams({ path: pathname })}` + }); + } else { + navigate({ + pathname: `../location/${locationId}`, + search: pathname === '/' ? undefined : `${createSearchParams({ path: pathname })}` + }); + } + }; + + return ( +
+ {paths.map((path) => ( + handleOnClick(path)} + disabled={path.pathname === (searchPath ?? (location && '/'))} + /> + ))} + + {selectedItem && (!queryPath || filePathname) && ( +
+ + + {getExplorerItemData(selectedItem).fullName} + +
+ )} +
+ ); +}); + +interface PathProps { + path: { name: string; pathname: string; locationId?: number }; + onClick: () => void; + disabled: boolean; +} + +const Path = ({ path, onClick, disabled }: PathProps) => { + const isDark = useIsDark(); + + const { setDroppableRef, className, isDroppable } = useExplorerDroppable({ + data: { + type: 'location', + path: path.pathname, + data: path.locationId ? { id: path.locationId, path: path.pathname } : undefined + }, + allow: ['Path', 'NonIndexedPath', 'Object'], + navigateTo: onClick, + disabled + }); + + return ( + + ); +}; diff --git a/interface/app/$libraryId/Explorer/FilePath/LayeredFileIcon.tsx b/interface/app/$libraryId/Explorer/FilePath/LayeredFileIcon.tsx index c0217b89b..4656dc4b9 100644 --- a/interface/app/$libraryId/Explorer/FilePath/LayeredFileIcon.tsx +++ b/interface/app/$libraryId/Explorer/FilePath/LayeredFileIcon.tsx @@ -1,6 +1,6 @@ import { getLayeredIcon } from '@sd/assets/util'; import clsx from 'clsx'; -import { type ImgHTMLAttributes } from 'react'; +import { forwardRef, type ImgHTMLAttributes } from 'react'; import { type ObjectKindKey } from '@sd/client'; interface LayeredFileIconProps extends ImgHTMLAttributes { @@ -16,28 +16,32 @@ const positionConfig: Record = { Config: 'flex h-full w-full items-center justify-center' }; -const LayeredFileIcon = ({ kind, extension, ...props }: LayeredFileIconProps) => { - const iconImg = ; +const LayeredFileIcon = forwardRef( + ({ kind, extension, ...props }, ref) => { + const iconImg = ; - if (SUPPORTED_ICONS.includes(kind) === false) { - return iconImg; - } + if (SUPPORTED_ICONS.includes(kind) === false) { + return iconImg; + } - const IconComponent = extension ? getLayeredIcon(kind, extension) : null; + const IconComponent = extension ? getLayeredIcon(kind, extension) : null; - const positionClass = - positionConfig[kind] || 'flex h-full w-full items-end justify-end pb-4 pr-2'; + const positionClass = + positionConfig[kind] || 'flex h-full w-full items-end justify-end pb-4 pr-2'; - return IconComponent == null ? ( - iconImg - ) : ( -
- {iconImg} -
- + return IconComponent == null ? ( + iconImg + ) : ( +
+ {iconImg} +
+ +
-
- ); -}; + ); + } +); export default LayeredFileIcon; diff --git a/interface/app/$libraryId/Explorer/FilePath/Original.tsx b/interface/app/$libraryId/Explorer/FilePath/Original.tsx index 2b09f0a96..f1f7a83d0 100644 --- a/interface/app/$libraryId/Explorer/FilePath/Original.tsx +++ b/interface/app/$libraryId/Explorer/FilePath/Original.tsx @@ -182,7 +182,7 @@ interface VideoProps extends VideoHTMLAttributes { blackBarsSize?: number; } -const Video = memo(({ paused, blackBars, blackBarsSize, className, ...props }: VideoProps) => { +const Video = ({ paused, blackBars, blackBarsSize, className, ...props }: VideoProps) => { const ref = useRef(null); const size = useSize(ref); @@ -220,4 +220,4 @@ const Video = memo(({ paused, blackBars, blackBarsSize, className, ...props }: V

Video preview is not supported.

); -}); +}; diff --git a/interface/app/$libraryId/Explorer/FilePath/RenameTextBox.tsx b/interface/app/$libraryId/Explorer/FilePath/RenameTextBox.tsx index 00b1e3cf6..eabf38cc9 100644 --- a/interface/app/$libraryId/Explorer/FilePath/RenameTextBox.tsx +++ b/interface/app/$libraryId/Explorer/FilePath/RenameTextBox.tsx @@ -1,31 +1,41 @@ import clsx from 'clsx'; -import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; +import { + forwardRef, + memo, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState +} from 'react'; import TruncateMarkup from 'react-truncate-markup'; import { Tooltip } from '@sd/ui'; import { useOperatingSystem, useShortcut } from '~/hooks'; -import { useExplorerViewContext } from '../ViewContext'; +import { getExplorerStore, useExplorerStore } from '../store'; -interface Props extends React.HTMLAttributes { +export interface RenameTextBoxProps extends React.HTMLAttributes { name: string; onRename: (newName: string) => void; disabled?: boolean; lines?: number; + // Temporary solution for TruncatedText in list view + idleClassName?: string; } -export const RenameTextBox = forwardRef( - ({ name, onRename, disabled, className, lines, ...props }, _ref) => { - const explorerView = useExplorerViewContext(); +export const RenameTextBox = forwardRef( + ({ name, onRename, disabled, className, idleClassName, lines, ...props }, _ref) => { const os = useOperatingSystem(); + const explorerStore = useExplorerStore(); - const [allowRename, setAllowRename] = useState(false); - const [isTruncated, setIsTruncated] = useState(false); + const ref = useRef(null); + useImperativeHandle(_ref, () => ref.current); const renamable = useRef(false); const timeout = useRef(null); - const ref = useRef(null); - useImperativeHandle(_ref, () => ref.current); + const [allowRename, setAllowRename] = useState(false); + const [isTruncated, setIsTruncated] = useState(false); // Highlight file name up to extension or // fully if it's a directory, hidden file or has no extension @@ -99,12 +109,6 @@ export const RenameTextBox = forwardRef( } }; - const ellipsis = useCallback(() => { - const extension = name.lastIndexOf('.'); - if (extension !== -1) return `...${name.slice(-(name.length - extension + 2))}`; - return `...${name.slice(-8)}`; - }, [name]); - useShortcut('renameObject', (e) => { e.preventDefault(); if (allowRename) blur(); @@ -128,10 +132,10 @@ export const RenameTextBox = forwardRef( useEffect(() => { if (!disabled) { - if (explorerView.isRenaming && !allowRename) setAllowRename(true); - else explorerView.setIsRenaming(allowRename); + if (explorerStore.isRenaming && !allowRename) setAllowRename(true); + else getExplorerStore().isRenaming = allowRename; } else resetState(); - }, [explorerView.isRenaming, disabled, allowRename, explorerView]); + }, [explorerStore.isRenaming, disabled, allowRename]); useEffect(() => { const onMouseDown = (event: MouseEvent) => { @@ -146,7 +150,11 @@ export const RenameTextBox = forwardRef(
( className={clsx( 'cursor-default overflow-hidden rounded-md px-1.5 py-px text-xs text-ink outline-none', allowRename && 'whitespace-normal bg-app !text-ink ring-2 ring-accent-deep', + !allowRename && idleClassName, className )} onDoubleClick={(e) => { @@ -176,7 +185,7 @@ export const RenameTextBox = forwardRef( onBlur={() => { handleRename(); resetState(); - explorerView.setIsRenaming(false); + getExplorerStore().isRenaming = false; }} onKeyDown={handleKeyDown} {...props} @@ -184,16 +193,30 @@ export const RenameTextBox = forwardRef( {allowRename ? ( name ) : ( - -
{name}
-
+ )}
); } ); + +interface TruncatedTextProps { + text: string; + lines?: number; + onTruncate: (wasTruncated: boolean) => void; +} + +const TruncatedText = memo(({ text, lines, onTruncate }: TruncatedTextProps) => { + const ellipsis = useCallback(() => { + const extension = text.lastIndexOf('.'); + if (extension !== -1) return `...${text.slice(-(text.length - extension + 2))}`; + return `...${text.slice(-8)}`; + }, [text]); + + return ( + +
{text}
+
+ ); +}); diff --git a/interface/app/$libraryId/Explorer/FilePath/Thumb.tsx b/interface/app/$libraryId/Explorer/FilePath/Thumb.tsx index 6ac4e2d3d..02170cbe5 100644 --- a/interface/app/$libraryId/Explorer/FilePath/Thumb.tsx +++ b/interface/app/$libraryId/Explorer/FilePath/Thumb.tsx @@ -1,13 +1,20 @@ import { getIcon, getIconByName } from '@sd/assets/util'; import clsx from 'clsx'; -import { memo, SyntheticEvent, useMemo, useRef, useState, type ImgHTMLAttributes } from 'react'; +import { + forwardRef, + HTMLAttributes, + SyntheticEvent, + useImperativeHandle, + useMemo, + useRef, + useState +} from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { getItemFilePath, useLibraryContext, type ExplorerItem } from '@sd/client'; import { useIsDark } from '~/hooks'; import { pdfViewerEnabled } from '~/util/pdfViewer'; import { usePlatform } from '~/util/Platform'; -import { useExplorerContext } from '../Context'; import { useExplorerItemData } from '../util'; import { Image, ImageProps } from './Image'; import LayeredFileIcon from './LayeredFileIcon'; @@ -32,18 +39,18 @@ export interface ThumbProps { frameClassName?: string; childClassName?: string | ((type: ThumbType) => string | undefined); isSidebarPreview?: boolean; + childProps?: HTMLAttributes; } type ThumbType = { variant: 'original' } | { variant: 'thumbnail' } | { variant: 'icon' }; -export const FileThumb = memo((props: ThumbProps) => { +export const FileThumb = forwardRef((props, ref) => { const isDark = useIsDark(); const platform = usePlatform(); const itemData = useExplorerItemData(props.data); const filePath = getItemFilePath(props.data); - const { parent } = useExplorerContext(); const { library } = useLibraryContext(); const [loadState, setLoadState] = useState<{ @@ -72,14 +79,11 @@ export const FileThumb = memo((props: ThumbProps) => { }, [itemData, loadState]); const src = useMemo(() => { - const locationId = - itemData.locationId ?? (parent?.type === 'Location' ? parent.location.id : null); - switch (thumbType.variant) { case 'original': if (filePath && (itemData.extension !== 'pdf' || pdfViewerEnabled())) { - if ('id' in filePath && locationId) - return platform.getFileUrl(library.uuid, locationId, filePath.id); + if ('id' in filePath && itemData.locationId) + return platform.getFileUrl(library.uuid, itemData.locationId, filePath.id); else if ('path' in filePath) return platform.getFileUrlByPath(filePath.path); } break; @@ -100,7 +104,7 @@ export const FileThumb = memo((props: ThumbProps) => { itemData.isDir ); } - }, [filePath, isDark, library.uuid, itemData, platform, thumbType, parent]); + }, [filePath, isDark, library.uuid, itemData, platform, thumbType]); const onLoad = (s: 'original' | 'thumbnail' | 'icon') => { setLoadState((state) => ({ ...state, [s]: 'loaded' })); @@ -133,12 +137,14 @@ export const FileThumb = memo((props: ThumbProps) => { const className = clsx(childClassName, _childClassName); const thumbnail = (() => { - if (!src) return null; + if (!src) return <>; switch (thumbType.variant) { case 'thumbnail': return ( onLoad('thumbnail')} @@ -169,6 +175,8 @@ export const FileThumb = memo((props: ThumbProps) => { case 'icon': return ( { /> ); default: - return null; + return <>; } })(); @@ -233,17 +241,16 @@ interface ThumbnailProps extends Omit { extension?: string; } -const Thumbnail = memo( - ({ - crossOrigin, - blackBars, - blackBarsSize, - extension, - cover, - className, - ...props - }: ThumbnailProps) => { +const Thumbnail = forwardRef( + ( + { crossOrigin, blackBars, blackBarsSize, extension, cover, className, style, ...props }, + _ref + ) => { const ref = useRef(null); + useImperativeHandle( + _ref, + () => ref.current + ); const size = useSize(ref); @@ -259,7 +266,7 @@ const Thumbnail = memo( blackBarsStyle && size.width === 0 && 'invisible' ), cover, - style: blackBars ? blackBarsStyle : undefined, + style: { ...style, ...(blackBars ? blackBarsStyle : undefined) }, size, ref }} @@ -274,7 +281,7 @@ const Thumbnail = memo( }) }} className={clsx( - 'absolute rounded bg-black/60 px-1 py-0.5 text-[9px] font-semibold uppercase text-white opacity-70', + 'pointer-events-none absolute rounded bg-black/60 px-1 py-0.5 text-[9px] font-semibold uppercase text-white opacity-70', cover ? 'bottom-1 right-1' : 'left-1/2 top-1/2 -translate-x-full -translate-y-full' diff --git a/interface/app/$libraryId/Explorer/Inspector/index.tsx b/interface/app/$libraryId/Explorer/Inspector/index.tsx index ad85da22f..16fd91318 100644 --- a/interface/app/$libraryId/Explorer/Inspector/index.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/index.tsx @@ -400,7 +400,7 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => { const { libraryId } = useZodRouteParams(LibraryIdParamsSchema); const tagsQuery = useLibraryQuery(['tags.list'], { - enabled: readyToFetch && !explorerStore.isDragging, + enabled: readyToFetch && !explorerStore.isDragSelecting, suspense: true }); useNodes(tagsQuery.data?.nodes); @@ -408,7 +408,7 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => { const tagsWithObjects = useLibraryQuery( ['tags.getWithObjects', selectedObjects.map(({ id }) => id)], - { enabled: readyToFetch && !explorerStore.isDragging } + { enabled: readyToFetch && !explorerStore.isDragSelecting } ); const getDate = useCallback((metadataDate: MetadataDate, date: Date) => { diff --git a/interface/app/$libraryId/Explorer/ViewContext.ts b/interface/app/$libraryId/Explorer/View/Context.ts similarity index 65% rename from interface/app/$libraryId/Explorer/ViewContext.ts rename to interface/app/$libraryId/Explorer/View/Context.ts index 5d2bfb155..b339c98d2 100644 --- a/interface/app/$libraryId/Explorer/ViewContext.ts +++ b/interface/app/$libraryId/Explorer/View/Context.ts @@ -1,18 +1,10 @@ import { createContext, useContext, type ReactNode, type RefObject } from 'react'; -import { ExplorerViewPadding } from './View'; - export interface ExplorerViewContext { ref: RefObject; top?: number; bottom?: number; contextMenu?: ReactNode; - isContextMenuOpen?: boolean; - setIsContextMenuOpen?: (isOpen: boolean) => void; - isRenaming: boolean; - setIsRenaming: (isRenaming: boolean) => void; - padding?: Omit; - gap?: number | { x?: number; y?: number }; selectable: boolean; listViewOptions?: { hideHeaderBorder?: boolean; diff --git a/interface/app/$libraryId/Explorer/View/DragScrollable.tsx b/interface/app/$libraryId/Explorer/View/DragScrollable.tsx new file mode 100644 index 000000000..119e06c8f --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/DragScrollable.tsx @@ -0,0 +1,33 @@ +import { useExplorerLayoutStore } from '@sd/client'; +import { tw } from '@sd/ui'; + +import { useTopBarContext } from '../../TopBar/Layout'; +import { useExplorerContext } from '../Context'; +import { PATH_BAR_HEIGHT } from '../ExplorerPath'; +import { useDragScrollable } from './useDragScrollable'; + +const Trigger = tw.div`absolute inset-x-0 h-10 pointer-events-none`; + +export const DragScrollable = () => { + const topBar = useTopBarContext(); + const explorer = useExplorerContext(); + const explorerSettings = explorer.useSettingsSnapshot(); + + const layoutStore = useExplorerLayoutStore(); + const showPathBar = explorer.showPathBar && layoutStore.showPathBar; + + const { ref: dragScrollableUpRef } = useDragScrollable({ direction: 'up' }); + const { ref: dragScrollableDownRef } = useDragScrollable({ direction: 'down' }); + + return ( + <> + {explorerSettings.layoutMode !== 'list' && ( + + )} + + + ); +}; diff --git a/interface/app/$libraryId/Explorer/View/EmptyNotice.tsx b/interface/app/$libraryId/Explorer/View/EmptyNotice.tsx new file mode 100644 index 000000000..a0beb65a1 --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/EmptyNotice.tsx @@ -0,0 +1,41 @@ +import { Columns, GridFour, Icon, MonitorPlay, Rows } from '@phosphor-icons/react'; +import { isValidElement, ReactNode } from 'react'; + +import { useExplorerContext } from '../Context'; + +export const EmptyNotice = (props: { + icon?: Icon | ReactNode; + message?: ReactNode; + loading?: boolean; +}) => { + const { layoutMode } = useExplorerContext().useSettingsSnapshot(); + + const emptyNoticeIcon = (icon?: Icon) => { + const Icon = + icon ?? + { + grid: GridFour, + media: MonitorPlay, + columns: Columns, + list: Rows + }[layoutMode]; + + return ; + }; + + if (props.loading) return null; + + return ( +
+ {props.icon + ? isValidElement(props.icon) + ? props.icon + : emptyNoticeIcon(props.icon as Icon) + : emptyNoticeIcon()} + +

+ {props.message !== undefined ? props.message : 'This list is empty'} +

+
+ ); +}; diff --git a/interface/app/$libraryId/Explorer/View/ExplorerPath.tsx b/interface/app/$libraryId/Explorer/View/ExplorerPath.tsx deleted file mode 100644 index ff9f69e40..000000000 --- a/interface/app/$libraryId/Explorer/View/ExplorerPath.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import { CaretRight } from '@phosphor-icons/react'; -import clsx from 'clsx'; -import { ComponentProps, memo, useCallback, useEffect, useMemo, useRef } from 'react'; -import { useMatch, useNavigate } from 'react-router'; -import { ExplorerItem, FilePath, FilePathWithObject, useLibraryQuery } from '@sd/client'; -import { LibraryIdParamsSchema, SearchParamsSchema } from '~/app/route-schemas'; -import { Icon } from '~/components'; -import { useOperatingSystem, useZodRouteParams, useZodSearchParams } from '~/hooks'; - -import { useExplorerContext } from '../Context'; -import { FileThumb } from '../FilePath/Thumb'; -import { useExplorerSearchParams } from '../util'; - -export const PATH_BAR_HEIGHT = 32; - -export const ExplorerPath = memo(() => { - const isEphemeralLocation = useMatch('/:libraryId/ephemeral/:ephemeralId'); - const os = useOperatingSystem(); - const realOs = useOperatingSystem(true); - const navigate = useNavigate(); - const libraryId = useZodRouteParams(LibraryIdParamsSchema).libraryId; - const pathSlashOS = os === 'browser' ? '/' : realOs === 'windows' ? '\\' : '/'; - const firstRenderCached = useRef(null); - - const explorerContext = useExplorerContext(); - const fullPathOnClick = explorerContext.parent?.type === 'Tag'; - const [{ path }] = useExplorerSearchParams(); - const [_, setSearchParams] = useZodSearchParams(SearchParamsSchema); - const selectedItem = useMemo(() => { - if (explorerContext.selectedItems.size !== 1) return; - return [...explorerContext.selectedItems][0]; - }, [explorerContext.selectedItems]); - - // On initial render, check if the location is nested - // If it is, return the number of times it is nested - const isLocationNested = useCallback(() => { - if (!explorerContext.parent || explorerContext.parent.type !== 'Location') return false; - firstRenderCached.current = true; - const { path: locationPath, name: locationName } = explorerContext.parent.location || {}; - - if (!locationPath || !locationName) return false; - const count = locationPath - .split(pathSlashOS) - .filter((part) => part === locationName).length; - - return count > 1 ? count - 1 : false; - }, [explorerContext.parent, pathSlashOS]); - - // On the first render of a location, check if the location is nested - useEffect(() => { - if (explorerContext.parent?.type === 'Location') { - isLocationNested(); - } - return () => { - firstRenderCached.current = null; - }; - }, [explorerContext.parent, isLocationNested]); - - const filePathData = () => { - if (!selectedItem) return; - let filePathData: FilePath | FilePathWithObject | null = null; - const item = selectedItem as ExplorerItem; - switch (item.type) { - case 'Path': { - filePathData = item.item; - break; - } - case 'Object': { - filePathData = item.item.file_paths[0] ?? null; - break; - } - case 'SpacedropPeer': { - // objectData = item.item as unknown as Object; - // filePathData = item.item.file_paths[0] ?? null; - break; - } - } - return filePathData; - }; - - //this is being used with tag page route - when clicking on an object - //we get the full path of the object and use it to build the path bar - const queriedFullPath = useLibraryQuery(['files.getPath', filePathData()?.id ?? -1], { - enabled: selectedItem != null && fullPathOnClick - }); - - const indexedPath = fullPathOnClick - ? queriedFullPath.data - : explorerContext.parent?.type === 'Location' && explorerContext.parent.location.path; - - //There are cases where the path ends with a '/' and cases where it doesn't - const pathInfo = indexedPath - ? indexedPath + (path ? path.slice(0, -1) : '') - : path?.endsWith(pathSlashOS) - ? path?.slice(0, -1) - : path; - - const pathBuilder = (pathsToSplit: string, clickedPath: string): string => { - const slashCheck = isEphemeralLocation ? pathSlashOS : '/'; //in ephemeral locations, the path is built with '\' instead of '/' for windows - const splitPaths = pathsToSplit?.split(slashCheck); - const indexOfClickedPath = splitPaths?.indexOf(clickedPath); - const newPath = - splitPaths?.slice(0, (indexOfClickedPath as number) + 1).join(slashCheck) + slashCheck; - return newPath; - }; - - const pathRedirectHandler = (pathName: string, index: number): void => { - let newPath: string | undefined; - if (fullPathOnClick) { - if (!explorerContext.selectedItems) return; - const objectData = Array.from(explorerContext.selectedItems)[0]; - if (!objectData) return; - if ('file_paths' in objectData.item && objectData) { - newPath = pathBuilder(pathInfo as string, pathName); - navigate({ - pathname: `/${libraryId}/ephemeral/0`, - search: `?path=${newPath}` - }); - } - } else if (isEphemeralLocation) { - const currentPaths = data?.map((p) => p.name).join(pathSlashOS); - newPath = `${pathSlashOS}${pathBuilder(currentPaths as string, pathName)}`; - setSearchParams((params) => ({ ...params, path: newPath })); - } else { - newPath = pathBuilder(path as string, pathName); - setSearchParams((params) => ({ ...params, path: index === 0 ? '' : newPath })); - } - }; - - const pathNameLocationName = - explorerContext.parent?.type === 'Location' && explorerContext.parent?.location.name; - const data = useMemo(() => { - if (!pathInfo) return; - const splitPaths = pathInfo?.replaceAll('/', pathSlashOS).split(pathSlashOS); //replace all '/' with '\' for windows - - //if the path is a full path - if (fullPathOnClick && queriedFullPath.data) { - if (!selectedItem) return; - const selectedItemFilePaths = - 'file_paths' in selectedItem.item && selectedItem.item.file_paths[0]; - if (!selectedItemFilePaths) return; - const updatedData = splitPaths - .map((path) => ({ - kind: 'Folder', - extension: '', - name: path - })) - //remove duplicate path names upon selection + from the result of the full path query - .filter( - (path) => - path.name !== - `${selectedItemFilePaths.name}.${selectedItemFilePaths.extension}` && - path.name !== '' && - path.name !== selectedItemFilePaths.name - ); - return updatedData; - - //handling ephemeral and location paths - } else { - let updatedPathData: string[] = []; - const nestedCount = isLocationNested(); - const startIndex = isEphemeralLocation - ? 1 - : pathNameLocationName - ? splitPaths.indexOf(pathNameLocationName) - : -1; - if (nestedCount) { - updatedPathData = splitPaths.slice(startIndex + nestedCount); - } else updatedPathData = splitPaths.slice(startIndex); - const updatedData = updatedPathData.map((path) => ({ - kind: 'Folder', - extension: '', - name: path - })); - return updatedData; - } - }, [ - pathInfo, - isLocationNested, - pathSlashOS, - isEphemeralLocation, - pathNameLocationName, - fullPathOnClick, - queriedFullPath.data, - selectedItem - ]); - - return ( -
- {data?.map((p, index) => { - return ( - p.name)} - path={p} - index={index} - fullPathOnClick={fullPathOnClick} - onClick={() => pathRedirectHandler(p.name, index)} - /> - ); - })} - {selectedItem && ( -
- {data && data.length > 0 && } - <> - - {'name' in selectedItem.item ? ( - - {selectedItem.item.name} - - ) : ( - - {selectedItem.item.file_paths[0]?.name} - - )} - -
- )} -
- ); -}); - -interface Props extends ComponentProps<'div'> { - paths: string[]; - path: { - name: string; - }; - fullPathOnClick: boolean; - index: number; -} - -const Path = ({ paths, path, fullPathOnClick, index, ...rest }: Props) => { - return ( -
- - - {path.name} - - {index !== (paths?.length as number) - 1 && ( - - )} -
- ); -}; diff --git a/interface/app/$libraryId/Explorer/View/Grid/Item.tsx b/interface/app/$libraryId/Explorer/View/Grid/Item.tsx new file mode 100644 index 000000000..fc1fda8e3 --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/Grid/Item.tsx @@ -0,0 +1,86 @@ +import { HTMLAttributes, useEffect, useMemo } from 'react'; +import { type ExplorerItem } from '@sd/client'; + +import { RenderItem } from '.'; +import { useExplorerContext } from '../../Context'; +import { getExplorerStore, isCut } from '../../store'; +import { uniqueId } from '../../util'; +import { useExplorerViewContext } from '../Context'; +import { useGridContext } from './context'; + +interface Props extends Omit, 'children'> { + index: number; + item: ExplorerItem; + children: RenderItem; +} + +export const GridItem = ({ children, item, ...props }: Props) => { + const grid = useGridContext(); + const explorer = useExplorerContext(); + const explorerView = useExplorerViewContext(); + const explorerStore = getExplorerStore(); + + const itemId = useMemo(() => uniqueId(item), [item]); + + const selected = useMemo( + // Even though this checks object equality, it should still be safe since `selectedItems` + // will be re-calculated before this memo runs. + () => explorer.selectedItems.has(item), + [explorer.selectedItems, item] + ); + + const cut = useMemo( + () => isCut(item, explorerStore.cutCopyState), + [explorerStore.cutCopyState, item] + ); + + useEffect(() => { + if (!grid.selecto?.current || !grid.selectoUnselected.current.has(itemId)) return; + + if (!selected) { + grid.selectoUnselected.current.delete(itemId); + return; + } + + const element = grid.getElementById(itemId); + + if (!element) return; + + grid.selectoUnselected.current.delete(itemId); + grid.selecto.current.setSelectedTargets([ + ...grid.selecto.current.getSelectedTargets(), + element as HTMLElement + ]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!grid.selecto) return; + + return () => { + const element = grid.getElementById(itemId); + if (selected && !element) grid.selectoUnselected.current.add(itemId); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selected]); + + return ( +
{ + if (explorerView.selectable && !explorer.selectedItems.has(item)) { + explorer.resetSelectedItems([item]); + grid.selecto?.current?.setSelectedTargets([e.currentTarget]); + } + }} + > + {children({ item: item, selected, cut })} +
+ ); +}; diff --git a/interface/app/$libraryId/Explorer/View/Grid/context.tsx b/interface/app/$libraryId/Explorer/View/Grid/context.tsx new file mode 100644 index 000000000..64494527b --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/Grid/context.tsx @@ -0,0 +1,18 @@ +import { createContext, useContext } from 'react'; +import Selecto from 'react-selecto'; + +interface GridContext { + selecto?: React.RefObject; + selectoUnselected: React.MutableRefObject>; + getElementById: (id: string) => Element | null | undefined; +} + +export const GridContext = createContext(null); + +export const useGridContext = () => { + const ctx = useContext(GridContext); + + if (ctx === null) throw new Error('GridContext.Provider not found!'); + + return ctx; +}; diff --git a/interface/app/$libraryId/Explorer/View/GridList.tsx b/interface/app/$libraryId/Explorer/View/Grid/index.tsx similarity index 77% rename from interface/app/$libraryId/Explorer/View/GridList.tsx rename to interface/app/$libraryId/Explorer/View/Grid/index.tsx index c9bd47608..c95cfc678 100644 --- a/interface/app/$libraryId/Explorer/View/GridList.tsx +++ b/interface/app/$libraryId/Explorer/View/Grid/index.tsx @@ -1,138 +1,63 @@ import { Grid, useGrid } from '@virtual-grid/react'; -import { - createContext, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, - type ReactNode -} from 'react'; +import { memo, ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import Selecto from 'react-selecto'; import { type ExplorerItem } from '@sd/client'; import { useOperatingSystem, useShortcut } from '~/hooks'; -import { useExplorerContext } from '../Context'; -import { getQuickPreviewStore, useQuickPreviewStore } from '../QuickPreview/store'; -import { getExplorerStore, isCut, useExplorerStore } from '../store'; -import { uniqueId } from '../util'; -import { useExplorerViewContext } from '../ViewContext'; +import { useExplorerContext } from '../../Context'; +import { getQuickPreviewStore, useQuickPreviewStore } from '../../QuickPreview/store'; +import { getExplorerStore } from '../../store'; +import { uniqueId } from '../../util'; +import { useExplorerViewContext } from '../Context'; +import { GridContext } from './context'; +import { GridItem } from './Item'; -const SelectoContext = createContext<{ - selecto: React.RefObject; - selectoUnSelected: React.MutableRefObject>; -} | null>(null); - -type RenderItem = (item: { item: ExplorerItem; selected: boolean; cut: boolean }) => ReactNode; - -const GridListItem = (props: { - index: number; +export type RenderItem = (item: { item: ExplorerItem; - children: RenderItem; - onMouseDown: (e: React.MouseEvent) => void; - getElementById: (id: string) => Element | null | undefined; -}) => { - const explorer = useExplorerContext(); - const explorerStore = useExplorerStore(); - const explorerView = useExplorerViewContext(); - - const selecto = useContext(SelectoContext); - - const cut = isCut(props.item, explorerStore.cutCopyState); - - const selected = useMemo( - // Even though this checks object equality, it should still be safe since `selectedItems` - // will be re-calculated before this memo runs. - () => explorer.selectedItems.has(props.item), - [explorer.selectedItems, props.item] - ); - - const itemId = uniqueId(props.item); - - useEffect(() => { - if (!selecto?.selecto.current || !selecto.selectoUnSelected.current.has(itemId)) return; - - if (!selected) { - selecto.selectoUnSelected.current.delete(itemId); - return; - } - - const element = props.getElementById(itemId); - - if (!element) return; - - selecto.selectoUnSelected.current.delete(itemId); - selecto.selecto.current.setSelectedTargets([ - ...selecto.selecto.current.getSelectedTargets(), - element as HTMLElement - ]); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (!selecto) return; - - return () => { - const element = props.getElementById(itemId); - if (selected && !element) selecto.selectoUnSelected.current.add(itemId); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selected]); - - return ( -
{ - if (explorerView.selectable && !explorer.selectedItems.has(props.item)) { - explorer.resetSelectedItems([props.item]); - selecto?.selecto.current?.setSelectedTargets([e.currentTarget]); - } - }} - > - {props.children({ item: props.item, selected, cut })} -
- ); -}; + selected: boolean; + cut: boolean; +}) => ReactNode; const CHROME_REGEX = /Chrome/; -export default ({ children }: { children: RenderItem }) => { +export default memo(({ children }: { children: RenderItem }) => { const os = useOperatingSystem(); const realOS = useOperatingSystem(true); const isChrome = CHROME_REGEX.test(navigator.userAgent); const explorer = useExplorerContext(); - const settings = explorer.useSettingsSnapshot(); const explorerView = useExplorerViewContext(); + const explorerSettings = explorer.useSettingsSnapshot(); const quickPreviewStore = useQuickPreviewStore(); const selecto = useRef(null); - const selectoUnSelected = useRef>(new Set()); + const selectoUnselected = useRef>(new Set()); const selectoFirstColumn = useRef(); const selectoLastColumn = useRef(); + // The item that further selection will move from (shift + arrow for example). + // This used to be calculated from the last item of selectedItems, + // but Set ordering isn't reliable. + // Ref bc we never actually render this. + const activeItem = useRef(null); + const [dragFromThumbnail, setDragFromThumbnail] = useState(false); - const itemDetailsHeight = 44 + (settings.showBytesInGridView ? 20 : 0); - const itemHeight = settings.gridItemSize + itemDetailsHeight; - - const padding = settings.layoutMode === 'grid' ? 12 : 0; + const itemDetailsHeight = 44 + (explorerSettings.showBytesInGridView ? 20 : 0); + const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight; + const padding = explorerSettings.layoutMode === 'grid' ? 12 : 0; const grid = useGrid({ scrollRef: explorer.scrollRef, count: explorer.items?.length ?? 0, totalCount: explorer.count, - ...(settings.layoutMode === 'grid' - ? { columns: 'auto', size: { width: settings.gridItemSize, height: itemHeight } } - : { columns: settings.mediaColumns }), + ...(explorerSettings.layoutMode === 'grid' + ? { + columns: 'auto', + size: { width: explorerSettings.gridItemSize, height: itemHeight } + } + : { columns: explorerSettings.mediaColumns }), rowVirtualizer: { overscan: explorer.overscan ?? 5 }, onLoadMore: explorer.loadMore, getItemId: useCallback( @@ -144,14 +69,11 @@ export default ({ children }: { children: RenderItem }) => { ), getItemData: useCallback((index: number) => explorer.items?.[index], [explorer.items]), padding: { - ...explorerView.padding, - bottom: explorerView.bottom - ? (explorerView.padding?.bottom ?? padding) + explorerView.bottom - : undefined, + bottom: explorerView.bottom ? padding + explorerView.bottom : undefined, x: padding, y: padding }, - gap: explorerView.gap || (settings.layoutMode === 'grid' ? settings.gridGap : undefined) + gap: explorerSettings.layoutMode === 'grid' ? explorerSettings.gridGap : 1 }); const getElementById = useCallback( @@ -201,6 +123,16 @@ export default ({ children }: { children: RenderItem }) => { return activeItem; } + function handleDragEnd() { + getExplorerStore().isDragSelecting = false; + selectoFirstColumn.current = undefined; + selectoLastColumn.current = undefined; + setDragFromThumbnail(false); + + const allSelected = selecto.current?.getSelectedTargets() ?? []; + activeItem.current = getActiveItem(allSelected); + } + useEffect( () => { const element = explorer.scrollRef.current; @@ -234,7 +166,7 @@ export default ({ children }: { children: RenderItem }) => { return selected; }); - selectoUnSelected.current = set; + selectoUnselected.current = set; selecto.current.setSelectedTargets(items as HTMLElement[]); activeItem.current = getActiveItem(items); @@ -242,16 +174,10 @@ export default ({ children }: { children: RenderItem }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [grid.columnCount, explorer.items]); - // The item that further selection will move from (shift + arrow for example). - // This used to be calculated from the last item of selectedItems, - // but Set ordering isn't reliable. - // Ref bc we never actually render this. - const activeItem = useRef(null); - useEffect(() => { if (explorer.selectedItems.size !== 0) return; - selectoUnSelected.current = new Set(); + selectoUnselected.current = new Set(); // Accessing refs during render is bad activeItem.current = null; }, [explorer.selectedItems]); @@ -289,7 +215,7 @@ export default ({ children }: { children: RenderItem }) => { } else { explorer.resetSelectedItems([newSelectedItem.data]); selecto.current?.setSelectedTargets([selectedItemElement as HTMLElement]); - if (selectoUnSelected.current.size > 0) selectoUnSelected.current = new Set(); + if (selectoUnselected.current.size > 0) selectoUnselected.current = new Set(); } } @@ -414,14 +340,14 @@ export default ({ children }: { children: RenderItem }) => { const element = getElementById(itemId); - if (!element) selectoUnSelected.current = new Set(itemId); + if (!element) selectoUnselected.current = new Set(itemId); else selecto.current.setSelectedTargets([element as HTMLElement]); activeItem.current = item; }, [explorer.items, explorer.selectedItems, quickPreviewStore.open, realOS, getElementById]); return ( - + {explorer.allowMultiSelect && ( { selectableTargets={['[data-selectable]']} toggleContinueSelect="shift" hitRate={0} - // selectFromInside={explorerStore.layoutMode === 'media'} - onDragStart={(e) => { - getExplorerStore().isDragging = true; - if ((e.inputEvent as MouseEvent).target instanceof HTMLImageElement) { + onDrag={(e) => { + if (!getExplorerStore().drag) return; + e.stop(); + handleDragEnd(); + }} + onDragStart={({ inputEvent }) => { + getExplorerStore().isDragSelecting = true; + + if ((inputEvent as MouseEvent).target instanceof HTMLImageElement) { setDragFromThumbnail(true); } }} - onDragEnd={() => { - getExplorerStore().isDragging = false; - selectoFirstColumn.current = undefined; - selectoLastColumn.current = undefined; - setDragFromThumbnail(false); - - const allSelected = selecto.current?.getSelectedTargets() ?? []; - activeItem.current = getActiveItem(allSelected); - }} + onDragEnd={handleDragEnd} onScroll={({ direction }) => { selecto.current?.findSelectableTargets(); explorer.scrollRef.current?.scrollBy( @@ -482,7 +405,7 @@ export default ({ children }: { children: RenderItem }) => { if (explorer.selectedItems.has(item.data)) { selecto.current?.setSelectedTargets(e.beforeSelected); } else { - selectoUnSelected.current = new Set(); + selectoUnselected.current = new Set(); explorer.resetSelectedItems([item.data]); } @@ -657,8 +580,8 @@ export default ({ children }: { children: RenderItem }) => { } if (unselectedItems.length > 0) { - selectoUnSelected.current = new Set([ - ...selectoUnSelected.current, + selectoUnselected.current = new Set([ + ...selectoUnselected.current, ...unselectedItems ]); } @@ -673,11 +596,10 @@ export default ({ children }: { children: RenderItem }) => { if (!item) return null; return ( - { if (e.button !== 0 || !explorerView.selectable) return; @@ -698,10 +620,10 @@ export default ({ children }: { children: RenderItem }) => { }} > {children} - + ); }} - + ); -}; +}); diff --git a/interface/app/$libraryId/Explorer/View/GridView.tsx b/interface/app/$libraryId/Explorer/View/GridView.tsx deleted file mode 100644 index 04805b14f..000000000 --- a/interface/app/$libraryId/Explorer/View/GridView.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import clsx from 'clsx'; -import { memo } from 'react'; -import { useMatch } from 'react-router'; -import { - byteSize, - getExplorerItemData, - getItemFilePath, - getItemLocation, - type ExplorerItem -} from '@sd/client'; - -import { useExplorerContext } from '../Context'; -import { FileThumb } from '../FilePath/Thumb'; -import { useExplorerViewContext } from '../ViewContext'; -import GridList from './GridList'; -import { RenamableItemText } from './RenamableItemText'; -import { ViewItem } from './ViewItem'; - -interface GridViewItemProps { - data: ExplorerItem; - selected: boolean; - isRenaming: boolean; - cut: boolean; -} - -const GridViewItem = memo(({ data, selected, cut, isRenaming }: GridViewItemProps) => { - const explorer = useExplorerContext(); - const { showBytesInGridView } = explorer.useSettingsSnapshot(); - - const explorerItemData = getExplorerItemData(data); - const filePathData = getItemFilePath(data); - const location = getItemLocation(data); - const isEphemeralLocation = useMatch('/:libraryId/ephemeral/:ephemeralId'); - const isFolder = filePathData?.is_dir; - const hidden = filePathData?.hidden; - - const showSize = - showBytesInGridView && - !location && - !isFolder && - (!isEphemeralLocation || !isFolder) && - (!isRenaming || !selected); - - return ( - - ); -}); - -export default () => { - const explorerView = useExplorerViewContext(); - - return ( - - {({ item, selected, cut }) => ( - - )} - - ); -}; diff --git a/interface/app/$libraryId/Explorer/View/GridView/Item/Context.tsx b/interface/app/$libraryId/Explorer/View/GridView/Item/Context.tsx new file mode 100644 index 000000000..cb7f9e592 --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/GridView/Item/Context.tsx @@ -0,0 +1,13 @@ +import { createContext, useContext } from 'react'; + +import { GridViewItemProps } from '.'; + +export const GridViewItemContext = createContext(null); + +export const useGridViewItemContext = () => { + const ctx = useContext(GridViewItemContext); + + if (ctx === null) throw new Error('GridViewItemContext.Provider not found!'); + + return ctx; +}; diff --git a/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx b/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx new file mode 100644 index 000000000..9277bbda2 --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx @@ -0,0 +1,128 @@ +import clsx from 'clsx'; +import { memo, useMemo } from 'react'; +import { byteSize, getItemFilePath, type ExplorerItem } from '@sd/client'; + +import { useExplorerContext } from '../../../Context'; +import { ExplorerDraggable } from '../../../ExplorerDraggable'; +import { ExplorerDroppable, useExplorerDroppableContext } from '../../../ExplorerDroppable'; +import { FileThumb } from '../../../FilePath/Thumb'; +import { useExplorerStore } from '../../../store'; +import { useExplorerDraggable } from '../../../useExplorerDraggable'; +import { RenamableItemText } from '../../RenamableItemText'; +import { ViewItem } from '../../ViewItem'; +import { GridViewItemContext, useGridViewItemContext } from './Context'; + +export interface GridViewItemProps { + data: ExplorerItem; + selected: boolean; + cut: boolean; +} + +export const GridViewItem = memo((props: GridViewItemProps) => { + const filePath = getItemFilePath(props.data); + + const isHidden = filePath?.hidden; + const isFolder = filePath?.is_dir; + const isLocation = props.data.type === 'Location'; + + return ( + + + + + + + + ); +}); + +const InnerDroppable = () => { + const item = useGridViewItemContext(); + const { isDroppable } = useExplorerDroppableContext(); + + return ( + <> +
+ +
+ + + + + + + ); +}; + +const ItemFileThumb = () => { + const item = useGridViewItemContext(); + + const { attributes, listeners, style, setDraggableRef } = useExplorerDraggable({ + data: item.data + }); + + return ( + + ); +}; + +const ItemSize = () => { + const item = useGridViewItemContext(); + const { showBytesInGridView } = useExplorerContext().useSettingsSnapshot(); + const { isRenaming } = useExplorerStore(); + + const filePath = getItemFilePath(item.data); + + const isLocation = item.data.type === 'Location'; + const isEphemeral = item.data.type === 'NonIndexedPath'; + const isFolder = filePath?.is_dir; + + const showSize = + showBytesInGridView && + filePath?.size_in_bytes_bytes && + !isLocation && + !isFolder && + (!isEphemeral || !isFolder) && + (!isRenaming || !item.selected); + + const bytes = useMemo( + () => showSize && byteSize(filePath?.size_in_bytes_bytes), + [filePath?.size_in_bytes_bytes, showSize] + ); + + if (!showSize) return null; + + return ( +
+ {`${bytes}`} +
+ ); +}; diff --git a/interface/app/$libraryId/Explorer/View/GridView/index.tsx b/interface/app/$libraryId/Explorer/View/GridView/index.tsx new file mode 100644 index 000000000..8d4c24bf0 --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/GridView/index.tsx @@ -0,0 +1,12 @@ +import Grid from '../Grid'; +import { GridViewItem } from './Item'; + +export const GridView = () => { + return ( + + {({ item, selected, cut }) => ( + + )} + + ); +}; diff --git a/interface/app/$libraryId/Explorer/View/ListView/Item.tsx b/interface/app/$libraryId/Explorer/View/ListView/Item.tsx new file mode 100644 index 000000000..89badcd9a --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/ListView/Item.tsx @@ -0,0 +1,84 @@ +import { flexRender, type Cell } from '@tanstack/react-table'; +import clsx from 'clsx'; +import { memo, useMemo } from 'react'; +import { getItemFilePath, type ExplorerItem } from '@sd/client'; + +import { TABLE_PADDING_X } from '.'; +import { ExplorerDraggable } from '../../ExplorerDraggable'; +import { ExplorerDroppable, useExplorerDroppableContext } from '../../ExplorerDroppable'; +import { ViewItem } from '../ViewItem'; +import { useTableContext } from './context'; + +interface Props { + data: ExplorerItem; + selected: boolean; + cells: Cell[]; +} + +export const ListViewItem = memo(({ data, selected, cells }: Props) => { + const filePath = getItemFilePath(data); + + return ( + + + + + {cells.map((cell) => ( + + ))} + + + + ); +}); + +const DroppableOverlay = () => { + const { isDroppable } = useExplorerDroppableContext(); + if (!isDroppable) return null; + + return
; +}; + +const Cell = ({ cell, selected }: { cell: Cell; selected: boolean }) => { + useTableContext(); // Force re-render for column sizing + + return ; +}; + +const InnerCell = memo( + (props: { cell: Cell; size: number; selected: boolean }) => { + const value = useMemo(() => props.cell.getValue(), [props.cell]); + + return ( +
+ {value + ? `${value}` + : flexRender(props.cell.column.columnDef.cell, { + ...props.cell.getContext(), + selected: props.selected + })} +
+ ); + } +); diff --git a/interface/app/$libraryId/Explorer/View/ListView/TableRow.tsx b/interface/app/$libraryId/Explorer/View/ListView/TableRow.tsx new file mode 100644 index 000000000..53c9f1d08 --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/ListView/TableRow.tsx @@ -0,0 +1,57 @@ +import { type Row } from '@tanstack/react-table'; +import clsx from 'clsx'; +import { useMemo } from 'react'; +import { type ExplorerItem } from '@sd/client'; + +import { TABLE_PADDING_X } from '.'; +import { useExplorerContext } from '../../Context'; +import { ListViewItem } from './Item'; + +interface Props { + row: Row; + previousRow?: Row; + nextRow?: Row; +} + +export const TableRow = ({ row, previousRow, nextRow }: Props) => { + const explorer = useExplorerContext(); + + const selected = useMemo(() => { + return explorer.selectedItems.has(row.original); + }, [explorer.selectedItems, row.original]); + + const isPreviousRowSelected = useMemo(() => { + if (!previousRow) return; + return explorer.selectedItems.has(previousRow.original); + }, [explorer.selectedItems, previousRow]); + + const isNextRowSelected = useMemo(() => { + if (!nextRow) return; + return explorer.selectedItems.has(nextRow.original); + }, [explorer.selectedItems, nextRow]); + + const cells = row.getVisibleCells(); + + return ( + <> +
+ {isPreviousRowSelected && ( +
+ )} +
+ + + + ); +}; diff --git a/interface/app/$libraryId/Explorer/View/ListView/context.tsx b/interface/app/$libraryId/Explorer/View/ListView/context.tsx new file mode 100644 index 000000000..c3b9f32fe --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/ListView/context.tsx @@ -0,0 +1,16 @@ +import { ColumnSizingState } from '@tanstack/react-table'; +import { createContext, useContext } from 'react'; + +interface TableContext { + columnSizing: ColumnSizingState; +} + +export const TableContext = createContext(null); + +export const useTableContext = () => { + const ctx = useContext(TableContext); + + if (ctx === null) throw new Error('TableContext.Provider not found!'); + + return ctx; +}; diff --git a/interface/app/$libraryId/Explorer/View/ListView/index.tsx b/interface/app/$libraryId/Explorer/View/ListView/index.tsx index 8b80f135d..55e38d630 100644 --- a/interface/app/$libraryId/Explorer/View/ListView/index.tsx +++ b/interface/app/$libraryId/Explorer/View/ListView/index.tsx @@ -1,112 +1,46 @@ import { CaretDown, CaretUp } from '@phosphor-icons/react'; -import { - flexRender, - VisibilityState, - type ColumnSizingState, - type Row -} from '@tanstack/react-table'; +import { flexRender, type ColumnSizingState, type Row } from '@tanstack/react-table'; import { useVirtualizer } from '@tanstack/react-virtual'; import clsx from 'clsx'; import React, { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import BasicSticky from 'react-sticky-el'; import { useWindowEventListener } from 'rooks'; import useResizeObserver from 'use-resize-observer'; -import { getItemFilePath, type ExplorerItem } from '@sd/client'; -import { ContextMenu, Tooltip } from '@sd/ui'; -import { useIsTextTruncated, useShortcut } from '~/hooks'; +import { type ExplorerItem } from '@sd/client'; +import { ContextMenu } from '@sd/ui'; +import { TruncatedText } from '~/components'; +import { useShortcut } from '~/hooks'; import { isNonEmptyObject } from '~/util'; import { useLayoutContext } from '../../../Layout/Context'; import { useExplorerContext } from '../../Context'; import { getQuickPreviewStore, useQuickPreviewStore } from '../../QuickPreview/store'; -import { - createOrdering, - getOrderingDirection, - isCut, - orderingKey, - useExplorerStore -} from '../../store'; +import { createOrdering, getOrderingDirection, orderingKey } from '../../store'; import { uniqueId } from '../../util'; -import { useExplorerViewContext } from '../../ViewContext'; -import { useExplorerViewPadding } from '../util'; -import { ViewItem } from '../ViewItem'; -import { getRangeDirection, Range, useRanges } from './util/ranges'; -import { useTable } from './util/table'; - -interface ListViewItemProps { - row: Row; - paddingLeft: number; - paddingRight: number; - // Props below are passed to trigger a rerender - // TODO: Find a better solution - columnSizing: ColumnSizingState; - columnVisibility: VisibilityState; - isCut: boolean; -} - -const ListViewItem = memo((props: ListViewItemProps) => { - const filePathData = getItemFilePath(props.row.original); - const hidden = filePathData?.hidden ?? false; - - return ( - - {props.row.getVisibleCells().map((cell) => ( - - ))} - - ); -}); - -const HeaderColumnName = ({ name }: { name: string }) => { - const textRef = useRef(null); - - const isTruncated = useIsTextTruncated(textRef); - - return ( -
- {isTruncated ? ( - - {name} - - ) : ( - {name} - )} -
- ); -}; +import { useExplorerViewContext } from '../Context'; +import { useDragScrollable } from '../useDragScrollable'; +import { TableContext } from './context'; +import { TableRow } from './TableRow'; +import { getRangeDirection, Range, useRanges } from './useRanges'; +import { useTable } from './useTable'; const ROW_HEIGHT = 45; -const PADDING_X = 16; -const PADDING_Y = 12; +export const TABLE_PADDING_X = 16; +export const TABLE_PADDING_Y = 12; -export default () => { +export const ListView = memo(() => { const layout = useLayoutContext(); const explorer = useExplorerContext(); - const explorerStore = useExplorerStore(); const explorerView = useExplorerViewContext(); - const settings = explorer.useSettingsSnapshot(); + const explorerSettings = explorer.useSettingsSnapshot(); const quickPreview = useQuickPreviewStore(); const tableRef = useRef(null); - const tableHeaderRef = useRef(null); + const tableHeaderRef = useRef(null); const tableBodyRef = useRef(null); + const { ref: scrollableRef } = useDragScrollable({ direction: 'up' }); + const [sized, setSized] = useState(false); const [initialized, setInitialized] = useState(false); const [locked, setLocked] = useState(false); @@ -125,23 +59,12 @@ export default () => { rows: rowsById }); - const viewPadding = useExplorerViewPadding(explorerView.padding); - - const padding = { - top: viewPadding.top ?? PADDING_Y, - bottom: viewPadding.bottom ?? PADDING_Y, - left: viewPadding.left ?? PADDING_X, - right: viewPadding.right ?? PADDING_X - }; - - const count = !explorer.count ? rows.length : Math.max(rows.length, explorer.count); - const rowVirtualizer = useVirtualizer({ - count: count, + count: !explorer.count ? rows.length : Math.max(rows.length, explorer.count), getScrollElement: useCallback(() => explorer.scrollRef.current, [explorer.scrollRef]), estimateSize: useCallback(() => ROW_HEIGHT, []), - paddingStart: padding.top, - paddingEnd: padding.bottom + (explorerView.bottom ?? 0), + paddingStart: TABLE_PADDING_Y, + paddingEnd: TABLE_PADDING_Y + (explorerView.bottom ?? 0), scrollMargin: listOffset, overscan: explorer.overscan ?? 10 }); @@ -424,7 +347,7 @@ export default () => { } }; - function handleRowContextMenu(row: Row) { + const handleRowContextMenu = (row: Row) => { if (explorerView.contextMenu === undefined) return; const item = row.original; @@ -434,7 +357,7 @@ export default () => { const hash = uniqueId(item); setRanges([[hash, hash]]); } - } + }; const scrollToRow = useCallback( (row: Row) => { @@ -457,14 +380,14 @@ export default () => { const rowBottom = rowTop + ROW_HEIGHT; if (rowTop < tableTop) { - const scrollBy = rowTop - tableTop - (row.index === 0 ? padding.top : 0); + const scrollBy = rowTop - tableTop - (row.index === 0 ? TABLE_PADDING_Y : 0); explorer.scrollRef.current.scrollBy({ top: scrollBy }); } else if (rowBottom > scrollRect.height - (explorerView.bottom ?? 0)) { const scrollBy = rowBottom - scrollRect.height + (explorerView.bottom ?? 0) + - (row.index === rows.length - 1 ? padding.bottom : 0); + (row.index === rows.length - 1 ? TABLE_PADDING_Y : 0); explorer.scrollRef.current.scrollBy({ top: scrollBy }); } @@ -473,139 +396,12 @@ export default () => { explorer.scrollRef, explorerView.bottom, explorerView.top, - padding.bottom, - padding.top, rowVirtualizer.options.paddingStart, rows.length, top ] ); - useEffect(() => setRanges([]), [settings.order]); - - useEffect(() => { - if (!getQuickPreviewStore().open || explorer.selectedItems.size !== 1) return; - - const [item] = [...explorer.selectedItems]; - if (!item) return; - - const itemId = uniqueId(item); - setRanges([[itemId, itemId]]); - }, [explorer.selectedItems]); - - useEffect(() => { - if (initialized || !sized || !explorer.count || explorer.selectedItems.size === 0) { - if (explorer.selectedItems.size === 0 && !initialized) setInitialized(true); - return; - } - - const rows = [...explorer.selectedItems] - .reduce((rows, item) => { - const row = rowsById[uniqueId(item)]; - if (row) rows.push(row); - return rows; - }, [] as Row[]) - .sort((a, b) => a.index - b.index); - - const lastRow = rows[rows.length - 1]; - if (!lastRow) return; - - scrollToRow(lastRow); - setRanges(rows.map((row) => [uniqueId(row.original), uniqueId(row.original)] as Range)); - setInitialized(true); - }, [explorer.count, explorer.selectedItems, initialized, rowsById, scrollToRow, sized]); - - // Measure initial column widths - useEffect(() => { - if ( - !tableRef.current || - sized || - !isNonEmptyObject(columnSizing) || - !isNonEmptyObject(columnVisibility) - ) { - return; - } - - const sizing = table - .getVisibleLeafColumns() - .reduce( - (sizing, column) => ({ ...sizing, [column.id]: column.getSize() }), - {} as ColumnSizingState - ); - - const tableWidth = tableRef.current.offsetWidth; - const columnsWidth = - Object.values(sizing).reduce((a, b) => a + b, 0) + (padding.left + padding.right); - - if (columnsWidth < tableWidth) { - const nameWidth = (sizing.name ?? 0) + (tableWidth - columnsWidth); - table.setColumnSizing({ ...sizing, name: nameWidth }); - setLocked(true); - } else if (columnsWidth > tableWidth) { - const nameColSize = sizing.name ?? 0; - const minNameColSize = table.getColumn('name')?.columnDef.minSize; - - const difference = columnsWidth - tableWidth; - - if (minNameColSize !== undefined && nameColSize - difference >= minNameColSize) { - table.setColumnSizing({ ...sizing, name: nameColSize - difference }); - setLocked(true); - } - } else if (columnsWidth === tableWidth) { - setLocked(true); - } - - setSized(true); - }, [columnSizing, columnVisibility, padding.left, padding.right, sized, table]); - - // Load more items - useEffect(() => { - if (!explorer.loadMore) return; - - const lastRow = virtualRows[virtualRows.length - 1]; - if (!lastRow) return; - - const loadMoreFromRow = Math.ceil(rows.length * 0.75); - - if (lastRow.index >= loadMoreFromRow - 1) explorer.loadMore.call(undefined); - }, [virtualRows, rows.length, explorer.loadMore]); - - // Sync scroll - useEffect(() => { - const table = tableRef.current; - const header = tableHeaderRef.current; - const body = tableBodyRef.current; - - if (!table || !header || !body || quickPreview.open) return; - - const handleWheel = (event: WheelEvent) => { - if (Math.abs(event.deltaX) < Math.abs(event.deltaY)) return; - event.preventDefault(); - header.scrollLeft += event.deltaX; - body.scrollLeft += event.deltaX; - }; - - const handleScroll = (element: HTMLDivElement) => { - if (isLeftMouseDown) return; - // Sorting sometimes resets scrollLeft - // so we reset it here in case it does - // to keep the scroll in sync - // TODO: Find a better solution - header.scrollLeft = element.scrollLeft; - body.scrollLeft = element.scrollLeft; - }; - - table.addEventListener('wheel', handleWheel); - header.addEventListener('scroll', () => handleScroll(header)); - body.addEventListener('scroll', () => handleScroll(body)); - - return () => { - table.removeEventListener('wheel', handleWheel); - header.addEventListener('scroll', () => handleScroll(header)); - body.addEventListener('scroll', () => handleScroll(body)); - }; - }, [sized, isLeftMouseDown]); - const keyboardHandler = (e: KeyboardEvent, direction: 'ArrowDown' | 'ArrowUp') => { if (!explorerView.selectable) return; @@ -760,6 +556,130 @@ export default () => { scrollToRow(nextRow); }; + useEffect(() => setRanges([]), [explorerSettings.order]); + + useEffect(() => { + if (!getQuickPreviewStore().open || explorer.selectedItems.size !== 1) return; + + const [item] = [...explorer.selectedItems]; + if (!item) return; + + const itemId = uniqueId(item); + setRanges([[itemId, itemId]]); + }, [explorer.selectedItems]); + + useEffect(() => { + if (initialized || !sized || !explorer.count || explorer.selectedItems.size === 0) { + if (explorer.selectedItems.size === 0 && !initialized) setInitialized(true); + return; + } + + const rows = [...explorer.selectedItems] + .reduce((rows, item) => { + const row = rowsById[uniqueId(item)]; + if (row) rows.push(row); + return rows; + }, [] as Row[]) + .sort((a, b) => a.index - b.index); + + const lastRow = rows[rows.length - 1]; + if (!lastRow) return; + + scrollToRow(lastRow); + setRanges(rows.map((row) => [uniqueId(row.original), uniqueId(row.original)] as Range)); + setInitialized(true); + }, [explorer.count, explorer.selectedItems, initialized, rowsById, scrollToRow, sized]); + + // Measure initial column widths + useEffect(() => { + if ( + !tableRef.current || + sized || + !isNonEmptyObject(columnSizing) || + !isNonEmptyObject(columnVisibility) + ) { + return; + } + + const sizing = table + .getVisibleLeafColumns() + .reduce( + (sizing, column) => ({ ...sizing, [column.id]: column.getSize() }), + {} as ColumnSizingState + ); + + const tableWidth = tableRef.current.offsetWidth; + const columnsWidth = Object.values(sizing).reduce((a, b) => a + b, 0) + TABLE_PADDING_X * 2; + + if (columnsWidth < tableWidth) { + const nameWidth = (sizing.name ?? 0) + (tableWidth - columnsWidth); + table.setColumnSizing({ ...sizing, name: nameWidth }); + setLocked(true); + } else if (columnsWidth > tableWidth) { + const nameColSize = sizing.name ?? 0; + const minNameColSize = table.getColumn('name')?.columnDef.minSize; + + const difference = columnsWidth - tableWidth; + + if (minNameColSize !== undefined && nameColSize - difference >= minNameColSize) { + table.setColumnSizing({ ...sizing, name: nameColSize - difference }); + setLocked(true); + } + } else if (columnsWidth === tableWidth) { + setLocked(true); + } + + setSized(true); + }, [columnSizing, columnVisibility, sized, table]); + + // Load more items + useEffect(() => { + if (!explorer.loadMore) return; + + const lastRow = virtualRows[virtualRows.length - 1]; + if (!lastRow) return; + + const loadMoreFromRow = Math.ceil(rows.length * 0.75); + + if (lastRow.index >= loadMoreFromRow - 1) explorer.loadMore.call(undefined); + }, [virtualRows, rows.length, explorer.loadMore]); + + // Sync scroll + useEffect(() => { + const table = tableRef.current; + const header = tableHeaderRef.current; + const body = tableBodyRef.current; + + if (!table || !header || !body || quickPreview.open) return; + + const handleWheel = (event: WheelEvent) => { + if (Math.abs(event.deltaX) < Math.abs(event.deltaY)) return; + event.preventDefault(); + header.scrollLeft += event.deltaX; + body.scrollLeft += event.deltaX; + }; + + const handleScroll = (element: HTMLDivElement) => { + if (isLeftMouseDown) return; + // Sorting sometimes resets scrollLeft + // so we reset it here in case it does + // to keep the scroll in sync + // TODO: Find a better solution + header.scrollLeft = element.scrollLeft; + body.scrollLeft = element.scrollLeft; + }; + + table.addEventListener('wheel', handleWheel); + header.addEventListener('scroll', () => handleScroll(header)); + body.addEventListener('scroll', () => handleScroll(body)); + + return () => { + table.removeEventListener('wheel', handleWheel); + header.addEventListener('scroll', () => handleScroll(header)); + body.addEventListener('scroll', () => handleScroll(body)); + }; + }, [sized, isLeftMouseDown, quickPreview.open]); + useShortcut('explorerEscape', () => { if (!explorerView.selectable || explorer.selectedItems.size === 0) return; explorer.resetSelectedItems([]); @@ -799,7 +719,7 @@ export default () => { ); const columnsWidth = - Object.values(sizing).reduce((a, b) => a + b, 0) + (padding.left + padding.right); + Object.values(sizing).reduce((a, b) => a + b, 0) + TABLE_PADDING_X * 2; if (locked) { const newNameSize = (sizing.name ?? 0) + (width - columnsWidth); @@ -843,263 +763,237 @@ export default () => { useLayoutEffect(() => setListOffset(tableRef.current?.offsetTop ?? 0), []); return ( -
{ - if (e.button !== 0) return; - - e.stopPropagation(); - setIsLeftMouseDown(true); - }} - className={clsx(!initialized && 'invisible')} - > - {sized && ( - <> - - - {table.getHeaderGroups().map((headerGroup) => ( -
- {headerGroup.headers.map((header, i) => { - const size = header.column.getSize(); - - const orderKey = - settings.order && orderingKey(settings.order); - - const orderingDirection = - orderKey && - settings.order && - (orderKey.startsWith('object.') - ? orderKey.split('object.')[1] === header.id - : orderKey === header.id) - ? getOrderingDirection(settings.order) - : null; - - const cellContent = flexRender( - header.column.columnDef.header, - header.getContext() - ); - - return ( -
{ - if (resizing) return; - - // Split table into smaller parts - // cause this looks hideous - const orderKey = - explorer.orderingKeys?.options.find( - (o) => { - if ( - typeof o.value !== - 'string' - ) - return; - - const value = - o.value as string; - - return value.startsWith( - 'object.' - ) - ? value.split( - 'object.' - )[1] === header.id - : value === header.id; - } - ); - - if (!orderKey) return; - - explorer.settingsStore.order = - createOrdering( - orderKey.value, - orderingDirection === 'Asc' - ? 'Desc' - : 'Asc' - ); - }} - > - {header.isPlaceholder ? null : ( - <> - {typeof cellContent === 'string' ? ( - - ) : ( - cellContent - )} - - {orderingDirection === 'Asc' && ( - - )} - - {orderingDirection === 'Desc' && ( - - )} - -
{ - setResizing(true); - setLocked(false); - - header.getResizeHandler()( - e - ); - - if (layout.ref.current) { - layout.ref.current.style.cursor = - 'col-resize'; - } - }} - onTouchStart={header.getResizeHandler()} - className="absolute right-0 h-[70%] w-2 cursor-col-resize border-r border-sidebar-divider" - /> - - )} -
- ); - })} -
- ))} -
- } + +
{ + if (e.button !== 0) return; + e.stopPropagation(); + setIsLeftMouseDown(true); + }} + className={clsx(!initialized && 'invisible')} + > + {sized && ( + <> + - {table.getAllLeafColumns().map((column) => { - if (column.id === 'name') return null; - return ( - - ); - })} - - - -
-
- {virtualRows.map((virtualRow) => { - const row = rows[virtualRow.index]; - if (!row) return null; - - const selected = explorer.isItemSelected(row.original); - const cut = isCut(row.original, explorerStore.cutCopyState); - - const previousRow = rows[virtualRow.index - 1]; - const nextRow = rows[virtualRow.index + 1]; - - const selectedPrior = - previousRow && explorer.isItemSelected(previousRow.original); - - const selectedNext = - nextRow && explorer.isItemSelected(nextRow.original); - - return ( + { + tableHeaderRef.current = element; + scrollableRef(element); }} - onMouseDown={(e) => handleRowClick(e, row)} - onContextMenu={() => handleRowContextMenu(row)} + className={clsx( + 'top-bar-blur !border-sidebar-divider bg-app/90', + explorerView.listViewOptions?.hideHeaderBorder + ? 'border-b' + : 'border-y', + // Prevent drag scroll + isLeftMouseDown + ? 'overflow-hidden' + : 'no-scrollbar overflow-x-auto overscroll-x-none' + )} > -
- {selectedPrior && ( -
- )} -
+ {table.getHeaderGroups().map((headerGroup) => ( +
+ {headerGroup.headers.map((header, i) => { + const size = header.column.getSize(); - + const orderKey = + explorerSettings.order && + orderingKey(explorerSettings.order); + + const orderingDirection = + orderKey && + explorerSettings.order && + (orderKey.startsWith('object.') + ? orderKey.split('object.')[1] === + header.id + : orderKey === header.id) + ? getOrderingDirection( + explorerSettings.order + ) + : null; + + const cellContent = flexRender( + header.column.columnDef.header, + header.getContext() + ); + + return ( +
{ + if (resizing) return; + + // Split table into smaller parts + // cause this looks hideous + const orderKey = + explorer.orderingKeys?.options.find( + (o) => { + if ( + typeof o.value !== + 'string' + ) + return; + + const value = + o.value as string; + + return value.startsWith( + 'object.' + ) + ? value.split( + 'object.' + )[1] === header.id + : value === + header.id; + } + ); + + if (!orderKey) return; + + explorer.settingsStore.order = + createOrdering( + orderKey.value, + orderingDirection === 'Asc' + ? 'Desc' + : 'Asc' + ); + }} + > + {header.isPlaceholder ? null : ( + <> + + {cellContent} + + + {orderingDirection === + 'Asc' && ( + + )} + + {orderingDirection === + 'Desc' && ( + + )} + +
{ + setResizing(true); + setLocked(false); + + header.getResizeHandler()( + e + ); + + if ( + layout.ref.current + ) { + layout.ref.current.style.cursor = + 'col-resize'; + } + }} + onTouchStart={header.getResizeHandler()} + className="absolute right-0 h-[70%] w-2 cursor-col-resize border-r border-sidebar-divider" + /> + + )} +
+ ); + })} +
+ ))}
- ); - })} + } + > + {table.getAllLeafColumns().map((column) => { + if (column.id === 'name') return null; + return ( + + ); + })} + + + +
+
+ {virtualRows.map((virtualRow) => { + const row = rows[virtualRow.index]; + if (!row) return null; + + const previousRow = rows[virtualRow.index - 1]; + const nextRow = rows[virtualRow.index + 1]; + + return ( +
handleRowClick(e, row)} + onContextMenu={() => handleRowContextMenu(row)} + > + +
+ ); + })} +
-
- - )} -
+ + )} +
+ ); -}; +}); diff --git a/interface/app/$libraryId/Explorer/View/ListView/util/ranges.tsx b/interface/app/$libraryId/Explorer/View/ListView/useRanges.tsx similarity index 100% rename from interface/app/$libraryId/Explorer/View/ListView/util/ranges.tsx rename to interface/app/$libraryId/Explorer/View/ListView/useRanges.tsx diff --git a/interface/app/$libraryId/Explorer/View/ListView/util/table.tsx b/interface/app/$libraryId/Explorer/View/ListView/useTable.tsx similarity index 77% rename from interface/app/$libraryId/Explorer/View/ListView/util/table.tsx rename to interface/app/$libraryId/Explorer/View/ListView/useTable.tsx index dd955a493..0c7fb981e 100644 --- a/interface/app/$libraryId/Explorer/View/ListView/util/table.tsx +++ b/interface/app/$libraryId/Explorer/View/ListView/useTable.tsx @@ -1,4 +1,5 @@ import { + CellContext, getCoreRowModel, useReactTable, type ColumnDef, @@ -7,7 +8,7 @@ import { } from '@tanstack/react-table'; import clsx from 'clsx'; import dayjs from 'dayjs'; -import { useEffect, useMemo, useState } from 'react'; +import { memo, useEffect, useMemo, useState } from 'react'; import { stringify } from 'uuid'; import { byteSize, @@ -19,16 +20,44 @@ import { } from '@sd/client'; import { isNonEmptyObject } from '~/util'; -import { useExplorerContext } from '../../../Context'; -import { FileThumb } from '../../../FilePath/Thumb'; -import { InfoPill } from '../../../Inspector'; -import { isCut, useExplorerStore } from '../../../store'; -import { uniqueId } from '../../../util'; -import { RenamableItemText } from '../../RenamableItemText'; +import { useExplorerContext } from '../../Context'; +import { FileThumb } from '../../FilePath/Thumb'; +import { InfoPill } from '../../Inspector'; +import { CutCopyState, isCut, useExplorerStore } from '../../store'; +import { uniqueId } from '../../util'; +import { RenamableItemText } from '../RenamableItemText'; + +const NameCell = memo(({ item, selected }: { item: ExplorerItem; selected: boolean }) => { + const { cutCopyState } = useExplorerStore(); + + const cut = useMemo(() => isCut(item, cutCopyState as CutCopyState), [cutCopyState, item]); + + return ( +
+ + + +
+ ); +}); + +type Cell = CellContext & { selected?: boolean }; export const useTable = () => { const explorer = useExplorerContext(); - const explorerStore = useExplorerStore(); const [columnSizing, setColumnSizing] = useState({}); const [columnVisibility, setColumnVisibility] = useState({}); @@ -40,29 +69,9 @@ export const useTable = () => { header: 'Name', minSize: 200, maxSize: undefined, - cell: ({ row }) => { - const item = row.original; - const cut = isCut(item, explorerStore.cutCopyState); - - return ( -
- - - -
- ); - } + cell: ({ row, selected }: Cell) => ( + + ) }, { id: 'kind', @@ -132,7 +141,7 @@ export const useTable = () => { } } ], - [explorerStore.cutCopyState] + [] ); const table = useReactTable({ diff --git a/interface/app/$libraryId/Explorer/View/MediaView.tsx b/interface/app/$libraryId/Explorer/View/MediaView.tsx deleted file mode 100644 index 4031e4e98..000000000 --- a/interface/app/$libraryId/Explorer/View/MediaView.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { ArrowsOutSimple } from '@phosphor-icons/react'; -import clsx from 'clsx'; -import { memo } from 'react'; -import { ExplorerItem, getItemFilePath } from '@sd/client'; -import { Button } from '@sd/ui'; - -import { useExplorerContext } from '../Context'; -import { FileThumb } from '../FilePath/Thumb'; -import { getQuickPreviewStore } from '../QuickPreview/store'; -import GridList from './GridList'; -import { ViewItem } from './ViewItem'; - -interface MediaViewItemProps { - data: ExplorerItem; - selected: boolean; - cut: boolean; -} - -const MediaViewItem = memo(({ data, selected, cut }: MediaViewItemProps) => { - const settings = useExplorerContext().useSettingsSnapshot(); - const filePathData = getItemFilePath(data); - const hidden = filePathData?.hidden ?? false; - - return ( - - ); -}); - -export default () => { - return ( - - {({ item, selected, cut }) => ( - - )} - - ); -}; diff --git a/interface/app/$libraryId/Explorer/View/MediaView/Item.tsx b/interface/app/$libraryId/Explorer/View/MediaView/Item.tsx new file mode 100644 index 000000000..554dbf7c2 --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/MediaView/Item.tsx @@ -0,0 +1,67 @@ +import { ArrowsOutSimple } from '@phosphor-icons/react'; +import clsx from 'clsx'; +import { memo } from 'react'; +import { ExplorerItem, getItemFilePath } from '@sd/client'; +import { Button } from '@sd/ui'; + +import { FileThumb } from '../../FilePath/Thumb'; +import { getQuickPreviewStore } from '../../QuickPreview/store'; +import { useExplorerDraggable } from '../../useExplorerDraggable'; +import { ViewItem } from '../ViewItem'; + +interface Props { + data: ExplorerItem; + selected: boolean; + cut: boolean; + cover: boolean; +} + +export const MediaViewItem = memo(({ data, selected, cut, cover }: Props) => { + return ( + + + + + + ); +}); + +const ItemFileThumb = (props: Pick) => { + const filePath = getItemFilePath(props.data); + + const { attributes, listeners, style, setDraggableRef } = useExplorerDraggable({ + data: props.data + }); + + return ( + + ); +}; diff --git a/interface/app/$libraryId/Explorer/View/MediaView/index.tsx b/interface/app/$libraryId/Explorer/View/MediaView/index.tsx new file mode 100644 index 000000000..83d44d66e --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/MediaView/index.tsx @@ -0,0 +1,20 @@ +import { useExplorerContext } from '../../Context'; +import Grid from '../Grid'; +import { MediaViewItem } from './Item'; + +export const MediaView = () => { + const explorerSettings = useExplorerContext().useSettingsSnapshot(); + + return ( + + {({ item, selected, cut }) => ( + + )} + + ); +}; diff --git a/interface/app/$libraryId/Explorer/View/RenamableItemText.tsx b/interface/app/$libraryId/Explorer/View/RenamableItemText.tsx index d3efbc793..371add27b 100644 --- a/interface/app/$libraryId/Explorer/View/RenamableItemText.tsx +++ b/interface/app/$libraryId/Explorer/View/RenamableItemText.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import { useMemo, useRef } from 'react'; +import { useRef } from 'react'; import { getEphemeralPath, getExplorerItemData, @@ -12,31 +12,31 @@ import { toast } from '@sd/ui'; import { useIsDark } from '~/hooks'; import { useExplorerContext } from '../Context'; -import { RenameTextBox } from '../FilePath/RenameTextBox'; +import { RenameTextBox, RenameTextBoxProps } from '../FilePath/RenameTextBox'; import { useQuickPreviewStore } from '../QuickPreview/store'; +import { useExplorerStore } from '../store'; -interface Props { +interface Props extends Pick { item: ExplorerItem; allowHighlight?: boolean; style?: React.CSSProperties; - lines?: number; + highlight?: boolean; + selected?: boolean; } -export const RenamableItemText = ({ item, allowHighlight = true, style, lines }: Props) => { - const rspc = useRspcLibraryContext(); - const explorer = useExplorerContext(); - const quickPreviewStore = useQuickPreviewStore(); +export const RenamableItemText = ({ allowHighlight = true, ...props }: Props) => { const isDark = useIsDark(); + const rspc = useRspcLibraryContext(); - const itemData = getExplorerItemData(item); + const explorer = useExplorerContext({ suspense: false }); + const explorerStore = useExplorerStore(); + + const quickPreviewStore = useQuickPreviewStore(); + + const itemData = getExplorerItemData(props.item); const ref = useRef(null); - const selected = useMemo( - () => explorer.selectedItems.has(item), - [explorer.selectedItems, item] - ); - const renameFile = useLibraryMutation(['files.renameFile'], { onError: () => reset(), onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths']) @@ -59,9 +59,9 @@ export const RenamableItemText = ({ item, allowHighlight = true, style, lines }: const handleRename = async (newName: string) => { try { - switch (item.type) { + switch (props.item.type) { case 'Location': { - const locationId = item.item.id; + const locationId = props.item.item.id; if (!locationId) throw new Error('Missing location id'); await renameLocation.mutateAsync({ @@ -79,7 +79,7 @@ export const RenamableItemText = ({ item, allowHighlight = true, style, lines }: case 'Path': case 'Object': { - const filePathData = getIndexedItemFilePath(item); + const filePathData = getIndexedItemFilePath(props.item); if (!filePathData) throw new Error('Failed to get file path object'); @@ -101,7 +101,7 @@ export const RenamableItemText = ({ item, allowHighlight = true, style, lines }: } case 'NonIndexedPath': { - const ephemeralFile = getEphemeralPath(item); + const ephemeralFile = getEphemeralPath(props.item); if (!ephemeralFile) throw new Error('Failed to get ephemeral file object'); @@ -130,10 +130,12 @@ export const RenamableItemText = ({ item, allowHighlight = true, style, lines }: }; const disabled = - !selected || + !props.selected || + explorerStore.drag?.type === 'dragging' || + !explorer || explorer.selectedItems.size > 1 || quickPreviewStore.open || - item.type === 'SpacedropPeer'; + props.item.type === 'SpacedropPeer'; return ( ); }; diff --git a/interface/app/$libraryId/Explorer/View/ViewItem.tsx b/interface/app/$libraryId/Explorer/View/ViewItem.tsx index 06f78d853..64b171040 100644 --- a/interface/app/$libraryId/Explorer/View/ViewItem.tsx +++ b/interface/app/$libraryId/Explorer/View/ViewItem.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, type HTMLAttributes, type PropsWithChildren } from 'react'; +import { useCallback, type HTMLAttributes, type PropsWithChildren } from 'react'; import { createSearchParams, useNavigate } from 'react-router-dom'; import { isPath, @@ -15,8 +15,9 @@ import { usePlatform } from '~/util/Platform'; import { useExplorerContext } from '../Context'; import { getQuickPreviewStore } from '../QuickPreview/store'; +import { getExplorerStore } from '../store'; import { uniqueId } from '../util'; -import { useExplorerViewContext } from '../ViewContext'; +import { useExplorerViewContext } from './Context'; export const useViewItemDoubleClick = () => { const navigate = useNavigate(); @@ -58,7 +59,7 @@ export const useViewItemDoubleClick = () => { : selectedItem.item.file_paths; for (const filePath of paths) { - if (isPath(selectedItem) && selectedItem.item.is_dir) { + if (filePath.is_dir) { items.dirs.splice(sameAsClicked ? 0 : -1, 0, filePath); } else { items.paths.splice(sameAsClicked ? 0 : -1, 0, filePath); @@ -176,7 +177,7 @@ interface ViewItemProps extends PropsWithChildren, HTMLAttributes { +export const ViewItem = ({ data, children, ...props }: ViewItemProps) => { const explorerView = useExplorerViewContext(); const { doubleClick } = useViewItemDoubleClick(); @@ -184,15 +185,15 @@ export const ViewItem = memo(({ data, children, ...props }: ViewItemProps) => { return ( doubleClick(data)} {...props}> +
doubleClick(data)}> {children}
} - onOpenChange={explorerView.setIsContextMenuOpen} + onOpenChange={(open) => (getExplorerStore().isContextMenuOpen = open)} disabled={explorerView.contextMenu === undefined} onMouseDown={(e) => e.stopPropagation()} > {explorerView.contextMenu}
); -}); +}; diff --git a/interface/app/$libraryId/Explorer/View/index.tsx b/interface/app/$libraryId/Explorer/View/index.tsx index 95a30f4ba..6ce612eb2 100644 --- a/interface/app/$libraryId/Explorer/View/index.tsx +++ b/interface/app/$libraryId/Explorer/View/index.tsx @@ -1,25 +1,10 @@ -import { Columns, GridFour, MonitorPlay, Rows, type Icon } from '@phosphor-icons/react'; -import clsx from 'clsx'; -import { - isValidElement, - memo, - useCallback, - useEffect, - useRef, - useState, - type ReactNode -} from 'react'; +import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { - ExplorerLayout, - getExplorerLayoutStore, - getItemObject, - useExplorerLayoutStore, - type Object -} from '@sd/client'; -import { dialogManager, ModifierKeys } from '@sd/ui'; +import { useKeys } from 'rooks'; +import { ExplorerLayout, getExplorerLayoutStore, getItemObject, type Object } from '@sd/client'; +import { dialogManager } from '@sd/ui'; import { Loader } from '~/components'; -import { useKeyCopyCutPaste, useOperatingSystem, useShortcut } from '~/hooks'; +import { useKeyCopyCutPaste, useKeyMatcher, useShortcut } from '~/hooks'; import { isNonEmpty } from '~/util'; import CreateDialog from '../../settings/library/tags/CreateDialog'; @@ -27,226 +12,196 @@ import { useExplorerContext } from '../Context'; import { QuickPreview } from '../QuickPreview'; import { useQuickPreviewContext } from '../QuickPreview/Context'; import { getQuickPreviewStore, useQuickPreviewStore } from '../QuickPreview/store'; -import { ViewContext, type ExplorerViewContext } from '../ViewContext'; -import GridView from './GridView'; -import ListView from './ListView'; -import MediaView from './MediaView'; -import { useExplorerViewPadding } from './util'; +import { getExplorerStore, useExplorerStore } from '../store'; +import { useExplorerDroppable } from '../useExplorerDroppable'; +import { useExplorerSearchParams } from '../util'; +import { ViewContext, type ExplorerViewContext } from './Context'; +import { DragScrollable } from './DragScrollable'; +import { GridView } from './GridView'; +import { ListView } from './ListView'; +import { MediaView } from './MediaView'; import { useViewItemDoubleClick } from './ViewItem'; -export interface ExplorerViewPadding { - x?: number; - y?: number; - top?: number; - bottom?: number; - left?: number; - right?: number; -} - export interface ExplorerViewProps - extends Omit< - ExplorerViewContext, - | 'selectable' - | 'isRenaming' - | 'isContextMenuOpen' - | 'setIsRenaming' - | 'setIsContextMenuOpen' - | 'ref' - | 'padding' - > { - className?: string; - style?: React.CSSProperties; + extends Omit { emptyNotice?: JSX.Element; - padding?: number | ExplorerViewPadding; } -export default memo( - ({ className, style, emptyNotice, padding, ...contextProps }: ExplorerViewProps) => { - const explorer = useExplorerContext(); - const quickPreview = useQuickPreviewContext(); - const quickPreviewStore = useQuickPreviewStore(); - const layoutStore = useExplorerLayoutStore(); - const { doubleClick } = useViewItemDoubleClick(); +export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => { + const explorer = useExplorerContext(); + const explorerStore = useExplorerStore(); + const { layoutMode } = explorer.useSettingsSnapshot(); - const { layoutMode } = explorer.useSettingsSnapshot(); + const quickPreview = useQuickPreviewContext(); + const quickPreviewStore = useQuickPreviewStore(); - const ref = useRef(null); + const [{ path }] = useExplorerSearchParams(); - const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); - const [isRenaming, setIsRenaming] = useState(false); - const [showLoading, setShowLoading] = useState(false); + const ref = useRef(null); - const viewPadding = useExplorerViewPadding(padding); + const [showLoading, setShowLoading] = useState(false); - useKeyDownHandlers({ - disabled: isRenaming || quickPreviewStore.open - }); + const selectable = + explorer.selectable && + !explorerStore.isContextMenuOpen && + !explorerStore.isRenaming && + !quickPreviewStore.open; - useEffect(() => { - if (!isContextMenuOpen || explorer.selectedItems.size !== 0) return; - // Close context menu when no items are selected - document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); - setIsContextMenuOpen(false); - }, [explorer.selectedItems, isContextMenuOpen]); + // Can stay here until we add columns view + // Once added, the provided parent related logic should move to useExplorerDroppable + // that way we don't have to re-use the same logic for each view + const { setDroppableRef } = useExplorerDroppable({ + ...(explorer.parent?.type === 'Location' && { + allow: ['Path', 'NonIndexedPath'], + data: { type: 'location', path: path ?? '/', data: explorer.parent.location }, + disabled: + explorerStore.drag?.type === 'dragging' && + explorer.parent.location.id === explorerStore.drag.sourceLocationId && + (path ?? '/') === explorerStore.drag.sourcePath + }), + ...(explorer.parent?.type === 'Ephemeral' && { + allow: ['Path', 'NonIndexedPath'], + data: { type: 'location', path: explorer.parent.path }, + disabled: + explorerStore.drag?.type === 'dragging' && + explorer.parent.path === explorerStore.drag.sourcePath + }), + ...(explorer.parent?.type === 'Tag' && { + allow: 'Path', + data: { type: 'tag', data: explorer.parent.tag }, + disabled: + explorerStore.drag?.type === 'dragging' && + explorer.parent.tag.id === explorerStore.drag.sourceTagId + }) + }); - useEffect(() => { - if (explorer.isFetchingNextPage) { - const timer = setTimeout(() => setShowLoading(true), 100); - return () => clearTimeout(timer); - } else setShowLoading(false); - }, [explorer.isFetchingNextPage]); + useShortcuts(); - useEffect(() => { - if (explorer.layouts[layoutMode]) return; - // If the current layout mode is not available, switch to the first available layout mode - const layout = (Object.keys(explorer.layouts) as ExplorerLayout[]).find( - (key) => explorer.layouts[key] - ); - explorer.settingsStore.layoutMode = layout ?? 'grid'; - }, [layoutMode, explorer.layouts, explorer.settingsStore]); + useEffect(() => { + if (!explorerStore.isContextMenuOpen || explorer.selectedItems.size !== 0) return; + // Close context menu when no items are selected + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); + getExplorerStore().isContextMenuOpen = false; + }, [explorer.selectedItems, explorerStore.isContextMenuOpen]); - useShortcut('openObject', (e) => { - e.stopPropagation(); - e.preventDefault(); - if (quickPreviewStore.open || isRenaming) return; - doubleClick(); - }); + useEffect(() => { + if (explorer.isFetchingNextPage) { + const timer = setTimeout(() => setShowLoading(true), 100); + return () => clearTimeout(timer); + } else setShowLoading(false); + }, [explorer.isFetchingNextPage]); - useShortcut('showImageSlider', (e) => { - e.stopPropagation(); - getExplorerLayoutStore().showImageSlider = !layoutStore.showImageSlider; - }); + useEffect(() => { + if (explorer.layouts[layoutMode]) return; + // If the current layout mode is not available, switch to the first available layout mode + const layout = (Object.keys(explorer.layouts) as ExplorerLayout[]).find( + (key) => explorer.layouts[key] + ); + explorer.settingsStore.layoutMode = layout ?? 'grid'; + }, [layoutMode, explorer.layouts, explorer.settingsStore]); - useShortcut('toggleQuickPreview', (e) => { - if (isRenaming) return; - e.preventDefault(); - getQuickPreviewStore().open = !quickPreviewStore.open; - }); + useEffect(() => { + return () => { + const store = getExplorerStore(); + store.isRenaming = false; + store.isContextMenuOpen = false; + store.isDragSelecting = false; + }; + }, [layoutMode]); - useKeyCopyCutPaste(); + // Handle wheel scroll while dragging items + useEffect(() => { + const element = explorer.scrollRef.current; + if (!element || explorerStore.drag?.type !== 'dragging') return; - if (!explorer.layouts[layoutMode]) return null; + const handleWheel = (e: WheelEvent) => { + element.scrollBy({ top: e.deltaY }); + }; - return ( - <> -
{ - if (e.button === 2 || (e.button === 0 && e.shiftKey)) return; + element.addEventListener('wheel', handleWheel); + return () => element.removeEventListener('wheel', handleWheel); + }, [explorer.scrollRef, explorerStore.drag?.type]); - explorer.resetSelectedItems(); - }} - > + if (!explorer.layouts[layoutMode]) return null; + + return ( + +
{ + if (e.button === 2 || (e.button === 0 && e.shiftKey)) return; + explorer.selectedItems.size !== 0 && explorer.resetSelectedItems(); + }} + > +
{explorer.items === null || (explorer.items && explorer.items.length > 0) ? ( - + <> {layoutMode === 'grid' && } {layoutMode === 'list' && } {layoutMode === 'media' && } {showLoading && ( )} - + ) : ( emptyNotice )}
+
- {quickPreview.ref && createPortal(, quickPreview.ref)} - - ); - } -); + {/* TODO: Move when adding columns view */} + -export const EmptyNotice = (props: { - icon?: Icon | ReactNode; - message?: ReactNode; - loading?: boolean; -}) => { - const { layoutMode } = useExplorerContext().useSettingsSnapshot(); - - const emptyNoticeIcon = (icon?: Icon) => { - const Icon = - icon ?? - { - grid: GridFour, - media: MonitorPlay, - columns: Columns, - list: Rows - }[layoutMode]; - - return ; - }; - - if (props.loading) return null; - - return ( -
- {props.icon - ? isValidElement(props.icon) - ? props.icon - : emptyNoticeIcon(props.icon as Icon) - : emptyNoticeIcon()} - -

- {props.message !== undefined ? props.message : 'This list is empty'} -

-
+ {quickPreview.ref && createPortal(, quickPreview.ref)} +
); }; -const useKeyDownHandlers = ({ disabled }: { disabled: boolean }) => { +const useShortcuts = () => { const explorer = useExplorerContext(); + const explorerStore = useExplorerStore(); + const quickPreviewStore = useQuickPreviewStore(); - const os = useOperatingSystem(); + const meta = useKeyMatcher('Meta'); + const { doubleClick } = useViewItemDoubleClick(); - const handleNewTag = useCallback( - async (event: KeyboardEvent) => { - const objects: Object[] = []; + useKeyCopyCutPaste(); - for (const item of explorer.selectedItems) { - const object = getItemObject(item); - if (!object) return; - objects.push(object); - } + useShortcut('toggleQuickPreview', (e) => { + if (explorerStore.isRenaming) return; + e.preventDefault(); + getQuickPreviewStore().open = !quickPreviewStore.open; + }); - if ( - !isNonEmpty(objects) || - event.key.toUpperCase() !== 'N' || - !event.getModifierState(os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control) - ) - return; + useShortcut('openObject', (e) => { + if (explorerStore.isRenaming || quickPreviewStore.open) return; + e.stopPropagation(); + e.preventDefault(); + doubleClick(); + }); - dialogManager.create((dp) => ( - ({ type: 'Object', item }))} /> - )); - }, - [os, explorer.selectedItems] - ); + useShortcut('showImageSlider', (e) => { + if (explorerStore.isRenaming) return; + e.stopPropagation(); + getExplorerLayoutStore().showImageSlider = !getExplorerLayoutStore().showImageSlider; + }); - useEffect(() => { - const handlers = [handleNewTag]; - const handler = (event: KeyboardEvent) => { - if (event.repeat || disabled) return; - for (const handler of handlers) handler(event); - }; - document.body.addEventListener('keydown', handler); - return () => document.body.removeEventListener('keydown', handler); - }, [disabled, handleNewTag]); + useKeys([meta.key, 'KeyN'], () => { + if (explorerStore.isRenaming || quickPreviewStore.open) return; + + const objects: Object[] = []; + + for (const item of explorer.selectedItems) { + const object = getItemObject(item); + if (!object) return; + objects.push(object); + } + + if (!isNonEmpty(objects)) return; + + dialogManager.create((dp) => ( + ({ type: 'Object', item }))} /> + )); + }); }; diff --git a/interface/app/$libraryId/Explorer/View/useDragScrollable.tsx b/interface/app/$libraryId/Explorer/View/useDragScrollable.tsx new file mode 100644 index 000000000..fe59967da --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/useDragScrollable.tsx @@ -0,0 +1,63 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { useExplorerContext } from '../Context'; +import { getExplorerStore } from '../store'; + +/** + * Custom explorer dnd scroll handler as the default auto-scroll from dnd-kit is presenting issues + */ +export const useDragScrollable = ({ direction }: { direction: 'up' | 'down' }) => { + const explorer = useExplorerContext(); + + const [node, setNode] = useState(null); + + const timeout = useRef(null); + const interval = useRef(null); + + useEffect(() => { + const element = node; + const scrollElement = explorer.scrollRef.current; + if (!element || !scrollElement) return; + + const reset = () => { + if (timeout.current) { + clearTimeout(timeout.current); + timeout.current = null; + } + + if (interval.current) { + clearInterval(interval.current); + interval.current = null; + } + }; + + const handleMouseMove = ({ clientX, clientY }: MouseEvent) => { + if (getExplorerStore().drag?.type !== 'dragging') return reset(); + + const rect = element.getBoundingClientRect(); + + const isInside = + clientX >= rect.left && + clientX <= rect.right && + clientY >= rect.top && + clientY <= rect.bottom; + + if (!isInside) return reset(); + + if (timeout.current) return; + + timeout.current = setTimeout(() => { + interval.current = setInterval(() => { + scrollElement.scrollBy({ top: direction === 'up' ? -10 : 10 }); + }, 5); + }, 1000); + }; + + window.addEventListener('mousemove', handleMouseMove); + return () => window.removeEventListener('mouseover', handleMouseMove); + }, [direction, explorer.scrollRef, node]); + + const ref = useCallback((nodeElement: HTMLElement | null) => setNode(nodeElement), []); + + return { ref }; +}; diff --git a/interface/app/$libraryId/Explorer/View/util.ts b/interface/app/$libraryId/Explorer/View/util.ts deleted file mode 100644 index 0ca5488ca..000000000 --- a/interface/app/$libraryId/Explorer/View/util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useCallback } from 'react'; - -import { ExplorerViewPadding } from '.'; - -export const useExplorerViewPadding = (padding?: number | ExplorerViewPadding) => { - const getPadding = useCallback( - (key: keyof ExplorerViewPadding) => (typeof padding === 'object' ? padding[key] : padding), - [padding] - ); - - return { - top: getPadding('top') ?? getPadding('y'), - bottom: getPadding('bottom') ?? getPadding('y'), - left: getPadding('left') ?? getPadding('x'), - right: getPadding('right') ?? getPadding('x') - }; -}; diff --git a/interface/app/$libraryId/Explorer/index.tsx b/interface/app/$libraryId/Explorer/index.tsx index 93c692649..79bf07161 100644 --- a/interface/app/$libraryId/Explorer/index.tsx +++ b/interface/app/$libraryId/Explorer/index.tsx @@ -7,16 +7,19 @@ import { useTopBarContext } from '../TopBar/Layout'; import { useExplorerContext } from './Context'; import ContextMenu from './ContextMenu'; import DismissibleNotice from './DismissibleNotice'; +import { ExplorerPath, PATH_BAR_HEIGHT } from './ExplorerPath'; import { Inspector, INSPECTOR_WIDTH } from './Inspector'; import ExplorerContextMenu from './ParentContextMenu'; import { getQuickPreviewStore } from './QuickPreview/store'; import { getExplorerStore, useExplorerStore } from './store'; import { useKeyRevealFinder } from './useKeyRevealFinder'; -import View, { EmptyNotice, ExplorerViewProps } from './View'; -import { ExplorerPath, PATH_BAR_HEIGHT } from './View/ExplorerPath'; +import { ExplorerViewProps, View } from './View'; +import { EmptyNotice } from './View/EmptyNotice'; import 'react-slidedown/lib/slidedown.css'; +import { useExplorerDnd } from './useExplorerDnd'; + interface Props { emptyNotice?: ExplorerViewProps['emptyNotice']; contextMenu?: () => ReactNode; @@ -64,44 +67,43 @@ export default function Explorer(props: PropsWithChildren) { useKeyRevealFinder(); + useExplorerDnd(); + const topBar = useTopBarContext(); return ( <> -
-
- {explorer.items && explorer.items.length > 0 && } +
+ {explorer.items && explorer.items.length > 0 && } - } - emptyNotice={ - props.emptyNotice ?? ( - - ) - } - listViewOptions={{ hideHeaderBorder: true }} - bottom={showPathBar ? PATH_BAR_HEIGHT : undefined} - /> -
+ } + emptyNotice={ + props.emptyNotice ?? ( + + ) + } + listViewOptions={{ hideHeaderBorder: true }} + bottom={showPathBar ? PATH_BAR_HEIGHT : undefined} + />
+ {showPathBar && } {explorerStore.showInspector && ( diff --git a/interface/app/$libraryId/Explorer/store.ts b/interface/app/$libraryId/Explorer/store.ts index 6c3324387..c90a0bc6c 100644 --- a/interface/app/$libraryId/Explorer/store.ts +++ b/interface/app/$libraryId/Explorer/store.ts @@ -1,4 +1,3 @@ -import type { ReadonlyDeep } from 'type-fest'; import { proxy, useSnapshot } from 'valtio'; import { proxySet } from 'valtio/utils'; import { z } from 'zod'; @@ -107,7 +106,7 @@ export const createDefaultExplorerSettings = (args?: { } }) satisfies ExplorerSettings; -type CutCopyState = +export type CutCopyState = | { type: 'Idle'; } @@ -123,6 +122,18 @@ type CutCopyState = }; }; +type DragState = + | { + type: 'touched'; + } + | { + type: 'dragging'; + items: ExplorerItem[]; + sourcePath?: string; + sourceLocationId?: number; + sourceTagId?: number; + }; + const state = { tagAssignMode: false, showInspector: false, @@ -131,7 +142,10 @@ const state = { mediaPlayerVolume: 0.7, newThumbnails: proxySet() as Set, cutCopyState: { type: 'Idle' } as CutCopyState, - isDragging: false + drag: null as null | DragState, + isDragSelecting: false, + isRenaming: false, + isContextMenuOpen: false }; export function flattenThumbnailKey(thumbKey: string[]) { @@ -160,7 +174,7 @@ export function getExplorerStore() { return explorerStore; } -export function isCut(item: ExplorerItem, cutCopyState: ReadonlyDeep) { +export function isCut(item: ExplorerItem, cutCopyState: CutCopyState) { switch (item.type) { case 'NonIndexedPath': return ( diff --git a/interface/app/$libraryId/Explorer/useExplorerDnd.tsx b/interface/app/$libraryId/Explorer/useExplorerDnd.tsx new file mode 100644 index 000000000..893276dc2 --- /dev/null +++ b/interface/app/$libraryId/Explorer/useExplorerDnd.tsx @@ -0,0 +1,211 @@ +import { useDndMonitor } from '@dnd-kit/core'; +import { useState } from 'react'; +import { + ExplorerItem, + getIndexedItemFilePath, + getItemFilePath, + libraryClient, + useLibraryMutation, + useZodForm +} from '@sd/client'; +import { Dialog, RadixCheckbox, useDialog, UseDialogProps } from '@sd/ui'; +import { Icon } from '~/components'; +import { isNonEmptyObject } from '~/util'; + +import { useAssignItemsToTag } from '../settings/library/tags/CreateDialog'; +import { useExplorerContext } from './Context'; +import { getExplorerStore } from './store'; +import { explorerDroppableSchema } from './useExplorerDroppable'; +import { useExplorerSearchParams } from './util'; + +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 + ); +}; + +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; + getExplorerStore().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 } = getExplorerStore(); + getExplorerStore().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: () => (getExplorerStore().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); + + return ( + props.onConfirm({ dismissNotice: dismissNotice }))} + dialog={useDialog(props)} + title="Move Files" + icon={} + description={ + + Are you sure you want to move {props.count} file{props.count > 1 ? 's' : ''} to{' '} + {props.path}? + + } + ctaDanger + ctaLabel="Continue" + closeLabel="Cancel" + buttonsSideContent={ + typeof val === 'boolean' && setDismissNotice(val)} + /> + } + /> + ); +}; diff --git a/interface/app/$libraryId/Explorer/useExplorerDraggable.tsx b/interface/app/$libraryId/Explorer/useExplorerDraggable.tsx new file mode 100644 index 000000000..d34ca14cb --- /dev/null +++ b/interface/app/$libraryId/Explorer/useExplorerDraggable.tsx @@ -0,0 +1,50 @@ +import { useDraggable, UseDraggableArguments } from '@dnd-kit/core'; +import { CSSProperties, HTMLAttributes } from 'react'; +import { ExplorerItem } from '@sd/client'; + +import { getExplorerStore } from './store'; +import { uniqueId } from './util'; + +export interface UseExplorerDraggableProps extends Omit { + data: ExplorerItem; +} + +const draggableTypes: ExplorerItem['type'][] = ['Path', 'NonIndexedPath', 'Object']; + +export const useExplorerDraggable = (props: UseExplorerDraggableProps) => { + const disabled = props.disabled || !draggableTypes.includes(props.data.type); + + const { setNodeRef, ...draggable } = useDraggable({ + ...props, + id: uniqueId(props.data), + disabled: disabled + }); + + const onMouseDown = () => { + if (!disabled) getExplorerStore().drag = { type: 'touched' }; + }; + + const onMouseLeave = () => { + const explorerStore = getExplorerStore(); + if (explorerStore.drag?.type !== 'dragging') explorerStore.drag = null; + }; + + const onMouseUp = () => (getExplorerStore().drag = null); + + const style = { + cursor: 'default', + outline: 'none' + } satisfies CSSProperties; + + return { + ...draggable, + setDraggableRef: setNodeRef, + listeners: { + ...draggable.listeners, + onMouseDown, + onMouseLeave, + onMouseUp + } satisfies HTMLAttributes, + style + }; +}; diff --git a/interface/app/$libraryId/Explorer/useExplorerDroppable.tsx b/interface/app/$libraryId/Explorer/useExplorerDroppable.tsx new file mode 100644 index 000000000..e808e5337 --- /dev/null +++ b/interface/app/$libraryId/Explorer/useExplorerDroppable.tsx @@ -0,0 +1,211 @@ +import { useDroppable, UseDroppableArguments } from '@dnd-kit/core'; +import { useEffect, useId, useMemo, useState } from 'react'; +import { NavigateOptions, To, useNavigate } from 'react-router'; +import { createSearchParams } from 'react-router-dom'; +import { z } from 'zod'; +import { ExplorerItem, getItemFilePath, Location, Tag } from '@sd/client'; + +import { useExplorerContext } from './Context'; +import { getExplorerStore } from './store'; + +type ExplorerItemType = ExplorerItem['type']; + +const droppableTypes = [ + 'Location', + 'NonIndexedPath', + 'Object', + 'Path', + 'SpacedropPeer' +] satisfies ExplorerItemType[]; + +export interface UseExplorerDroppableProps extends Omit { + id?: string; + data?: + | { + type: 'location'; + data?: Location | z.infer['data']; + path: string; + } + | { type: 'explorer-item'; data: ExplorerItem } + | { type: 'tag'; data: Tag }; + allow?: ExplorerItemType | ExplorerItemType[]; + navigateTo?: To | { to: To; options?: NavigateOptions } | number | (() => void); +} + +const explorerPathSchema = z.object({ + type: z.literal('Path'), + item: z.object({ + id: z.number(), + name: z.string(), + location_id: z.number(), + materialized_path: z.string() + }) +}); + +const explorerObjectSchema = z.object({ + type: z.literal('Object'), + item: z.object({ + file_paths: explorerPathSchema.shape.item.array() + }) +}); + +const explorerNonIndexedPathSchema = z.object({ + type: z.literal('NonIndexedPath'), + item: z.object({ + name: z.string(), + path: z.string() + }) +}); + +const explorerItemLocationSchema = z.object({ + type: z.literal('Location'), + item: z.object({ id: z.number(), path: z.string() }) +}); + +const explorerItemSchema = z.object({ + type: z.literal('explorer-item'), + data: explorerPathSchema + .or(explorerNonIndexedPathSchema) + .or(explorerItemLocationSchema) + .or(explorerObjectSchema) +}); + +const explorerLocationSchema = z.object({ + type: z.literal('location'), + data: z.object({ id: z.number(), path: z.string() }).optional(), + path: z.string() +}); + +const explorerTagSchema = z.object({ + type: z.literal('tag'), + data: z.object({ id: z.number() }) +}); + +export const explorerDroppableSchema = explorerItemSchema + .or(explorerLocationSchema) + .or(explorerTagSchema); + +export const useExplorerDroppable = ({ + allow, + navigateTo, + ...props +}: UseExplorerDroppableProps) => { + const id = useId(); + const navigate = useNavigate(); + + const explorer = useExplorerContext({ suspense: false }); + + const [canNavigate, setCanNavigate] = useState(true); + + const { setNodeRef, ...droppable } = useDroppable({ + ...props, + id: props.id ?? id, + disabled: (!props.data && !navigateTo) || props.disabled + }); + + const isDroppable = useMemo(() => { + if (!droppable.isOver) return false; + + const { drag } = getExplorerStore(); + if (!drag || drag.type === 'touched') return false; + + let allowedType: ExplorerItemType | ExplorerItemType[] | undefined = allow; + + if (!allowedType) { + if (explorer?.parent) { + switch (explorer.parent.type) { + case 'Location': + case 'Ephemeral': { + allowedType = ['Path', 'NonIndexedPath', 'Object']; + break; + } + + case 'Tag': { + allowedType = ['Path', 'Object']; + break; + } + } + } else if (props.data?.type === 'explorer-item') { + switch (props.data.data.type) { + case 'Path': + case 'NonIndexedPath': { + allowedType = ['Path', 'NonIndexedPath', 'Object']; + break; + } + + case 'Object': { + allowedType = ['Path', 'Object']; + break; + } + } + } else allowedType = droppableTypes; + + if (!allowedType) return false; + } + + const schema = z.object({ + type: Array.isArray(allowedType) + ? z.union( + allowedType.map((type) => z.literal(type)) as unknown as [ + z.ZodLiteral, + z.ZodLiteral, + ...z.ZodLiteral[] + ] + ) + : z.literal(allowedType) + }); + + return schema.safeParse(drag.items[0]).success; + }, [allow, droppable.isOver, explorer?.parent, props.data]); + + const filePath = props.data?.type === 'explorer-item' && getItemFilePath(props.data.data); + const isLocation = props.data?.type === 'explorer-item' && props.data.data.type === 'Location'; + + const isNavigable = isDroppable && canNavigate && (filePath || navigateTo || isLocation); + + useEffect(() => { + if (!isNavigable) return; + + const timeout = setTimeout(() => { + if (navigateTo) { + if (typeof navigateTo === 'function') { + navigateTo(); + } else if (typeof navigateTo === 'object' && 'to' in navigateTo) { + navigate(navigateTo.to, navigateTo.options); + } else if (typeof navigateTo === 'number') { + navigate(navigateTo); + } else { + navigate(navigateTo); + } + } else if (filePath) { + if ('id' in filePath) { + navigate({ + pathname: `../location/${filePath.location_id}`, + search: `${createSearchParams({ + path: `${filePath.materialized_path}${filePath.name}/` + })}` + }); + } else { + navigate({ search: `${createSearchParams({ path: filePath.path })}` }); + } + } else if ( + props.data?.type === 'explorer-item' && + props.data.data.type === 'Location' + ) { + navigate(`../location/${props.data.data.item.id}`); + } + + // Timeout navigation + setCanNavigate(false); + setTimeout(() => setCanNavigate(true), 1250); + }, 1250); + + return () => clearTimeout(timeout); + }, [navigate, props.data, navigateTo, filePath, isNavigable]); + + const className = isNavigable + ? 'animate-pulse transition-opacity duration-200 [animation-delay:1000ms]' + : undefined; + + return { setDroppableRef: setNodeRef, ...droppable, isDroppable, className }; +}; diff --git a/interface/app/$libraryId/Layout/DndContext.tsx b/interface/app/$libraryId/Layout/DndContext.tsx new file mode 100644 index 000000000..df0c1266e --- /dev/null +++ b/interface/app/$libraryId/Layout/DndContext.tsx @@ -0,0 +1,24 @@ +import * as Dnd from '@dnd-kit/core'; +import { PropsWithChildren } from 'react'; + +export const DndContext = ({ children }: PropsWithChildren) => { + const sensors = Dnd.useSensors( + Dnd.useSensor(Dnd.PointerSensor, { + activationConstraint: { + distance: 4 + } + }) + ); + + return ( + + {children} + + ); +}; diff --git a/interface/app/$libraryId/Layout/Sidebar/Devices/index.tsx b/interface/app/$libraryId/Layout/Sidebar/Devices/index.tsx new file mode 100644 index 000000000..27cbf0a43 --- /dev/null +++ b/interface/app/$libraryId/Layout/Sidebar/Devices/index.tsx @@ -0,0 +1,41 @@ +import { Link } from 'react-router-dom'; +import { useBridgeQuery, useFeatureFlag } from '@sd/client'; +import { Button, Tooltip } from '@sd/ui'; +import { Icon, SubtleButton } from '~/components'; + +import SidebarLink from '../Link'; +import Section from '../Section'; + +export const Devices = () => { + const { data: node } = useBridgeQuery(['nodeState']); + const isPairingEnabled = useFeatureFlag('p2pPairing'); + + return ( +
+ + + ) + } + > + {node && ( + + + {node.name} + + )} + + + + +
+ ); +}; diff --git a/interface/app/$libraryId/Layout/Sidebar/EphemeralSection.tsx b/interface/app/$libraryId/Layout/Sidebar/EphemeralSection.tsx index 74d615d64..a3fc9577a 100644 --- a/interface/app/$libraryId/Layout/Sidebar/EphemeralSection.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/EphemeralSection.tsx @@ -1,11 +1,13 @@ import { EjectSimple } from '@phosphor-icons/react'; import clsx from 'clsx'; -import { useMemo } from 'react'; +import { PropsWithChildren, useMemo } from 'react'; import { useBridgeQuery, useCache, useLibraryQuery, useNodes } from '@sd/client'; import { Button, toast, tw } from '@sd/ui'; import { Icon, IconName } from '~/components'; import { useHomeDir } from '~/hooks/useHomeDir'; +import { useExplorerDroppable } from '../../Explorer/useExplorerDroppable'; +import { useExplorerSearchParams } from '../../Explorer/util'; import SidebarLink from './Link'; import Section from './Section'; import { SeeMore } from './SeeMore'; @@ -78,36 +80,45 @@ export const EphemeralSection = () => { Network + {homeDir.data && ( - Home - + )} + {mountPoints.map((item) => { if (!item) return; const locationId = locationIdsForVolumes[item.mountPoint ?? '']; const key = `${item.volumeIndex}-${item.index}`; + const name = item.mountPoint === '/' ? 'Root' : item.index === 0 ? item.volume.name : item.mountPoint; + const toPath = locationId !== undefined ? `location/${locationId}` : `ephemeral/${key}?path=${item.mountPoint}`; + return ( - { /> {name} {item.volume.disk_type === 'Removable' && } - + ); })} ); }; + +const EphemeralLocation = ({ + children, + path, + navigateTo +}: PropsWithChildren<{ path: string; navigateTo: string }>) => { + const [{ path: ephemeralPath }] = useExplorerSearchParams(); + + const { isDroppable, className, setDroppableRef } = useExplorerDroppable({ + id: `sidebar-ephemeral-location-${path}`, + allow: ['Path', 'NonIndexedPath', 'Object'], + data: { type: 'location', path }, + disabled: navigateTo.startsWith('location/') || ephemeralPath === path, + navigateTo: navigateTo + }); + + return ( + + {children} + + ); +}; diff --git a/interface/app/$libraryId/Layout/Sidebar/LibrarySection.tsx b/interface/app/$libraryId/Layout/Sidebar/LibrarySection.tsx index 46ceccfa9..ca41374d8 100644 --- a/interface/app/$libraryId/Layout/Sidebar/LibrarySection.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/LibrarySection.tsx @@ -1,219 +1,15 @@ -import { X } from '@phosphor-icons/react'; -import clsx from 'clsx'; -import { useMatch, useNavigate, useResolvedPath } from 'react-router'; -import { Link, NavLink } from 'react-router-dom'; -import { - arraysEqual, - useBridgeQuery, - useCache, - useFeatureFlag, - useLibraryMutation, - useLibraryQuery, - useNodes, - useOnlineLocations -} from '@sd/client'; -import { Button, Tooltip } from '@sd/ui'; -import { AddLocationButton } from '~/app/$libraryId/settings/library/locations/AddLocationButton'; -import { Folder, Icon, SubtleButton } from '~/components'; - -import SidebarLink from './Link'; -import LocationsContextMenu from './LocationsContextMenu'; -import Section from './Section'; -import { SeeMore } from './SeeMore'; -import TagsContextMenu from './TagsContextMenu'; - -export const LibrarySection = () => ( - <> - - - - - -); - -function SavedSearches() { - const savedSearches = useLibraryQuery(['search.saved.list']); - - const path = useResolvedPath('saved-search/:id'); - const match = useMatch(path.pathname); - const currentSearchId = match?.params?.id; - - const currentIndex = currentSearchId - ? savedSearches.data?.findIndex((s) => s.id === Number(currentSearchId)) - : undefined; - - const navigate = useNavigate(); - - const deleteSavedSearch = useLibraryMutation(['search.saved.delete'], { - onSuccess() { - if (currentIndex !== undefined && savedSearches.data) { - const nextIndex = Math.min(currentIndex + 1, savedSearches.data.length - 2); - - const search = savedSearches.data[nextIndex]; - - if (search) navigate(`saved-search/${search.id}`); - else navigate(`./`); - } - } - }); - - if (!savedSearches.data || savedSearches.data.length < 1) return null; +import { Devices } from './Devices'; +import { Locations } from './Locations'; +import { SavedSearches } from './SavedSearches'; +import { Tags } from './Tags'; +export const LibrarySection = () => { return ( -
- // - // - // } - > - - {savedSearches.data.map((search, i) => ( - -
- -
- - {search.name} - - -
- ))} -
-
+ <> + + + + + ); -} - -function Devices() { - const node = useBridgeQuery(['nodeState']); - const isPairingEnabled = useFeatureFlag('p2pPairing'); - - return ( -
- - - ) - } - > - {node.data && ( - - - {node.data.name} - - )} - - - -
- ); -} - -function Locations() { - const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true }); - useNodes(locationsQuery.data?.nodes); - const locations = useCache(locationsQuery.data?.items); - const onlineLocations = useOnlineLocations(); - - return ( -
- - - } - > - - {locations?.map((location) => ( - - -
- -
arraysEqual(location.pub_id, l)) - ? 'bg-green-500' - : 'bg-red-500' - )} - /> -
- - {location.name} - - - ))} - - -
- ); -} - -function Tags() { - const result = useLibraryQuery(['tags.list'], { keepPreviousData: true }); - useNodes(result.data?.nodes); - const tags = useCache(result.data?.items); - - if (!tags?.length) return; - - return ( -
- - - } - > - - {tags?.map((tag) => ( - - -
- {tag.name} - - - ))} - -
- ); -} +}; diff --git a/interface/app/$libraryId/Layout/Sidebar/LocationsContextMenu.tsx b/interface/app/$libraryId/Layout/Sidebar/Locations/ContextMenu.tsx similarity index 91% rename from interface/app/$libraryId/Layout/Sidebar/LocationsContextMenu.tsx rename to interface/app/$libraryId/Layout/Sidebar/Locations/ContextMenu.tsx index 1bbe6d541..a9b17c73b 100644 --- a/interface/app/$libraryId/Layout/Sidebar/LocationsContextMenu.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/Locations/ContextMenu.tsx @@ -1,4 +1,5 @@ import { Pencil, Plus, Trash } from '@phosphor-icons/react'; +import { PropsWithChildren } from 'react'; import { useNavigate } from 'react-router'; import { useLibraryContext } from '@sd/client'; import { ContextMenu as CM, dialogManager, toast } from '@sd/ui'; @@ -7,12 +8,10 @@ import DeleteDialog from '~/app/$libraryId/settings/library/locations/DeleteDial import { openDirectoryPickerDialog } from '~/app/$libraryId/settings/library/locations/openDirectoryPickerDialog'; import { usePlatform } from '~/util/Platform'; -interface Props { - children: React.ReactNode; - locationId: number; -} - -export default ({ children, locationId }: Props) => { +export const ContextMenu = ({ + children, + locationId +}: PropsWithChildren<{ locationId: number }>) => { const navigate = useNavigate(); const platform = usePlatform(); const libraryId = useLibraryContext().library.uuid; diff --git a/interface/app/$libraryId/Layout/Sidebar/Locations/index.tsx b/interface/app/$libraryId/Layout/Sidebar/Locations/index.tsx new file mode 100644 index 000000000..bf0645e4b --- /dev/null +++ b/interface/app/$libraryId/Layout/Sidebar/Locations/index.tsx @@ -0,0 +1,87 @@ +import clsx from 'clsx'; +import { Link, useMatch } from 'react-router-dom'; +import { + arraysEqual, + Location as LocationType, + useCache, + useLibraryQuery, + useNodes, + useOnlineLocations +} from '@sd/client'; +import { useExplorerDroppable } from '~/app/$libraryId/Explorer/useExplorerDroppable'; +import { useExplorerSearchParams } from '~/app/$libraryId/Explorer/util'; +import { AddLocationButton } from '~/app/$libraryId/settings/library/locations/AddLocationButton'; +import { Icon, SubtleButton } from '~/components'; + +import SidebarLink from '../Link'; +import Section from '../Section'; +import { SeeMore } from '../SeeMore'; +import { ContextMenu } from './ContextMenu'; + +export const Locations = () => { + const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true }); + useNodes(locationsQuery.data?.nodes); + const locations = useCache(locationsQuery.data?.items); + const onlineLocations = useOnlineLocations(); + + return ( +
+ + + } + > + + {locations?.map((location) => ( + arraysEqual(location.pub_id, l))} + /> + ))} + + +
+ ); +}; + +const Location = ({ location, online }: { location: LocationType; online: boolean }) => { + const locationId = useMatch('/:libraryId/location/:locationId')?.params.locationId; + const [{ path }] = useExplorerSearchParams(); + + const { isDroppable, className, setDroppableRef } = useExplorerDroppable({ + id: `sidebar-location-${location.id}`, + allow: ['Path', 'NonIndexedPath', 'Object'], + data: { type: 'location', path: '/', data: location }, + disabled: Number(locationId) === location.id && !path, + navigateTo: `location/${location.id}` + }); + + return ( + + +
+ +
+
+ + {location.name} + + + ); +}; diff --git a/interface/app/$libraryId/Layout/Sidebar/SavedSearches/index.tsx b/interface/app/$libraryId/Layout/Sidebar/SavedSearches/index.tsx new file mode 100644 index 000000000..5b85ca75e --- /dev/null +++ b/interface/app/$libraryId/Layout/Sidebar/SavedSearches/index.tsx @@ -0,0 +1,103 @@ +import { X } from '@phosphor-icons/react'; +import clsx from 'clsx'; +import { useMatch, useNavigate, useResolvedPath } from 'react-router'; +import { useLibraryMutation, useLibraryQuery, type SavedSearch } from '@sd/client'; +import { Button } from '@sd/ui'; +import { useExplorerDroppable } from '~/app/$libraryId/Explorer/useExplorerDroppable'; +import { Folder } from '~/components'; + +import SidebarLink from '../Link'; +import Section from '../Section'; +import { SeeMore } from '../SeeMore'; + +export const SavedSearches = () => { + const savedSearches = useLibraryQuery(['search.saved.list']); + + const path = useResolvedPath('saved-search/:id'); + const match = useMatch(path.pathname); + const currentSearchId = match?.params?.id; + + const currentIndex = currentSearchId + ? savedSearches.data?.findIndex((s) => s.id === Number(currentSearchId)) + : undefined; + + const navigate = useNavigate(); + + const deleteSavedSearch = useLibraryMutation(['search.saved.delete'], { + onSuccess() { + if (currentIndex !== undefined && savedSearches.data) { + const nextIndex = Math.min(currentIndex + 1, savedSearches.data.length - 2); + + const search = savedSearches.data[nextIndex]; + + if (search) navigate(`saved-search/${search.id}`); + else navigate(`./`); + } + } + }); + + if (!savedSearches.data || savedSearches.data.length < 1) return null; + + return ( +
+ // + // + // } + > + + {savedSearches.data.map((search, i) => ( + deleteSavedSearch.mutate(search.id)} + /> + ))} + +
+ ); +}; + +const SavedSearch = ({ search, onDelete }: { search: SavedSearch; onDelete(): void }) => { + const searchId = useMatch('/:libraryId/saved-search/:searchId')?.params.searchId; + + const { isDroppable, className, setDroppableRef } = useExplorerDroppable({ + id: `sidebar-saved-search-${search.id}`, + allow: ['Path', 'NonIndexedPath', 'Object'], + disabled: Number(searchId) === search.id, + navigateTo: `saved-search/${search.id}` + }); + + return ( + +
+ +
+ + {search.name} + + +
+ ); +}; diff --git a/interface/app/$libraryId/Layout/Sidebar/TagsContextMenu.tsx b/interface/app/$libraryId/Layout/Sidebar/Tags/ContextMenu.tsx similarity index 88% rename from interface/app/$libraryId/Layout/Sidebar/TagsContextMenu.tsx rename to interface/app/$libraryId/Layout/Sidebar/Tags/ContextMenu.tsx index 0b92b5673..f968c87de 100644 --- a/interface/app/$libraryId/Layout/Sidebar/TagsContextMenu.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/Tags/ContextMenu.tsx @@ -1,17 +1,14 @@ import { Pencil, Plus, Trash } from '@phosphor-icons/react'; +import { PropsWithChildren } from 'react'; import { useNavigate } from 'react-router'; import { Link } from 'react-router-dom'; import { ContextMenu as CM, dialogManager } from '@sd/ui'; import CreateDialog from '~/app/$libraryId/settings/library/tags/CreateDialog'; import DeleteDialog from '~/app/$libraryId/settings/library/tags/DeleteDialog'; -interface Props { - children: React.ReactNode; - tagId: number; -} - -export default ({ children, tagId }: Props) => { +export const ContextMenu = ({ children, tagId }: PropsWithChildren<{ tagId: number }>) => { const navigate = useNavigate(); + return ( { + const result = useLibraryQuery(['tags.list'], { keepPreviousData: true }); + useNodes(result.data?.nodes); + const tags = useCache(result.data?.items); + + if (!tags?.length) return null; + + return ( +
+ + + } + > + + {tags.map((tag) => ( + + ))} + +
+ ); +}; + +const Tag = ({ tag }: { tag: Tag }) => { + const tagId = useMatch('/:libraryId/tag/:tagId')?.params.tagId; + + const { isDroppable, className, setDroppableRef } = useExplorerDroppable({ + id: `sidebar-tag-${tag.id}`, + allow: ['Path', 'Object'], + data: { type: 'tag', data: tag }, + navigateTo: `tag/${tag.id}`, + disabled: Number(tagId) === tag.id + }); + + return ( + + +
+ {tag.name} + + + ); +}; diff --git a/interface/app/$libraryId/Layout/index.tsx b/interface/app/$libraryId/Layout/index.tsx index 72d379f4c..a1536ed20 100644 --- a/interface/app/$libraryId/Layout/index.tsx +++ b/interface/app/$libraryId/Layout/index.tsx @@ -23,8 +23,10 @@ import { } from '~/hooks'; import { usePlatform } from '~/util/Platform'; +import { DragOverlay } from '../Explorer/DragOverlay'; import { QuickPreviewContextProvider } from '../Explorer/QuickPreview/Context'; import { LayoutContext } from './Context'; +import { DndContext } from './DndContext'; import Sidebar from './Sidebar'; const Layout = () => { @@ -69,27 +71,32 @@ const Layout = () => { e.preventDefault(); }} > - -
- {library ? ( - - - }> - - - - - ) : ( -

- Please select or create a library in the sidebar. -

- )} -
+ + +
+ {library ? ( + + + } + > + + + + + + ) : ( +

+ Please select or create a library in the sidebar. +

+ )} +
+
); diff --git a/interface/app/$libraryId/TopBar/Layout.tsx b/interface/app/$libraryId/TopBar/Layout.tsx index 6456a183d..807087e36 100644 --- a/interface/app/$libraryId/TopBar/Layout.tsx +++ b/interface/app/$libraryId/TopBar/Layout.tsx @@ -1,8 +1,9 @@ -import { createContext, useContext, useState } from 'react'; +import { createContext, useContext, useEffect, useState } from 'react'; import { Outlet } from 'react-router'; import { SearchFilterArgs } from '@sd/client'; import TopBar from '.'; +import { getExplorerStore } from '../Explorer/store'; const TopBarContext = createContext | null>(null); @@ -33,6 +34,13 @@ function useContextValue() { export const Component = () => { const value = useContextValue(); + // Reset drag state + useEffect(() => { + return () => { + getExplorerStore().drag = null; + }; + }, []); + return ( diff --git a/interface/app/$libraryId/TopBar/NavigationButtons.tsx b/interface/app/$libraryId/TopBar/NavigationButtons.tsx index f3f94409d..01e032b56 100644 --- a/interface/app/$libraryId/TopBar/NavigationButtons.tsx +++ b/interface/app/$libraryId/TopBar/NavigationButtons.tsx @@ -1,10 +1,12 @@ import { ArrowLeft, ArrowRight } from '@phosphor-icons/react'; +import clsx from 'clsx'; import { useEffect } from 'react'; import { useNavigate } from 'react-router'; import { Tooltip } from '@sd/ui'; import { useKeyMatcher, useOperatingSystem, useShortcut } from '~/hooks'; import { useRoutingContext } from '~/RoutingContext'; +import { useExplorerDroppable } from '../Explorer/useExplorerDroppable'; import TopBarButton from './TopBarButton'; export const NavigationButtons = () => { @@ -17,6 +19,16 @@ export const NavigationButtons = () => { const canGoBack = currentIndex !== 0; const canGoForward = currentIndex !== maxIndex; + const droppableBack = useExplorerDroppable({ + navigateTo: -1, + disabled: !canGoBack + }); + + const droppableForward = useExplorerDroppable({ + navigateTo: 1, + disabled: !canGoForward + }); + useShortcut('navBackwardHistory', () => { if (!canGoBack) return; navigate(-1); @@ -49,9 +61,13 @@ export const NavigationButtons = () => { navigate(-1)} disabled={!canGoBack} + ref={droppableBack.setDroppableRef} + className={clsx( + droppableBack.isDroppable && '!bg-app-selected', + droppableBack.className + )} > @@ -59,9 +75,13 @@ export const NavigationButtons = () => { navigate(1)} disabled={!canGoForward} + ref={droppableForward.setDroppableRef} + className={clsx( + droppableForward.isDroppable && '!bg-app-selected', + droppableForward.className + )} > diff --git a/interface/app/$libraryId/TopBar/index.tsx b/interface/app/$libraryId/TopBar/index.tsx index 5dc2a521a..cc491dff3 100644 --- a/interface/app/$libraryId/TopBar/index.tsx +++ b/interface/app/$libraryId/TopBar/index.tsx @@ -15,7 +15,7 @@ import { NavigationButtons } from './NavigationButtons'; const TopBar = () => { const transparentBg = useShowControls().transparentBg; - const { isDragging } = useExplorerStore(); + const { isDragSelecting } = useExplorerStore(); const ref = useRef(null); const tabs = useTabsContext(); @@ -53,7 +53,7 @@ const TopBar = () => { className={clsx( 'flex h-12 items-center gap-3.5 overflow-hidden px-3.5', 'duration-250 transition-[background-color,border-color] ease-out', - isDragging && 'pointer-events-none' + isDragSelecting && 'pointer-events-none' )} >
{ + return (tagId: number, items: AssignTagItems, unassign: boolean = false) => { const targets = items.map((item) => { if (item.type === 'Object') { return { Object: item.item.id }; diff --git a/interface/app/$libraryId/tag/$id.tsx b/interface/app/$libraryId/tag/$id.tsx index d8ac07d3e..7fc370e4b 100644 --- a/interface/app/$libraryId/tag/$id.tsx +++ b/interface/app/$libraryId/tag/$id.tsx @@ -10,7 +10,7 @@ import { useObjectsExplorerQuery } from '../Explorer/queries/useObjectsExplorerQ import { createDefaultExplorerSettings, objectOrderingKeysSchema } from '../Explorer/store'; import { DefaultTopBarOptions } from '../Explorer/TopBarOptions'; import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer'; -import { EmptyNotice } from '../Explorer/View'; +import { EmptyNotice } from '../Explorer/View/EmptyNotice'; import SearchOptions, { SearchContextProvider, useSearch } from '../Search'; import SearchBar from '../Search/SearchBar'; import { TopBarPortal } from '../TopBar/Portal'; diff --git a/interface/hooks/index.ts b/interface/hooks/index.ts index 41aafbbc9..9afbfec3b 100644 --- a/interface/hooks/index.ts +++ b/interface/hooks/index.ts @@ -4,26 +4,25 @@ export * from './useClickOutside'; export * from './useCounter'; export * from './useDebouncedForm'; export * from './useDismissibleNoticeStore'; -export * from './useDragSelect'; export * from './useFocusState'; export * from './useInputState'; export * from './useIsDark'; export * from './useKeyDeleteFile'; -export * from './useKeybindEventHandler'; export * from './useKeybind'; +export * from './useKeybindEventHandler'; export * from './useOperatingSystem'; export * from './useScrolled'; // export * from './useSearchStore'; +export * from './useIsLocationIndexing'; +export * from './useIsTextTruncated'; +export * from './useKeyCopyCutPaste'; +export * from './useKeyMatcher'; +export * from './useRedirectToNewLocation'; +export * from './useRouteTitle'; export * from './useShortcut'; export * from './useShowControls'; export * from './useSpacedropState'; export * from './useTheme'; +export * from './useWindowState'; export * from './useZodRouteParams'; export * from './useZodSearchParams'; -export * from './useIsTextTruncated'; -export * from './useKeyMatcher'; -export * from './useKeyCopyCutPaste'; -export * from './useRedirectToNewLocation'; -export * from './useWindowState'; -export * from './useIsLocationIndexing'; -export * from './useRouteTitle'; diff --git a/interface/hooks/useDismissibleNoticeStore.tsx b/interface/hooks/useDismissibleNoticeStore.tsx index 4d0cbf32f..72fc18004 100644 --- a/interface/hooks/useDismissibleNoticeStore.tsx +++ b/interface/hooks/useDismissibleNoticeStore.tsx @@ -5,7 +5,8 @@ export const dismissibleNoticeStore = valtioPersist('dismissible-notice', { mediaView: false, gridView: false, listView: false, - ephemeral: false + ephemeral: false, + ephemeralMoveFiles: false }); export function useDismissibleNoticeStore() { diff --git a/interface/hooks/useDragSelect.tsx b/interface/hooks/useDragSelect.tsx deleted file mode 100644 index 2f624bd9e..000000000 --- a/interface/hooks/useDragSelect.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import DragSelect from 'dragselect'; -import React, { createContext, useContext, useEffect, useState } from 'react'; - -type ProviderProps = { - children: React.ReactNode; - settings?: ConstructorParameters[0]; -}; - -const Context = createContext(undefined); - -function DragSelectProvider({ children, settings = {} }: ProviderProps) { - const [ds, setDS] = useState(); - - useEffect(() => { - setDS((prevState) => { - if (prevState) return prevState; - return new DragSelect({}); - }); - return () => { - if (ds) { - console.log('stop'); - ds.stop(); - setDS(undefined); - } - }; - }, [ds]); - - useEffect(() => { - ds?.setSettings(settings); - }, [ds, settings]); - - return {children}; -} - -function useDragSelect() { - return useContext(Context); -} - -export { DragSelectProvider, useDragSelect }; diff --git a/interface/package.json b/interface/package.json index 05b9ff2fb..68c22b971 100644 --- a/interface/package.json +++ b/interface/package.json @@ -9,6 +9,8 @@ "typecheck": "tsc -b" }, "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/utilities": "^3.2.2", "@fontsource/inter": "^4.5.15", "@headlessui/react": "^1.7.17", "@icons-pack/react-simple-icons": "^9.1.0", @@ -35,7 +37,6 @@ "clsx": "^2.0.0", "crypto-random-string": "^5.0.0", "dayjs": "^1.11.10", - "dragselect": "^2.7.4", "framer-motion": "^10.16.4", "immer": "^10.0.3", "prismjs": "^1.29.0", diff --git a/packages/assets/icons/MoveLocation.png b/packages/assets/icons/MoveLocation.png new file mode 100644 index 0000000000000000000000000000000000000000..27be2bc94004df20fd5a7c9c621f8c0de64dd5be GIT binary patch literal 81629 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 SXCVam@kw4yu13Zp?Ee6QV)jn} literal 0 HcmV?d00001 diff --git a/packages/assets/icons/MoveLocation_Light.png b/packages/assets/icons/MoveLocation_Light.png new file mode 100644 index 0000000000000000000000000000000000000000..18b401ea4f313c55ffcc0b396a9de34f466e265e GIT binary patch literal 65758 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 literal 0 HcmV?d00001 diff --git a/packages/assets/icons/index.ts b/packages/assets/icons/index.ts index ea881c3f1..a18232d4a 100644 --- a/packages/assets/icons/index.ts +++ b/packages/assets/icons/index.ts @@ -121,6 +121,8 @@ import Mesh20 from './Mesh-20.png'; import Mesh from './Mesh.png'; import Mobile_Light from './Mobile_Light.png'; import Mobile from './Mobile.png'; +import MoveLocation_Light from './MoveLocation_Light.png'; +import MoveLocation from './MoveLocation.png'; import Movie_Light from './Movie_Light.png'; import Movie from './Movie.png'; import NewLocation from './NewLocation.png'; @@ -290,6 +292,8 @@ export { Mesh_Light, Mobile, Mobile_Light, + MoveLocation, + MoveLocation_Light, Movie, Movie_Light, NewLocation, diff --git a/packages/ui/src/Dialog.tsx b/packages/ui/src/Dialog.tsx index 8be1f90f5..a9e69ea8d 100644 --- a/packages/ui/src/Dialog.tsx +++ b/packages/ui/src/Dialog.tsx @@ -2,7 +2,6 @@ import * as RDialog from '@radix-ui/react-dialog'; import { animated, useTransition } from '@react-spring/web'; -import { iconNames } from '@sd/assets/util'; import clsx from 'clsx'; import { ReactElement, ReactNode, useEffect } from 'react'; import { FieldValues, UseFormHandleSubmit } from 'react-hook-form'; @@ -123,7 +122,7 @@ export interface DialogProps ctaDanger?: boolean; closeLabel?: string; cancelBtn?: boolean; - description?: string; + description?: ReactNode; onCancelled?: boolean | (() => void); submitDisabled?: boolean; transformOrigin?: string; @@ -250,7 +249,7 @@ export function Dialog({

{form.formState.isSubmitting && } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94829086c8ad16a3cb818da1550ed2d7dd152d8b..66e9d84d327835b6183fca3486a3b2a7cbaf2b27 100644 GIT binary patch delta 676 zcmZ3}X+CG7`G#WF$@5(KH}|lbx&s+Jljjr!O+S5`m2>jGk=W}AM$o||*J z!7OgI>FdvPEAhiUk(^(Y>HzYY%Jc`SEUKYi#>s(B&N)sdAz>Be?)k>vu35#!?yeDj zUWM*q;T4AFi9sPzhWSYbZaJAjrk+`m=^+(4rjcGr-XT?vW}a0=C8;_2B_4*BWoDJ; zrTLygp-}w%nLDT@d`lweS|QZUvtoSwLqMN%9+e2c-7Mw0~-WTp!gu^3M` zIK-_a2KNCh8j2l&p+8;m9j9riwy(FNm#cqTqEo7AaBi4ciLpt6g@LcHcScrOXq7=( zP*GW!S5iQTLAYy9RieL_zF|eNwu`p6OPO|wi@u+$X=zqkk#mM?a&bnANt#cdL3u%D zA>0#4;n;4^%?!jWK+L+`o|`SDd-{Xbtaj5C_OdCq|D49Q{pU1xL9g~T57~j31Bf|+ Xm@&HMSq87IH-Rh@j?)o-(s|7R}Fl%mA+;?$hf z