botserver/web/desktop/drive/index.html
Rodrigo Rodriguez (Pragmatismo) 4b185f00f9 feat: add HTTP server and refactor initialization
- 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.
2025-11-15 09:48:46 -03:00

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>