528 lines
29 KiB
HTML
528 lines
29 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>Drive - General Bots</title>
|
||
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||
|
|
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/ws.js"></script>
|
||
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
||
|
|
<style>
|
||
|
|
[data-loading] { display: none; }
|
||
|
|
.htmx-request [data-loading] { display: block; }
|
||
|
|
.htmx-request [data-content] { opacity: 0.5; }
|
||
|
|
.file-item:hover { background-color: var(--hover-bg, #f3f4f6); }
|
||
|
|
.dark .file-item:hover { --hover-bg: #374151; }
|
||
|
|
.selected { background-color: #dbeafe !important; }
|
||
|
|
.dark .selected { background-color: #1e3a5a !important; }
|
||
|
|
.drop-target { border: 2px dashed #3b82f6 !important; background-color: #eff6ff !important; }
|
||
|
|
.context-menu { position: fixed; z-index: 1000; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen">
|
||
|
|
<!-- Top Navigation -->
|
||
|
|
<nav class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-3">
|
||
|
|
<div class="flex items-center justify-between">
|
||
|
|
<div class="flex items-center gap-4">
|
||
|
|
<a href="/" class="flex items-center gap-2">
|
||
|
|
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14l-5-5 1.41-1.41L12 14.17l4.59-4.58L18 11l-6 6z"/>
|
||
|
|
</svg>
|
||
|
|
<span class="text-xl font-semibold">Drive</span>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<!-- Search -->
|
||
|
|
<div class="relative ml-4">
|
||
|
|
<input type="search"
|
||
|
|
name="query"
|
||
|
|
placeholder="Search files..."
|
||
|
|
class="w-80 px-4 py-2 pl-10 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
|
|
hx-get="/api/files/search"
|
||
|
|
hx-trigger="input changed delay:300ms"
|
||
|
|
hx-target="#file-list"
|
||
|
|
hx-indicator="#search-spinner">
|
||
|
|
<svg class="w-5 h-5 absolute left-3 top-2.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||
|
|
</svg>
|
||
|
|
<div id="search-spinner" class="htmx-indicator absolute right-3 top-2.5">
|
||
|
|
<svg class="animate-spin h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24">
|
||
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="flex items-center gap-3">
|
||
|
|
<!-- View Toggle -->
|
||
|
|
<div class="flex rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden">
|
||
|
|
<button onclick="setView('grid')" id="view-grid" class="px-3 py-2 bg-blue-600 text-white">
|
||
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path d="M4 4h4v4H4V4zm6 0h4v4h-4V4zm6 0h4v4h-4V4zM4 10h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4zM4 16h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4z"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
<button onclick="setView('list')" id="view-list" class="px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path d="M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- User Menu -->
|
||
|
|
<div class="relative">
|
||
|
|
<button class="flex items-center gap-2 p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700">
|
||
|
|
<div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white font-medium">
|
||
|
|
{{ user_name | first | upper }}
|
||
|
|
</div>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<div class="flex h-[calc(100vh-65px)]">
|
||
|
|
<!-- Sidebar -->
|
||
|
|
<aside class="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 p-4 flex flex-col">
|
||
|
|
<!-- New Button -->
|
||
|
|
<button onclick="showUploadModal()"
|
||
|
|
class="w-full mb-4 px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium flex items-center justify-center gap-2 transition-colors">
|
||
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||
|
|
</svg>
|
||
|
|
New
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<!-- Navigation -->
|
||
|
|
<nav class="flex-1 space-y-1">
|
||
|
|
<a href="#" hx-get="/api/files/list" hx-target="#file-list" hx-vals='{"path": ""}'
|
||
|
|
class="flex items-center gap-3 px-3 py-2 rounded-lg bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-medium">
|
||
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
|
||
|
|
</svg>
|
||
|
|
My Drive
|
||
|
|
</a>
|
||
|
|
<a href="#" hx-get="/api/files/shared" hx-target="#file-list"
|
||
|
|
class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">
|
||
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5z"/>
|
||
|
|
</svg>
|
||
|
|
Shared with me
|
||
|
|
</a>
|
||
|
|
<a href="#" hx-get="/api/files/recent" hx-target="#file-list"
|
||
|
|
class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">
|
||
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/>
|
||
|
|
</svg>
|
||
|
|
Recent
|
||
|
|
</a>
|
||
|
|
<a href="#" hx-get="/api/files/favorite" hx-target="#file-list"
|
||
|
|
class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">
|
||
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
|
||
|
|
</svg>
|
||
|
|
Starred
|
||
|
|
</a>
|
||
|
|
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">
|
||
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
|
||
|
|
</svg>
|
||
|
|
Trash
|
||
|
|
</a>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<!-- Storage Info -->
|
||
|
|
<div class="mt-auto pt-4 border-t border-gray-200 dark:border-gray-700"
|
||
|
|
hx-get="/api/files/quota" hx-trigger="load" hx-swap="innerHTML">
|
||
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||
|
|
<div class="flex justify-between mb-1">
|
||
|
|
<span>Storage</span>
|
||
|
|
<span>Loading...</span>
|
||
|
|
</div>
|
||
|
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||
|
|
<div class="bg-blue-600 h-2 rounded-full" style="width: 0%"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</aside>
|
||
|
|
|
||
|
|
<!-- Main Content -->
|
||
|
|
<main class="flex-1 flex flex-col overflow-hidden">
|
||
|
|
<!-- Breadcrumb & Actions -->
|
||
|
|
<div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-3 flex items-center justify-between">
|
||
|
|
<div id="breadcrumb" class="flex items-center gap-2 text-sm">
|
||
|
|
<a href="#" hx-get="/api/files/list" hx-vals='{"path": ""}' hx-target="#file-list"
|
||
|
|
class="text-gray-600 dark:text-gray-400 hover:text-blue-600">My Drive</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<!-- Sort -->
|
||
|
|
<select class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm"
|
||
|
|
hx-get="/api/files/list" hx-target="#file-list" hx-include="[name='path']">
|
||
|
|
<option value="name">Name</option>
|
||
|
|
<option value="modified">Modified</option>
|
||
|
|
<option value="size">Size</option>
|
||
|
|
<option value="type">Type</option>
|
||
|
|
</select>
|
||
|
|
|
||
|
|
<!-- Actions for selected -->
|
||
|
|
<div id="selection-actions" class="hidden flex items-center gap-2">
|
||
|
|
<button onclick="downloadSelected()" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg" title="Download">
|
||
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
<button onclick="shareSelected()" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg" title="Share">
|
||
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
<button onclick="deleteSelected()" class="p-2 hover:bg-red-100 dark:hover:bg-red-900/30 text-red-600 rounded-lg" title="Delete">
|
||
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- File List -->
|
||
|
|
<div class="flex-1 overflow-auto p-6"
|
||
|
|
id="file-container"
|
||
|
|
ondrop="handleDrop(event)"
|
||
|
|
ondragover="handleDragOver(event)"
|
||
|
|
ondragleave="handleDragLeave(event)">
|
||
|
|
|
||
|
|
<!-- Loading indicator -->
|
||
|
|
<div id="loading-indicator" class="hidden flex items-center justify-center h-full">
|
||
|
|
<svg class="animate-spin h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24">
|
||
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- File list populated via HTMX -->
|
||
|
|
<div id="file-list" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4"
|
||
|
|
hx-get="/api/files/list"
|
||
|
|
hx-trigger="load"
|
||
|
|
hx-indicator="#loading-indicator">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<!-- Details Panel (hidden by default) -->
|
||
|
|
<aside id="details-panel" class="hidden w-80 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 p-4 overflow-auto">
|
||
|
|
<div class="flex items-center justify-between mb-4">
|
||
|
|
<h3 class="font-semibold">Details</h3>
|
||
|
|
<button onclick="hideDetailsPanel()" class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
|
||
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div id="details-content">
|
||
|
|
<!-- Populated dynamically -->
|
||
|
|
</div>
|
||
|
|
</aside>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Upload Modal -->
|
||
|
|
<div id="upload-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||
|
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-md w-full mx-4 p-6">
|
||
|
|
<div class="flex items-center justify-between mb-4">
|
||
|
|
<h3 class="text-lg font-semibold">Upload Files</h3>
|
||
|
|
<button onclick="hideUploadModal()" class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
|
||
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<form hx-post="/api/files/upload" hx-encoding="multipart/form-data" hx-target="#file-list" hx-on::after-request="hideUploadModal()">
|
||
|
|
<input type="hidden" name="path" id="upload-path" value="">
|
||
|
|
|
||
|
|
<div class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-xl p-8 text-center hover:border-blue-500 transition-colors">
|
||
|
|
<input type="file" name="file" id="file-input" class="hidden" multiple onchange="updateFileLabel(this)">
|
||
|
|
<label for="file-input" class="cursor-pointer">
|
||
|
|
<svg class="w-12 h-12 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
||
|
|
</svg>
|
||
|
|
<p class="text-gray-600 dark:text-gray-400">
|
||
|
|
<span class="text-blue-600 font-medium">Click to upload</span> or drag and drop
|
||
|
|
</p>
|
||
|
|
<p id="file-label" class="text-sm text-gray-500 mt-1">Any file up to 100MB</p>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="mt-4 flex gap-3">
|
||
|
|
<button type="button" onclick="hideUploadModal()" class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700">
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
<button type="submit" class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||
|
|
Upload
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- New Folder Modal -->
|
||
|
|
<div id="folder-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||
|
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-md w-full mx-4 p-6">
|
||
|
|
<div class="flex items-center justify-between mb-4">
|
||
|
|
<h3 class="text-lg font-semibold">New Folder</h3>
|
||
|
|
<button onclick="hideFolderModal()" class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
|
||
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<form hx-post="/api/files/create-folder" hx-target="#file-list" hx-on::after-request="hideFolderModal()">
|
||
|
|
<input type="hidden" name="path" id="folder-path" value="">
|
||
|
|
<input type="text" name="name" placeholder="Folder name" required
|
||
|
|
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||
|
|
|
||
|
|
<div class="mt-4 flex gap-3">
|
||
|
|
<button type="button" onclick="hideFolderModal()" class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700">
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
<button type="submit" class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||
|
|
Create
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Versions Modal -->
|
||
|
|
<div id="versions-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||
|
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full mx-4 p-6">
|
||
|
|
<div class="flex items-center justify-between mb-4">
|
||
|
|
<h3 class="text-lg font-semibold">Version History</h3>
|
||
|
|
<button onclick="hideVersionsModal()" class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
|
||
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div id="versions-content" class="max-h-96 overflow-auto">
|
||
|
|
<!-- Populated via HTMX -->
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Context Menu -->
|
||
|
|
<div id="context-menu" class="hidden context-menu bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 py-1 min-w-48">
|
||
|
|
<button onclick="openFile()" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3">
|
||
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||
|
|
</svg>
|
||
|
|
Open
|
||
|
|
</button>
|
||
|
|
<button onclick="downloadFile()" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3">
|
||
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||
|
|
</svg>
|
||
|
|
Download
|
||
|
|
</button>
|
||
|
|
<button onclick="shareFile()" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3">
|
||
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
|
||
|
|
</svg>
|
||
|
|
Share
|
||
|
|
</button>
|
||
|
|
<hr class="my-1 border-gray-200 dark:border-gray-700">
|
||
|
|
<button onclick="showVersions()" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3">
|
||
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||
|
|
</svg>
|
||
|
|
Version History
|
||
|
|
</button>
|
||
|
|
<button onclick="renameFile()" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3">
|
||
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||
|
|
</svg>
|
||
|
|
Rename
|
||
|
|
</button>
|
||
|
|
<button onclick="copyFile()" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3">
|
||
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||
|
|
</svg>
|
||
|
|
Copy
|
||
|
|
</button>
|
||
|
|
<button onclick="moveFile()" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3">
|
||
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"/>
|
||
|
|
</svg>
|
||
|
|
Move
|
||
|
|
</button>
|
||
|
|
<hr class="my-1 border-gray-200 dark:border-gray-700">
|
||
|
|
<button onclick="deleteFile()" class="w-full px-4 py-2 text-left hover:bg-red-50 dark:hover:bg-red-900/30 text-red-600 flex items-center gap-3">
|
||
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||
|
|
</svg>
|
||
|
|
Delete
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
let currentPath = '';
|
||
|
|
let selectedFiles = [];
|
||
|
|
let contextTarget = null;
|
||
|
|
|
||
|
|
// View toggle
|
||
|
|
function setView(view) {
|
||
|
|
const fileList = document.getElementById('file-list');
|
||
|
|
const gridBtn = document.getElementById('view-grid');
|
||
|
|
const listBtn = document.getElementById('view-list');
|
||
|
|
|
||
|
|
if (view === 'grid') {
|
||
|
|
fileList.className = 'grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4';
|
||
|
|
gridBtn.className = 'px-3 py-2 bg-blue-600 text-white';
|
||
|
|
listBtn.className = 'px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700';
|
||
|
|
} else {
|
||
|
|
fileList.className = 'space-y-1';
|
||
|
|
gridBtn.className = 'px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700';
|
||
|
|
listBtn.className = 'px-3 py-2 bg-blue-600 text-white';
|
||
|
|
}
|
||
|
|
localStorage.setItem('driveView', view);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Modals
|
||
|
|
function showUploadModal() {
|
||
|
|
document.getElementById('upload-path').value = currentPath;
|
||
|
|
document.getElementById('upload-modal').classList.remove('hidden');
|
||
|
|
}
|
||
|
|
function hideUploadModal() {
|
||
|
|
document.getElementById('upload-modal').classList.add('hidden');
|
||
|
|
}
|
||
|
|
function showFolderModal() {
|
||
|
|
document.getElementById('folder-path').value = currentPath;
|
||
|
|
document.getElementById('folder-modal').classList.remove('hidden');
|
||
|
|
}
|
||
|
|
function hideFolderModal() {
|
||
|
|
document.getElementById('folder-modal').classList.add('hidden');
|
||
|
|
}
|
||
|
|
function showVersionsModal(path) {
|
||
|
|
document.getElementById('versions-modal').classList.remove('hidden');
|
||
|
|
htmx.ajax('GET', `/api/files/versions?path=${encodeURIComponent(path)}`, '#versions-content');
|
||
|
|
}
|
||
|
|
function hideVersionsModal() {
|
||
|
|
document.getElementById('versions-modal').classList.add('hidden');
|
||
|
|
}
|
||
|
|
|
||
|
|
// File label update
|
||
|
|
function updateFileLabel(input) {
|
||
|
|
const label = document.getElementById('file-label');
|
||
|
|
if (input.files.length > 0) {
|
||
|
|
label.textContent = `${input.files.length} file(s) selected`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Context menu
|
||
|
|
function showContextMenu(event, element) {
|
||
|
|
event.preventDefault();
|
||
|
|
contextTarget = element;
|
||
|
|
const menu = document.getElementById('context-menu');
|
||
|
|
menu.style.left = event.pageX + 'px';
|
||
|
|
menu.style.top = event.pageY + 'px';
|
||
|
|
menu.classList.remove('hidden');
|
||
|
|
}
|
||
|
|
|
||
|
|
document.addEventListener('click', () => {
|
||
|
|
document.getElementById('context-menu').classList.add('hidden');
|
||
|
|
});
|
||
|
|
|
||
|
|
// File operations
|
||
|
|
function showVersions() {
|
||
|
|
if (contextTarget) {
|
||
|
|
showVersionsModal(contextTarget.dataset.path);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function restoreVersion(path, versionId) {
|
||
|
|
htmx.ajax('POST', '/api/files/restore', {
|
||
|
|
values: { path: path, version_id: versionId },
|
||
|
|
target: '#file-list'
|
||
|
|
}).then(() => {
|
||
|
|
hideVersionsModal();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Drag and drop
|
||
|
|
function handleDragOver(event) {
|
||
|
|
event.preventDefault();
|
||
|
|
event.currentTarget.classList.add('drop-target');
|
||
|
|
}
|
||
|
|
function handleDragLeave(event) {
|
||
|
|
event.currentTarget.classList.remove('drop-target');
|
||
|
|
}
|
||
|
|
function handleDrop(event) {
|
||
|
|
event.preventDefault();
|
||
|
|
event.currentTarget.classList.remove('drop-target');
|
||
|
|
const files = event.dataTransfer.files;
|
||
|
|
if (files.length > 0) {
|
||
|
|
const formData = new FormData();
|
||
|
|
formData.append('path', currentPath);
|
||
|
|
for (let file of files) {
|
||
|
|
formData.append('file', file);
|
||
|
|
}
|
||
|
|
fetch('/api/files/upload', { method: 'POST', body: formData })
|
||
|
|
.then(() => htmx.trigger('#file-list', 'refresh'));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Details panel
|
||
|
|
function showDetailsPanel(path) {
|
||
|
|
document.getElementById('details-panel').classList.remove('hidden');
|
||
|
|
htmx.ajax('GET', `/api/files/details?path=${encodeURIComponent(path)}`, '#details-content');
|
||
|
|
}
|
||
|
|
function hideDetailsPanel() {
|
||
|
|
document.getElementById('details-panel').classList.add('hidden');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Selection handling
|
||
|
|
function toggleSelect(element) {
|
||
|
|
element.classList.toggle('selected');
|
||
|
|
const path = element.dataset.path;
|
||
|
|
const idx = selectedFiles.indexOf(path);
|
||
|
|
if (idx > -1) {
|
||
|
|
selectedFiles.splice(idx, 1);
|
||
|
|
} else {
|
||
|
|
selectedFiles.push(path);
|
||
|
|
}
|
||
|
|
document.getElementById('selection-actions').classList.toggle('hidden', selectedFiles.length === 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Keyboard shortcuts
|
||
|
|
document.addEventListener('keydown', (e) => {
|
||
|
|
if (e.key === 'Delete' && selectedFiles.length > 0) {
|
||
|
|
deleteSelected();
|
||
|
|
}
|
||
|
|
if (e.ctrlKey && e.key === 'a') {
|
||
|
|
e.preventDefault();
|
||
|
|
document.querySelectorAll('.file-item').forEach(el => {
|
||
|
|
el.classList.add('selected');
|
||
|
|
selectedFiles.push(el.dataset.path);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Load saved view preference
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
const savedView = localStorage.getItem('driveView') || 'grid';
|
||
|
|
setView(savedView);
|
||
|
|
});
|
||
|
|
|
||
|
|
// HTMX events
|
||
|
|
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||
|
|
if (event.detail.target.id === 'file-list') {
|
||
|
|
selectedFiles = [];
|
||
|
|
document.getElementById('selection-actions').classList.add('hidden');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|