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.
This commit is contained in:
Jamie Pine 2025-12-10 09:23:00 -08:00
parent d8984a0688
commit 7f3eee3848
3 changed files with 134 additions and 23 deletions

View File

@ -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 (
<div className="relative" style={{ width: size, height: size }}>
{displayFiles.map((file, index) => (
<div
key={file.id}
className="absolute inset-0 transition-transform"
style={{
transform: `rotate(${rotations[index]}deg) translateY(${index * -2}px)`,
zIndex: index,
}}
>
<File.Thumb file={file} size={size} />
</div>
))}
{/* Show count badge if more than 3 files */}
{remainingCount > 0 && (
<div className="absolute -bottom-1 -right-1 size-6 rounded-full bg-accent text-white text-xs font-bold flex items-center justify-center shadow-lg border-2 border-app z-10">
+{remainingCount}
</div>
)}
</div>
);
}

View File

@ -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";

View File

@ -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]"
>
<div className="space-y-5 py-2">
{/* Source → Destination visual */}
<div className="flex items-center gap-4">
{/* Source */}
<div className="flex-1 flex flex-col items-center gap-2 p-3 bg-app rounded-lg">
<Files className="size-8 text-ink-dull" weight="fill" />
<div className="text-center">
<div className="text-xs text-ink-dull mb-0.5">From</div>
<div className="text-sm font-medium text-ink">
{sourceCount} {pluralItems}
</div>
{sourceCount === 1 && (
<div className="text-xs text-ink-faint mt-1 truncate max-w-full">
{getFileName(props.sources[0])}
<div className="flex-1 flex flex-col items-center gap-2 min-w-0">
{sourceFiles.length > 0 ? (
<>
{sourceFiles.length === 1 ? (
<File.Thumb file={sourceFiles[0]} size={80} />
) : (
<FileStack files={sourceFiles} size={80} />
)}
<div className="text-center w-full">
<div className="text-xs text-ink-dull mb-0.5">Source</div>
{sourceFiles.length === 1 ? (
<div className="text-sm font-medium text-ink truncate w-full">
{sourceFiles[0].name}
</div>
) : (
<div className="text-sm font-medium text-ink">
{sourceCount} {pluralItems}
</div>
)}
</div>
)}
</div>
</>
) : (
<>
<Files className="size-20 text-ink-dull" weight="fill" />
<div className="text-center">
<div className="text-xs text-ink-dull mb-0.5">Source</div>
<div className="text-sm font-medium text-ink">
{sourceCount} {pluralItems}
</div>
</div>
</>
)}
</div>
{/* Arrow */}
@ -238,14 +291,28 @@ function FileOperationDialog(props: FileOperationDialogProps) {
</div>
{/* Destination */}
<div className="flex-1 flex flex-col items-center gap-2 p-3 bg-app rounded-lg">
<FolderOpen className="size-8 text-accent" weight="fill" />
<div className="text-center">
<div className="text-xs text-ink-dull mb-0.5">To</div>
<div className="text-sm font-medium text-ink truncate max-w-full">
{getFileName(props.destination)}
</div>
</div>
<div className="flex-1 flex flex-col items-center gap-2 min-w-0">
{destFile ? (
<>
<File.Thumb file={destFile} size={80} />
<div className="text-center w-full">
<div className="text-xs text-ink-dull mb-0.5">To</div>
<div className="text-sm font-medium text-ink truncate w-full">
{destFile.name}
</div>
</div>
</>
) : (
<>
<FolderOpen className="size-20 text-accent" weight="fill" />
<div className="text-center">
<div className="text-xs text-ink-dull mb-0.5">To</div>
<div className="text-sm font-medium text-ink truncate max-w-full">
{getFileName(props.destination)}
</div>
</div>
</>
)}
</div>
</div>