
Some checks failed
GBCI / build (push) Failing after 10m45s
- Introduced a new CSS theme for Orange, featuring a modern color palette with distinct foreground and background colors. - Added an XTree Gold theme that emulates the classic 1980s DOS interface, complete with authentic colors and styles for file management elements. - Both themes include variables for customization and specific styles for various UI components such as cards, popovers, and menus.
793 lines
No EOL
26 KiB
TypeScript
793 lines
No EOL
26 KiB
TypeScript
"use client";
|
|
import React, { useState, useMemo, useRef } from 'react';
|
|
import {
|
|
Search, Plus, Upload, Download, Trash2, Share, Star,
|
|
Grid, List, MoreVertical, Home, ChevronRight,
|
|
Folder, File, Image, Video, Music, FileText, Code, Database,
|
|
Package, Archive, Clock, Users, Eye, Edit3, Copy, Scissors,
|
|
FolderPlus, FilePlus, RefreshCw, Info, Lock, Unlock,
|
|
ArrowRight, ExternalLink, History, Settings
|
|
} from 'lucide-react';
|
|
import { cn } from "@/lib/utils";
|
|
import Footer from '../footer';
|
|
|
|
// Simple date formatting functions
|
|
const formatDate = (dateString) => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
});
|
|
};
|
|
|
|
const formatDateTime = (dateString) => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
});
|
|
};
|
|
|
|
const formatDistanceToNow = (date) => {
|
|
const now = new Date();
|
|
const diffMs = now - new Date(date);
|
|
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffMinutes < 1) return 'now';
|
|
if (diffMinutes < 60) return `${diffMinutes}m ago`;
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
if (diffDays < 7) return `${diffDays}d ago`;
|
|
return formatDate(date);
|
|
};
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuSubTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuTrigger,
|
|
ContextMenuSeparator,
|
|
ContextMenuSub,
|
|
ContextMenuSubContent,
|
|
ContextMenuSubTrigger,
|
|
} from "@/components/ui/context-menu";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
TooltipProvider,
|
|
} from "@/components/ui/tooltip";
|
|
import {
|
|
ResizableHandle,
|
|
ResizablePanel,
|
|
ResizablePanelGroup,
|
|
} from "@/components/ui/resizable";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
|
|
// File system data
|
|
const fileSystemData = {
|
|
"": {
|
|
id: "root",
|
|
name: "My Drive",
|
|
path: "",
|
|
is_dir: true,
|
|
children: ["projects", "documents", "media", "shared"]
|
|
},
|
|
"projects": {
|
|
id: "projects",
|
|
name: "Projects",
|
|
path: "projects",
|
|
is_dir: true,
|
|
modified: "2025-01-15T10:30:00Z",
|
|
starred: true,
|
|
shared: false,
|
|
children: ["web-apps", "mobile-apps", "ai-research"]
|
|
},
|
|
"projects/web-apps": {
|
|
id: "web-apps",
|
|
name: "Web Applications",
|
|
path: "projects/web-apps",
|
|
is_dir: true,
|
|
modified: "2025-01-14T16:45:00Z",
|
|
starred: false,
|
|
shared: true,
|
|
children: ["dashboard-pro", "package.json", "README.md"]
|
|
},
|
|
"projects/web-apps/package.json": {
|
|
id: "package-json",
|
|
name: "package.json",
|
|
path: "projects/web-apps/package.json",
|
|
is_dir: false,
|
|
size: 2048,
|
|
type: "json",
|
|
modified: "2025-01-13T14:20:00Z",
|
|
starred: false,
|
|
shared: false
|
|
},
|
|
"projects/web-apps/README.md": {
|
|
id: "readme-md",
|
|
name: "README.md",
|
|
path: "projects/web-apps/README.md",
|
|
is_dir: false,
|
|
size: 5120,
|
|
type: "markdown",
|
|
modified: "2025-01-12T09:30:00Z",
|
|
starred: false,
|
|
shared: true
|
|
},
|
|
"documents": {
|
|
id: "documents",
|
|
name: "Documents",
|
|
path: "documents",
|
|
is_dir: true,
|
|
modified: "2025-01-14T12:00:00Z",
|
|
starred: false,
|
|
shared: false,
|
|
children: ["proposals", "Q1-Strategy.pdf", "Budget-2025.xlsx"]
|
|
},
|
|
"documents/Q1-Strategy.pdf": {
|
|
id: "q1-strategy",
|
|
name: "Q1 Strategy.pdf",
|
|
path: "documents/Q1-Strategy.pdf",
|
|
is_dir: false,
|
|
size: 1048576,
|
|
type: "pdf",
|
|
modified: "2025-01-10T15:30:00Z",
|
|
starred: true,
|
|
shared: true
|
|
},
|
|
"media": {
|
|
id: "media",
|
|
name: "Media",
|
|
path: "media",
|
|
is_dir: true,
|
|
modified: "2025-01-13T18:45:00Z",
|
|
starred: false,
|
|
shared: false,
|
|
children: ["photos", "videos", "vacation-2024.jpg"]
|
|
},
|
|
"media/vacation-2024.jpg": {
|
|
id: "vacation-photo",
|
|
name: "vacation-2024.jpg",
|
|
path: "media/vacation-2024.jpg",
|
|
is_dir: false,
|
|
size: 3145728,
|
|
type: "image",
|
|
modified: "2024-12-25T20:00:00Z",
|
|
starred: true,
|
|
shared: false
|
|
},
|
|
"shared": {
|
|
id: "shared",
|
|
name: "Shared",
|
|
path: "shared",
|
|
is_dir: true,
|
|
modified: "2025-01-12T11:20:00Z",
|
|
starred: false,
|
|
shared: true,
|
|
children: ["team-resources", "client-files"]
|
|
}
|
|
};
|
|
|
|
const getFileIcon = (item) => {
|
|
if (item.is_dir) {
|
|
return <Folder className="w-4 h-4 text-blue-600" />;
|
|
}
|
|
|
|
const iconMap = {
|
|
pdf: <FileText className="w-4 h-4 text-red-500" />,
|
|
xlsx: <Database className="w-4 h-4 text-green-600" />,
|
|
json: <Code className="w-4 h-4 text-yellow-600" />,
|
|
markdown: <Edit3 className="w-4 h-4 text-purple-500" />,
|
|
md: <Edit3 className="w-4 h-4 text-purple-500" />,
|
|
jpg: <Image className="w-4 h-4 text-pink-500" />,
|
|
jpeg: <Image className="w-4 h-4 text-pink-500" />,
|
|
png: <Image className="w-4 h-4 text-pink-500" />,
|
|
mp4: <Video className="w-4 h-4 text-red-600" />,
|
|
mp3: <Music className="w-4 h-4 text-green-600" />
|
|
};
|
|
|
|
return iconMap[item.type] || <File className="w-4 h-4 text-gray-500" />;
|
|
};
|
|
|
|
const formatFileSize = (bytes) => {
|
|
if (!bytes) return '';
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
|
};
|
|
|
|
const FileContextMenu = ({ file, children, onAction }) => {
|
|
const contextMenuItems = [
|
|
{ icon: Eye, label: "Open", action: "open" },
|
|
{ icon: Download, label: "Download", action: "download" },
|
|
{ separator: true },
|
|
{
|
|
icon: Share, label: "Share", action: "share", submenu: [
|
|
{ icon: Users, label: "Share with team", action: "share-team" },
|
|
{ icon: ExternalLink, label: "Get link", action: "get-link" },
|
|
{ icon: Lock, label: "Restrict access", action: "restrict" },
|
|
]
|
|
},
|
|
{ icon: Star, label: file?.starred ? "Remove from starred" : "Add to starred", action: "star" },
|
|
{ separator: true },
|
|
{ icon: Copy, label: "Copy", action: "copy" },
|
|
{ icon: Scissors, label: "Cut", action: "cut" },
|
|
{ icon: Edit3, label: "Rename", action: "rename" },
|
|
{ separator: true },
|
|
{
|
|
icon: FolderPlus, label: "Move to", action: "move", submenu: [
|
|
{ icon: Folder, label: "Documents", action: "move-documents" },
|
|
{ icon: Folder, label: "Projects", action: "move-projects" },
|
|
{ icon: Folder, label: "Choose folder...", action: "move-choose" },
|
|
]
|
|
},
|
|
{ icon: Copy, label: "Make a copy", action: "duplicate" },
|
|
{ separator: true },
|
|
{ icon: History, label: "Version history", action: "history" },
|
|
{ icon: Info, label: "Details", action: "details" },
|
|
{ separator: true },
|
|
{ icon: Trash2, label: "Move to trash", action: "trash", destructive: true },
|
|
];
|
|
|
|
const renderContextMenuItem = (item, index) => {
|
|
if (item.separator) {
|
|
return <ContextMenuSeparator key={index} />;
|
|
}
|
|
|
|
if (item.submenu) {
|
|
return (
|
|
<ContextMenuSub key={index}>
|
|
<ContextMenuSubTrigger className="flex items-center gap-2">
|
|
<item.icon className="w-4 h-4" />
|
|
{item.label}
|
|
</ContextMenuSubTrigger>
|
|
<ContextMenuSubContent>
|
|
{item.submenu.map((subItem, subIndex) => (
|
|
<ContextMenuItem
|
|
key={subIndex}
|
|
onClick={() => onAction(subItem.action, file)}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<subItem.icon className="w-4 h-4" />
|
|
{subItem.label}
|
|
</ContextMenuItem>
|
|
))}
|
|
</ContextMenuSubContent>
|
|
</ContextMenuSub>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ContextMenuItem
|
|
key={index}
|
|
onClick={() => onAction(item.action, file)}
|
|
className={cn(
|
|
"flex items-center gap-2",
|
|
item.destructive && "text-destructive focus:text-destructive"
|
|
)}
|
|
>
|
|
<item.icon className="w-4 h-4" />
|
|
{item.label}
|
|
</ContextMenuItem>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<ContextMenu>
|
|
<ContextMenuTrigger asChild>
|
|
{children}
|
|
</ContextMenuTrigger>
|
|
<ContextMenuContent className="w-56">
|
|
{contextMenuItems.map(renderContextMenuItem)}
|
|
</ContextMenuContent>
|
|
</ContextMenu>
|
|
);
|
|
};
|
|
|
|
const FolderTree = ({ onSelect, selectedPath, isCollapsed }) => {
|
|
const [expanded, setExpanded] = useState({ "": true, "projects": true });
|
|
|
|
const toggleExpand = (path) => {
|
|
setExpanded(prev => ({ ...prev, [path]: !prev[path] }));
|
|
onSelect(path);
|
|
};
|
|
|
|
const navLinks = [
|
|
{ title: "My Drive", path: "", icon: Home },
|
|
{ title: "Shared", path: "shared", icon: Users },
|
|
{ title: "Starred", path: "starred", icon: Star },
|
|
{ title: "Recent", path: "recent", icon: Clock },
|
|
{ title: "Trash", path: "trash", icon: Trash2 },
|
|
];
|
|
|
|
const renderTreeItem = (path, level = 0) => {
|
|
const item = fileSystemData[path];
|
|
if (!item || !item.is_dir) return null;
|
|
|
|
const isExpanded = expanded[path];
|
|
const isSelected = selectedPath === path;
|
|
|
|
return (
|
|
<div key={item.id}>
|
|
<button
|
|
onClick={() => toggleExpand(path)}
|
|
className={cn(
|
|
"flex items-center w-full px-2 py-1.5 text-sm rounded-md hover:bg-accent transition-colors",
|
|
isSelected && "bg-muted",
|
|
isCollapsed ? "justify-center" : "justify-start"
|
|
)}
|
|
>
|
|
{!isCollapsed && (
|
|
<>
|
|
<ChevronRight className={cn("w-4 h-4 mr-1 transition-transform", isExpanded && "rotate-90")} />
|
|
{getFileIcon(item)}
|
|
<span className="ml-2 truncate">{item.name}</span>
|
|
{item.starred && <Star className="w-3 h-3 ml-auto text-yellow-500 fill-current" />}
|
|
</>
|
|
)}
|
|
{isCollapsed && getFileIcon(item)}
|
|
</button>
|
|
{!isCollapsed && isExpanded && item.children && (
|
|
<div className="ml-4">
|
|
{item.children.map(childPath => {
|
|
const fullPath = path ? `${path}/${childPath}` : childPath;
|
|
return renderTreeItem(fullPath, level + 1);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<div className={cn("p-2", isCollapsed ? "px-1" : "")}>
|
|
<nav className="space-y-1">
|
|
{navLinks.map((link) =>
|
|
isCollapsed ? (
|
|
<Tooltip key={link.path} delayDuration={0}>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant={selectedPath === link.path ? "default" : "ghost"}
|
|
size="icon"
|
|
className="w-9 h-9"
|
|
onClick={() => onSelect(link.path)}
|
|
>
|
|
<link.icon className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">{link.title}</TooltipContent>
|
|
</Tooltip>
|
|
) : (
|
|
<Button
|
|
key={link.path}
|
|
variant={selectedPath === link.path ? "default" : "ghost"}
|
|
className="w-full justify-start"
|
|
onClick={() => onSelect(link.path)}
|
|
>
|
|
<link.icon className="mr-2 h-4 w-4" />
|
|
{link.title}
|
|
</Button>
|
|
)
|
|
)}
|
|
</nav>
|
|
</div>
|
|
{!isCollapsed && (
|
|
<>
|
|
<Separator />
|
|
<ScrollArea className="flex-1 p-2">
|
|
{renderTreeItem("")}
|
|
</ScrollArea>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile, onContextAction }) => {
|
|
const files = useMemo(() => {
|
|
const currentItem = fileSystemData[path];
|
|
if (!currentItem || !currentItem.is_dir || !currentItem.children) return [];
|
|
|
|
let items = currentItem.children.map(childName => {
|
|
const childPath = path ? `${path}/${childName}` : childName;
|
|
return fileSystemData[childPath];
|
|
}).filter(Boolean);
|
|
|
|
if (searchTerm) {
|
|
items = items.filter(item =>
|
|
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
}
|
|
|
|
if (filterType && filterType !== 'all') {
|
|
items = items.filter(item => {
|
|
if (filterType === 'folders') return item.is_dir;
|
|
if (filterType === 'files') return !item.is_dir;
|
|
if (filterType === 'starred') return item.starred;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
return items.sort((a, b) => {
|
|
if (a.is_dir && !b.is_dir) return -1;
|
|
if (!a.is_dir && b.is_dir) return 1;
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
}, [path, searchTerm, filterType]);
|
|
|
|
return (
|
|
<ScrollArea className="h-full">
|
|
<div className="flex flex-col">
|
|
{files.map((item) => (
|
|
<FileContextMenu
|
|
key={item.id}
|
|
file={item}
|
|
onAction={onContextAction}
|
|
>
|
|
<button
|
|
className={cn(
|
|
"flex items-center gap-3 p-3 text-left text-sm transition-all hover:bg-accent border-b",
|
|
selectedFile?.id === item.id && "bg-muted"
|
|
)}
|
|
onClick={() => setSelectedFile(item)}
|
|
>
|
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
{getFileIcon(item)}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<div className="font-medium truncate">{item.name}</div>
|
|
{item.starred && <Star className="w-3 h-3 text-yellow-500 fill-current" />}
|
|
{item.shared && <Users className="w-3 h-3 text-blue-500" />}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{item.is_dir ? 'Folder' : formatFileSize(item.size)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{formatDistanceToNow(new Date(item.modified), { addSuffix: true })}
|
|
</div>
|
|
</button>
|
|
</FileContextMenu>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
);
|
|
};
|
|
|
|
const FileDisplay = ({ file }) => {
|
|
if (!file) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-full text-center text-muted-foreground">
|
|
<File className="w-12 h-12 mb-4" />
|
|
<div className="text-lg font-medium">No file selected</div>
|
|
<div className="text-sm">Select a file to view details</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<div className="flex items-center p-2">
|
|
<div className="flex items-center gap-2">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon">
|
|
<Download className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Download</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon">
|
|
<Share className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Share</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon">
|
|
<Star className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Star</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
<div className="ml-auto">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon">
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem>Rename</DropdownMenuItem>
|
|
<DropdownMenuItem>Make a copy</DropdownMenuItem>
|
|
<DropdownMenuItem>Move to trash</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
<Separator />
|
|
<div className="flex-1 flex-col">
|
|
<div className="flex items-start gap-4 p-4">
|
|
<div className="p-2 rounded-lg bg-accent">
|
|
{getFileIcon(file)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-semibold">{file.name}</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{file.is_dir ? 'Folder' : `${file.type?.toUpperCase() || 'File'} • ${formatFileSize(file.size)}`}
|
|
</div>
|
|
</div>
|
|
{file.modified && (
|
|
<div className="text-xs text-muted-foreground">
|
|
{formatDate(file.modified)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Separator />
|
|
<div className="p-4 space-y-4">
|
|
<div>
|
|
<div className="text-sm font-medium">Location</div>
|
|
<div className="text-sm text-muted-foreground">/{file.path || ''}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium">Modified</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{formatDateTime(file.modified)}
|
|
</div>
|
|
</div>
|
|
{!file.is_dir && (
|
|
<div>
|
|
<div className="text-sm font-medium">Size</div>
|
|
<div className="text-sm text-muted-foreground">{formatFileSize(file.size)}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default function FileManager() {
|
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
const [currentPath, setCurrentPath] = useState('');
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [filterType, setFilterType] = useState('all');
|
|
const [selectedFile, setSelectedFile] = useState(null);
|
|
|
|
const currentItem = fileSystemData[currentPath];
|
|
|
|
// Drive-specific keyboard shortcuts
|
|
// XTreeGold classic shortcut layout - two rows
|
|
// XTree/NC-style: only unique, non-browser, non-reserved keys for file ops
|
|
// No F1-F12, Ctrl+F, Ctrl+T, Ctrl+W, Ctrl+N, Ctrl+R, Ctrl+P, etc.
|
|
// Use Q, W, E, R, T, Y, U, I, O, P, A, S, D, G, H, J, K, L, Z, X, C, V, B, M with Ctrl/Shift if needed
|
|
const shortcuts = [
|
|
// File operations row (unique qkeys)
|
|
[
|
|
{ key: 'Q', label: 'Rename', action: () => console.log('Rename') },
|
|
{ key: 'W', label: 'View', action: () => console.log('View') },
|
|
{ key: 'E', label: 'Edit', action: () => console.log('Edit') },
|
|
{ key: 'R', label: 'Move', action: () => console.log('Move') },
|
|
{ key: 'T', label: 'MkDir', action: () => console.log('Make Directory') },
|
|
{ key: 'Y', label: 'Delete', action: () => console.log('Delete') },
|
|
{ key: 'U', label: 'Copy', action: () => console.log('Copy') },
|
|
{ key: 'I', label: 'Cut', action: () => console.log('Cut') },
|
|
{ key: 'O', label: 'Paste', action: () => console.log('Paste') },
|
|
{ key: 'P', label: 'Duplicate', action: () => console.log('Duplicate') },
|
|
],
|
|
// Navigation/info row (unique qkeys)
|
|
[
|
|
{ key: 'A', label: 'Select', action: () => console.log('Select') },
|
|
{ key: 'S', label: 'Select All', action: () => console.log('Select All') },
|
|
{ key: 'D', label: 'Deselect', action: () => console.log('Deselect') },
|
|
{ key: 'G', label: 'Details', action: () => console.log('Details') },
|
|
{ key: 'H', label: 'History', action: () => console.log('History') },
|
|
{ key: 'J', label: 'Share', action: () => console.log('Share') },
|
|
{ key: 'K', label: 'Star', action: () => console.log('Star') },
|
|
{ key: 'L', label: 'Download', action: () => console.log('Download') },
|
|
{ key: 'Z', label: 'Upload', action: () => console.log('Upload') },
|
|
{ key: 'X', label: 'Refresh', action: () => console.log('Refresh') },
|
|
]
|
|
];
|
|
|
|
|
|
const handleContextAction = (action, file) => {
|
|
console.log(`Context action: ${action}`, file);
|
|
// Handle context menu actions here
|
|
switch (action) {
|
|
case 'star':
|
|
// Toggle star status
|
|
console.log('Toggle star for', file.name);
|
|
break;
|
|
case 'share':
|
|
console.log('Share', file.name);
|
|
break;
|
|
case 'download':
|
|
console.log('Download', file.name);
|
|
break;
|
|
case 'trash':
|
|
console.log('Move to trash', file.name);
|
|
break;
|
|
case 'copy':
|
|
console.log('Copy', file.name);
|
|
break;
|
|
case 'cut':
|
|
console.log('Cut', file.name);
|
|
break;
|
|
case 'rename':
|
|
console.log('Rename', file.name);
|
|
break;
|
|
case 'duplicate':
|
|
console.log('Duplicate', file.name);
|
|
break;
|
|
case 'details':
|
|
console.log('Show details for', file.name);
|
|
setSelectedFile(file);
|
|
break;
|
|
default:
|
|
console.log('Unknown action:', action);
|
|
}
|
|
};
|
|
|
|
// Keyboard shortcut handler
|
|
React.useEffect(() => {
|
|
const handleKeyDown = (e) => {
|
|
const isCtrl = e.ctrlKey || e.metaKey;
|
|
const isShift = e.shiftKey;
|
|
|
|
if (isCtrl && e.key === 'n' && isShift) {
|
|
e.preventDefault();
|
|
console.log('New folder shortcut');
|
|
} else if (isCtrl && e.key === 'n') {
|
|
e.preventDefault();
|
|
console.log('New file shortcut');
|
|
} else if (isCtrl && e.key === 'u') {
|
|
e.preventDefault();
|
|
console.log('Upload shortcut');
|
|
} else if (isCtrl && e.key === 'f') {
|
|
e.preventDefault();
|
|
document.querySelector('input[placeholder="Search files"]')?.focus();
|
|
} else if (e.key === 'Delete' && selectedFile) {
|
|
e.preventDefault();
|
|
console.log('Delete shortcut');
|
|
} else if (e.key === 'F2' && selectedFile) {
|
|
e.preventDefault();
|
|
console.log('Rename shortcut');
|
|
}
|
|
};
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
}, [selectedFile]);
|
|
|
|
return (
|
|
<div className="flex flex-col h-[calc(100vh-40px)]"> {/* Adjust based on your nav height */}
|
|
<TooltipProvider delayDuration={0}>
|
|
<ResizablePanelGroup direction="horizontal" className="flex-1 min-h-0">
|
|
{/* Left Sidebar */}
|
|
<ResizablePanel
|
|
defaultSize={20}
|
|
collapsedSize={4}
|
|
collapsible={true}
|
|
minSize={15}
|
|
maxSize={30}
|
|
onCollapse={() => setIsCollapsed(true)}
|
|
onResize={() => setIsCollapsed(false)}
|
|
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300")}
|
|
>
|
|
<FolderTree
|
|
onSelect={setCurrentPath}
|
|
selectedPath={currentPath}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
</ResizablePanel>
|
|
|
|
<ResizableHandle withHandle />
|
|
|
|
{/* Middle File List */}
|
|
<ResizablePanel defaultSize={50} minSize={30}>
|
|
<Tabs defaultValue="all" className="flex flex-col h-full">
|
|
<div className="flex items-center px-4 py-2">
|
|
<h1 className="text-xl font-bold">{currentItem?.name || 'My Drive'}</h1>
|
|
<TabsList className="ml-auto">
|
|
<TabsTrigger value="all">All</TabsTrigger>
|
|
<TabsTrigger value="starred">Starred</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
<Separator />
|
|
<div className="bg-background/95 p-4 backdrop-blur">
|
|
<div className="flex items-center gap-2">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search files"
|
|
className="pl-8"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
<Select value={filterType} onValueChange={setFilterType}>
|
|
<SelectTrigger className="w-[140px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All items</SelectItem>
|
|
<SelectItem value="folders">Folders</SelectItem>
|
|
<SelectItem value="files">Files</SelectItem>
|
|
<SelectItem value="starred">Starred</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<TabsContent value="all" className="m-0 flex-1">
|
|
<FileList
|
|
path={currentPath}
|
|
searchTerm={searchTerm}
|
|
filterType={filterType}
|
|
selectedFile={selectedFile}
|
|
setSelectedFile={setSelectedFile}
|
|
onContextAction={handleContextAction}
|
|
/>
|
|
</TabsContent>
|
|
<TabsContent value="starred" className="m-0 flex-1">
|
|
<FileList
|
|
path={currentPath}
|
|
searchTerm={searchTerm}
|
|
filterType="starred"
|
|
selectedFile={selectedFile}
|
|
setSelectedFile={setSelectedFile}
|
|
onContextAction={handleContextAction}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</ResizablePanel>
|
|
|
|
<ResizableHandle withHandle />
|
|
|
|
{/* Right File Details */}
|
|
<ResizablePanel defaultSize={30} minSize={25}>
|
|
<FileDisplay file={selectedFile} />
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</TooltipProvider>
|
|
|
|
{/* Footer with Status Bar */}
|
|
<Footer shortcuts={shortcuts} />
|
|
</div>
|
|
);
|
|
} |