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 { File } from "./File";
|
||||||
|
export { FileStack } from "./FileStack";
|
||||||
export { Thumb, Icon } from "./Thumb";
|
export { Thumb, Icon } from "./Thumb";
|
||||||
export { Title } from "./Title";
|
export { Title } from "./Title";
|
||||||
export { Metadata } from "./Metadata";
|
export { Metadata } from "./Metadata";
|
||||||
|
|||||||
@ -9,16 +9,21 @@ import {
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
Copy as CopyIcon,
|
Copy as CopyIcon,
|
||||||
ArrowsLeftRight,
|
ArrowsLeftRight,
|
||||||
|
File as FileIcon,
|
||||||
|
Image,
|
||||||
|
FileText,
|
||||||
|
FilmStrip,
|
||||||
|
MusicNote,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
dialogManager,
|
dialogManager,
|
||||||
useDialog,
|
useDialog,
|
||||||
} from "@sd/ui";
|
} from "@sd/ui";
|
||||||
import type { SdPath } from "@sd/ts-client";
|
import type { SdPath, File as FileType } from "@sd/ts-client";
|
||||||
import { useLibraryMutation } from "../context";
|
import { useLibraryMutation, useLibraryQuery } from "../context";
|
||||||
import { sounds } from "@sd/assets/sounds";
|
import { sounds } from "@sd/assets/sounds";
|
||||||
import { File } from "./Explorer/File";
|
import { File, FileStack } from "./Explorer/File";
|
||||||
|
|
||||||
interface FileOperationDialogProps {
|
interface FileOperationDialogProps {
|
||||||
id: number;
|
id: number;
|
||||||
@ -52,6 +57,34 @@ function FileOperationDialog(props: FileOperationDialogProps) {
|
|||||||
|
|
||||||
const copyFiles = useLibraryMutation("files.copy");
|
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
|
// Check if any source is the same as destination
|
||||||
const hasSameSourceDest = props.sources.some((source) => {
|
const hasSameSourceDest = props.sources.some((source) => {
|
||||||
if ("Physical" in source && "Physical" in props.destination) {
|
if ("Physical" in source && "Physical" in props.destination) {
|
||||||
@ -212,24 +245,44 @@ function FileOperationDialog(props: FileOperationDialogProps) {
|
|||||||
ctaLabel={operation === "copy" ? "Copy" : "Move"}
|
ctaLabel={operation === "copy" ? "Copy" : "Move"}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onCancelled={handleCancel}
|
onCancelled={handleCancel}
|
||||||
|
formClassName="!min-w-[400px] !max-w-[400px]"
|
||||||
>
|
>
|
||||||
<div className="space-y-5 py-2">
|
<div className="space-y-5 py-2">
|
||||||
{/* Source → Destination visual */}
|
{/* Source → Destination visual */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Source */}
|
{/* Source */}
|
||||||
<div className="flex-1 flex flex-col items-center gap-2 p-3 bg-app rounded-lg">
|
<div className="flex-1 flex flex-col items-center gap-2 min-w-0">
|
||||||
<Files className="size-8 text-ink-dull" weight="fill" />
|
{sourceFiles.length > 0 ? (
|
||||||
<div className="text-center">
|
<>
|
||||||
<div className="text-xs text-ink-dull mb-0.5">From</div>
|
{sourceFiles.length === 1 ? (
|
||||||
<div className="text-sm font-medium text-ink">
|
<File.Thumb file={sourceFiles[0]} size={80} />
|
||||||
{sourceCount} {pluralItems}
|
) : (
|
||||||
</div>
|
<FileStack files={sourceFiles} size={80} />
|
||||||
{sourceCount === 1 && (
|
)}
|
||||||
<div className="text-xs text-ink-faint mt-1 truncate max-w-full">
|
<div className="text-center w-full">
|
||||||
{getFileName(props.sources[0])}
|
<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>
|
||||||
)}
|
</>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Arrow */}
|
{/* Arrow */}
|
||||||
@ -238,14 +291,28 @@ function FileOperationDialog(props: FileOperationDialogProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Destination */}
|
{/* Destination */}
|
||||||
<div className="flex-1 flex flex-col items-center gap-2 p-3 bg-app rounded-lg">
|
<div className="flex-1 flex flex-col items-center gap-2 min-w-0">
|
||||||
<FolderOpen className="size-8 text-accent" weight="fill" />
|
{destFile ? (
|
||||||
<div className="text-center">
|
<>
|
||||||
<div className="text-xs text-ink-dull mb-0.5">To</div>
|
<File.Thumb file={destFile} size={80} />
|
||||||
<div className="text-sm font-medium text-ink truncate max-w-full">
|
<div className="text-center w-full">
|
||||||
{getFileName(props.destination)}
|
<div className="text-xs text-ink-dull mb-0.5">To</div>
|
||||||
</div>
|
<div className="text-sm font-medium text-ink truncate w-full">
|
||||||
</div>
|
{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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user