mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2025-12-11 20:15:30 +01:00
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:
parent
d8984a0688
commit
7f3eee3848
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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="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>
|
||||
{sourceCount === 1 && (
|
||||
<div className="text-xs text-ink-faint mt-1 truncate max-w-full">
|
||||
{getFileName(props.sources[0])}
|
||||
</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="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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user