Spaces:
Running
Running
| 'use client' | |
| import React, { useState, useEffect } from 'react' | |
| import { | |
| X, | |
| Minus, | |
| Square, | |
| CaretLeft, | |
| CaretRight, | |
| House, | |
| MagnifyingGlass, | |
| Folder as FolderIcon, | |
| File, | |
| Image as ImageIcon, | |
| FilePdf, | |
| FileDoc, | |
| FileText, | |
| Upload, | |
| Download, | |
| Trash, | |
| Plus, | |
| Eye, | |
| Users, | |
| Globe, | |
| Code, | |
| AppWindow, | |
| Desktop, | |
| DownloadSimple, | |
| Flask, | |
| DeviceMobile, | |
| Function, | |
| Calendar as CalendarIcon, | |
| Clock as ClockIcon, | |
| Sparkle, | |
| Lightning, | |
| Brain, | |
| Lock, | |
| Key, | |
| List | |
| } from '@phosphor-icons/react' | |
| import { motion, AnimatePresence } from 'framer-motion' | |
| import { FilePreview } from './FilePreview' | |
| import Window from './Window' | |
| interface FileManagerProps { | |
| currentPath: string | |
| onNavigate: (path: string) => void | |
| onClose: () => void | |
| onMinimize?: () => void | |
| onMaximize?: () => void | |
| onFocus?: () => void | |
| zIndex?: number | |
| onOpenFlutterApp?: (appFile: any) => void | |
| onOpenApp?: (appId: string) => void | |
| onOpenTextFile?: (fileData: { content: string, fileName: string, filePath: string, passkey: string }) => void | |
| } | |
| interface FileItem { | |
| name: string | |
| type: 'folder' | 'file' | 'flutter_app' | |
| size?: number | |
| modified?: string | |
| path: string | |
| extension?: string | |
| dartCode?: string | |
| dependencies?: string[] | |
| pubspecYaml?: string | |
| } | |
| export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp, onMinimize, onMaximize, onFocus, zIndex, onOpenApp, onOpenTextFile }: FileManagerProps) { | |
| const [files, setFiles] = useState<FileItem[]>([]) | |
| const [loading, setLoading] = useState(false) | |
| const [searchQuery, setSearchQuery] = useState('') | |
| const [uploadModalOpen, setUploadModalOpen] = useState(false) | |
| const [previewFile, setPreviewFile] = useState<FileItem | null>(null) | |
| const [sidebarSelection, setSidebarSelection] = useState('public') // Default to public | |
| const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) | |
| // Secure Data State | |
| const [passkey, setPasskey] = useState('') | |
| const [showPasskeyModal, setShowPasskeyModal] = useState(false) | |
| const [tempPasskey, setTempPasskey] = useState('') | |
| // Helper function to check if a file can be edited as text | |
| const isTextFile = (extension: string): boolean => { | |
| const textExtensions = [ | |
| 'txt', 'md', 'tex', 'json', 'xml', 'html', 'htm', 'css', 'scss', 'sass', | |
| 'js', 'jsx', 'ts', 'tsx', 'py', 'java', 'c', 'cpp', 'h', 'hpp', | |
| 'sh', 'bash', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf', | |
| 'sql', 'php', 'rb', 'go', 'rs', 'swift', 'kt', 'scala', | |
| 'r', 'dart', 'vue', 'svelte', 'astro', 'csv', 'log' | |
| ] | |
| return textExtensions.includes(extension.toLowerCase()) | |
| } | |
| // Load files when path or selection changes | |
| useEffect(() => { | |
| if (currentPath === 'Applications') { | |
| setSidebarSelection('applications') | |
| setLoading(false) | |
| setFiles([]) | |
| return | |
| } | |
| if (currentPath === 'public' || currentPath.startsWith('public/')) { | |
| setSidebarSelection('public') | |
| loadFiles() | |
| } else if (sidebarSelection === 'secure') { | |
| if (passkey) { | |
| loadFiles() | |
| } else { | |
| setFiles([]) | |
| setShowPasskeyModal(true) | |
| } | |
| } else { | |
| // Default to public if nothing else matches | |
| setSidebarSelection('public') | |
| onNavigate('public') | |
| } | |
| }, [currentPath, sidebarSelection, passkey]) | |
| const loadFiles = async () => { | |
| setLoading(true) | |
| try { | |
| let response | |
| if (sidebarSelection === 'public' || currentPath.startsWith('public')) { | |
| const publicPath = currentPath === 'public' ? '' : currentPath.replace('public/', '') | |
| response = await fetch(`/api/public?folder=${encodeURIComponent(publicPath)}`) | |
| } else if (sidebarSelection === 'secure' && passkey) { | |
| response = await fetch(`/api/data?key=${encodeURIComponent(passkey)}&folder=${encodeURIComponent(currentPath)}`) | |
| } else { | |
| setLoading(false) | |
| return | |
| } | |
| const data = await response.json() | |
| if (data.error) { | |
| console.error('Error loading files:', data.error) | |
| setFiles([]) | |
| if (data.error === 'Invalid passkey' || data.error === 'Access denied') { | |
| setPasskey('') | |
| setShowPasskeyModal(true) | |
| } | |
| return | |
| } | |
| let normalizedFiles = (data.files || []).map((file: any) => ({ | |
| ...file, | |
| path: file.path || file.name || '', | |
| })) | |
| setFiles(normalizedFiles) | |
| } catch (error) { | |
| console.error('Error loading files:', error) | |
| setFiles([]) | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| const handlePasskeySubmit = () => { | |
| if (tempPasskey.trim()) { | |
| setPasskey(tempPasskey.trim()) | |
| setShowPasskeyModal(false) | |
| setTempPasskey('') | |
| onNavigate('') // Reset to root of secure drive | |
| } | |
| } | |
| const handleLock = () => { | |
| setPasskey('') | |
| setFiles([]) | |
| setShowPasskeyModal(true) | |
| } | |
| const handleUpload = async (file: File, targetFolder: string) => { | |
| const formData = new FormData() | |
| formData.append('file', file) | |
| try { | |
| let response | |
| if (sidebarSelection === 'public') { | |
| const publicPath = currentPath === 'public' ? '' : currentPath.replace('public/', '') | |
| formData.append('folder', publicPath) | |
| formData.append('uploadedBy', 'User') | |
| response = await fetch('/api/public', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| } else if (sidebarSelection === 'secure' && passkey) { | |
| formData.append('folder', targetFolder) | |
| formData.append('key', passkey) | |
| response = await fetch('/api/data', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| } else { | |
| alert('Cannot upload here') | |
| return | |
| } | |
| const result = await response.json() | |
| if (result.success) { | |
| loadFiles() | |
| setUploadModalOpen(false) | |
| } else { | |
| alert(`Upload failed: ${result.error}`) | |
| } | |
| } catch (error) { | |
| console.error('Error uploading file:', error) | |
| alert('Failed to upload file') | |
| } | |
| } | |
| const handleDownload = (file: FileItem) => { | |
| if (sidebarSelection === 'public') { | |
| window.open(`/api/sessions/download?file=${encodeURIComponent(file.path)}&public=true`, '_blank') | |
| } else if (sidebarSelection === 'secure' && passkey) { | |
| window.open(`/api/sessions/download?file=${encodeURIComponent(file.path)}&key=${encodeURIComponent(passkey)}`, '_blank') | |
| } | |
| } | |
| const handlePreview = (file: FileItem) => { | |
| setPreviewFile(file) | |
| } | |
| const handleDelete = async (file: FileItem) => { | |
| if (!confirm(`Delete ${file.name}?`)) return | |
| try { | |
| let response | |
| if (sidebarSelection === 'secure' && passkey) { | |
| response = await fetch(`/api/data?key=${encodeURIComponent(passkey)}&path=${encodeURIComponent(file.path)}`, { | |
| method: 'DELETE' | |
| }) | |
| } else { | |
| // Public delete (if allowed) or fallback | |
| const headers: Record<string, string> = {} | |
| response = await fetch(`/api/files?path=${encodeURIComponent(file.path)}`, { | |
| method: 'DELETE' | |
| }) | |
| } | |
| const result = await response.json() | |
| if (result.success) { | |
| loadFiles() | |
| } else { | |
| alert(`Delete failed: ${result.error}`) | |
| } | |
| } catch (error) { | |
| console.error('Error deleting file:', error) | |
| alert('Failed to delete file') | |
| } | |
| } | |
| const handleCreateFolder = async () => { | |
| const folderName = prompt('Enter folder name:') | |
| if (!folderName) return | |
| // Folder creation for secure data is implicit on upload usually, | |
| // but we can implement it if needed. | |
| // For now, let's skip explicit empty folder creation for secure data | |
| // as our API is simple. | |
| alert('Folder creation is handled automatically when uploading files.') | |
| } | |
| const getFileIcon = (file: FileItem) => { | |
| if (file.type === 'folder') { | |
| if (file.path === 'public' || file.name === 'Public Folder') { | |
| return <Globe size={48} weight="fill" className="text-purple-400" /> | |
| } | |
| return <FolderIcon size={48} weight="fill" className="text-blue-400" /> | |
| } | |
| if (file.type === 'flutter_app') { | |
| return <Code size={48} weight="fill" className="text-cyan-500" /> | |
| } | |
| const ext = file.extension?.toLowerCase() | |
| switch (ext) { | |
| case 'pdf': return <FilePdf size={48} weight="fill" className="text-red-500" /> | |
| case 'doc': | |
| case 'docx': return <FileDoc size={48} weight="fill" className="text-blue-500" /> | |
| case 'txt': | |
| case 'md': return <FileText size={48} weight="fill" className="text-gray-600" /> | |
| case 'dart': return <Code size={48} weight="fill" className="text-blue-400" /> | |
| case 'tex': return <Function size={48} weight="fill" className="text-green-600" /> | |
| case 'jpg': | |
| case 'jpeg': | |
| case 'png': | |
| case 'gif': return <ImageIcon size={48} weight="fill" className="text-green-500" /> | |
| default: return <File size={48} weight="regular" className="text-gray-500" /> | |
| } | |
| } | |
| const formatFileSize = (bytes?: number) => { | |
| if (!bytes) return '' | |
| const units = ['B', 'KB', 'MB', 'GB'] | |
| let size = bytes | |
| let unitIndex = 0 | |
| while (size >= 1024 && unitIndex < units.length - 1) { | |
| size /= 1024 | |
| unitIndex++ | |
| } | |
| return `${size.toFixed(1)} ${units[unitIndex]}` | |
| } | |
| const filteredFiles = files.filter(file => | |
| file.name.toLowerCase().includes(searchQuery.toLowerCase()) | |
| ) | |
| const handleSidebarClick = (item: string) => { | |
| setSidebarSelection(item) | |
| if (item === 'applications') { | |
| onNavigate('Applications') | |
| } else if (item === 'secure') { | |
| onNavigate('') | |
| if (!passkey) { | |
| setShowPasskeyModal(true) | |
| } | |
| } else if (item === 'public') { | |
| onNavigate('public') | |
| } | |
| } | |
| const applications = [ | |
| { id: 'files', name: 'Finder', icon: <div className="bg-gradient-to-br from-blue-400 to-cyan-200 w-full h-full rounded-xl flex items-center justify-center border border-white/30"><FolderIcon size={32} weight="regular" className="text-blue-900" /></div> }, | |
| { id: 'calendar', name: 'Calendar', icon: <div className="w-full h-full"><CalendarIcon size={48} weight="regular" className="text-red-500" /></div> }, | |
| { id: 'clock', name: 'Clock', icon: <div className="w-full h-full"><ClockIcon size={48} weight="regular" className="text-black" /></div> }, | |
| { | |
| id: 'gemini', name: 'Gemini', icon: ( | |
| <div className="bg-gradient-to-b from-white to-blue-50 w-full h-full rounded-[22%] flex items-center justify-center shadow-lg border-[0.5px] border-white/50 relative overflow-hidden"> | |
| <div className="absolute inset-0 bg-gradient-to-b from-white/40 to-transparent" /> | |
| <Sparkle size={32} weight="fill" className="text-blue-500 drop-shadow-sm" /> | |
| </div> | |
| ) | |
| }, | |
| { | |
| id: 'flutter-editor', name: 'Flutter IDE', icon: ( | |
| <div className="bg-gradient-to-b from-[#54C5F8] to-[#29B6F6] w-full h-full rounded-[22%] flex items-center justify-center shadow-lg border-[0.5px] border-white/20 relative overflow-hidden"> | |
| <div className="absolute inset-0 bg-gradient-to-b from-white/20 to-transparent opacity-50" /> | |
| <DeviceMobile size={32} weight="fill" className="text-white relative z-10 drop-shadow-md" /> | |
| <div className="absolute top-1.5 right-1.5"> | |
| <Lightning size={12} weight="fill" className="text-yellow-300 drop-shadow-md" /> | |
| </div> | |
| </div> | |
| ) | |
| }, | |
| { | |
| id: 'latex-editor', name: 'LaTeX Studio', icon: ( | |
| <div className="bg-gradient-to-b from-slate-700 to-slate-900 w-full h-full rounded-[22%] flex items-center justify-center shadow-lg border-[0.5px] border-white/20 relative overflow-hidden"> | |
| <div className="absolute inset-0 bg-gradient-to-b from-white/10 to-transparent opacity-30" /> | |
| <div className="relative z-10 flex flex-col items-center justify-center"> | |
| <Function size={32} weight="bold" className="text-green-400 drop-shadow-md" /> | |
| <span className="text-[8px] font-black text-gray-300 tracking-widest mt-0.5">TEX</span> | |
| </div> | |
| </div> | |
| ) | |
| }, | |
| { | |
| id: 'quiz', name: 'Quiz Master', icon: ( | |
| <div className="bg-gradient-to-br from-teal-400 to-emerald-500 w-full h-full rounded-[22%] flex items-center justify-center shadow-lg border-[0.5px] border-white/20 relative overflow-hidden"> | |
| <div className="absolute inset-0 bg-gradient-to-b from-white/20 to-transparent opacity-50" /> | |
| <Brain size={32} weight="fill" className="text-white relative z-10 drop-shadow-md" /> | |
| </div> | |
| ) | |
| }, | |
| ] | |
| return ( | |
| <> | |
| <Window | |
| id="files" | |
| title={sidebarSelection === 'secure' ? 'Secure Data' : (currentPath || 'Public Files')} | |
| isOpen={true} | |
| onClose={onClose} | |
| onMinimize={onMinimize} | |
| onMaximize={onMaximize} | |
| onFocus={onFocus} | |
| zIndex={zIndex} | |
| width={1000} | |
| height={650} | |
| x={60} | |
| y={60} | |
| className="file-manager-window" | |
| > | |
| <div className="flex h-full bg-[#F5F5F5] relative"> | |
| {/* Mobile Sidebar Overlay */} | |
| <AnimatePresence> | |
| {mobileSidebarOpen && ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| className="absolute inset-0 bg-black/30 z-20 md:hidden" | |
| onClick={() => setMobileSidebarOpen(false)} | |
| /> | |
| )} | |
| </AnimatePresence> | |
| {/* Sidebar */} | |
| <AnimatePresence> | |
| {(mobileSidebarOpen || typeof window !== 'undefined') && ( | |
| <motion.div | |
| initial={{ x: -192 }} | |
| animate={{ x: mobileSidebarOpen ? 0 : (typeof window !== 'undefined' && window.innerWidth >= 768 ? 0 : -192) }} | |
| className={`${mobileSidebarOpen ? 'absolute z-30 h-full' : 'hidden'} md:relative md:flex w-40 sm:w-48 bg-[#F3F3F3]/95 md:bg-[#F3F3F3]/90 backdrop-blur-xl border-r border-gray-200 pt-3 sm:pt-4 flex-col`} | |
| > | |
| <div className="px-3 sm:px-4 mb-2 flex items-center justify-between"> | |
| <span className="text-[10px] sm:text-xs font-bold text-gray-400">Locations</span> | |
| <button | |
| onClick={() => setMobileSidebarOpen(false)} | |
| className="p-1 hover:bg-gray-200 rounded md:hidden" | |
| > | |
| <X size={14} /> | |
| </button> | |
| </div> | |
| <nav className="space-y-1 px-1.5 sm:px-2"> | |
| <button | |
| onClick={() => { handleSidebarClick('public'); setMobileSidebarOpen(false); }} | |
| className={`w-full flex items-center gap-1.5 sm:gap-2 px-2 py-1.5 rounded-md text-xs sm:text-sm ${sidebarSelection === 'public' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`} | |
| > | |
| <Globe size={16} weight="fill" className="text-purple-500 sm:w-[18px] sm:h-[18px]" /> | |
| <span className="truncate">Public Files</span> | |
| </button> | |
| <button | |
| onClick={() => { handleSidebarClick('secure'); setMobileSidebarOpen(false); }} | |
| className={`w-full flex items-center gap-1.5 sm:gap-2 px-2 py-1.5 rounded-md text-xs sm:text-sm ${sidebarSelection === 'secure' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`} | |
| > | |
| <Lock size={16} weight="fill" className="text-blue-500 sm:w-[18px] sm:h-[18px]" /> | |
| <span className="truncate">Secure Data</span> | |
| </button> | |
| <button | |
| onClick={() => { handleSidebarClick('applications'); setMobileSidebarOpen(false); }} | |
| className={`w-full flex items-center gap-1.5 sm:gap-2 px-2 py-1.5 rounded-md text-xs sm:text-sm ${sidebarSelection === 'applications' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`} | |
| > | |
| <AppWindow size={16} weight="fill" className="text-green-500 sm:w-[18px] sm:h-[18px]" /> | |
| <span className="truncate">Applications</span> | |
| </button> | |
| </nav> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| {/* Main Content */} | |
| <div className="flex-1 flex flex-col bg-white relative min-w-0"> | |
| {/* Toolbar */} | |
| <div className="h-10 sm:h-12 bg-white border-b border-gray-200 flex items-center px-2 sm:px-4 gap-2 sm:gap-4 justify-between"> | |
| <div className="flex items-center gap-1 sm:gap-2 min-w-0 flex-1"> | |
| <button | |
| onClick={() => setMobileSidebarOpen(true)} | |
| className="p-1.5 hover:bg-gray-100 rounded-md md:hidden flex-shrink-0" | |
| > | |
| <List size={18} weight="bold" className="text-gray-600" /> | |
| </button> | |
| <button | |
| onClick={() => { | |
| const parent = currentPath.split('/').slice(0, -1).join('/') | |
| onNavigate(parent) | |
| }} | |
| disabled={!currentPath || currentPath === 'Applications'} | |
| className="p-1 sm:p-1.5 hover:bg-gray-100 rounded-md disabled:opacity-30 transition-colors flex-shrink-0" | |
| > | |
| <CaretLeft size={16} weight="bold" className="text-gray-600 sm:w-[18px] sm:h-[18px]" /> | |
| </button> | |
| <span className="text-xs sm:text-sm font-semibold text-gray-700 truncate"> | |
| {currentPath === '' ? (sidebarSelection === 'secure' ? 'Secure Data' : 'Public') : currentPath} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-1 sm:gap-2 flex-shrink-0"> | |
| {sidebarSelection === 'secure' && passkey && ( | |
| <button | |
| onClick={handleLock} | |
| className="hidden xs:flex items-center gap-1 px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 rounded-md text-[10px] sm:text-xs font-medium text-gray-600 transition-colors" | |
| > | |
| <Lock size={12} className="sm:w-3.5 sm:h-3.5" /> | |
| <span className="hidden sm:inline">Lock</span> | |
| </button> | |
| )} | |
| {currentPath !== 'Applications' && ( | |
| <button | |
| onClick={() => setUploadModalOpen(true)} | |
| className="p-1 sm:p-1.5 hover:bg-gray-100 rounded-md transition-colors" | |
| title="Upload File" | |
| > | |
| <Upload size={16} weight="bold" className="text-gray-600 sm:w-[18px] sm:h-[18px]" /> | |
| </button> | |
| )} | |
| <div className="flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 rounded-md w-24 sm:w-32 md:w-48"> | |
| <MagnifyingGlass size={12} weight="bold" className="text-gray-400 sm:w-3.5 sm:h-3.5 flex-shrink-0" /> | |
| <input | |
| type="text" | |
| placeholder="Search" | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="bg-transparent text-xs sm:text-sm outline-none w-full placeholder-gray-400 min-w-0" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* File Grid */} | |
| <div className="flex-1 overflow-auto p-3 sm:p-6"> | |
| {currentPath === 'Applications' ? ( | |
| <div className="grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-4 md:grid-cols-5 gap-2 sm:gap-4 md:gap-6"> | |
| {applications.map((app) => ( | |
| <button | |
| key={app.id} | |
| onDoubleClick={() => onOpenApp && onOpenApp(app.id)} | |
| className="flex flex-col items-center gap-1.5 sm:gap-3 p-2 sm:p-4 hover:bg-blue-50 rounded-xl transition-colors group" | |
| > | |
| <div className="w-10 h-10 sm:w-14 sm:h-14 md:w-16 md:h-16 flex items-center justify-center drop-shadow-sm group-hover:scale-105 transition-transform"> | |
| {app.icon} | |
| </div> | |
| <span className="text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 text-center line-clamp-2">{app.name}</span> | |
| </button> | |
| ))} | |
| </div> | |
| ) : ( | |
| <> | |
| {loading ? ( | |
| <div className="flex items-center justify-center h-full text-gray-400 text-sm"> | |
| Loading files... | |
| </div> | |
| ) : filteredFiles.length === 0 ? ( | |
| <div className="flex items-center justify-center h-full text-gray-400 text-xs sm:text-sm"> | |
| {searchQuery ? 'No files found' : 'Folder is empty'} | |
| </div> | |
| ) : ( | |
| <div className="grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-4 md:grid-cols-5 gap-2 sm:gap-4 md:gap-6"> | |
| {filteredFiles.map((file) => ( | |
| <div | |
| key={file.path} | |
| className="group relative" | |
| > | |
| <button | |
| onDoubleClick={async () => { | |
| if (file.type === 'folder') { | |
| onNavigate(file.path) | |
| } else if (file.type === 'flutter_app' && onOpenFlutterApp) { | |
| onOpenFlutterApp(file) | |
| } else if (file.extension === 'dart' || file.extension === 'flutter') { | |
| // Load the file content and open Flutter IDE with it | |
| try { | |
| let fileContent = '' | |
| if (sidebarSelection === 'secure' && passkey) { | |
| const response = await fetch(`/api/data?key=${encodeURIComponent(passkey)}&folder=${encodeURIComponent(currentPath)}`) | |
| const data = await response.json() | |
| const fileData = data.files?.find((f: any) => f.name === file.name) | |
| fileContent = fileData?.content || '' | |
| // Store for auto-save in Flutter IDE | |
| sessionStorage.setItem('currentPasskey', passkey) | |
| sessionStorage.setItem('currentFileName', file.name) | |
| } else if (sidebarSelection === 'public') { | |
| // Load from public folder | |
| const folderPath = currentPath.replace('public/', '').replace('public', '') | |
| const response = await fetch(`/api/public?folder=${encodeURIComponent(folderPath)}`) | |
| if (response.ok) { | |
| const data = await response.json() | |
| const fileData = data.files?.find((f: any) => f.name === file.name) | |
| fileContent = fileData?.content || '' | |
| } | |
| } | |
| console.log('Opening dart file:', file.name, 'Content length:', fileContent.length) | |
| // Open Flutter IDE with the file content | |
| if (onOpenApp) { | |
| // Store the content temporarily for the Flutter IDE to pick up | |
| sessionStorage.setItem('flutterFileContent', fileContent) | |
| sessionStorage.setItem('currentFileName', file.name) | |
| if (passkey) { | |
| sessionStorage.setItem('currentPasskey', passkey) | |
| } | |
| onOpenApp('flutter-editor') | |
| } | |
| } catch (error) { | |
| console.error('Error loading file:', error) | |
| alert('Failed to load dart file for editing') | |
| } | |
| } else if (isTextFile(file.extension || '')) { | |
| // Load the file content and open Text Editor with it | |
| try { | |
| let fileContent = '' | |
| if (sidebarSelection === 'secure' && passkey) { | |
| const response = await fetch(`/api/data?key=${encodeURIComponent(passkey)}&folder=${encodeURIComponent(currentPath)}`) | |
| const data = await response.json() | |
| const fileData = data.files?.find((f: any) => f.name === file.name) | |
| fileContent = fileData?.content || '' | |
| } else if (sidebarSelection === 'public') { | |
| // Load from public folder | |
| const folderPath = currentPath.replace('public/', '').replace('public', '') | |
| const response = await fetch(`/api/public?folder=${encodeURIComponent(folderPath)}`) | |
| if (response.ok) { | |
| const data = await response.json() | |
| const fileData = data.files?.find((f: any) => f.name === file.name) | |
| fileContent = fileData?.content || '' | |
| } | |
| } | |
| console.log('Opening file:', file.name, 'Content length:', fileContent.length) | |
| // Open Text Editor with the file content | |
| if (onOpenTextFile) { | |
| onOpenTextFile({ | |
| content: fileContent, | |
| fileName: file.name, | |
| filePath: currentPath, | |
| passkey: passkey || '' | |
| }) | |
| } | |
| } catch (error) { | |
| console.error('Error loading file:', error) | |
| alert('Failed to load file for editing') | |
| } | |
| } else { | |
| handlePreview(file) | |
| } | |
| }} | |
| className="flex flex-col items-center gap-1.5 sm:gap-3 p-2 sm:p-4 hover:bg-blue-50 rounded-xl w-full transition-colors" | |
| > | |
| <div className="w-10 h-10 sm:w-14 sm:h-14 md:w-16 md:h-16 flex items-center justify-center drop-shadow-sm"> | |
| {getFileIcon(file)} | |
| </div> | |
| <span className="text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 text-center break-all w-full line-clamp-2"> | |
| {file.name} | |
| </span> | |
| {file.size && ( | |
| <span className="text-[9px] sm:text-xs text-gray-400 hidden sm:block"> | |
| {formatFileSize(file.size)} | |
| </span> | |
| )} | |
| </button> | |
| {file.type === 'file' && ( | |
| <div className="absolute top-1 right-1 sm:top-2 sm:right-2 hidden group-hover:flex gap-0.5 sm:gap-1 bg-white/90 rounded-lg shadow-sm p-0.5 sm:p-1"> | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation() | |
| handlePreview(file) | |
| }} | |
| className="p-1 sm:p-1.5 hover:bg-gray-100 rounded-md text-gray-600" | |
| title="Preview" | |
| > | |
| <Eye size={12} weight="bold" className="sm:w-3.5 sm:h-3.5" /> | |
| </button> | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation() | |
| handleDelete(file) | |
| }} | |
| className="p-1 sm:p-1.5 hover:bg-red-50 rounded-md text-red-500" | |
| title="Delete" | |
| > | |
| <Trash size={12} weight="bold" className="sm:w-3.5 sm:h-3.5" /> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| {/* Status Bar */} | |
| <div className="h-6 sm:h-8 bg-white border-t border-gray-200 flex items-center px-2 sm:px-4 text-[10px] sm:text-xs text-gray-500 font-medium"> | |
| {currentPath === 'Applications' | |
| ? `${applications.length} items` | |
| : `${filteredFiles.length} items` | |
| } | |
| </div> | |
| {/* Passkey Modal */} | |
| <AnimatePresence> | |
| {showPasskeyModal && ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| className="absolute inset-0 bg-white/80 backdrop-blur-md z-50 flex items-center justify-center p-4" | |
| > | |
| <motion.div | |
| initial={{ scale: 0.9, y: 20 }} | |
| animate={{ scale: 1, y: 0 }} | |
| className="bg-white p-4 sm:p-8 rounded-2xl shadow-2xl border border-gray-200 w-full max-w-xs sm:max-w-sm text-center" | |
| > | |
| <div className="w-12 h-12 sm:w-16 sm:h-16 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-3 sm:mb-4"> | |
| <Lock size={24} weight="fill" className="text-blue-500 sm:w-8 sm:h-8" /> | |
| </div> | |
| <h3 className="text-lg sm:text-xl font-bold text-gray-900 mb-1 sm:mb-2">Secure Storage</h3> | |
| <p className="text-gray-500 text-xs sm:text-sm mb-4 sm:mb-6">Enter your passkey to access your files.</p> | |
| <input | |
| type="password" | |
| value={tempPasskey} | |
| onChange={(e) => setTempPasskey(e.target.value)} | |
| onKeyDown={(e) => e.key === 'Enter' && handlePasskeySubmit()} | |
| placeholder="Enter Passkey" | |
| className="w-full px-3 sm:px-4 py-2 sm:py-3 rounded-xl border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none mb-3 sm:mb-4 text-center text-base sm:text-lg tracking-widest" | |
| autoFocus | |
| /> | |
| <div className="flex gap-2 sm:gap-3"> | |
| <button | |
| onClick={() => { | |
| setShowPasskeyModal(false) | |
| setSidebarSelection('public') | |
| onNavigate('public') | |
| }} | |
| className="flex-1 px-3 sm:px-4 py-2 sm:py-2.5 bg-gray-100 text-gray-700 rounded-xl text-sm sm:text-base font-medium hover:bg-gray-200 transition-colors" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| onClick={handlePasskeySubmit} | |
| disabled={!tempPasskey} | |
| className="flex-1 px-4 py-2.5 bg-blue-600 text-white rounded-xl font-medium hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| Unlock | |
| </button> | |
| </div> | |
| </motion.div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| </div> | |
| </Window> | |
| {/* Upload Modal */} | |
| {uploadModalOpen && ( | |
| <UploadModal | |
| currentPath={currentPath} | |
| onUpload={handleUpload} | |
| onClose={() => setUploadModalOpen(false)} | |
| /> | |
| )} | |
| {/* File Preview Modal */} | |
| {previewFile && ( | |
| <FilePreview | |
| file={previewFile} | |
| onClose={() => setPreviewFile(null)} | |
| onDownload={() => handleDownload(previewFile)} | |
| passkey={passkey} | |
| isPublic={sidebarSelection === 'public'} | |
| /> | |
| )} | |
| </> | |
| ) | |
| } | |
| // Upload Modal Component | |
| function UploadModal({ | |
| currentPath, | |
| onUpload, | |
| onClose | |
| }: { | |
| currentPath: string | |
| onUpload: (file: File, folder: string) => void | |
| onClose: () => void | |
| }) { | |
| const [selectedFile, setSelectedFile] = useState<File | null>(null) | |
| const [targetFolder, setTargetFolder] = useState(currentPath) | |
| const handleSubmit = () => { | |
| if (selectedFile) { | |
| onUpload(selectedFile, targetFolder) | |
| } | |
| } | |
| return ( | |
| <div className="fixed inset-0 bg-black/20 backdrop-blur-sm flex items-center justify-center z-[100]"> | |
| <div className="bg-white rounded-xl shadow-2xl p-6 w-[400px] border border-gray-200"> | |
| <h2 className="text-lg font-semibold mb-4 text-gray-800">Upload File</h2> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-sm font-medium mb-2 text-gray-600">Select File</label> | |
| <input | |
| type="file" | |
| onChange={(e) => setSelectedFile(e.target.files?.[0] || null)} | |
| className="w-full p-2 border border-gray-300 rounded-lg text-sm" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium mb-2 text-gray-600">Upload to Folder</label> | |
| <input | |
| type="text" | |
| value={targetFolder} | |
| onChange={(e) => setTargetFolder(e.target.value)} | |
| placeholder="e.g., homework/math" | |
| className="w-full p-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none" | |
| /> | |
| <p className="text-xs text-gray-400 mt-1"> | |
| Leave empty for root, or enter path like "folder/subfolder" | |
| </p> | |
| </div> | |
| <div className="flex justify-end gap-2 pt-2"> | |
| <button | |
| onClick={onClose} | |
| className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg text-sm font-medium transition-colors" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| onClick={handleSubmit} | |
| disabled={!selectedFile} | |
| className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 text-sm font-medium shadow-lg shadow-blue-500/20 transition-all" | |
| > | |
| Upload | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |