/* 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, """); } 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 ''; } 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 ``; } 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 = `

No drive storage found

Please contact your administrator to set up storage.

`; } } } 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 ? `` : `

Max retries reached. Please refresh the page.

`; content.innerHTML = `

Drive connection error

${escapeHtml(err.message)}

${retryMsg}
`; } } } 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 = `

Retrying in ${delay / 1000}s... (attempt ${retryCount}/${MAX_RETRIES})

`; } 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) => ``, ) .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}`; // Use global ApiClient if available for automatic auth token injection if (window.ApiClient) { try { return await window.ApiClient.request(url, options); } catch (err) { console.error(`API Error [${endpoint}]:`, err); throw err; } } // Fallback if ApiClient not loaded const defaultHeaders = { "Content-Type": "application/json" }; // Try to get auth token from storage const token = localStorage.getItem("gb-access-token") || sessionStorage.getItem("gb-access-token"); if (token) { defaultHeaders["Authorization"] = `Bearer ${token}`; } 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 = '

Loading files...

'; 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 = `

Failed to load files

${escapeHtml(err.message)}

`; } } 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 = `

This folder is empty

Upload files or create a new folder to get started

`; 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 = `
${sorted.map((f) => renderFileCard(f)).join("")}
`; } else { content.innerHTML = `
Name
Modified
Size
${sorted.map((f) => renderFileRow(f)).join("")}
`; } 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 `
${iconSvg}
${escapeHtml(file.name)}
${sizeText}
`; } 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 = ``; const moreIcon = ``; const downloadBtn = !file.is_dir ? `` : ""; return `
${iconSvg}${escapeHtml(file.name)}
${modifiedText}
${sizeText}
${downloadBtn}
`; } 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 { openFile(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 = ``; let cumulativePath = ""; parts.forEach((part, idx) => { cumulativePath += (cumulativePath ? "/" : "") + part; const isLast = idx === parts.length - 1; html += `/`; html += isLast ? `${escapeHtml(part)}` : ``; }); 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 = '

Loading...

'; try { const files = await apiRequest("/recent"); renderFiles(files); } catch (err) { content.innerHTML = `

No recent files

`; } } async function loadStarredFiles() { const content = document.getElementById("drive-content") || document.getElementById("file-grid"); if (!content) return; content.innerHTML = '

Loading...

'; try { const files = await apiRequest("/favorite"); renderFiles(files); } catch (err) { content.innerHTML = `

No starred files

`; } } async function loadSharedFiles() { const content = document.getElementById("drive-content") || document.getElementById("file-grid"); if (!content) return; content.innerHTML = '

Loading...

'; try { const files = await apiRequest("/shared"); renderFiles(files); } catch (err) { content.innerHTML = `

No shared files

`; } } async function loadTrashFiles() { const content = document.getElementById("drive-content") || document.getElementById("file-grid"); if (!content) return; content.innerHTML = `

Trash is empty

`; } 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 icons = { open: ``, download: ``, edit: ``, copy: ``, cut: ``, rename: ``, delete: ``, }; const hideMenu = `document.getElementById('context-menu').style.display='none';`; menu.innerHTML = ` ${ isFolder ? `
${icons.open}Open
` : `
${icons.open}Open
${icons.download}Download
` }
${icons.copy}Copy
${icons.cut}Cut
${icons.rename}Rename
${icons.delete}Delete
`; 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 = '

Searching...

'; 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 = `

Search failed

`; } } 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"); } } async function openFile(path) { try { const response = await apiRequest("/open", { method: "POST", body: JSON.stringify({ bucket: currentBucket, path: path }), }); const { app, url } = response; if (window.htmx) { htmx.ajax("GET", url, { target: "#main-content", swap: "innerHTML", }); window.history.pushState( {}, "", `/#${app}?bucket=${encodeURIComponent(currentBucket)}&path=${encodeURIComponent(path)}`, ); } else { window.location.href = url; } } 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 = `
📝 ${escapeHtml(fileName)}
`; const bodyHtml = `
`; const footerHtml = ` `; modal.innerHTML = ``; 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); } // ============================================================================= // MISSING FUNCTIONS FOR HTML ONCLICK HANDLERS // ============================================================================= function toggleView(type) { setView(type); } function setView(type) { const gridBtn = document.getElementById("grid-view-btn"); const listBtn = document.getElementById("list-view-btn"); const fileGrid = document.getElementById("file-grid"); const fileList = document.getElementById("file-list"); const fileView = document.getElementById("file-view"); if (type === "grid") { gridBtn?.classList.add("active"); listBtn?.classList.remove("active"); if (fileGrid) fileGrid.style.display = "grid"; if (fileList) fileList.style.display = "none"; if (fileView) fileView.className = "file-grid"; } else { gridBtn?.classList.remove("active"); listBtn?.classList.add("active"); if (fileGrid) fileGrid.style.display = "none"; if (fileList) fileList.style.display = "block"; if (fileView) fileView.className = "file-list"; } } function openFolder(el) { const path = el?.dataset?.path || el?.querySelector(".file-name")?.textContent; if (path) { currentPath = path.startsWith("/") ? path : currentPath + "/" + path; loadFiles(currentPath); } } function selectFile(el) { const path = el?.dataset?.path; if (path) { toggleSelection(path); el.classList.toggle("selected", selectedFiles.has(path)); } else { // Toggle visual selection document.querySelectorAll(".file-item.selected").forEach((item) => { if (item !== el) item.classList.remove("selected"); }); el.classList.toggle("selected"); } updateSelectionUI(); } function setActiveNav(el) { document.querySelectorAll(".nav-item").forEach((item) => { item.classList.remove("active"); }); el.classList.add("active"); } function toggleInfoPanel() { const panel = document.getElementById("info-panel") || document.getElementById("details-panel"); if (panel) { panel.classList.toggle("open"); panel.classList.toggle("hidden"); } } function toggleAIPanel() { const panel = document.getElementById("ai-panel") || document.querySelector(".ai-panel"); if (panel) { panel.classList.toggle("open"); } } function aiAction(action) { const messages = { organize: "I'll help you organize your files. What folder would you like to organize?", find: "What file are you looking for?", analyze: "Select a file and I'll analyze its contents.", share: "Select files to share. Who would you like to share with?", }; addAIMessage("assistant", messages[action] || "How can I help you?"); } function sendAIMessage() { const input = document.getElementById("ai-input"); if (!input || !input.value.trim()) return; const message = input.value.trim(); input.value = ""; addAIMessage("user", message); // Simulate AI response setTimeout(() => { addAIMessage("assistant", `Processing your request: "${message}"`); }, 500); } function addAIMessage(type, content) { const container = document.getElementById("ai-messages") || document.querySelector(".ai-messages"); if (!container) return; const div = document.createElement("div"); div.className = `ai-message ${type}`; div.innerHTML = `
${content}
`; container.appendChild(div); container.scrollTop = container.scrollHeight; } function updateSelectionUI() { const count = selectedFiles.size; const bulkActions = document.getElementById("bulk-actions"); if (bulkActions) { bulkActions.style.display = count > 0 ? "flex" : "none"; } const countEl = document.getElementById("selection-count"); if (countEl) { countEl.textContent = `${count} selected`; } } function uploadFile() { triggerUpload(); } window.DriveModule = { init, loadFiles, loadStorageInfo, discoverBuckets, retryWithBackoff, toggleSelection, selectAll, clearSelection, downloadFile, openFile, deleteItem, deleteSelected, renameItem, createFolder, copyToClipboard, cutToClipboard, copySelected, cutSelected, pasteFiles, showContextMenuFor, navigateUp, }; // Export functions for HTML onclick handlers window.toggleView = toggleView; window.setView = setView; window.openFolder = openFolder; window.selectFile = selectFile; window.setActiveNav = setActiveNav; window.toggleInfoPanel = toggleInfoPanel; window.toggleAIPanel = toggleAIPanel; window.aiAction = aiAction; window.sendAIMessage = sendAIMessage; window.uploadFile = uploadFile; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();