From 7f3eee384832f44c80b8d19f3d62a3692ea67448 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Wed, 10 Dec 2025 09:23:00 -0800 Subject: [PATCH] Enhance FileOperationModal and introduce FileStack component - Updated FileOperationModal to fetch and display file information for both source and destination, improving user experience. - Added FileStack component to visually represent multiple files stacked with rotation, enhancing the file selection interface. - Refactored source and destination file displays to conditionally render file thumbnails or icons based on availability. - Improved layout and styling for better clarity and consistency in file operations. --- .../components/Explorer/File/FileStack.tsx | 43 +++++++ .../src/components/Explorer/File/index.ts | 1 + .../src/components/FileOperationModal.tsx | 113 ++++++++++++++---- 3 files changed, 134 insertions(+), 23 deletions(-) create mode 100644 packages/interface/src/components/Explorer/File/FileStack.tsx diff --git a/packages/interface/src/components/Explorer/File/FileStack.tsx b/packages/interface/src/components/Explorer/File/FileStack.tsx new file mode 100644 index 000000000..e1aae14c5 --- /dev/null +++ b/packages/interface/src/components/Explorer/File/FileStack.tsx @@ -0,0 +1,43 @@ +import type { File as FileType } from "@sd/ts-client"; +import { File } from "./File"; + +interface FileStackProps { + files: FileType[]; + size?: number; +} + +/** + * FileStack - Renders multiple files stacked with rotation + * Shows up to 3 files stacked on top of each other with slight rotation + */ +export function FileStack({ files, size = 64 }: FileStackProps) { + const displayFiles = files.slice(0, 3); + const remainingCount = Math.max(0, files.length - 3); + + // Rotation angles for visual stacking effect + const rotations = [-4, 0, 4]; + + return ( +
+ {displayFiles.map((file, index) => ( +
+ +
+ ))} + + {/* Show count badge if more than 3 files */} + {remainingCount > 0 && ( +
+ +{remainingCount} +
+ )} +
+ ); +} diff --git a/packages/interface/src/components/Explorer/File/index.ts b/packages/interface/src/components/Explorer/File/index.ts index 4b5a4f470..a215b6d6b 100644 --- a/packages/interface/src/components/Explorer/File/index.ts +++ b/packages/interface/src/components/Explorer/File/index.ts @@ -1,4 +1,5 @@ export { File } from "./File"; +export { FileStack } from "./FileStack"; export { Thumb, Icon } from "./Thumb"; export { Title } from "./Title"; export { Metadata } from "./Metadata"; diff --git a/packages/interface/src/components/FileOperationModal.tsx b/packages/interface/src/components/FileOperationModal.tsx index 1b8ca49d5..4742c6e79 100644 --- a/packages/interface/src/components/FileOperationModal.tsx +++ b/packages/interface/src/components/FileOperationModal.tsx @@ -9,16 +9,21 @@ import { ArrowRight, Copy as CopyIcon, ArrowsLeftRight, + File as FileIcon, + Image, + FileText, + FilmStrip, + MusicNote, } from "@phosphor-icons/react"; import { Dialog, dialogManager, useDialog, } from "@sd/ui"; -import type { SdPath } from "@sd/ts-client"; -import { useLibraryMutation } from "../context"; +import type { SdPath, File as FileType } from "@sd/ts-client"; +import { useLibraryMutation, useLibraryQuery } from "../context"; import { sounds } from "@sd/assets/sounds"; -import { File } from "./Explorer/File"; +import { File, FileStack } from "./Explorer/File"; interface FileOperationDialogProps { id: number; @@ -52,6 +57,34 @@ function FileOperationDialog(props: FileOperationDialogProps) { const copyFiles = useLibraryMutation("files.copy"); + // Fetch file info for sources (up to 3 for FileStack) + const sourcePaths = props.sources.slice(0, 3).map(s => + "Physical" in s ? s.Physical.path : null + ).filter(Boolean); + + const sourceFileQueries = sourcePaths.map(path => + useLibraryQuery({ + type: "files.by_path", + input: { path }, + enabled: !!path, + }) + ); + + const sourceFiles = sourceFileQueries + .map(q => q.data) + .filter((f): f is FileType => f !== undefined && f !== null); + + // Fetch destination folder info + const destPath = "Physical" in props.destination + ? props.destination.Physical.path + : null; + + const { data: destFile } = useLibraryQuery({ + type: "files.by_path", + input: { path: destPath }, + enabled: !!destPath, + }); + // Check if any source is the same as destination const hasSameSourceDest = props.sources.some((source) => { if ("Physical" in source && "Physical" in props.destination) { @@ -212,24 +245,44 @@ function FileOperationDialog(props: FileOperationDialogProps) { ctaLabel={operation === "copy" ? "Copy" : "Move"} onSubmit={handleSubmit} onCancelled={handleCancel} + formClassName="!min-w-[400px] !max-w-[400px]" >
{/* Source → Destination visual */}
{/* Source */} -
- -
-
From
-
- {sourceCount} {pluralItems} -
- {sourceCount === 1 && ( -
- {getFileName(props.sources[0])} +
+ {sourceFiles.length > 0 ? ( + <> + {sourceFiles.length === 1 ? ( + + ) : ( + + )} +
+
Source
+ {sourceFiles.length === 1 ? ( +
+ {sourceFiles[0].name} +
+ ) : ( +
+ {sourceCount} {pluralItems} +
+ )}
- )} -
+ + ) : ( + <> + +
+
Source
+
+ {sourceCount} {pluralItems} +
+
+ + )}
{/* Arrow */} @@ -238,14 +291,28 @@ function FileOperationDialog(props: FileOperationDialogProps) {
{/* Destination */} -
- -
-
To
-
- {getFileName(props.destination)} -
-
+
+ {destFile ? ( + <> + +
+
To
+
+ {destFile.name} +
+
+ + ) : ( + <> + +
+
To
+
+ {getFileName(props.destination)} +
+
+ + )}