📜
filemanager.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
(function(){ const FileManager = { id: "fileManager", label: "📁 File Manager", html: ` <div class="fm-container"> <div class="fm-toolbar"> <div class="fm-toolbar-left" data-toolbar></div> <span id="fmStatus" class="fm-status">Not connected</span> </div> <div class="fm-breadcrumb" id="fmBreadcrumb"> <span class="fm-crumb" data-path="/">🏠 Root</span> </div> <div id="fmList" class="fm-list"> <div class="fm-empty"> <div class="fm-empty-icon">📂</div> <div class="fm-empty-text">Connect to a server to browse files</div> </div> </div> </div> <style> .fm-container { display: flex; flex-direction: column; height: 100%; padding: 1rem; gap: 1rem; } .fm-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; flex-wrap: wrap; } .fm-toolbar-left { display: flex; gap: 0.5rem; flex-wrap: wrap; } .fm-btn { padding: 0.5rem 1rem; background: #2a3648; color: #e6edf3; border: 1px solid #3b4557; border-radius: 8px; font-weight: 600; font-size: 0.875rem; cursor: pointer; transition: all 0.15s; white-space: nowrap; } .fm-btn:hover { background: #334155; border-color: #3b82f6; } .fm-btn:active { transform: scale(0.98); } .fm-status { font-size: 0.875rem; color: #94a3b8; padding: 0.5rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .fm-breadcrumb { display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem; background: rgba(30, 41, 59, 0.5); border-radius: 8px; font-size: 0.875rem; overflow-x: auto; scrollbar-width: thin; -webkit-overflow-scrolling: touch; } .fm-crumb { cursor: pointer; color: #3b82f6; transition: color 0.15s; white-space: nowrap; padding: 0.25rem 0.5rem; border-radius: 4px; } .fm-crumb:hover { color: #60a5fa; background: rgba(59, 130, 246, 0.1); } .fm-crumb-sep { color: #64748b; } .fm-list { flex: 1; overflow-y: auto; background: rgba(15, 23, 37, 0.5); border: 1px solid #2a3648; border-radius: 12px; padding: 1rem; } .fm-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #64748b; text-align: center; padding: 2rem; } .fm-empty-icon { font-size: 4rem; margin-bottom: 1rem; opacity: 0.5; } .fm-empty-text { font-size: 1rem; } .fm-item { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.875rem; background: rgba(30, 41, 59, 0.3); border: 1px solid #2a3648; border-radius: 8px; margin-bottom: 0.5rem; cursor: pointer; transition: all 0.15s; } .fm-item:hover { background: rgba(30, 41, 59, 0.6); border-color: #3b82f6; } .fm-item:active { transform: scale(0.99); } .fm-item-left { flex: 1; min-width: 0; } .fm-name { font-weight: 600; color: #e6edf3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.9375rem; } .fm-meta { font-size: 0.8125rem; color: #94a3b8; margin-top: 0.25rem; } .fm-item-actions { display: flex; gap: 0.25rem; flex-shrink: 0; } .fm-icon-btn { background: transparent; border: none; color: #9aa4b2; cursor: pointer; padding: 0.5rem; border-radius: 6px; font-size: 1.125rem; transition: all 0.15s; display: flex; align-items: center; justify-content: center; min-width: 2rem; min-height: 2rem; } .fm-icon-btn:hover { background: #263244; color: #e6edf3; } .fm-icon-btn:active { transform: scale(0.9); } /* Mobile optimizations */ @media (max-width: 640px) { .fm-container { padding: 0.75rem; } .fm-toolbar { gap: 0.5rem; } .fm-btn { padding: 0.625rem 0.875rem; font-size: 0.8125rem; } .fm-status { font-size: 0.75rem; flex: 1; text-align: right; } .fm-breadcrumb { padding: 0.5rem; gap: 0.25rem; } .fm-item { flex-direction: column; align-items: flex-start; gap: 0.5rem; padding: 0.75rem; } .fm-item-left { width: 100%; } .fm-item-actions { width: 100%; justify-content: flex-end; border-top: 1px solid #2a3648; padding-top: 0.5rem; } .fm-name { font-size: 0.875rem; } .fm-meta { font-size: 0.75rem; } } /* Loading state */ .fm-loading { color: #94a3b8; text-align: center; padding: 2rem; } /* Error state */ .fm-error { color: #ef4444; text-align: center; padding: 2rem; } </style> `, toolbar: [ { id: "up", label: "⬅️ Up", action: "goUpDirectory" }, { id: "refresh", label: "🔄 Refresh", action: "reload" } ], currentPath: "/", async onRender(el) { this.el = el; this.renderToolbar(); await this.checkConnectionAndLoad(); }, async checkConnectionAndLoad() { const fmStatus = this.el.querySelector("#fmStatus"); const fmList = this.el.querySelector("#fmList"); try { const formData = new FormData(); formData.append('sftp_action', 'status'); const res = await fetch(window.location.href, { method: 'POST', body: formData }); const data = await res.json(); if (data.success && data.data?.connected) { fmStatus.textContent = `Connected to ${data.data.config?.host || 'server'}`; await this.loadFiles("/"); } else { fmList.innerHTML = ` <div class="fm-empty"> <div class="fm-empty-icon">🔌</div> <div class="fm-empty-text">Not connected to any SFTP server.<br>Please connect first.</div> </div> `; fmStatus.textContent = "Not connected"; } } catch (err) { fmList.innerHTML = ` <div class="fm-error"> Error checking connection: ${err.message} </div> `; fmStatus.textContent = "Connection error"; } }, renderToolbar() { const bar = this.el.querySelector("[data-toolbar]"); bar.innerHTML = ""; this.toolbar.forEach(btn => { const el = document.createElement("button"); el.className = "fm-btn"; el.textContent = btn.label; el.addEventListener("click", () => this.runAction(btn.action)); bar.appendChild(el); }); }, runAction(action) { if (FileManagerActions[action]) FileManagerActions[action](this); }, async loadFiles(path="/") { const fmList = this.el.querySelector("#fmList"); const fmStatus = this.el.querySelector("#fmStatus"); fmList.innerHTML = `<div class="fm-loading">Loading ${path}...</div>`; fmStatus.textContent = `Loading ${path}...`; this.currentPath = path; try { const formData = new FormData(); formData.append('sftp_action', 'list'); formData.append('path', path); const res = await fetch(window.location.href, { method: "POST", body: formData }); const text = await res.text(); let data; try { data = JSON.parse(text); } catch { throw new Error("Server returned non-JSON:\n" + text); } if (!data.success) throw new Error(data.message); this.renderFiles(data.data); this.updateBreadcrumb(path); fmStatus.textContent = `${data.data.length} items`; } catch (err) { fmList.innerHTML = `<div class="fm-error">${err.message}</div>`; fmStatus.textContent = "Error loading files"; } }, updateBreadcrumb(path) { const breadcrumb = this.el.querySelector("#fmBreadcrumb"); const parts = path.split("/").filter(Boolean); let html = '<span class="fm-crumb" data-path="/">🏠 Root</span>'; let currentPath = ''; parts.forEach((part, index) => { currentPath += '/' + part; const pathForClick = currentPath; html += ` <span class="fm-crumb-sep">/</span> <span class="fm-crumb" data-path="${pathForClick}">${part}</span>`; }); breadcrumb.innerHTML = html; // Add click handlers to breadcrumb items breadcrumb.querySelectorAll('.fm-crumb').forEach(crumb => { crumb.addEventListener('click', () => { const targetPath = crumb.getAttribute('data-path'); this.loadFiles(targetPath); }); }); }, renderFiles(files) { const fmList = this.el.querySelector("#fmList"); fmList.innerHTML = ""; if (!files || files.length === 0) { fmList.innerHTML = ` <div class="fm-empty"> <div class="fm-empty-icon">📂</div> <div class="fm-empty-text">This folder is empty</div> </div> `; return; } files.forEach(f => { const div = document.createElement("div"); div.className = "fm-item"; const isDir = f.type === 'directory' || f.is_dir; const icon = isDir ? "📁" : this.getFileIcon(f.name); const fullPath = this.currentPath === '/' ? '/' + f.name : this.currentPath + '/' + f.name; div.innerHTML = ` <div class="fm-item-left"> <div class="fm-name">${icon} ${this.escapeHtml(f.name)}</div> <div class="fm-meta">${isDir ? "Folder" : this.formatBytes(f.size)} • ${f.modified || ''}</div> </div> ${!isDir ? ` <div class="fm-item-actions"> <button class="fm-icon-btn" data-action="download" title="Download">⬇️</button> <button class="fm-icon-btn" data-action="delete" title="Delete">🗑️</button> </div> ` : ''} `; // Folder click = open next directory if (isDir) { div.addEventListener("click", (e) => { if (!e.target.closest('.fm-icon-btn')) { this.loadFiles(fullPath); } }); } else { // File actions const downloadBtn = div.querySelector('[data-action="download"]'); const deleteBtn = div.querySelector('[data-action="delete"]'); if (downloadBtn) { downloadBtn.addEventListener('click', (e) => { e.stopPropagation(); this.downloadFile(fullPath, f.name); }); } if (deleteBtn) { deleteBtn.addEventListener('click', async (e) => { e.stopPropagation(); if (confirm(`Delete ${f.name}?`)) { await this.deleteFile(fullPath); } }); } } fmList.appendChild(div); }); }, async downloadFile(path, filename) { // This would need to be implemented on the server side alert('Download functionality needs server implementation'); }, async deleteFile(path) { try { const formData = new FormData(); formData.append('sftp_action', 'delete'); formData.append('path', path); const res = await fetch(window.location.href, { method: 'POST', body: formData }); const data = await res.json(); if (data.success) { await this.loadFiles(this.currentPath); } else { alert('Delete failed: ' + data.message); } } catch (err) { alert('Delete error: ' + err.message); } }, getFileIcon(filename) { const ext = filename.split('.').pop().toLowerCase(); const icons = { 'pdf': '📄', 'doc': '📝', 'docx': '📝', 'txt': '📝', 'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'svg': '🖼️', 'zip': '📦', 'rar': '📦', 'tar': '📦', 'gz': '📦', 'mp3': '🎵', 'wav': '🎵', 'mp4': '🎬', 'avi': '🎬', 'html': '🌐', 'css': '🎨', 'js': '⚙️', 'json': '📋', 'php': '🐘', 'py': '🐍', 'java': '☕', 'cpp': '⚡' }; return icons[ext] || '📄'; }, formatBytes(bytes) { if (!bytes) return ""; if (bytes < 1024) return bytes + " B"; const units = ["KB", "MB", "GB", "TB"]; let u = -1; do { bytes /= 1024; ++u; } while (bytes >= 1024 && u < units.length - 1); return bytes.toFixed(1) + " " + units[u]; }, escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }; const FileManagerActions = { reload: ctx => ctx.loadFiles(ctx.currentPath), goUpDirectory: ctx => { const parts = ctx.currentPath.split("/").filter(Boolean); parts.pop(); const newPath = "/" + parts.join("/"); ctx.loadFiles(newPath || "/"); } }; window.AppItems = window.AppItems || []; window.AppItems.push(FileManager); console.log('[filemanager.js] Loaded - using integrated SFTP connection'); })();