function _setFsLoading(container) { container.replaceChildren(); const wrapper = document.createElement("div"); wrapper.style.padding = "2rem"; wrapper.style.textAlign = "center"; wrapper.style.color = "var(--text-muted)"; wrapper.appendChild(createMaterialIcon("progress_activity", "material-icons-round spin")); container.appendChild(wrapper); } function _setFsMessage( container, message, { color = "var(--text-muted)", center = false, padding = "2rem" } = {} ) { container.replaceChildren(); const wrapper = document.createElement("div"); wrapper.style.padding = padding; wrapper.style.color = color; if (center) { wrapper.style.textAlign = "center"; } wrapper.textContent = message; container.appendChild(wrapper); } function _appendFsBreadcrumbSeparator(breadcrumb) { const separator = createMaterialIcon("chevron_right"); separator.style.fontSize = "12px"; breadcrumb.appendChild(separator); } function _appendFsBreadcrumbItem(breadcrumb, label, onClick, { active = false } = {}) { const item = document.createElement("span"); item.className = active ? "breadcrumb-item active" : "breadcrumb-item"; item.textContent = label; if (typeof onClick === "function") { item.addEventListener("click", onClick); } breadcrumb.appendChild(item); } function _renderFsBreadcrumb(breadcrumb, path, activeLabel = null) { if (!breadcrumb) return; breadcrumb.replaceChildren(); _appendFsBreadcrumbItem(breadcrumb, "root", () => window.loadFs(".")); const parts = path.split(/[/\\]/).filter((part) => part && part !== "."); let currentPartPath = ""; parts.forEach((part, index) => { currentPartPath += (index === 0 ? "" : "/") + part; _appendFsBreadcrumbSeparator(breadcrumb); _appendFsBreadcrumbItem(breadcrumb, part, () => window.loadFs(currentPartPath)); }); if (activeLabel !== null) { _appendFsBreadcrumbSeparator(breadcrumb); _appendFsBreadcrumbItem(breadcrumb, activeLabel, null, { active: true }); } } function _createFsRow({ icon, name, size = "", mtime = "", isDir = false, onClick }) { const row = document.createElement("div"); row.className = isDir ? "fs-item is-dir" : "fs-item"; if (typeof onClick === "function") { row.addEventListener("click", onClick); } row.appendChild(createMaterialIcon(icon, "material-icons-round fs-item-icon")); const nameEl = document.createElement("span"); nameEl.className = "fs-item-name"; nameEl.title = name; nameEl.textContent = name; row.appendChild(nameEl); const sizeEl = document.createElement("span"); sizeEl.className = "fs-item-size"; sizeEl.textContent = size; row.appendChild(sizeEl); const mtimeEl = document.createElement("span"); mtimeEl.className = "fs-item-mtime"; mtimeEl.textContent = mtime; row.appendChild(mtimeEl); return row; } // ── File Handling ───────────────────────────────────────────── function initFileHandlers() { if (state.fileHandlersInitialized) return; state.fileHandlersInitialized = true; const btnAttach = $("btn-attach"); const fileInput = $("file-input"); const dragOverlay = $("drag-overlay"); console.debug("[SHIBA] Initializing file handlers", { btnAttach: !!btnAttach, fileInput: !!fileInput }); if (btnAttach && fileInput) { btnAttach.onclick = () => fileInput.click(); fileInput.onchange = (e) => { handleFileUpload(e.target.files); fileInput.value = ""; }; } window.addEventListener("dragover", (e) => { e.preventDefault(); dragOverlay.classList.add("active"); }); window.addEventListener("dragleave", (e) => { if (e.relatedTarget === null || !dragOverlay.contains(e.relatedTarget)) { dragOverlay.classList.remove("active"); } }); window.addEventListener("drop", (e) => { e.preventDefault(); dragOverlay.classList.remove("active"); handleFileUpload(e.dataTransfer.files); }); window.addEventListener("paste", (e) => { const items = e.clipboardData.items; const files = []; for (let item of items) { if (item.kind === "file") { files.push(item.getAsFile()); } } console.debug("[SHIBA] Paste event", { count: files.length }); if (files.length > 0) handleFileUpload(files); }); } async function handleFileUpload(files) { console.debug("[SHIBA] handleFileUpload", files); if (!files || files.length === 0) return; for (const file of files) { const formData = new FormData(); formData.append("file", file); try { const res = await authFetch("/api/upload", { method: "POST", body: formData }); if (res.ok) { const data = await res.json(); const uploadedFile = data.files[0]; state.stagedFiles.push({ name: uploadedFile.filename, url: uploadedFile.url, type: file.type, stagedAt: Date.now() }); updateStagingUI(); } else { const err = await res.json(); shibaDialog("alert", "Upload Failed", `Could not upload ${file.name}: ${err.error}`, { danger: true }); } } catch (e) { console.error("Upload error:", e); } } } function updateStagingUI() { const container = $("attachment-staging"); if (!container) return; container.replaceChildren(); if (state.stagedFiles.length === 0) { container.style.display = "none"; return; } container.style.display = "flex"; state.stagedFiles.forEach((file, idx) => { const item = document.createElement("div"); item.className = "staged-file"; const isImage = typeof file.type === "string" && file.type.startsWith("image/"); if (isImage) { const thumb = document.createElement("img"); thumb.src = file.url; thumb.className = "staged-file-thumb"; item.appendChild(thumb); } else { item.appendChild(createMaterialIcon("insert_drive_file")); } const nameEl = document.createElement("span"); nameEl.className = "staged-file-name"; nameEl.title = file.name; nameEl.textContent = file.name; item.appendChild(nameEl); const removeBtn = document.createElement("button"); removeBtn.className = "btn-remove-staged"; removeBtn.appendChild(createMaterialIcon("close")); removeBtn.addEventListener("click", () => window.removeStagedFile(idx)); item.appendChild(removeBtn); container.appendChild(item); }); } window.removeStagedFile = function(idx) { state.stagedFiles.splice(idx, 1); updateStagingUI(); }; // ── File Explorer ───────────────────────────────────────────── window.loadFs = async function(path = ".") { const list = $("fs-content"); const breadcrumb = $("fs-breadcrumb"); if (!list) return; state.currentFsPath = path; _setFsLoading(list); _renderFsBreadcrumb(breadcrumb, path); const parts = path.split(/[/\\]/).filter(p => p && p !== "."); try { const res = await authFetch(`/api/fs/explore?path=${encodeURIComponent(path)}`); const data = await res.json(); if (data.error) { _setFsMessage(list, data.error, { color: "var(--accent-red)" }); return; } list.replaceChildren(); if (path !== "." && path !== "/" && parts.length > 0) { const parentPath = parts.slice(0, -1).join("/") || "."; const row = _createFsRow({ icon: "folder_open", name: "..", onClick: () => window.loadFs(parentPath), }); list.appendChild(row); } data.items.forEach(f => { const icon = f.is_dir ? "folder" : "insert_drive_file"; const size = f.is_dir ? "" : formatSize(f.size); const mtime = new Date(f.mtime * 1000).toLocaleString(); const row = _createFsRow({ icon, name: f.name, size, mtime, isDir: f.is_dir, onClick: () => { if (f.is_dir) { window.loadFs(f.path); } else { openFileEditor(f.path, f.name); } }, }); list.appendChild(row); }); } catch (e) { _setFsMessage(list, "Error loading files", { color: "var(--accent-red)" }); } }; function formatSize(bytes) { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; } // ── File Editor ─────────────────────────────────────────────── const TEXT_EXTENSIONS = /\.(txt|md|py|js|ts|jsx|tsx|json|yaml|yml|toml|sh|bash|zsh|env|cfg|conf|ini|html|css|scss|xml|csv|log|rst|Dockerfile|gitignore|editorconfig|lock|sql|go|rs|rb|java|c|cpp|h|php)$/i; window.openFileEditor = async function(filePath, fileName) { const content = $("fs-content"); const breadcrumb = $("fs-breadcrumb"); if (!content) return; _setFsLoading(content); _renderFsBreadcrumb(breadcrumb, state.currentFsPath, fileName); const isText = TEXT_EXTENSIONS.test(fileName); if (!isText) { content.innerHTML = `
Binary file — preview not available