feat: Enhance file manager with mobile support and UI improvements
Some checks failed
GBCI / build (push) Failing after 10m3s
Some checks failed
GBCI / build (push) Failing after 10m3s
This commit is contained in:
parent
21a8236516
commit
fbe6db37a6
4 changed files with 277 additions and 130 deletions
|
@ -1,14 +1,30 @@
|
||||||
"use client";
|
"use client";
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Search, Download, Trash2, Share, Star,
|
Search, Download, Trash2, Share, Star,
|
||||||
MoreVertical, Home, ChevronRight,
|
MoreVertical, Home, ChevronRight, ChevronLeft,
|
||||||
Folder, File, Image, Video, Music, FileText, Code, Database,
|
Folder, File, Image, Video, Music, FileText, Code, Database,
|
||||||
Clock, Users, Eye, Edit3, Copy, Scissors,
|
Clock, Users, Eye, Edit3, Copy, Scissors,
|
||||||
FolderPlus, Info, Lock,
|
FolderPlus, Info, Lock, Menu,
|
||||||
ExternalLink, History} from 'lucide-react';
|
ExternalLink, History, X
|
||||||
|
} from 'lucide-react';
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Footer from '../footer';
|
import Footer from '../footer';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from '@/components/ui/context-menu';
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||||
|
import {
|
||||||
|
ResizableHandle,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizablePanelGroup,
|
||||||
|
} from "@/components/ui/resizable";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
|
|
||||||
// Simple date formatting functions
|
// Simple date formatting functions
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
|
@ -46,46 +62,6 @@ const formatDistanceToNow = (date) => {
|
||||||
return formatDate(date);
|
return formatDate(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
|
||||||
} 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
|
// File system data
|
||||||
const fileSystemData = {
|
const fileSystemData = {
|
||||||
"": {
|
"": {
|
||||||
|
@ -193,7 +169,7 @@ const fileSystemData = {
|
||||||
|
|
||||||
const getFileIcon = (item) => {
|
const getFileIcon = (item) => {
|
||||||
if (item.is_dir) {
|
if (item.is_dir) {
|
||||||
return <Folder className="w-4 h-4 text-blue-600" />;
|
return <Folder className="w-4 h-4 text-yellow-500" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
|
@ -209,7 +185,7 @@ const getFileIcon = (item) => {
|
||||||
mp3: <Music className="w-4 h-4 text-green-600" />
|
mp3: <Music className="w-4 h-4 text-green-600" />
|
||||||
};
|
};
|
||||||
|
|
||||||
return iconMap[item.type] || <File className="w-4 h-4 text-gray-500" />;
|
return iconMap[item.type] || <File className="w-4 h-4 text-muted-foreground" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatFileSize = (bytes) => {
|
const formatFileSize = (bytes) => {
|
||||||
|
@ -264,7 +240,7 @@ const FileContextMenu = ({ file, children, onAction }) => {
|
||||||
<item.icon className="w-4 h-4" />
|
<item.icon className="w-4 h-4" />
|
||||||
{item.label}
|
{item.label}
|
||||||
</ContextMenuSubTrigger>
|
</ContextMenuSubTrigger>
|
||||||
<ContextMenuSubContent>
|
<ContextMenuSubContent className="bg-popover">
|
||||||
{item.submenu.map((subItem, subIndex) => (
|
{item.submenu.map((subItem, subIndex) => (
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
key={subIndex}
|
key={subIndex}
|
||||||
|
@ -300,14 +276,14 @@ const FileContextMenu = ({ file, children, onAction }) => {
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuContent className="w-56">
|
<ContextMenuContent className="w-56 bg-popover">
|
||||||
{contextMenuItems.map(renderContextMenuItem)}
|
{contextMenuItems.map(renderContextMenuItem)}
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FolderTree = ({ onSelect, selectedPath, isCollapsed }) => {
|
const FolderTree = ({ onSelect, selectedPath = undefined, isCollapsed = undefined, isMobile = undefined }) => {
|
||||||
const [expanded, setExpanded] = useState({ "": true, "projects": true });
|
const [expanded, setExpanded] = useState({ "": true, "projects": true });
|
||||||
|
|
||||||
const toggleExpand = (path) => {
|
const toggleExpand = (path) => {
|
||||||
|
@ -336,7 +312,7 @@ const FolderTree = ({ onSelect, selectedPath, isCollapsed }) => {
|
||||||
onClick={() => toggleExpand(path)}
|
onClick={() => toggleExpand(path)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center w-full px-2 py-1.5 text-sm rounded-md hover:bg-accent transition-colors",
|
"flex items-center w-full px-2 py-1.5 text-sm rounded-md hover:bg-accent transition-colors",
|
||||||
isSelected && "bg-muted",
|
isSelected && "bg-accent",
|
||||||
isCollapsed ? "justify-center" : "justify-start"
|
isCollapsed ? "justify-center" : "justify-start"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -363,7 +339,7 @@ const FolderTree = ({ onSelect, selectedPath, isCollapsed }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className={cn("flex flex-col h-full bg-background border-r border-border", isMobile ? "w-full" : "")}>
|
||||||
<div className={cn("p-2", isCollapsed ? "px-1" : "")}>
|
<div className={cn("p-2", isCollapsed ? "px-1" : "")}>
|
||||||
<nav className="space-y-1">
|
<nav className="space-y-1">
|
||||||
{navLinks.map((link) =>
|
{navLinks.map((link) =>
|
||||||
|
@ -373,19 +349,19 @@ const FolderTree = ({ onSelect, selectedPath, isCollapsed }) => {
|
||||||
<Button
|
<Button
|
||||||
variant={selectedPath === link.path ? "default" : "ghost"}
|
variant={selectedPath === link.path ? "default" : "ghost"}
|
||||||
size="icon"
|
size="icon"
|
||||||
className="w-9 h-9"
|
className="w-9 h-9 bg-background hover:bg-accent"
|
||||||
onClick={() => onSelect(link.path)}
|
onClick={() => onSelect(link.path)}
|
||||||
>
|
>
|
||||||
<link.icon className="h-4 w-4" />
|
<link.icon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">{link.title}</TooltipContent>
|
<TooltipContent side="right" className="bg-popover">{link.title}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
key={link.path}
|
key={link.path}
|
||||||
variant={selectedPath === link.path ? "default" : "ghost"}
|
variant={selectedPath === link.path ? "default" : "ghost"}
|
||||||
className="w-full justify-start"
|
className="w-full justify-start bg-background hover:bg-accent"
|
||||||
onClick={() => onSelect(link.path)}
|
onClick={() => onSelect(link.path)}
|
||||||
>
|
>
|
||||||
<link.icon className="mr-2 h-4 w-4" />
|
<link.icon className="mr-2 h-4 w-4" />
|
||||||
|
@ -397,7 +373,7 @@ const FolderTree = ({ onSelect, selectedPath, isCollapsed }) => {
|
||||||
</div>
|
</div>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<>
|
<>
|
||||||
<Separator />
|
<Separator className="bg-border" />
|
||||||
<ScrollArea className="flex-1 p-2">
|
<ScrollArea className="flex-1 p-2">
|
||||||
{renderTreeItem("")}
|
{renderTreeItem("")}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
@ -407,7 +383,7 @@ const FolderTree = ({ onSelect, selectedPath, isCollapsed }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile, onContextAction }) => {
|
const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile, onContextAction, isMobile }) => {
|
||||||
const files = useMemo(() => {
|
const files = useMemo(() => {
|
||||||
const currentItem = fileSystemData[path];
|
const currentItem = fileSystemData[path];
|
||||||
if (!currentItem || !currentItem.is_dir || !currentItem.children) return [];
|
if (!currentItem || !currentItem.is_dir || !currentItem.children) return [];
|
||||||
|
@ -440,7 +416,7 @@ const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile,
|
||||||
}, [path, searchTerm, filterType]);
|
}, [path, searchTerm, filterType]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className={cn("h-full bg-background", isMobile ? "w-full" : "")}>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{files.map((item) => (
|
{files.map((item) => (
|
||||||
<FileContextMenu
|
<FileContextMenu
|
||||||
|
@ -450,8 +426,8 @@ const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile,
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 p-3 text-left text-sm transition-all hover:bg-accent border-b",
|
"flex items-center gap-3 p-3 text-left text-sm transition-all hover:bg-accent border-b border-border",
|
||||||
selectedFile?.id === item.id && "bg-muted"
|
selectedFile?.id === item.id && "bg-accent"
|
||||||
)}
|
)}
|
||||||
onClick={() => setSelectedFile(item)}
|
onClick={() => setSelectedFile(item)}
|
||||||
>
|
>
|
||||||
|
@ -479,10 +455,10 @@ const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FileDisplay = ({ file }) => {
|
const FileDisplay = ({ file, isMobile }) => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-center text-muted-foreground">
|
<div className={cn("flex flex-col items-center justify-center h-full text-center text-muted-foreground bg-background", isMobile ? "w-full" : "")}>
|
||||||
<File className="w-12 h-12 mb-4" />
|
<File className="w-12 h-12 mb-4" />
|
||||||
<div className="text-lg font-medium">No file selected</div>
|
<div className="text-lg font-medium">No file selected</div>
|
||||||
<div className="text-sm">Select a file to view details</div>
|
<div className="text-sm">Select a file to view details</div>
|
||||||
|
@ -491,53 +467,59 @@ const FileDisplay = ({ file }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className={cn("flex flex-col h-full bg-background", isMobile ? "w-full" : "")}>
|
||||||
<div className="flex items-center p-2">
|
<div className="flex items-center p-2 bg-secondary border-b border-border">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip>
|
<TooltipProvider>
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Button variant="ghost" size="icon">
|
<TooltipTrigger asChild>
|
||||||
<Download className="h-4 w-4" />
|
<Button variant="ghost" size="icon" className="hover:bg-accent">
|
||||||
</Button>
|
<Download className="h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipContent>Download</TooltipContent>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<TooltipContent className="bg-popover">Download</TooltipContent>
|
||||||
<Tooltip>
|
</Tooltip>
|
||||||
<TooltipTrigger asChild>
|
</TooltipProvider>
|
||||||
<Button variant="ghost" size="icon">
|
<TooltipProvider>
|
||||||
<Share className="h-4 w-4" />
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger asChild>
|
||||||
</TooltipTrigger>
|
<Button variant="ghost" size="icon" className="hover:bg-accent">
|
||||||
<TooltipContent>Share</TooltipContent>
|
<Share className="h-4 w-4" />
|
||||||
</Tooltip>
|
</Button>
|
||||||
<Tooltip>
|
</TooltipTrigger>
|
||||||
<TooltipTrigger asChild>
|
<TooltipContent className="bg-popover">Share</TooltipContent>
|
||||||
<Button variant="ghost" size="icon">
|
</Tooltip>
|
||||||
<Star className="h-4 w-4" />
|
</TooltipProvider>
|
||||||
</Button>
|
<TooltipProvider>
|
||||||
</TooltipTrigger>
|
<Tooltip>
|
||||||
<TooltipContent>Star</TooltipContent>
|
<TooltipTrigger asChild>
|
||||||
</Tooltip>
|
<Button variant="ghost" size="icon" className="hover:bg-accent">
|
||||||
|
<Star className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="bg-popover">Star</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon" className="hover:bg-accent">
|
||||||
<MoreVertical className="h-4 w-4" />
|
<MoreVertical className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end" className="bg-popover">
|
||||||
<DropdownMenuItem>Rename</DropdownMenuItem>
|
<DropdownMenuItem>Rename</DropdownMenuItem>
|
||||||
<DropdownMenuItem>Make a copy</DropdownMenuItem>
|
<DropdownMenuItem>Make a copy</DropdownMenuItem>
|
||||||
<DropdownMenuItem>Move to trash</DropdownMenuItem>
|
<DropdownMenuItem className="text-destructive">Move to trash</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator className="bg-border" />
|
||||||
<div className="flex-1 flex-col">
|
<div className="flex-1 flex-col">
|
||||||
<div className="flex items-start gap-4 p-4">
|
<div className="flex items-start gap-4 p-4">
|
||||||
<div className="p-2 rounded-lg bg-accent">
|
<div className="p-2 rounded-lg bg-secondary">
|
||||||
{getFileIcon(file)}
|
{getFileIcon(file)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
@ -552,7 +534,7 @@ const FileDisplay = ({ file }) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator className="bg-border" />
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">Location</div>
|
<div className="text-sm font-medium">Location</div>
|
||||||
|
@ -582,16 +564,13 @@ export default function FileManager() {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [filterType, setFilterType] = useState('all');
|
const [filterType, setFilterType] = useState('all');
|
||||||
const [selectedFile, setSelectedFile] = useState(null);
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
const [showMobileMenu, setShowMobileMenu] = useState(false);
|
||||||
|
const [activePanel, setActivePanel] = useState('files'); // 'tree', 'files', 'preview'
|
||||||
|
|
||||||
const currentItem = fileSystemData[currentPath];
|
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 = [
|
const shortcuts = [
|
||||||
// File operations row (unique qkeys)
|
|
||||||
[
|
[
|
||||||
{ key: 'Q', label: 'Rename', action: () => console.log('Rename') },
|
{ key: 'Q', label: 'Rename', action: () => console.log('Rename') },
|
||||||
{ key: 'W', label: 'View', action: () => console.log('View') },
|
{ key: 'W', label: 'View', action: () => console.log('View') },
|
||||||
|
@ -604,7 +583,6 @@ export default function FileManager() {
|
||||||
{ key: 'O', label: 'Paste', action: () => console.log('Paste') },
|
{ key: 'O', label: 'Paste', action: () => console.log('Paste') },
|
||||||
{ key: 'P', label: 'Duplicate', action: () => console.log('Duplicate') },
|
{ key: 'P', label: 'Duplicate', action: () => console.log('Duplicate') },
|
||||||
],
|
],
|
||||||
// Navigation/info row (unique qkeys)
|
|
||||||
[
|
[
|
||||||
{ key: 'A', label: 'Select', action: () => console.log('Select') },
|
{ key: 'A', label: 'Select', action: () => console.log('Select') },
|
||||||
{ key: 'S', label: 'Select All', action: () => console.log('Select All') },
|
{ key: 'S', label: 'Select All', action: () => console.log('Select All') },
|
||||||
|
@ -618,14 +596,11 @@ export default function FileManager() {
|
||||||
{ key: 'X', label: 'Refresh', action: () => console.log('Refresh') },
|
{ key: 'X', label: 'Refresh', action: () => console.log('Refresh') },
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
const handleContextAction = (action, file) => {
|
const handleContextAction = (action, file) => {
|
||||||
console.log(`Context action: ${action}`, file);
|
console.log(`Context action: ${action}`, file);
|
||||||
// Handle context menu actions here
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'star':
|
case 'star':
|
||||||
// Toggle star status
|
|
||||||
console.log('Toggle star for', file.name);
|
console.log('Toggle star for', file.name);
|
||||||
break;
|
break;
|
||||||
case 'share':
|
case 'share':
|
||||||
|
@ -652,14 +627,24 @@ export default function FileManager() {
|
||||||
case 'details':
|
case 'details':
|
||||||
console.log('Show details for', file.name);
|
console.log('Show details for', file.name);
|
||||||
setSelectedFile(file);
|
setSelectedFile(file);
|
||||||
|
if (isMobile) setActivePanel('preview');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log('Unknown action:', action);
|
console.log('Unknown action:', action);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keyboard shortcut handler
|
useEffect(() => {
|
||||||
React.useEffect(() => {
|
const handleResize = () => {
|
||||||
|
setIsMobile(window.innerWidth < 768);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
const isCtrl = e.ctrlKey || e.metaKey;
|
const isCtrl = e.ctrlKey || e.metaKey;
|
||||||
const isShift = e.shiftKey;
|
const isShift = e.shiftKey;
|
||||||
|
@ -675,7 +660,7 @@ export default function FileManager() {
|
||||||
console.log('Upload shortcut');
|
console.log('Upload shortcut');
|
||||||
} else if (isCtrl && e.key === 'f') {
|
} else if (isCtrl && e.key === 'f') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
(document.querySelector('input[placeholder="Search files"]') as HTMLInputElement | null)?.focus();
|
document.querySelector('input[placeholder="Search files"]').focus();
|
||||||
} else if (e.key === 'Delete' && selectedFile) {
|
} else if (e.key === 'Delete' && selectedFile) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log('Delete shortcut');
|
console.log('Delete shortcut');
|
||||||
|
@ -689,11 +674,93 @@ export default function FileManager() {
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [selectedFile]);
|
}, [selectedFile]);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<div className="flex flex-col h-screen bg-background">
|
||||||
|
<div className="flex items-center justify-between p-2 bg-secondary border-b border-border">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Sheet open={showMobileMenu} onOpenChange={setShowMobileMenu} >
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<Menu className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="left" className="w-[280px] p-0 bg-background">
|
||||||
|
<FolderTree
|
||||||
|
onSelect={(path) => {
|
||||||
|
setCurrentPath(path);
|
||||||
|
setShowMobileMenu(false);
|
||||||
|
setActivePanel('files');
|
||||||
|
}}
|
||||||
|
selectedPath={currentPath}
|
||||||
|
isMobile={true}
|
||||||
|
/>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
<h1 className="text-lg font-bold truncate">{currentItem?.name || 'My Drive'}</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{activePanel !== 'preview' && (
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setActivePanel('preview')}>
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{activePanel === 'preview' && (
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setActivePanel('files')}>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{activePanel === 'files' && (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="p-2 bg-secondary border-b border-border">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search files"
|
||||||
|
className="pl-8 w-full bg-background border border-border"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FileList
|
||||||
|
path={currentPath}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
filterType={filterType}
|
||||||
|
selectedFile={selectedFile}
|
||||||
|
setSelectedFile={(file) => {
|
||||||
|
setSelectedFile(file);
|
||||||
|
setActivePanel('preview');
|
||||||
|
}}
|
||||||
|
onContextAction={handleContextAction}
|
||||||
|
isMobile={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activePanel === 'preview' && (
|
||||||
|
<FileDisplay
|
||||||
|
file={selectedFile}
|
||||||
|
isMobile={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer shortcuts={shortcuts} isMobile={true} />
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-[calc(100vh-40px)]"> {/* Adjust based on your nav height */}
|
<TooltipProvider>
|
||||||
<TooltipProvider delayDuration={0}>
|
<div className="flex flex-col h-[calc(100vh-40px)] bg-background">
|
||||||
<ResizablePanelGroup direction="horizontal" className="flex-1 min-h-0">
|
<ResizablePanelGroup direction="horizontal" className="flex-1 min-h-0">
|
||||||
{/* Left Sidebar */}
|
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
defaultSize={20}
|
defaultSize={20}
|
||||||
collapsedSize={4}
|
collapsedSize={4}
|
||||||
|
@ -711,35 +778,34 @@ export default function FileManager() {
|
||||||
/>
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle className="bg-border" />
|
||||||
|
|
||||||
{/* Middle File List */}
|
<ResizablePanel defaultSize={50} minSize={30} className="bg-background border-r border-border">
|
||||||
<ResizablePanel defaultSize={50} minSize={30}>
|
|
||||||
<Tabs defaultValue="all" className="flex flex-col h-full">
|
<Tabs defaultValue="all" className="flex flex-col h-full">
|
||||||
<div className="flex items-center px-4 py-2">
|
<div className="flex items-center px-4 py-2 bg-secondary border-b border-border">
|
||||||
<h1 className="text-xl font-bold">{currentItem?.name || 'My Drive'}</h1>
|
<h1 className="text-xl font-bold">{currentItem?.name || 'My Drive'}</h1>
|
||||||
<TabsList className="ml-auto">
|
<TabsList className="ml-auto bg-background">
|
||||||
<TabsTrigger value="all">All</TabsTrigger>
|
<TabsTrigger value="all" className="data-[state=active]:bg-accent">All</TabsTrigger>
|
||||||
<TabsTrigger value="starred">Starred</TabsTrigger>
|
<TabsTrigger value="starred" className="data-[state=active]:bg-accent">Starred</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator className="bg-border" />
|
||||||
<div className="bg-background/95 p-4 backdrop-blur">
|
<div className="bg-secondary p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search files"
|
placeholder="Search files"
|
||||||
className="pl-8"
|
className="pl-8 bg-background border border-border"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select value={filterType} onValueChange={setFilterType}>
|
<Select value={filterType} onValueChange={setFilterType}>
|
||||||
<SelectTrigger className="w-[140px]">
|
<SelectTrigger className="w-[140px] bg-background border border-border">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent className="bg-popover">
|
||||||
<SelectItem value="all">All items</SelectItem>
|
<SelectItem value="all">All items</SelectItem>
|
||||||
<SelectItem value="folders">Folders</SelectItem>
|
<SelectItem value="folders">Folders</SelectItem>
|
||||||
<SelectItem value="files">Files</SelectItem>
|
<SelectItem value="files">Files</SelectItem>
|
||||||
|
@ -771,17 +837,14 @@ export default function FileManager() {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle className="bg-border" />
|
||||||
|
|
||||||
{/* Right File Details */}
|
<ResizablePanel defaultSize={30} minSize={25} className="bg-background">
|
||||||
<ResizablePanel defaultSize={30} minSize={25}>
|
|
||||||
<FileDisplay file={selectedFile} />
|
<FileDisplay file={selectedFile} />
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</TooltipProvider>
|
<Footer shortcuts={shortcuts} />
|
||||||
|
</div>
|
||||||
{/* Footer with Status Bar */}
|
</TooltipProvider>
|
||||||
<Footer shortcuts={shortcuts} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
64
app/prompt.txt
Normal file
64
app/prompt.txt
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
Pages
|
||||||
|
=====
|
||||||
|
|
||||||
|
Use themes variables
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
background
|
||||||
|
foreground
|
||||||
|
card
|
||||||
|
card
|
||||||
|
popover
|
||||||
|
popover
|
||||||
|
primary
|
||||||
|
primary
|
||||||
|
secondary
|
||||||
|
secondary
|
||||||
|
muted
|
||||||
|
muted
|
||||||
|
accent
|
||||||
|
accent
|
||||||
|
destructive
|
||||||
|
destructive
|
||||||
|
border
|
||||||
|
input
|
||||||
|
ring
|
||||||
|
radius
|
||||||
|
chart
|
||||||
|
chart
|
||||||
|
chart
|
||||||
|
chart
|
||||||
|
chart
|
||||||
|
|
||||||
|
|
||||||
|
Use resizable where is possible
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
ResizableHandle,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizablePanelGroup,
|
||||||
|
} from "@/components/ui/resizable"
|
||||||
|
|
||||||
|
Use shortcuts on footer
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
const shortcuts = [
|
||||||
|
[
|
||||||
|
{ 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: 'I', label: 'Cut', action: () => console.log('Cut') },
|
||||||
|
{ key: 'O', label: 'Paste', action: () => console.log('Paste') },
|
||||||
|
{ key: 'P', label: 'Duplicate', action: () => console.log('Duplicate') },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ 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') },
|
||||||
|
]
|
||||||
|
];
|
||||||
|
<Footer shortcuts={shortcuts} />
|
||||||
|
|
|
@ -2197,6 +2197,11 @@ body {
|
||||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-yellow-100 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(254 249 195 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.bg-gradient-to-br {
|
.bg-gradient-to-br {
|
||||||
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
|
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
|
||||||
}
|
}
|
||||||
|
@ -2693,6 +2698,11 @@ body {
|
||||||
color: hsl(var(--sidebar-foreground) / 0.7);
|
color: hsl(var(--sidebar-foreground) / 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-teal-600 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(13 148 136 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.text-white {
|
.text-white {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
|
@ -2708,6 +2718,11 @@ body {
|
||||||
color: rgb(202 138 4 / var(--tw-text-opacity));
|
color: rgb(202 138 4 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-yellow-800 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(133 77 14 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.underline {
|
.underline {
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
}
|
}
|
||||||
|
@ -2971,6 +2986,10 @@ body {
|
||||||
animation-timing-function: linear;
|
animation-timing-function: linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.running {
|
||||||
|
animation-play-state: running;
|
||||||
|
}
|
||||||
|
|
||||||
.dark .scrollbar-thin::-webkit-scrollbar-thumb {
|
.dark .scrollbar-thin::-webkit-scrollbar-thumb {
|
||||||
background: #4b5563;
|
background: #4b5563;
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,7 @@ const SheetContent = React.forwardRef<
|
||||||
>(({ side = "right", className, children, ...props }, ref) => (
|
>(({ side = "right", className, children, ...props }, ref) => (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.DialogTitle></SheetPrimitive.DialogTitle>
|
||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(sheetVariants({ side }), className)}
|
className={cn(sheetVariants({ side }), className)}
|
||||||
|
|
Loading…
Add table
Reference in a new issue