- Add JavaScript to load user profile from /api/auth/me endpoint - Save access_token to localStorage/sessionStorage on login - Update user menu to show actual user name and email - Toggle Sign in/Sign out based on authentication state - Add IDs to user menu elements for dynamic updates
1311 lines
44 KiB
JavaScript
1311 lines
44 KiB
JavaScript
/* Drive Module v1.0 - Full API Integration */
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
const API_BASE = "/api/files";
|
|
|
|
let currentBucket = "";
|
|
let currentPath = "";
|
|
let availableBuckets = [];
|
|
let selectedFiles = new Set();
|
|
let viewMode = "list";
|
|
let clipboardFiles = [];
|
|
let clipboardOperation = null;
|
|
let retryCount = 0;
|
|
const MAX_RETRIES = 3;
|
|
const RETRY_DELAYS = [1000, 3000, 10000]; // Exponential backoff: 1s, 3s, 10s
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return "";
|
|
return String(str)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function escapeJs(str) {
|
|
if (!str) return "";
|
|
return String(str)
|
|
.replace(/\\/g, "\\\\")
|
|
.replace(/'/g, "\\'")
|
|
.replace(/"/g, '\\"');
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if (!bytes || bytes === 0) return "0 B";
|
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + " " + units[i];
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return "";
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleDateString(undefined, {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric",
|
|
});
|
|
}
|
|
|
|
function getFileTypeClass(filename) {
|
|
const ext = (filename || "").split(".").pop().toLowerCase();
|
|
const types = {
|
|
document: ["doc", "docx", "pdf", "txt", "rtf", "odt"],
|
|
image: ["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"],
|
|
video: ["mp4", "avi", "mov", "mkv", "webm"],
|
|
audio: ["mp3", "wav", "ogg", "flac", "aac"],
|
|
archive: ["zip", "rar", "7z", "tar", "gz"],
|
|
code: [
|
|
"js",
|
|
"ts",
|
|
"py",
|
|
"rs",
|
|
"go",
|
|
"java",
|
|
"c",
|
|
"cpp",
|
|
"h",
|
|
"html",
|
|
"css",
|
|
"json",
|
|
"xml",
|
|
],
|
|
};
|
|
for (const [type, exts] of Object.entries(types)) {
|
|
if (exts.includes(ext)) return type;
|
|
}
|
|
return "file";
|
|
}
|
|
|
|
function getFolderIcon() {
|
|
return '<svg width="20" height="20" viewBox="0 0 24 24" fill="#5f6368" stroke="none"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>';
|
|
}
|
|
|
|
function getFileIcon(filename) {
|
|
const ext = (filename || "").split(".").pop().toLowerCase();
|
|
const colors = {
|
|
pdf: "#ea4335",
|
|
doc: "#4285f4",
|
|
docx: "#4285f4",
|
|
xls: "#0f9d58",
|
|
xlsx: "#0f9d58",
|
|
ppt: "#fbbc04",
|
|
pptx: "#fbbc04",
|
|
};
|
|
const color = colors[ext] || "#5f6368";
|
|
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="${color}" stroke="none"><path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/></svg>`;
|
|
}
|
|
|
|
function showNotification(message, type) {
|
|
const existing = document.querySelector(".drive-notification");
|
|
if (existing) existing.remove();
|
|
|
|
const notification = document.createElement("div");
|
|
notification.className = `drive-notification notification-${type || "info"}`;
|
|
notification.textContent = message;
|
|
notification.style.cssText =
|
|
"position:fixed;bottom:20px;right:20px;padding:12px 20px;border-radius:8px;background:#333;color:#fff;z-index:9999;animation:slideIn 0.3s ease;";
|
|
|
|
if (type === "error") notification.style.background = "#ef4444";
|
|
if (type === "success") notification.style.background = "#22c55e";
|
|
if (type === "warning") notification.style.background = "#f59e0b";
|
|
|
|
document.body.appendChild(notification);
|
|
setTimeout(() => notification.remove(), 4000);
|
|
}
|
|
|
|
async function init() {
|
|
bindNavigation();
|
|
bindViewToggle();
|
|
bindDragAndDrop();
|
|
bindContextMenu();
|
|
bindKeyboardShortcuts();
|
|
bindUploadButton();
|
|
bindNewFolderButton();
|
|
bindSearchInput();
|
|
|
|
await discoverBuckets();
|
|
loadStorageInfo();
|
|
loadFiles();
|
|
}
|
|
|
|
async function discoverBuckets() {
|
|
try {
|
|
const buckets = await apiRequest("/buckets");
|
|
availableBuckets = buckets || [];
|
|
retryCount = 0; // Reset on success
|
|
|
|
const gbai = availableBuckets.find((b) => b.is_gbai);
|
|
if (gbai) {
|
|
currentBucket = gbai.name;
|
|
} else if (availableBuckets.length > 0) {
|
|
currentBucket = availableBuckets[0].name;
|
|
}
|
|
|
|
updateBucketSelector();
|
|
|
|
if (!currentBucket) {
|
|
const content =
|
|
document.getElementById("drive-content") ||
|
|
document.getElementById("file-grid");
|
|
if (content) {
|
|
content.innerHTML = `<div class="empty-state"><svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg><h3>No drive storage found</h3><p>Please contact your administrator to set up storage.</p></div>`;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to discover buckets:", err);
|
|
const content =
|
|
document.getElementById("drive-content") ||
|
|
document.getElementById("file-grid");
|
|
if (content) {
|
|
const canRetry = retryCount < MAX_RETRIES;
|
|
const retryMsg = canRetry
|
|
? `<button class="btn-primary" onclick="DriveModule.retryWithBackoff()">Retry</button>`
|
|
: `<p class="text-muted">Max retries reached. Please refresh the page.</p>`;
|
|
content.innerHTML = `<div class="empty-state"><svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg><h3>Drive connection error</h3><p>${escapeHtml(err.message)}</p>${retryMsg}</div>`;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function retryWithBackoff() {
|
|
if (retryCount >= MAX_RETRIES) {
|
|
showNotification(
|
|
"Max retries reached. Please refresh the page.",
|
|
"error",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const delay =
|
|
RETRY_DELAYS[retryCount] || RETRY_DELAYS[RETRY_DELAYS.length - 1];
|
|
retryCount++;
|
|
|
|
const content =
|
|
document.getElementById("drive-content") ||
|
|
document.getElementById("file-grid");
|
|
if (content) {
|
|
content.innerHTML = `<div class="empty-state"><div class="spinner"></div><p>Retrying in ${delay / 1000}s... (attempt ${retryCount}/${MAX_RETRIES})</p></div>`;
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
await init();
|
|
}
|
|
|
|
function updateBucketSelector() {
|
|
const selector = document.getElementById("bucket-selector");
|
|
if (!selector) return;
|
|
|
|
if (availableBuckets.length <= 1) {
|
|
selector.style.display = "none";
|
|
return;
|
|
}
|
|
|
|
selector.style.display = "block";
|
|
selector.innerHTML = availableBuckets
|
|
.map(
|
|
(b) =>
|
|
`<option value="${escapeHtml(b.name)}" ${b.name === currentBucket ? "selected" : ""}>${escapeHtml(b.is_gbai ? b.name.replace(".gbai", "") : b.name)}</option>`,
|
|
)
|
|
.join("");
|
|
|
|
selector.removeEventListener("change", handleBucketChange);
|
|
selector.addEventListener("change", handleBucketChange);
|
|
}
|
|
|
|
function handleBucketChange(e) {
|
|
currentBucket = e.target.value;
|
|
currentPath = "";
|
|
loadFiles();
|
|
}
|
|
|
|
async function apiRequest(endpoint, options = {}) {
|
|
const url = `${API_BASE}${endpoint}`;
|
|
const defaultHeaders = { "Content-Type": "application/json" };
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: { ...defaultHeaders, ...options.headers },
|
|
...options,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response
|
|
.json()
|
|
.catch(() => ({ error: response.statusText }));
|
|
throw new Error(error.error || "Request failed");
|
|
}
|
|
|
|
return response.json();
|
|
} catch (err) {
|
|
console.error(`API Error [${endpoint}]:`, err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function loadFiles(path, bucket) {
|
|
if (path !== undefined) currentPath = path;
|
|
if (bucket !== undefined) currentBucket = bucket;
|
|
|
|
if (!currentBucket) {
|
|
await discoverBuckets();
|
|
if (!currentBucket) return;
|
|
}
|
|
|
|
const content =
|
|
document.getElementById("drive-content") ||
|
|
document.getElementById("file-grid");
|
|
if (!content) return;
|
|
|
|
content.innerHTML =
|
|
'<div class="loading-state"><div class="spinner"></div><p>Loading files...</p></div>';
|
|
updateBreadcrumb();
|
|
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (currentBucket) params.set("bucket", currentBucket);
|
|
if (currentPath) params.set("path", currentPath);
|
|
|
|
const files = await apiRequest(`/list?${params.toString()}`);
|
|
renderFiles(files);
|
|
} catch (err) {
|
|
content.innerHTML = `<div class="empty-state"><h3>Failed to load files</h3><p>${escapeHtml(err.message)}</p><button class="btn-primary" onclick="DriveModule.loadFiles()">Retry</button></div>`;
|
|
}
|
|
}
|
|
|
|
async function loadStorageInfo() {
|
|
try {
|
|
const quota = await apiRequest("/quota");
|
|
const usedEl = document.getElementById("storage-used");
|
|
const fillEl = document.getElementById("storage-fill");
|
|
const detailEl = document.getElementById("storage-detail");
|
|
|
|
if (usedEl)
|
|
usedEl.textContent = `${formatFileSize(quota.used_bytes)} of ${formatFileSize(quota.total_bytes)}`;
|
|
if (fillEl) fillEl.style.width = `${quota.percentage_used || 0}%`;
|
|
if (detailEl)
|
|
detailEl.textContent = `${formatFileSize(quota.available_bytes)} available`;
|
|
} catch (err) {
|
|
// Silently fail - don't retry storage info, it's not critical
|
|
console.error("Failed to load storage info:", err);
|
|
}
|
|
}
|
|
function renderFiles(files) {
|
|
const content =
|
|
document.getElementById("drive-content") ||
|
|
document.getElementById("file-grid");
|
|
if (!content) return;
|
|
|
|
if (!files || files.length === 0) {
|
|
content.innerHTML = `<div class="empty-state"><svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg><h3>This folder is empty</h3><p>Upload files or create a new folder to get started</p></div>`;
|
|
return;
|
|
}
|
|
|
|
const folders = files
|
|
.filter((f) => f.is_dir)
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
const regularFiles = files
|
|
.filter((f) => !f.is_dir)
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
const sorted = [...folders, ...regularFiles];
|
|
|
|
if (viewMode === "grid") {
|
|
content.innerHTML = `<div class="file-grid">${sorted.map((f) => renderFileCard(f)).join("")}</div>`;
|
|
} else {
|
|
content.innerHTML = `<div class="file-list"><div class="file-list-header"><div class="file-col file-name-col">Name</div><div class="file-col file-modified-col">Modified</div><div class="file-col file-size-col">Size</div><div class="file-col file-actions-col"></div></div>${sorted.map((f) => renderFileRow(f)).join("")}</div>`;
|
|
}
|
|
|
|
bindFileEvents();
|
|
updateSelectionBar();
|
|
}
|
|
|
|
function renderFileCard(file) {
|
|
const iconClass = file.is_dir ? "folder" : getFileTypeClass(file.name);
|
|
const iconSvg = file.is_dir ? getFolderIcon() : getFileIcon(file.name);
|
|
const sizeText = file.is_dir ? "" : formatFileSize(file.size);
|
|
const checked = selectedFiles.has(file.path) ? "checked" : "";
|
|
const selected = selectedFiles.has(file.path) ? "selected" : "";
|
|
|
|
return `<div class="file-card ${selected}" data-path="${escapeHtml(file.path)}" data-name="${escapeHtml(file.name)}" data-type="${file.is_dir ? "folder" : "file"}" data-size="${file.size || 0}"><input type="checkbox" class="file-checkbox" ${checked} onchange="DriveModule.toggleSelection('${escapeJs(file.path)}')"><div class="file-card-preview ${iconClass}">${iconSvg}</div><div class="file-card-info"><div class="file-card-name" title="${escapeHtml(file.name)}">${escapeHtml(file.name)}</div><div class="file-card-meta">${sizeText}</div></div></div>`;
|
|
}
|
|
|
|
function renderFileRow(file) {
|
|
const iconSvg = file.is_dir ? getFolderIcon() : getFileIcon(file.name);
|
|
const sizeText = file.is_dir ? "—" : formatFileSize(file.size);
|
|
const modifiedText = file.modified ? formatDate(file.modified) : "—";
|
|
const checked = selectedFiles.has(file.path) ? "checked" : "";
|
|
const selected = selectedFiles.has(file.path) ? "selected" : "";
|
|
|
|
const downloadIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`;
|
|
const moreIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg>`;
|
|
|
|
const downloadBtn = !file.is_dir
|
|
? `<button class="btn-icon-sm" title="Download" onclick="event.stopPropagation(); DriveModule.downloadFile('${escapeJs(file.path)}')">${downloadIcon}</button>`
|
|
: "";
|
|
|
|
return `<div class="drive-file-item ${file.is_dir ? "folder" : ""} ${selected}" data-path="${escapeHtml(file.path)}" data-name="${escapeHtml(file.name)}" data-type="${file.is_dir ? "folder" : "file"}" data-size="${file.size || 0}"><div class="file-col file-name-col"><input type="checkbox" class="file-checkbox" ${checked} onclick="event.stopPropagation()" onchange="DriveModule.toggleSelection('${escapeJs(file.path)}')">${iconSvg}<span>${escapeHtml(file.name)}</span></div><div class="file-col file-modified-col">${modifiedText}</div><div class="file-col file-size-col">${sizeText}</div><div class="file-col file-actions-col">${downloadBtn}<button class="btn-icon-sm" title="More" onclick="event.stopPropagation(); DriveModule.showContextMenuFor(event, '${escapeJs(file.path)}')">${moreIcon}</button></div></div>`;
|
|
}
|
|
|
|
function bindFileEvents() {
|
|
document.querySelectorAll(".file-card, .drive-file-item").forEach((el) => {
|
|
el.addEventListener("click", function (e) {
|
|
if (
|
|
e.target.closest(".file-checkbox") ||
|
|
e.target.closest(".btn-icon-sm")
|
|
)
|
|
return;
|
|
const path = this.dataset.path;
|
|
const type = this.dataset.type;
|
|
|
|
if (e.ctrlKey || e.metaKey) {
|
|
toggleSelection(path);
|
|
} else {
|
|
// Single click just selects, doesn't open
|
|
toggleSelection(path);
|
|
}
|
|
});
|
|
|
|
el.addEventListener("dblclick", function (e) {
|
|
if (e.target.closest(".file-checkbox")) return;
|
|
const path = this.dataset.path;
|
|
const type = this.dataset.type;
|
|
if (type === "folder") {
|
|
loadFiles(path, currentBucket);
|
|
} else {
|
|
openInlineEditor(path);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function toggleSelection(path) {
|
|
if (selectedFiles.has(path)) selectedFiles.delete(path);
|
|
else selectedFiles.add(path);
|
|
|
|
const el = document.querySelector(`[data-path="${CSS.escape(path)}"]`);
|
|
if (el) {
|
|
el.classList.toggle("selected", selectedFiles.has(path));
|
|
const checkbox = el.querySelector(".file-checkbox");
|
|
if (checkbox) checkbox.checked = selectedFiles.has(path);
|
|
}
|
|
updateSelectionBar();
|
|
}
|
|
|
|
function selectAll() {
|
|
document.querySelectorAll(".file-card, .drive-file-item").forEach((el) => {
|
|
selectedFiles.add(el.dataset.path);
|
|
el.classList.add("selected");
|
|
const checkbox = el.querySelector(".file-checkbox");
|
|
if (checkbox) checkbox.checked = true;
|
|
});
|
|
updateSelectionBar();
|
|
}
|
|
|
|
function clearSelection() {
|
|
selectedFiles.clear();
|
|
document
|
|
.querySelectorAll(".file-card.selected, .drive-file-item.selected")
|
|
.forEach((el) => {
|
|
el.classList.remove("selected");
|
|
const checkbox = el.querySelector(".file-checkbox");
|
|
if (checkbox) checkbox.checked = false;
|
|
});
|
|
updateSelectionBar();
|
|
}
|
|
|
|
function updateSelectionBar() {
|
|
const bar = document.getElementById("selection-bar");
|
|
const countEl = document.getElementById("selected-count");
|
|
if (bar) bar.style.display = selectedFiles.size > 0 ? "flex" : "none";
|
|
if (countEl) countEl.textContent = selectedFiles.size;
|
|
}
|
|
|
|
function updateBreadcrumb() {
|
|
const breadcrumb = document.querySelector(".breadcrumb, .drive-breadcrumb");
|
|
if (!breadcrumb) return;
|
|
|
|
const parts = currentPath ? currentPath.split("/").filter(Boolean) : [];
|
|
let html = `<button class="breadcrumb-item" onclick="DriveModule.loadFiles('', '${currentBucket}')">My Drive</button>`;
|
|
|
|
let cumulativePath = "";
|
|
parts.forEach((part, idx) => {
|
|
cumulativePath += (cumulativePath ? "/" : "") + part;
|
|
const isLast = idx === parts.length - 1;
|
|
html += `<span class="breadcrumb-sep">/</span>`;
|
|
html += isLast
|
|
? `<span class="breadcrumb-current">${escapeHtml(part)}</span>`
|
|
: `<button class="breadcrumb-item" onclick="DriveModule.loadFiles('${escapeJs(cumulativePath)}', '${currentBucket}')">${escapeHtml(part)}</button>`;
|
|
});
|
|
|
|
breadcrumb.innerHTML = html;
|
|
}
|
|
function bindNavigation() {
|
|
document.querySelectorAll(".drive-nav-item, .nav-item").forEach((item) => {
|
|
item.addEventListener("click", function () {
|
|
document
|
|
.querySelectorAll(".drive-nav-item, .nav-item")
|
|
.forEach((i) => i.classList.remove("active"));
|
|
this.classList.add("active");
|
|
|
|
const view = this.dataset.view || this.dataset.filter;
|
|
if (view === "my-drive" || !view) loadFiles("", currentBucket);
|
|
else if (view === "recent") loadRecentFiles();
|
|
else if (view === "starred" || view === "favorite") loadStarredFiles();
|
|
else if (view === "shared") loadSharedFiles();
|
|
else if (view === "trash") loadTrashFiles();
|
|
});
|
|
});
|
|
}
|
|
|
|
async function loadRecentFiles() {
|
|
const content =
|
|
document.getElementById("drive-content") ||
|
|
document.getElementById("file-grid");
|
|
if (!content) return;
|
|
content.innerHTML =
|
|
'<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>';
|
|
try {
|
|
const files = await apiRequest("/recent");
|
|
renderFiles(files);
|
|
} catch (err) {
|
|
content.innerHTML = `<div class="empty-state"><h3>No recent files</h3></div>`;
|
|
}
|
|
}
|
|
|
|
async function loadStarredFiles() {
|
|
const content =
|
|
document.getElementById("drive-content") ||
|
|
document.getElementById("file-grid");
|
|
if (!content) return;
|
|
content.innerHTML =
|
|
'<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>';
|
|
try {
|
|
const files = await apiRequest("/favorite");
|
|
renderFiles(files);
|
|
} catch (err) {
|
|
content.innerHTML = `<div class="empty-state"><h3>No starred files</h3></div>`;
|
|
}
|
|
}
|
|
|
|
async function loadSharedFiles() {
|
|
const content =
|
|
document.getElementById("drive-content") ||
|
|
document.getElementById("file-grid");
|
|
if (!content) return;
|
|
content.innerHTML =
|
|
'<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>';
|
|
try {
|
|
const files = await apiRequest("/shared");
|
|
renderFiles(files);
|
|
} catch (err) {
|
|
content.innerHTML = `<div class="empty-state"><h3>No shared files</h3></div>`;
|
|
}
|
|
}
|
|
|
|
async function loadTrashFiles() {
|
|
const content =
|
|
document.getElementById("drive-content") ||
|
|
document.getElementById("file-grid");
|
|
if (!content) return;
|
|
content.innerHTML = `<div class="empty-state"><h3>Trash is empty</h3></div>`;
|
|
}
|
|
|
|
function bindViewToggle() {
|
|
document.querySelectorAll(".view-toggle, .view-btn").forEach((btn) => {
|
|
btn.addEventListener("click", function () {
|
|
const view = this.dataset.view;
|
|
if (view) {
|
|
viewMode = view;
|
|
document
|
|
.querySelectorAll(".view-toggle, .view-btn")
|
|
.forEach((b) => b.classList.remove("active"));
|
|
this.classList.add("active");
|
|
loadFiles(currentPath, currentBucket);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function bindUploadButton() {
|
|
const uploadBtn = document.getElementById("upload-btn");
|
|
if (uploadBtn) uploadBtn.addEventListener("click", triggerUpload);
|
|
window.uploadFile = triggerUpload;
|
|
|
|
let fileInput = document.getElementById("file-input");
|
|
if (!fileInput) {
|
|
fileInput = document.createElement("input");
|
|
fileInput.type = "file";
|
|
fileInput.id = "file-input";
|
|
fileInput.multiple = true;
|
|
fileInput.style.display = "none";
|
|
document.body.appendChild(fileInput);
|
|
}
|
|
fileInput.addEventListener("change", handleFileInputChange);
|
|
}
|
|
|
|
function triggerUpload() {
|
|
const input = document.getElementById("file-input");
|
|
if (input) input.click();
|
|
}
|
|
|
|
function handleFileInputChange(e) {
|
|
const files = e.target.files;
|
|
if (files && files.length > 0) uploadFiles(Array.from(files));
|
|
e.target.value = "";
|
|
}
|
|
|
|
async function uploadFiles(files) {
|
|
showNotification(`Uploading ${files.length} file(s)...`, "info");
|
|
|
|
let uploaded = 0;
|
|
let failed = 0;
|
|
|
|
for (const file of files) {
|
|
try {
|
|
const content = await readFileAsBase64(file);
|
|
const filePath = currentPath
|
|
? `${currentPath}/${file.name}`
|
|
: file.name;
|
|
|
|
await apiRequest("/write", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
bucket: currentBucket,
|
|
path: filePath,
|
|
content: content,
|
|
}),
|
|
});
|
|
uploaded++;
|
|
} catch (err) {
|
|
console.error("Upload error:", err);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
if (failed === 0)
|
|
showNotification(`Uploaded ${uploaded} file(s)`, "success");
|
|
else showNotification(`Uploaded ${uploaded}, ${failed} failed`, "warning");
|
|
|
|
loadFiles(currentPath, currentBucket);
|
|
loadStorageInfo();
|
|
}
|
|
|
|
function readFileAsBase64(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
const base64 = reader.result.split(",")[1] || reader.result;
|
|
resolve(base64);
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
function bindDragAndDrop() {
|
|
const container = document.querySelector(".drive-container, .drive-main");
|
|
if (!container) return;
|
|
|
|
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
|
container.addEventListener(eventName, (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
});
|
|
});
|
|
|
|
container.addEventListener("dragenter", () => {
|
|
container.classList.add("drag-active");
|
|
const overlay = document.getElementById("drop-overlay");
|
|
if (overlay) overlay.classList.add("visible");
|
|
});
|
|
|
|
container.addEventListener("dragleave", (e) => {
|
|
if (!container.contains(e.relatedTarget)) {
|
|
container.classList.remove("drag-active");
|
|
const overlay = document.getElementById("drop-overlay");
|
|
if (overlay) overlay.classList.remove("visible");
|
|
}
|
|
});
|
|
|
|
container.addEventListener("drop", (e) => {
|
|
container.classList.remove("drag-active");
|
|
const overlay = document.getElementById("drop-overlay");
|
|
if (overlay) overlay.classList.remove("visible");
|
|
const files = e.dataTransfer.files;
|
|
if (files && files.length > 0) uploadFiles(Array.from(files));
|
|
});
|
|
}
|
|
function bindContextMenu() {
|
|
document.addEventListener("contextmenu", (e) => {
|
|
const fileEl = e.target.closest(".file-card, .drive-file-item");
|
|
if (fileEl) {
|
|
e.preventDefault();
|
|
showContextMenu(
|
|
e.clientX,
|
|
e.clientY,
|
|
fileEl.dataset.path,
|
|
fileEl.dataset.type,
|
|
);
|
|
}
|
|
});
|
|
|
|
document.addEventListener("click", (e) => {
|
|
const menu = document.getElementById("context-menu");
|
|
if (menu && !menu.contains(e.target)) {
|
|
menu.classList.add("hidden");
|
|
menu.style.display = "none";
|
|
}
|
|
});
|
|
}
|
|
|
|
function showContextMenu(x, y, path, type) {
|
|
let menu = document.getElementById("context-menu");
|
|
if (!menu) {
|
|
menu = document.createElement("div");
|
|
menu.id = "context-menu";
|
|
menu.className = "context-menu";
|
|
document.body.appendChild(menu);
|
|
}
|
|
|
|
const isFolder = type === "folder";
|
|
const ep = escapeJs(path);
|
|
const canEdit = !isFolder && isEditableFile(path);
|
|
|
|
const icons = {
|
|
open: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`,
|
|
download: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
|
|
edit: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
|
|
copy: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`,
|
|
cut: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="6" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><line x1="20" y1="4" x2="8.12" y2="15.88"></line><line x1="14.47" y1="14.48" x2="20" y2="20"></line><line x1="8.12" y1="8.12" x2="12" y2="12"></line></svg>`,
|
|
rename: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg>`,
|
|
delete: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`,
|
|
};
|
|
|
|
const hideMenu = `document.getElementById('context-menu').style.display='none';`;
|
|
|
|
menu.innerHTML = `
|
|
${
|
|
isFolder
|
|
? `<div class="context-menu-item" onclick="${hideMenu}DriveModule.loadFiles('${ep}', '${currentBucket}')">${icons.open}<span>Open</span></div>`
|
|
: `<div class="context-menu-item" onclick="${hideMenu}DriveModule.downloadFile('${ep}')">${icons.download}<span>Download</span></div>`
|
|
}
|
|
${canEdit ? `<div class="context-menu-item" onclick="${hideMenu}DriveModule.openInlineEditor('${ep}')">${icons.edit}<span>Edit</span></div>` : ""}
|
|
<div class="context-menu-divider"></div>
|
|
<div class="context-menu-item" onclick="${hideMenu}DriveModule.copyToClipboard('${ep}')">${icons.copy}<span>Copy</span></div>
|
|
<div class="context-menu-item" onclick="${hideMenu}DriveModule.cutToClipboard('${ep}')">${icons.cut}<span>Cut</span></div>
|
|
<div class="context-menu-item" onclick="${hideMenu}DriveModule.renameItem('${ep}')">${icons.rename}<span>Rename</span></div>
|
|
<div class="context-menu-divider"></div>
|
|
<div class="context-menu-item danger" onclick="${hideMenu}DriveModule.deleteItem('${ep}')">${icons.delete}<span>Delete</span></div>
|
|
`;
|
|
|
|
menu.style.display = "block";
|
|
menu.classList.remove("hidden");
|
|
|
|
const rect = menu.getBoundingClientRect();
|
|
menu.style.left =
|
|
(x + rect.width > window.innerWidth ? x - rect.width : x) + "px";
|
|
menu.style.top =
|
|
(y + rect.height > window.innerHeight ? y - rect.height : y) + "px";
|
|
}
|
|
|
|
function showContextMenuFor(event, path) {
|
|
const el = document.querySelector(`[data-path="${CSS.escape(path)}"]`);
|
|
const type = el ? el.dataset.type : "file";
|
|
showContextMenu(event.clientX, event.clientY, path, type);
|
|
}
|
|
|
|
function bindKeyboardShortcuts() {
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")
|
|
return;
|
|
|
|
if (e.key === "Delete" && selectedFiles.size > 0) {
|
|
e.preventDefault();
|
|
deleteSelected();
|
|
}
|
|
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
|
|
e.preventDefault();
|
|
selectAll();
|
|
}
|
|
if ((e.ctrlKey || e.metaKey) && e.key === "c" && selectedFiles.size > 0) {
|
|
e.preventDefault();
|
|
copySelected();
|
|
}
|
|
if ((e.ctrlKey || e.metaKey) && e.key === "x" && selectedFiles.size > 0) {
|
|
e.preventDefault();
|
|
cutSelected();
|
|
}
|
|
if (
|
|
(e.ctrlKey || e.metaKey) &&
|
|
e.key === "v" &&
|
|
clipboardFiles.length > 0
|
|
) {
|
|
e.preventDefault();
|
|
pasteFiles();
|
|
}
|
|
if (e.key === "Escape") {
|
|
clearSelection();
|
|
const menu = document.getElementById("context-menu");
|
|
if (menu) menu.style.display = "none";
|
|
}
|
|
if (e.key === "Backspace" && !e.ctrlKey && !e.metaKey) {
|
|
e.preventDefault();
|
|
navigateUp();
|
|
}
|
|
if (e.key === "F2" && selectedFiles.size === 1) {
|
|
e.preventDefault();
|
|
renameItem(Array.from(selectedFiles)[0]);
|
|
}
|
|
});
|
|
}
|
|
|
|
function navigateUp() {
|
|
if (!currentPath) return;
|
|
const parts = currentPath.split("/").filter(Boolean);
|
|
parts.pop();
|
|
loadFiles(parts.join("/"), currentBucket);
|
|
}
|
|
|
|
function bindNewFolderButton() {
|
|
const btn = document.getElementById("new-folder-btn");
|
|
if (btn) btn.addEventListener("click", createFolder);
|
|
window.createFolder = createFolder;
|
|
}
|
|
|
|
async function createFolder() {
|
|
const name = prompt("Enter folder name:");
|
|
if (!name || !name.trim()) return;
|
|
|
|
try {
|
|
await apiRequest("/createFolder", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
bucket: currentBucket,
|
|
path: currentPath,
|
|
name: name.trim(),
|
|
}),
|
|
});
|
|
showNotification(`Folder "${name}" created`, "success");
|
|
loadFiles(currentPath, currentBucket);
|
|
} catch (err) {
|
|
showNotification(`Failed to create folder: ${err.message}`, "error");
|
|
}
|
|
}
|
|
|
|
function bindSearchInput() {
|
|
const searchInput = document.querySelector(
|
|
".search-box input, #search-input",
|
|
);
|
|
if (!searchInput) return;
|
|
|
|
let debounceTimer;
|
|
searchInput.addEventListener("input", (e) => {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(() => {
|
|
const query = e.target.value.trim();
|
|
if (query) searchFiles(query);
|
|
else loadFiles(currentPath, currentBucket);
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
async function searchFiles(query) {
|
|
const content =
|
|
document.getElementById("drive-content") ||
|
|
document.getElementById("file-grid");
|
|
if (!content) return;
|
|
content.innerHTML =
|
|
'<div class="loading-state"><div class="spinner"></div><p>Searching...</p></div>';
|
|
|
|
try {
|
|
const params = new URLSearchParams();
|
|
params.set("query", query);
|
|
if (currentBucket) params.set("bucket", currentBucket);
|
|
const files = await apiRequest(`/search?${params.toString()}`);
|
|
renderFiles(files);
|
|
} catch (err) {
|
|
content.innerHTML = `<div class="empty-state"><h3>Search failed</h3></div>`;
|
|
}
|
|
}
|
|
async function downloadFile(path) {
|
|
try {
|
|
const response = await apiRequest("/download", {
|
|
method: "POST",
|
|
body: JSON.stringify({ bucket: currentBucket, path: path }),
|
|
});
|
|
|
|
const content = response.content;
|
|
const fileName = path.split("/").pop() || "download";
|
|
let blob;
|
|
|
|
const isBase64 =
|
|
/^[A-Za-z0-9+/=]+$/.test(content) && content.length > 100;
|
|
if (isBase64) {
|
|
try {
|
|
const byteCharacters = atob(content);
|
|
const byteNumbers = new Array(byteCharacters.length);
|
|
for (let i = 0; i < byteCharacters.length; i++)
|
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
blob = new Blob([new Uint8Array(byteNumbers)]);
|
|
} catch (e) {
|
|
blob = new Blob([content], { type: "text/plain" });
|
|
}
|
|
} else {
|
|
blob = new Blob([content], { type: "text/plain" });
|
|
}
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = fileName;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
showNotification(`Downloaded ${fileName}`, "success");
|
|
} catch (err) {
|
|
showNotification(`Download failed: ${err.message}`, "error");
|
|
}
|
|
}
|
|
|
|
function isBasicFile(path) {
|
|
const ext = "." + (path.split(".").pop() || "").toLowerCase();
|
|
return ext === ".bas";
|
|
}
|
|
|
|
function openInDesigner(path) {
|
|
const params = new URLSearchParams({
|
|
bucket: currentBucket,
|
|
path: path,
|
|
});
|
|
|
|
if (window.htmx) {
|
|
htmx.ajax("GET", `/designer.html?${params.toString()}`, {
|
|
target: "#main-content",
|
|
swap: "innerHTML",
|
|
});
|
|
window.history.pushState({}, "", `/#designer?${params.toString()}`);
|
|
} else {
|
|
window.location.href = `/designer.html?${params.toString()}`;
|
|
}
|
|
}
|
|
|
|
function isEditableFile(path) {
|
|
const editableExtensions = [
|
|
".txt",
|
|
".md",
|
|
".json",
|
|
".js",
|
|
".ts",
|
|
".css",
|
|
".html",
|
|
".htm",
|
|
".xml",
|
|
".yaml",
|
|
".yml",
|
|
".csv",
|
|
".vbs",
|
|
".sql",
|
|
".sh",
|
|
".bat",
|
|
".ps1",
|
|
".py",
|
|
".rb",
|
|
".php",
|
|
".java",
|
|
".c",
|
|
".cpp",
|
|
".h",
|
|
".rs",
|
|
".go",
|
|
".swift",
|
|
".kt",
|
|
".scala",
|
|
".r",
|
|
".lua",
|
|
".pl",
|
|
".ini",
|
|
".conf",
|
|
".config",
|
|
".env",
|
|
".gitignore",
|
|
".dockerfile",
|
|
".toml",
|
|
".lock",
|
|
".log",
|
|
".markdown",
|
|
".rst",
|
|
".tex",
|
|
".csv",
|
|
];
|
|
const ext = "." + (path.split(".").pop() || "").toLowerCase();
|
|
return editableExtensions.includes(ext);
|
|
}
|
|
|
|
async function openInlineEditor(path) {
|
|
const fileName = path.split("/").pop() || "file";
|
|
console.log("openInlineEditor called with path:", path);
|
|
|
|
const isBas = isBasicFile(path);
|
|
console.log("isBasicFile check:", path, "->", isBas);
|
|
|
|
if (isBas) {
|
|
console.log("Opening .bas file in designer:", path);
|
|
openInDesigner(path);
|
|
return;
|
|
}
|
|
|
|
if (!isEditableFile(path)) {
|
|
console.log("File not editable, downloading instead");
|
|
downloadFile(path);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log("Fetching file content for:", path);
|
|
const response = await apiRequest("/read", {
|
|
method: "POST",
|
|
body: JSON.stringify({ bucket: currentBucket, path: path }),
|
|
});
|
|
|
|
console.log("API response:", response);
|
|
const content = response.content || "";
|
|
console.log("Content length:", content.length);
|
|
showEditorModal(path, fileName, content);
|
|
} catch (err) {
|
|
console.error("Failed to open file:", err);
|
|
showNotification(`Failed to open file: ${err.message}`, "error");
|
|
}
|
|
}
|
|
|
|
function showEditorModal(path, fileName, content) {
|
|
console.log("showEditorModal called:", {
|
|
path,
|
|
fileName,
|
|
contentLength: content?.length,
|
|
});
|
|
|
|
let modal = document.getElementById("editor-modal");
|
|
if (modal) {
|
|
console.log("Removing existing modal");
|
|
modal.remove();
|
|
}
|
|
|
|
const ext = (fileName.split(".").pop() || "txt").toLowerCase();
|
|
|
|
modal = document.createElement("div");
|
|
modal.id = "editor-modal";
|
|
modal.className = "modal-overlay";
|
|
|
|
// Build modal HTML
|
|
const headerHtml = `
|
|
<div class="editor-header">
|
|
<div class="editor-title">
|
|
<span class="editor-icon">📝</span>
|
|
<span class="editor-filename">${escapeHtml(fileName)}</span>
|
|
<span class="editor-status" id="editor-status"></span>
|
|
</div>
|
|
<div class="editor-actions">
|
|
<button class="btn-secondary" onclick="DriveModule.closeEditor()">Cancel</button>
|
|
<button class="btn-primary" onclick="DriveModule.saveEditorContent()">
|
|
<span>💾</span> Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const bodyHtml = `
|
|
<div class="editor-body">
|
|
<textarea
|
|
id="editor-textarea"
|
|
class="editor-textarea"
|
|
spellcheck="false"
|
|
data-path="${escapeHtml(path)}"
|
|
data-ext="${ext}"
|
|
></textarea>
|
|
</div>
|
|
`;
|
|
|
|
const footerHtml = `
|
|
<div class="editor-footer">
|
|
<span class="editor-info">Line: <span id="editor-line">1</span>, Col: <span id="editor-col">1</span></span>
|
|
<span class="editor-info">${ext.toUpperCase()}</span>
|
|
</div>
|
|
`;
|
|
|
|
modal.innerHTML = `<div class="modal-content editor-modal-content">${headerHtml}${bodyHtml}${footerHtml}</div>`;
|
|
|
|
document.body.appendChild(modal);
|
|
console.log("Modal appended to body");
|
|
|
|
// Set content via value property to avoid HTML escaping issues
|
|
const textarea = document.getElementById("editor-textarea");
|
|
if (textarea) {
|
|
textarea.value = content || "";
|
|
console.log("Textarea content set, length:", textarea.value.length);
|
|
textarea.focus();
|
|
} else {
|
|
console.error("Textarea not found!");
|
|
return;
|
|
}
|
|
|
|
textarea.addEventListener("input", () => {
|
|
document.getElementById("editor-status").textContent = "● Modified";
|
|
});
|
|
|
|
textarea.addEventListener("click", updateEditorCursor);
|
|
textarea.addEventListener("keyup", updateEditorCursor);
|
|
|
|
textarea.addEventListener("keydown", (e) => {
|
|
if (e.key === "s" && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
saveEditorContent();
|
|
}
|
|
if (e.key === "Escape") {
|
|
closeEditor();
|
|
}
|
|
if (e.key === "Tab") {
|
|
e.preventDefault();
|
|
const start = textarea.selectionStart;
|
|
const end = textarea.selectionEnd;
|
|
textarea.value =
|
|
textarea.value.substring(0, start) +
|
|
" " +
|
|
textarea.value.substring(end);
|
|
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
|
}
|
|
});
|
|
|
|
modal.addEventListener("click", (e) => {
|
|
if (e.target === modal) closeEditor();
|
|
});
|
|
}
|
|
|
|
function updateEditorCursor() {
|
|
const textarea = document.getElementById("editor-textarea");
|
|
if (!textarea) return;
|
|
|
|
const text = textarea.value.substring(0, textarea.selectionStart);
|
|
const lines = text.split("\n");
|
|
const line = lines.length;
|
|
const col = lines[lines.length - 1].length + 1;
|
|
|
|
document.getElementById("editor-line").textContent = line;
|
|
document.getElementById("editor-col").textContent = col;
|
|
}
|
|
|
|
async function saveEditorContent() {
|
|
const textarea = document.getElementById("editor-textarea");
|
|
if (!textarea) return;
|
|
|
|
const path = textarea.dataset.path;
|
|
const content = textarea.value;
|
|
const statusEl = document.getElementById("editor-status");
|
|
|
|
statusEl.textContent = "Saving...";
|
|
|
|
try {
|
|
await apiRequest("/write", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
bucket: currentBucket,
|
|
path: path,
|
|
content: content,
|
|
}),
|
|
});
|
|
|
|
statusEl.textContent = "✓ Saved";
|
|
showNotification("File saved successfully", "success");
|
|
|
|
setTimeout(() => {
|
|
if (statusEl) statusEl.textContent = "";
|
|
}, 2000);
|
|
} catch (err) {
|
|
statusEl.textContent = "✗ Save failed";
|
|
showNotification(`Failed to save: ${err.message}`, "error");
|
|
}
|
|
}
|
|
|
|
function closeEditor() {
|
|
const modal = document.getElementById("editor-modal");
|
|
const statusEl = document.getElementById("editor-status");
|
|
|
|
if (statusEl && statusEl.textContent.includes("Modified")) {
|
|
if (!confirm("You have unsaved changes. Close anyway?")) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (modal) modal.remove();
|
|
}
|
|
|
|
async function deleteItem(path) {
|
|
const fileName = path.split("/").pop();
|
|
if (!confirm(`Delete "${fileName}"?`)) return;
|
|
|
|
try {
|
|
await apiRequest("/delete", {
|
|
method: "POST",
|
|
body: JSON.stringify({ bucket: currentBucket, path: path }),
|
|
});
|
|
showNotification("Item deleted", "success");
|
|
selectedFiles.delete(path);
|
|
loadFiles(currentPath, currentBucket);
|
|
loadStorageInfo();
|
|
} catch (err) {
|
|
showNotification(`Delete failed: ${err.message}`, "error");
|
|
}
|
|
}
|
|
|
|
async function deleteSelected() {
|
|
if (selectedFiles.size === 0) return;
|
|
const count = selectedFiles.size;
|
|
if (!confirm(`Delete ${count} item(s)?`)) return;
|
|
|
|
let deleted = 0;
|
|
for (const path of selectedFiles) {
|
|
try {
|
|
await apiRequest("/delete", {
|
|
method: "POST",
|
|
body: JSON.stringify({ bucket: currentBucket, path: path }),
|
|
});
|
|
deleted++;
|
|
} catch (err) {
|
|
console.error(`Failed to delete ${path}:`, err);
|
|
}
|
|
}
|
|
|
|
showNotification(
|
|
`Deleted ${deleted} of ${count} item(s)`,
|
|
deleted === count ? "success" : "warning",
|
|
);
|
|
clearSelection();
|
|
loadFiles(currentPath, currentBucket);
|
|
loadStorageInfo();
|
|
}
|
|
|
|
async function renameItem(path) {
|
|
const oldName = path.split("/").pop();
|
|
const newName = prompt("Enter new name:", oldName);
|
|
if (!newName || newName === oldName || !newName.trim()) return;
|
|
|
|
const parentPath = path.substring(0, path.lastIndexOf("/"));
|
|
const newPath = parentPath
|
|
? `${parentPath}/${newName.trim()}`
|
|
: newName.trim();
|
|
|
|
try {
|
|
await apiRequest("/move", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
source_bucket: currentBucket,
|
|
source_path: path,
|
|
dest_bucket: currentBucket,
|
|
dest_path: newPath,
|
|
}),
|
|
});
|
|
showNotification(`Renamed to "${newName}"`, "success");
|
|
loadFiles(currentPath, currentBucket);
|
|
} catch (err) {
|
|
showNotification(`Rename failed: ${err.message}`, "error");
|
|
}
|
|
}
|
|
|
|
function copyToClipboard(path) {
|
|
clipboardFiles = [path];
|
|
clipboardOperation = "copy";
|
|
showNotification("Copied to clipboard", "info");
|
|
}
|
|
|
|
function cutToClipboard(path) {
|
|
clipboardFiles = [path];
|
|
clipboardOperation = "cut";
|
|
showNotification("Cut to clipboard", "info");
|
|
}
|
|
|
|
function copySelected() {
|
|
clipboardFiles = Array.from(selectedFiles);
|
|
clipboardOperation = "copy";
|
|
showNotification(`${clipboardFiles.length} item(s) copied`, "info");
|
|
}
|
|
|
|
function cutSelected() {
|
|
clipboardFiles = Array.from(selectedFiles);
|
|
clipboardOperation = "cut";
|
|
showNotification(`${clipboardFiles.length} item(s) cut`, "info");
|
|
}
|
|
|
|
async function pasteFiles() {
|
|
if (clipboardFiles.length === 0) return;
|
|
|
|
const operation = clipboardOperation;
|
|
let processed = 0;
|
|
|
|
for (const sourcePath of clipboardFiles) {
|
|
const fileName = sourcePath.split("/").pop();
|
|
const destPath = currentPath ? `${currentPath}/${fileName}` : fileName;
|
|
|
|
try {
|
|
const endpoint = operation === "copy" ? "/copy" : "/move";
|
|
await apiRequest(endpoint, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
source_bucket: currentBucket,
|
|
source_path: sourcePath,
|
|
dest_bucket: currentBucket,
|
|
dest_path: destPath,
|
|
}),
|
|
});
|
|
processed++;
|
|
} catch (err) {
|
|
console.error(`Failed to ${operation} ${sourcePath}:`, err);
|
|
}
|
|
}
|
|
|
|
if (operation === "cut") {
|
|
clipboardFiles = [];
|
|
clipboardOperation = null;
|
|
}
|
|
|
|
showNotification(
|
|
`${operation === "copy" ? "Copied" : "Moved"} ${processed} item(s)`,
|
|
"success",
|
|
);
|
|
loadFiles(currentPath, currentBucket);
|
|
}
|
|
|
|
window.DriveModule = {
|
|
init,
|
|
loadFiles,
|
|
loadStorageInfo,
|
|
discoverBuckets,
|
|
retryWithBackoff,
|
|
toggleSelection,
|
|
selectAll,
|
|
clearSelection,
|
|
downloadFile,
|
|
openInlineEditor,
|
|
saveEditorContent,
|
|
closeEditor,
|
|
deleteItem,
|
|
deleteSelected,
|
|
renameItem,
|
|
createFolder,
|
|
copyToClipboard,
|
|
cutToClipboard,
|
|
copySelected,
|
|
cutSelected,
|
|
pasteFiles,
|
|
showContextMenuFor,
|
|
navigateUp,
|
|
};
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|