- Added HTTP server with CORS support and various endpoints - Introduced http_tx/http_rx channels for HTTP server control - Cleaned up build.rs by removing commented code - Updated .gitignore to use *.rdb pattern instead of .rdb - Simplified capabilities.json to empty object - Improved UI initialization with better error handling - Reorganized module imports in main.rs - Added worker count configuration for HTTP server The changes introduce a new HTTP server capability while cleaning up and improving existing code structure. The HTTP server includes authentication, session management, and websocket support.
585 lines
28 KiB
HTML
585 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>XTree Gold File Manager</title>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
[x-cloak] { display: none !important; }
|
|
|
|
/* XTree Gold inspired theme */
|
|
:root {
|
|
--bg-primary: #1a1a2e;
|
|
--bg-secondary: #16213e;
|
|
--bg-tertiary: #0f3460;
|
|
--text-primary: #e94560;
|
|
--text-secondary: #00d9ff;
|
|
--border: #533483;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Courier New', monospace;
|
|
background: var(--bg-primary);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.panel {
|
|
border: 2px solid var(--border);
|
|
background: var(--bg-secondary);
|
|
}
|
|
|
|
.tree-line {
|
|
color: var(--border);
|
|
}
|
|
|
|
.selected {
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.shortcut-key {
|
|
display: inline-block;
|
|
min-width: 2rem;
|
|
padding: 0.25rem 0.5rem;
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border);
|
|
border-radius: 0.25rem;
|
|
text-align: center;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.file-icon {
|
|
display: inline-block;
|
|
width: 1.5rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.folder-icon::before { content: '📁'; }
|
|
.file-icon.pdf::before { content: '📄'; }
|
|
.file-icon.xlsx::before { content: '📊'; }
|
|
.file-icon.json::before { content: '{}'; }
|
|
.file-icon.md::before { content: '📝'; }
|
|
.file-icon.jpg::before, .file-icon.jpeg::before, .file-icon.png::before { content: '🖼️'; }
|
|
.file-icon.mp4::before { content: '🎬'; }
|
|
.file-icon.mp3::before { content: '🎵'; }
|
|
.file-icon.default::before { content: '📋'; }
|
|
</style>
|
|
</head>
|
|
<body class="h-screen flex flex-col overflow-hidden" x-data="fileManager()" x-cloak>
|
|
<!-- Main Container -->
|
|
<div class="flex-1 flex overflow-hidden">
|
|
<!-- Left Sidebar - Folder Tree -->
|
|
<div class="panel w-64 flex flex-col overflow-hidden" :class="{ 'w-16': collapsed }">
|
|
<!-- Navigation Links -->
|
|
<div class="p-2 space-y-1">
|
|
<template x-for="link in navLinks" :key="link.path">
|
|
<button
|
|
@click="selectPath(link.path)"
|
|
:class="currentPath === link.path ? 'selected' : ''"
|
|
class="w-full px-3 py-2 text-left hover:bg-gray-700 rounded flex items-center gap-2"
|
|
>
|
|
<span x-text="link.icon" class="text-xl"></span>
|
|
<span x-show="!collapsed" x-text="link.title"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="border-t border-gray-600 my-2"></div>
|
|
|
|
<!-- Folder Tree -->
|
|
<div class="flex-1 overflow-auto p-2" x-show="!collapsed">
|
|
<template x-for="item in rootFolders" :key="item.id">
|
|
<div>
|
|
<button
|
|
@click="toggleFolder(item.path); selectPath(item.path)"
|
|
:class="currentPath === item.path ? 'selected' : ''"
|
|
class="w-full px-2 py-1 text-left hover:bg-gray-700 rounded flex items-center gap-1 text-sm"
|
|
>
|
|
<span x-show="item.is_dir" x-text="expanded[item.path] ? '▼' : '▶'" class="w-4"></span>
|
|
<span class="folder-icon"></span>
|
|
<span x-text="item.name"></span>
|
|
<span x-show="item.starred" class="ml-auto text-yellow-400">★</span>
|
|
</button>
|
|
|
|
<div x-show="expanded[item.path]" class="ml-4">
|
|
<template x-for="child in getChildren(item.path)" :key="child.id">
|
|
<button
|
|
@click="selectPath(child.path)"
|
|
:class="currentPath === child.path ? 'selected' : ''"
|
|
class="w-full px-2 py-1 text-left hover:bg-gray-700 rounded flex items-center gap-1 text-sm"
|
|
>
|
|
<span :class="child.is_dir ? 'folder-icon' : 'file-icon ' + (child.type || 'default')"></span>
|
|
<span x-text="child.name"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Collapse Toggle -->
|
|
<button
|
|
@click="collapsed = !collapsed"
|
|
class="p-2 border-t border-gray-600 hover:bg-gray-700 text-center"
|
|
>
|
|
<span x-text="collapsed ? '▶' : '◀'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Middle Panel - File List -->
|
|
<div class="panel flex-1 flex flex-col overflow-hidden mx-2">
|
|
<!-- Header -->
|
|
<div class="p-4 border-b border-gray-600">
|
|
<h1 class="text-2xl font-bold mb-2" x-text="currentItem?.name || 'My Drive'"></h1>
|
|
|
|
<!-- Search and Filter -->
|
|
<div class="flex gap-2">
|
|
<input
|
|
type="text"
|
|
x-model="searchTerm"
|
|
placeholder="Search files (Ctrl+F)"
|
|
class="flex-1 px-3 py-2 bg-gray-800 border border-gray-600 rounded focus:outline-none focus:border-blue-500"
|
|
/>
|
|
<select
|
|
x-model="filterType"
|
|
class="px-3 py-2 bg-gray-800 border border-gray-600 rounded focus:outline-none"
|
|
>
|
|
<option value="all">All items</option>
|
|
<option value="folders">Folders</option>
|
|
<option value="files">Files</option>
|
|
<option value="starred">Starred</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File List -->
|
|
<div class="flex-1 overflow-auto p-2">
|
|
<template x-for="file in filteredFiles" :key="file.id">
|
|
<button
|
|
@click="selectFile(file)"
|
|
@dblclick="openFile(file)"
|
|
@contextmenu.prevent="showContextMenu($event, file)"
|
|
:class="selectedFile?.id === file.id ? 'selected' : ''"
|
|
class="w-full p-3 text-left hover:bg-gray-700 rounded border-b border-gray-700 flex items-center gap-3"
|
|
>
|
|
<span :class="file.is_dir ? 'folder-icon' : 'file-icon ' + (file.type || 'default')"></span>
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2">
|
|
<span x-text="file.name" class="font-semibold"></span>
|
|
<span x-show="file.starred" class="text-yellow-400 text-sm">★</span>
|
|
<span x-show="file.shared" class="text-blue-400 text-sm">👥</span>
|
|
</div>
|
|
<div class="text-xs text-gray-400">
|
|
<span x-text="file.is_dir ? 'Folder' : formatFileSize(file.size)"></span>
|
|
</div>
|
|
</div>
|
|
<div class="text-xs text-gray-400" x-text="formatDate(file.modified)"></div>
|
|
</button>
|
|
</template>
|
|
|
|
<div x-show="filteredFiles.length === 0" class="text-center text-gray-500 py-8">
|
|
No files found
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Panel - File Details -->
|
|
<div class="panel w-80 flex flex-col overflow-hidden">
|
|
<div class="p-2 border-b border-gray-600 flex gap-2">
|
|
<button @click="downloadFile()" :disabled="!selectedFile" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 rounded disabled:opacity-50">
|
|
⬇ Download
|
|
</button>
|
|
<button @click="shareFile()" :disabled="!selectedFile" class="px-3 py-1 bg-green-600 hover:bg-green-700 rounded disabled:opacity-50">
|
|
🔗 Share
|
|
</button>
|
|
<button @click="toggleStar()" :disabled="!selectedFile" class="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 rounded disabled:opacity-50">
|
|
★ Star
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-auto p-4">
|
|
<template x-if="selectedFile">
|
|
<div>
|
|
<div class="flex items-start gap-4 mb-4">
|
|
<div class="p-3 bg-gray-700 rounded">
|
|
<span :class="selectedFile.is_dir ? 'folder-icon' : 'file-icon ' + (selectedFile.type || 'default')" class="text-3xl"></span>
|
|
</div>
|
|
<div>
|
|
<h3 class="font-bold text-lg" x-text="selectedFile.name"></h3>
|
|
<p class="text-sm text-gray-400" x-text="selectedFile.is_dir ? 'Folder' : (selectedFile.type?.toUpperCase() || 'File') + ' • ' + formatFileSize(selectedFile.size)"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3 text-sm">
|
|
<div>
|
|
<div class="font-semibold mb-1">Location</div>
|
|
<div class="text-gray-400" x-text="'/' + (selectedFile.path || '')"></div>
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold mb-1">Modified</div>
|
|
<div class="text-gray-400" x-text="formatDateTime(selectedFile.modified)"></div>
|
|
</div>
|
|
<div x-show="!selectedFile.is_dir">
|
|
<div class="font-semibold mb-1">Size</div>
|
|
<div class="text-gray-400" x-text="formatFileSize(selectedFile.size)"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="!selectedFile">
|
|
<div class="text-center text-gray-500 py-8">
|
|
<div class="text-4xl mb-4">📄</div>
|
|
<div class="text-lg font-semibold">No file selected</div>
|
|
<div class="text-sm">Select a file to view details</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer - Status Bar with Keyboard Shortcuts -->
|
|
<div class="panel p-2 border-t-2 border-gray-600">
|
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
|
<!-- Row 1 -->
|
|
<div class="flex flex-wrap gap-1">
|
|
<template x-for="shortcut in shortcuts[0]" :key="shortcut.key">
|
|
<button
|
|
@click="shortcut.action()"
|
|
class="shortcut-key hover:bg-gray-600"
|
|
:title="'Ctrl+' + shortcut.key"
|
|
>
|
|
<span x-text="shortcut.key"></span>
|
|
<span class="text-xs ml-1" x-text="shortcut.label"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Row 2 -->
|
|
<div class="flex flex-wrap gap-1">
|
|
<template x-for="shortcut in shortcuts[1]" :key="shortcut.key">
|
|
<button
|
|
@click="shortcut.action()"
|
|
class="shortcut-key hover:bg-gray-600"
|
|
:title="'Ctrl+' + shortcut.key"
|
|
>
|
|
<span x-text="shortcut.key"></span>
|
|
<span class="text-xs ml-1" x-text="shortcut.label"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Context Menu -->
|
|
<div
|
|
x-show="contextMenu.show"
|
|
@click.away="contextMenu.show = false"
|
|
:style="`top: ${contextMenu.y}px; left: ${contextMenu.x}px`"
|
|
class="fixed bg-gray-800 border border-gray-600 rounded shadow-lg z-50 py-1 min-w-48"
|
|
>
|
|
<template x-for="item in contextMenuItems" :key="item.label">
|
|
<button
|
|
@click="handleContextAction(item.action)"
|
|
class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center gap-2"
|
|
>
|
|
<span x-text="item.icon"></span>
|
|
<span x-text="item.label"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
|
|
<script>
|
|
function fileManager() {
|
|
return {
|
|
collapsed: false,
|
|
currentPath: '',
|
|
searchTerm: '',
|
|
filterType: 'all',
|
|
selectedFile: null,
|
|
expanded: { '': true, 'projects': true },
|
|
contextMenu: { show: false, x: 0, y: 0, file: null },
|
|
|
|
navLinks: [
|
|
{ title: 'My Drive', path: '', icon: '🏠' },
|
|
{ title: 'Shared', path: 'shared', icon: '👥' },
|
|
{ title: 'Starred', path: 'starred', icon: '⭐' },
|
|
{ title: 'Recent', path: 'recent', icon: '🕐' },
|
|
{ title: 'Trash', path: 'trash', icon: '🗑️' },
|
|
],
|
|
|
|
fileSystem: {
|
|
"": {
|
|
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: ["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"
|
|
},
|
|
"projects/web-apps/README.md": {
|
|
id: "readme-md", name: "README.md", path: "projects/web-apps/README.md",
|
|
is_dir: false, size: 5120, type: "md", modified: "2025-01-12T09:30:00Z", shared: true
|
|
},
|
|
"documents": {
|
|
id: "documents", name: "Documents", path: "documents", is_dir: true,
|
|
modified: "2025-01-14T12:00:00Z",
|
|
children: ["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
|
|
},
|
|
"documents/Budget-2025.xlsx": {
|
|
id: "budget-xlsx", name: "Budget-2025.xlsx", path: "documents/Budget-2025.xlsx",
|
|
is_dir: false, size: 524288, type: "xlsx", modified: "2025-01-09T11:00:00Z"
|
|
},
|
|
"media": {
|
|
id: "media", name: "Media", path: "media", is_dir: true,
|
|
modified: "2025-01-13T18:45:00Z",
|
|
children: ["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: "jpg", modified: "2024-12-25T20:00:00Z", starred: true
|
|
},
|
|
"shared": {
|
|
id: "shared", name: "Shared", path: "shared", is_dir: true,
|
|
modified: "2025-01-12T11:20:00Z", shared: true,
|
|
children: []
|
|
}
|
|
},
|
|
|
|
shortcuts: [
|
|
[
|
|
{ key: 'Q', label: 'Rename', action: () => this.renameFile() },
|
|
{ key: 'W', label: 'View', action: () => this.viewFile() },
|
|
{ key: 'E', label: 'Edit', action: () => this.editFile() },
|
|
{ key: 'R', label: 'Move', action: () => this.moveFile() },
|
|
{ key: 'T', label: 'MkDir', action: () => this.makeDirectory() },
|
|
{ key: 'Y', label: 'Delete', action: () => this.deleteFile() },
|
|
{ key: 'U', label: 'Copy', action: () => this.copyFile() },
|
|
{ key: 'I', label: 'Cut', action: () => this.cutFile() },
|
|
{ key: 'O', label: 'Paste', action: () => this.pasteFile() },
|
|
{ key: 'P', label: 'Duplicate', action: () => this.duplicateFile() },
|
|
],
|
|
[
|
|
{ key: 'A', label: 'Select', action: () => this.toggleSelect() },
|
|
{ key: 'S', label: 'Select All', action: () => this.selectAll() },
|
|
{ key: 'D', label: 'Deselect', action: () => this.deselectAll() },
|
|
{ key: 'G', label: 'Details', action: () => this.showDetails() },
|
|
{ key: 'H', label: 'History', action: () => this.showHistory() },
|
|
{ key: 'J', label: 'Share', action: () => this.shareFile() },
|
|
{ key: 'K', label: 'Star', action: () => this.toggleStar() },
|
|
{ key: 'L', label: 'Download', action: () => this.downloadFile() },
|
|
{ key: 'Z', label: 'Upload', action: () => this.uploadFile() },
|
|
{ key: 'X', label: 'Refresh', action: () => this.refresh() },
|
|
]
|
|
],
|
|
|
|
contextMenuItems: [
|
|
{ icon: '👁️', label: 'Open', action: 'open' },
|
|
{ icon: '⬇', label: 'Download', action: 'download' },
|
|
{ icon: '🔗', label: 'Share', action: 'share' },
|
|
{ icon: '⭐', label: 'Star/Unstar', action: 'star' },
|
|
{ icon: '📋', label: 'Copy', action: 'copy' },
|
|
{ icon: '✂️', label: 'Cut', action: 'cut' },
|
|
{ icon: '✏️', label: 'Rename', action: 'rename' },
|
|
{ icon: '🗑️', label: 'Delete', action: 'delete' },
|
|
],
|
|
|
|
get currentItem() {
|
|
return this.fileSystem[this.currentPath];
|
|
},
|
|
|
|
get rootFolders() {
|
|
const root = this.fileSystem[''];
|
|
if (!root || !root.children) return [];
|
|
return root.children.map(name => this.fileSystem[name]).filter(Boolean);
|
|
},
|
|
|
|
get filteredFiles() {
|
|
const current = this.currentItem;
|
|
if (!current || !current.is_dir || !current.children) return [];
|
|
|
|
let files = current.children
|
|
.map(childName => {
|
|
const path = this.currentPath ? `${this.currentPath}/${childName}` : childName;
|
|
return this.fileSystem[path];
|
|
})
|
|
.filter(Boolean);
|
|
|
|
if (this.searchTerm) {
|
|
files = files.filter(f =>
|
|
f.name.toLowerCase().includes(this.searchTerm.toLowerCase())
|
|
);
|
|
}
|
|
|
|
if (this.filterType !== 'all') {
|
|
if (this.filterType === 'folders') files = files.filter(f => f.is_dir);
|
|
else if (this.filterType === 'files') files = files.filter(f => !f.is_dir);
|
|
else if (this.filterType === 'starred') files = files.filter(f => f.starred);
|
|
}
|
|
|
|
return files.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);
|
|
});
|
|
},
|
|
|
|
getChildren(path) {
|
|
const item = this.fileSystem[path];
|
|
if (!item || !item.children) return [];
|
|
return item.children.map(name => {
|
|
const childPath = path ? `${path}/${name}` : name;
|
|
return this.fileSystem[childPath];
|
|
}).filter(Boolean);
|
|
},
|
|
|
|
selectPath(path) {
|
|
this.currentPath = path;
|
|
this.selectedFile = null;
|
|
},
|
|
|
|
selectFile(file) {
|
|
this.selectedFile = file;
|
|
},
|
|
|
|
openFile(file) {
|
|
if (file.is_dir) {
|
|
this.currentPath = file.path;
|
|
this.expanded[file.path] = true;
|
|
} else {
|
|
console.log('Opening file:', file.name);
|
|
}
|
|
},
|
|
|
|
toggleFolder(path) {
|
|
this.expanded[path] = !this.expanded[path];
|
|
},
|
|
|
|
showContextMenu(event, file) {
|
|
this.contextMenu = {
|
|
show: true,
|
|
x: event.clientX,
|
|
y: event.clientY,
|
|
file: file
|
|
};
|
|
this.selectedFile = file;
|
|
},
|
|
|
|
handleContextAction(action) {
|
|
console.log('Action:', action, 'File:', this.contextMenu.file);
|
|
this.contextMenu.show = false;
|
|
|
|
switch(action) {
|
|
case 'open': this.openFile(this.contextMenu.file); break;
|
|
case 'download': this.downloadFile(); break;
|
|
case 'share': this.shareFile(); break;
|
|
case 'star': this.toggleStar(); break;
|
|
case 'copy': this.copyFile(); break;
|
|
case 'cut': this.cutFile(); break;
|
|
case 'rename': this.renameFile(); break;
|
|
case 'delete': this.deleteFile(); break;
|
|
}
|
|
},
|
|
|
|
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]}`;
|
|
},
|
|
|
|
formatDate(dateString) {
|
|
if (!dateString) return '';
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diff = now - date;
|
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
|
|
if (days === 0) return 'Today';
|
|
if (days === 1) return 'Yesterday';
|
|
if (days < 7) return `${days}d ago`;
|
|
return date.toLocaleDateString();
|
|
},
|
|
|
|
formatDateTime(dateString) {
|
|
if (!dateString) return '';
|
|
return new Date(dateString).toLocaleString();
|
|
},
|
|
|
|
// Action methods
|
|
renameFile() { console.log('Rename:', this.selectedFile?.name); },
|
|
viewFile() { console.log('View:', this.selectedFile?.name); },
|
|
editFile() { console.log('Edit:', this.selectedFile?.name); },
|
|
moveFile() { console.log('Move:', this.selectedFile?.name); },
|
|
makeDirectory() { console.log('Make Directory'); },
|
|
deleteFile() { console.log('Delete:', this.selectedFile?.name); },
|
|
copyFile() { console.log('Copy:', this.selectedFile?.name); },
|
|
cutFile() { console.log('Cut:', this.selectedFile?.name); },
|
|
pasteFile() { console.log('Paste'); },
|
|
duplicateFile() { console.log('Duplicate:', this.selectedFile?.name); },
|
|
toggleSelect() { console.log('Toggle Select'); },
|
|
selectAll() { console.log('Select All'); },
|
|
deselectAll() { this.selectedFile = null; },
|
|
showDetails() { console.log('Show Details'); },
|
|
showHistory() { console.log('Show History'); },
|
|
shareFile() { console.log('Share:', this.selectedFile?.name); },
|
|
toggleStar() {
|
|
if (this.selectedFile) {
|
|
this.selectedFile.starred = !this.selectedFile.starred;
|
|
}
|
|
},
|
|
downloadFile() { console.log('Download:', this.selectedFile?.name); },
|
|
uploadFile() { console.log('Upload'); },
|
|
refresh() { console.log('Refresh'); },
|
|
|
|
init() {
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.ctrlKey || e.metaKey) {
|
|
const key = e.key.toUpperCase();
|
|
|
|
// Find and execute shortcut
|
|
for (const row of this.shortcuts) {
|
|
const shortcut = row.find(s => s.key === key);
|
|
if (shortcut) {
|
|
e.preventDefault();
|
|
shortcut.action();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Special shortcuts
|
|
if (key === 'F') {
|
|
e.preventDefault();
|
|
document.querySelector('input[placeholder*="Search"]')?.focus();
|
|
}
|
|
} else if (e.key === 'Delete' && this.selectedFile) {
|
|
e.preventDefault();
|
|
this.deleteFile();
|
|
} else if (e.key === 'F2' && this.selectedFile) {
|
|
e.preventDefault();
|
|
this.renameFile();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|