diff options
Diffstat (limited to '.bkp-fix-20261023_143429/filemgr.js')
| -rw-r--r-- | .bkp-fix-20261023_143429/filemgr.js | 1150 |
1 files changed, 1150 insertions, 0 deletions
diff --git a/.bkp-fix-20261023_143429/filemgr.js b/.bkp-fix-20261023_143429/filemgr.js new file mode 100644 index 0000000..01d1da9 --- /dev/null +++ b/.bkp-fix-20261023_143429/filemgr.js @@ -0,0 +1,1150 @@ +class FileManagerModule { + constructor() { + this.commands = {}; + this.commandsMeta = { + 'files': { argType: 'file' }, 'preview': { argType: 'file' }, + }; + this.terminal = null; + this.currentPath = []; + this._overlay = null; + this._selectedFile = null; + this._multiSelect = new Set(); // v4.5.78: multi-select for batch ops + this._selectMode = false; // v4.6.35: tap-to-select mode (touch devices) + this._itemCount = 0; // items rendered in the current directory/filter + this._clipboard = { mode: null, paths: [] }; // 'copy' or 'cut' + this._searchFilter = ''; + this._rawMode = false; + this._inlineHtml = false; + } + + requirements() { + return { met: true, missing: [] }; + } + + getHelpInfo() { + return { + 'files': 'files — Open the file manager overlay with VFS directory listing, preview, encrypt, zip', + 'preview': 'preview <path> — Preview a specific file', + }; + } + + registerCommands() { + this.commands['files'] = () => this._openOverlay(); + this.commands['preview'] = (args) => this._previewCommand(args); + } + + _previewCommand(args) { + if (!args[0]) return this.terminal.print('Usage: preview <path>', 'warning'); + const path = args.join(' '); + this._openOverlay(); + this._navigateToPathFromParts(path); + const content = this.terminal.fs.readFile(path); + if (content === null || content === false) { + this._renderPreviewError('File not found: ' + path); + return; + } + this._selectedFile = path; + this._renderPreview(path, content); + this._highlightSelected(); + } + + _navigateToPathFromParts(path) { + const parts = path.split('/').filter(p => p); + if (parts.length <= 1) { + this.currentPath = []; + return; + } + this.currentPath = parts.slice(0, -1); + } + + _detectType(filePath) { + const ext = filePath.split('.').pop().toLowerCase(); + if (!ext || ext === filePath) return 'other'; + const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico']; + const videoExts = ['mp4', 'webm', 'avi', 'mov', 'mkv']; + const audioExts = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a']; + const codeExts = ['js', 'py', 'css', 'yaml', 'yml', 'toml']; + if (ext === 'md') return 'md'; + if (imageExts.includes(ext)) return 'image'; + if (videoExts.includes(ext)) return 'video'; + if (audioExts.includes(ext)) return 'audio'; + if (codeExts.includes(ext)) return 'code'; + if (ext === 'html') return 'html'; + if (ext === 'json') return 'json'; + if (ext === 'csv') return 'csv'; + if (ext === 'pdf') return 'pdf'; + if (ext === 'svg') return 'svg'; + if (ext === 'txt') return 'text'; + return 'other'; + } + + _renderMarkdown(text) { + if (!text) return ''; + try { + let html = text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>'); + // code blocks first + html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>'); + // inline code + html = html.replace(/`([^`]+)`/g, '<code>$1</code>'); + // headers + html = html.replace(/^### (.+)$/gm, '<h3 style="color:var(--accent-color);margin:8px 0 4px">$1</h3>'); + html = html.replace(/^## (.+)$/gm, '<h2 style="color:var(--accent-color);margin:10px 0 4px">$1</h2>'); + html = html.replace(/^# (.+)$/gm, '<h1 style="color:var(--accent-color);margin:12px 0 6px">$1</h1>'); + // bold + html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); + // italic + html = html.replace(/\*(.+?)\*/g, '<em>$1</em>'); + // links — v4.5.67: sanitize URL to prevent javascript: scheme injection + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => { + const safeUrl = /^(https?:\/\/|mailto:|\/|#)/.test(url) ? url : '#'; + return `<a href="${safeUrl}" target="_blank" style="color:var(--info-color)">${text}</a>`; + }); + // horizontal rules + html = html.replace(/^---$/gm, '<hr style="border-color:var(--border-color);margin:8px 0">'); + // unordered lists + html = html.replace(/^[\s]*[-*]\s+(.+)$/gm, '<li style="margin-left:16px;list-style:disc">$1</li>'); + // paragraphs - double newlines + html = html.replace(/\n\n/g, '</p><p style="margin:6px 0">'); + html = '<p style="margin:6px 0">' + html + '</p>'; + return html; + } catch (e) { + return '<pre style="white-space:pre-wrap">' + text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '</pre>'; + } + } + + _getPreviewHtml(path, content) { + if (content === null || content === false) { + return '<div style="color:var(--error-color);padding:20px">File not found: ' + this._escapeHtml(path) + '</div>'; + } + + const type = this._detectType(path); + const ext = path.split('.').pop().toLowerCase(); + const fileName = path.split('/').pop() || path; + + // Handle binary detection for media types + if (typeof content !== 'string') { + return '<div style="padding:20px;text-align:center;color:var(--text-muted)">Cannot preview: non-string content</div>'; + } + const isLikelyBinary = content.length > 0 && !content.startsWith('\n') && !content.startsWith('\r') && !content.startsWith(' ') && !content.startsWith('<') && !content.startsWith('{') && !content.startsWith('[') && content.charCodeAt(0) < 32 && content.charCodeAt(0) !== 9 && content.charCodeAt(0) !== 10 && content.charCodeAt(0) !== 13; + + const isTextContent = typeof content === 'string' && (content.startsWith('{') || content.startsWith('[') || content.startsWith('<') || content.startsWith('#') || /^[\w\s\n\r\t.,;:!?()\-=+*@#$%^&"'\/\\|`~]/.test(content.substring(0, 100))); + + if (type === 'md' && isTextContent) { + return '<div class="markdown-preview" style="padding:12px;font-family:var(--font-primary);line-height:1.5">' + this._renderMarkdown(content) + '</div>'; + } + + if (type === 'html') { + // Render HTML in an inline sandbox frame + const safeHtml = content + .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') + .replace(/on\w+\s*=\s*"[^"]*"/gi, '') + .replace(/on\w+\s*=\s*'[^']*'/gi, ''); + return '<div style="padding:0;width:100%;height:60vh;border:none;overflow:hidden">' + + '<iframe srcdoc="' + this._escapeHtml(safeHtml.replace(/"/g, '"').replace(/'/g, ''')) + '" ' + + 'style="width:100%;height:100%;border:none;background:#fff" sandbox="allow-same-origin"></iframe></div>'; + } + + if (type === 'text') { + const truncated = content.substring(0, 2000); + const extra = content.length > 2000 ? '<div style="color:var(--text-muted);margin-top:8px">(truncated to 2000 chars)</div>' : ''; + return '<pre style="white-space:pre-wrap;padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">' + this._escapeHtml(truncated) + extra + '</pre>'; + } + + if (type === 'json') { + try { + const parsed = JSON.parse(content); + const formatted = JSON.stringify(parsed, null, 2); + const html = this._syntaxHighlightJson(formatted); + return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto;line-height:1.4">' + html + '</pre>'; + } catch (e) { + return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">' + this._escapeHtml(content.substring(0, 5000)) + '</pre>'; + } + } + + if (type === 'code') { + const lines = content.split('\n'); + const totalLines = lines.length; + const maxLines = 150; + const displayLines = lines.slice(0, maxLines); + let html = '<table style="border-collapse:collapse;width:100%;font-family:var(--font-primary);font-size:0.85em;margin:0"><tbody>'; + displayLines.forEach((line, i) => { + const lineNum = i + 1; + const escaped = this._escapeHtml(line); + html += '<tr><td style="color:var(--text-muted);padding:0 8px 0 4px;text-align:right;user-select:none;border-right:1px solid var(--border-color);vertical-align:top;width:40px">' + lineNum + '</td>'; + html += '<td style="padding:0 8px;white-space:pre;width:100%">' + (escaped || ' ') + '</td></tr>'; + }); + html += '</tbody></table>'; + if (totalLines > maxLines) { + html += '<div style="color:var(--text-muted);padding:8px;border-top:1px solid var(--border-color)">(showing ' + maxLines + ' of ' + totalLines + ' lines)</div>'; + } + return html; + } + + if (type === 'image') { + // VFS stores images as base64 strings — detect and render inline + const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100)); + if (isBase64) { + const mimeTypes = {jpg:'image/jpeg',jpeg:'image/jpeg',png:'image/png',gif:'image/gif',webp:'image/webp',bmp:'image/bmp',ico:'image/x-icon'}; + const mime = mimeTypes[ext] || 'image/png'; + return '<div style="padding:12px;text-align:center;max-width:100%"><img src="data:' + mime + ';base64,' + content + '" style="max-width:100%;max-height:70vh;border-radius:4px;border:1px solid var(--border-color)" alt="' + this._escapeHtml(fileName) + '"></div>'; + } + return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📷 Image — cannot render, file may be stored as binary</div>'; + } + + if (type === 'svg') { + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(content, 'text/html'); + const scripts = doc.querySelectorAll('script'); + scripts.forEach(s => s.remove()); + // Remove event handlers from svg elements + const allEls = doc.querySelectorAll('*'); + allEls.forEach(el => { + for (let i = el.attributes.length - 1; i >= 0; i--) { + const attr = el.attributes[i].name; + if (attr.startsWith('on')) el.removeAttribute(attr); + } + }); + const cleanSvg = doc.body.innerHTML; + return '<div style="padding:12px;text-align:center;max-width:100%;overflow:auto">' + cleanSvg + '</div>'; + } catch (e) { + return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;overflow-x:auto">' + this._escapeHtml(content.substring(0, 5000)) + '</pre>'; + } + } + + if (type === 'video') { + const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100)); + if (isBase64) { + const mimeTypes = {mp4:'video/mp4',webm:'video/webm',avi:'video/x-msvideo',mov:'video/quicktime',mkv:'video/x-matroska'}; + const mime = mimeTypes[ext] || 'video/mp4'; + return '<div style="padding:12px;text-align:center"><video controls style="max-width:90%;max-height:65vh;border-radius:4px"><source src="data:' + mime + ';base64,' + content + '" type="' + mime + '"></video></div>'; + } + return '<div style="padding:20px;text-align:center;color:var(--text-muted)">🎬 Video — cannot render</div>'; + } + + if (type === 'audio') { + const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100)); + if (isBase64) { + const mimeTypes = {mp3:'audio/mpeg',wav:'audio/wav',ogg:'audio/ogg',flac:'audio/flac',aac:'audio/aac',m4a:'audio/mp4'}; + const mime = mimeTypes[ext] || 'audio/mpeg'; + return '<div style="padding:12px;text-align:center"><audio controls style="max-width:90%"><source src="data:' + mime + ';base64,' + content + '" type="' + mime + '"></audio></div>'; + } + return '<div style="padding:20px;text-align:center;color:var(--text-muted)">🎵 Audio — cannot render</div>'; + } + + if (type === 'pdf') { + const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100)); + if (isBase64) { + return '<div style="padding:12px;text-align:center;height:70vh"><embed src="data:application/pdf;base64,' + content + '" type="application/pdf" style="width:100%;height:100%;border:1px solid var(--border-color);border-radius:4px"></div>'; + } + return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📄 PDF — cannot render</div>'; + } + + if (type === 'csv') { + try { + const lines = content.split('\n').filter(l => l.trim()); + const maxRows = 50; + const rows = lines.slice(0, maxRows).map(l => l.split(',').map(c => c.trim().replace(/^"|"$/g, ''))); + let html = '<table class="csv-table" style="border-collapse:collapse;width:100%;font-family:var(--font-primary);font-size:0.85em;margin:0;background:var(--bg-secondary)">'; + if (rows.length > 0) { + html += '<thead><tr>'; + rows[0].forEach(cell => { + html += '<th style="border:1px solid var(--border-color);padding:6px 8px;text-align:left;background:var(--bg-tertiary);color:var(--accent-color);font-weight:bold">' + this._escapeHtml(cell) + '</th>'; + }); + html += '</tr></thead><tbody>'; + rows.slice(1).forEach(row => { + html += '<tr>'; + row.forEach(cell => { + html += '<td style="border:1px solid var(--border-color);padding:4px 8px">' + this._escapeHtml(cell) + '</td>'; + }); + html += '</tr>'; + }); + html += '</tbody></table>'; + if (lines.length > maxRows) { + html += '<div style="color:var(--text-muted);padding:8px">(showing ' + maxRows + ' of ' + lines.length + ' rows)</div>'; + } + } + return html; + } catch (e) { + return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;overflow-x:auto">' + this._escapeHtml(content.substring(0, 5000)) + '</pre>'; + } + } + + // 'other' — raw text fallback + if (typeof content === 'string' && isTextContent) { + const lines = content.split('\n'); + const maxLines = 80; + const displayLines = lines.slice(0, maxLines); + const truncated = lines.length > maxLines; + let html = '<pre style="white-space:pre-wrap;padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">'; + displayLines.forEach((line, i) => { + html += this._escapeHtml(line) + '\n'; + }); + html += '</pre>'; + if (truncated) { + html += '<div style="color:var(--text-muted);padding:8px;border-top:1px solid var(--border-color)">(showing ' + maxLines + ' of ' + lines.length + ' lines)</div>'; + } + return html; + } + + return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📁 Preview not available for this file type</div>'; + } + + _syntaxHighlightJson(jsonStr) { + return jsonStr.replace(/("(?:\\.|[^"\\])*")\s*:/g, '<span style="color:var(--info-color)">$1</span>:') + .replace(/:\s*("(?:\\.|[^"\\])*")/g, ': <span style="color:var(--success-color)">$1</span>') + .replace(/:\s*(\d+\.?\d*)/g, ': <span style="color:var(--warning-color)">$1</span>') + .replace(/:\s*(true|false|null)/g, ': <span style="color:var(--error-color)">$1</span>'); + } + + _escapeHtml(str) { + if (typeof str !== 'string') return ''; + return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); + } + + _formatSize(size) { + if (!size && size !== 0) return ''; + if (size < 1024) return size + ' B'; + if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB'; + return (size / (1024 * 1024)).toFixed(1) + ' MB'; + } + + _createOverlay() { + if (this._overlay) return; + + const overlay = document.createElement('div'); + overlay.id = 'filemgr-overlay'; + overlay.style.cssText = 'position:fixed;inset:0;z-index:1800;display:flex;flex-direction:column;background:var(--bg-color);color:var(--text-color);font-family:var(--font-primary);font-size:14px;height:100vh;'; + + // Inject responsive styles for the overlay + const styleId = 'filemgr-styles'; + if (!document.getElementById(styleId)) { + const style = document.createElement('style'); + style.id = styleId; + style.textContent = ` + #filemgr-overlay.responsive .filemgr-main { + flex-direction: column !important; + } + #filemgr-overlay.responsive #filemgr-sidebar { + width: 100% !important; + max-height: 40vh; + } + #filemgr-overlay.responsive #filemgr-preview { + width: 100% !important; + } + #filemgr-overlay .filemgr-main { + display: flex; + flex: 1; + min-height: 0; + } + @media (max-width: 600px) { + #filemgr-overlay .filemgr-main { + flex-direction: column !important; + } + #filemgr-overlay #filemgr-sidebar { + width: 100% !important; + max-height: 40vh; + border-right: none !important; + border-bottom: 1px solid var(--border-color); + } + #filemgr-overlay #filemgr-preview { + width: 100% !important; + flex: 1 !important; + min-height: 0; + } + #filemgr-action-bar { + flex-wrap: wrap; + gap: 6px; + } + #filemgr-action-bar button { + min-width: 44px; + min-height: 44px; + font-size: 0.9em; + } + } + `; + document.head.appendChild(style); + } + + // Toolbar + const toolbar = document.createElement('div'); + toolbar.style.cssText = 'display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0;gap:8px;flex-wrap:wrap;'; + + const closeBtn = document.createElement('button'); + closeBtn.textContent = '✕'; + closeBtn.style.cssText = 'background:none;border:1px solid var(--border-color);color:var(--text-color);cursor:pointer;padding:4px 10px;border-radius:4px;font-family:inherit;font-size:1em;min-height:44px;touch-action:manipulation;'; + closeBtn.onclick = () => this._closeOverlay(); + toolbar.appendChild(closeBtn); + + const titleSpan = document.createElement('span'); + titleSpan.textContent = '📁 File Manager'; + titleSpan.style.cssText = 'font-weight:bold;margin-right:8px;'; + toolbar.appendChild(titleSpan); + + // Breadcrumbs + const breadcrumbs = document.createElement('div'); + breadcrumbs.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:wrap;font-size:0.9em;'; + breadcrumbs.id = 'filemgr-breadcrumbs'; + + // Root breadcrumb + const rootSeg = document.createElement('span'); + rootSeg.textContent = '/'; + rootSeg.style.cssText = 'color:var(--accent-color);cursor:pointer;padding:2px 4px;border-radius:3px;'; + rootSeg.onmouseenter = () => { rootSeg.style.background = 'var(--bg-tertiary)'; }; + rootSeg.onmouseleave = () => { rootSeg.style.background = 'transparent'; }; + rootSeg.onclick = () => this._navigateTo([]); + breadcrumbs.appendChild(rootSeg); + + toolbar.appendChild(breadcrumbs); + + // Search input + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = '🔍 Filter files...'; + searchInput.style.cssText = 'flex:1;min-width:120px;max-width:300px;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:4px;color:var(--text-color);font-family:var(--font-primary);font-size:0.9em;outline:none;'; + searchInput.oninput = (e) => { + this._searchFilter = e.target.value.toLowerCase(); + this._refreshFileList(); + }; + toolbar.appendChild(searchInput); + + // v4.5.78: Batch operation buttons + const batchBar = document.createElement('div'); + batchBar.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:wrap;'; + const mkBtn = (label, title, action) => { + const b = document.createElement('button'); + b.textContent = label; + b.title = title; + b.style.cssText = 'background:none;border:1px solid var(--border-color);color:var(--text-color);cursor:pointer;padding:4px 8px;border-radius:4px;font-family:inherit;font-size:0.85em;min-height:32px;touch-action:manipulation;'; + b.onclick = () => action(); + return b; + }; + batchBar.appendChild(mkBtn('📋 Copy', 'Copy selected files', () => this._batchCopy())); + batchBar.appendChild(mkBtn('✂️ Cut', 'Cut selected files', () => this._batchCut())); + batchBar.appendChild(mkBtn('📥 Paste', 'Paste from clipboard', () => this._batchPaste())); + batchBar.appendChild(mkBtn('🗑️ Del', 'Delete selected files', () => this._batchDelete())); + batchBar.appendChild(mkBtn('📝 Rename', 'Rename selected file', () => this._batchRename())); + batchBar.appendChild(mkBtn('☐ All', 'Toggle select all', () => this._toggleSelectAll())); + // v4.6.35: tap-to-select mode — the only usable multi-select path on touch devices + const selModeBtn = mkBtn('☑ Select', 'Toggle tap-to-select mode (touch)', () => { + this._selectMode = !this._selectMode; + selModeBtn.style.background = this._selectMode ? 'var(--accent-color)' : 'none'; + selModeBtn.style.color = this._selectMode ? '#fff' : 'var(--text-color)'; + this._updateStatus(this._selectMode ? 'Select mode ON — tap rows to select' : 'Select mode OFF'); + }); + selModeBtn.setAttribute('aria-pressed', 'false'); + batchBar.appendChild(selModeBtn); + toolbar.appendChild(batchBar); + + // 2026-10-21: publish a VFS file to the user's WebDAV space was terminal-only. + const wdavBtn = document.createElement('button'); + wdavBtn.textContent = '☁ WebDAV'; + wdavBtn.title = 'Upload the selected/current file to your WebDAV space and get its public link'; + wdavBtn.style.cssText = 'background:var(--bg-tertiary);border:1px solid var(--border-color);' + + 'color:var(--text-color);cursor:pointer;padding:4px 10px;border-radius:4px;' + + 'font-family:inherit;font-size:0.9em;min-height:32px;touch-action:manipulation;'; + wdavBtn.onclick = () => this._uploadSelectionToWebdav(); + toolbar.appendChild(wdavBtn); + + overlay.appendChild(toolbar); + + // Main area — wraps sidebar + preview in a responsive flex container + const mainArea = document.createElement('div'); + mainArea.id = 'filemgr-main-container'; + mainArea.className = 'filemgr-main'; + mainArea.style.cssText = 'flex:1;overflow:hidden;display:flex;min-height:0;'; + + // Sidebar + const sidebar = document.createElement('div'); + sidebar.id = 'filemgr-sidebar'; + sidebar.style.cssText = 'width:25%;min-width:150px;overflow-y:auto;background:var(--bg-secondary);border-right:1px solid var(--border-color);'; + + mainArea.appendChild(sidebar); + + // Preview pane + const preview = document.createElement('div'); + preview.id = 'filemgr-preview'; + preview.style.cssText = 'flex:1;overflow-y:auto;padding:0;'; + + mainArea.appendChild(preview); + + overlay.appendChild(mainArea); + + // Action bar + const actionBar = document.createElement('div'); + actionBar.id = 'filemgr-action-bar'; + actionBar.style.cssText = 'display:flex;align-items:center;padding:8px 12px;border-top:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0;gap:8px;flex-wrap:wrap;'; + + const statusSpan = document.createElement('span'); + statusSpan.id = 'filemgr-status'; + statusSpan.style.cssText = 'flex:1;color:var(--text-muted);font-size:0.85em;'; + statusSpan.textContent = 'No file selected'; + actionBar.appendChild(statusSpan); + + const encryptBtn = document.createElement('button'); + encryptBtn.textContent = '🔐 Encrypt'; + encryptBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--warning-color);color:var(--warning-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;'; + encryptBtn.onclick = () => this._actionEncrypt(); + actionBar.appendChild(encryptBtn); + + const compressBtn = document.createElement('button'); + compressBtn.textContent = '📦 Zip'; + compressBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--info-color);color:var(--info-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;'; + compressBtn.onclick = () => this._actionCompress(); + actionBar.appendChild(compressBtn); + + const deleteBtn = document.createElement('button'); + deleteBtn.textContent = '🗑 Delete'; + deleteBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--error-color);color:var(--error-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;'; + deleteBtn.onclick = () => this._actionDelete(); + actionBar.appendChild(deleteBtn); + + const downloadBtn = document.createElement('button'); + downloadBtn.textContent = '⬇ Download'; + downloadBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;'; + downloadBtn.onclick = () => this._actionDownload(); + actionBar.appendChild(downloadBtn); + + const editBtn = document.createElement('button'); + editBtn.textContent = '✏ Edit'; + editBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--accent-color);color:var(--accent-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;'; + editBtn.onclick = () => this._actionEdit(); + actionBar.appendChild(editBtn); + + const newFileBtn = document.createElement('button'); + newFileBtn.textContent = '📄 New'; + newFileBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--success-color);color:var(--success-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;'; + newFileBtn.onclick = () => this._actionNewFile(); + actionBar.appendChild(newFileBtn); + + const previewToggleBtn = document.createElement('button'); + previewToggleBtn.id = 'filemgr-toggle-raw'; + previewToggleBtn.textContent = '📄 Raw'; + previewToggleBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-muted);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;'; + previewToggleBtn.onclick = () => this._togglePreviewMode(); + actionBar.appendChild(previewToggleBtn); + + overlay.appendChild(actionBar); + + this._overlay = overlay; + return overlay; + } + + _openOverlay() { + if (this._overlay) { + this._overlay.style.display = 'flex'; + this._refreshFileList(); + return; + } + const overlay = this._createOverlay(); + document.body.appendChild(overlay); + this._navigateTo(this.currentPath); + } + + _closeOverlay() { + if (this._overlay) { + this._overlay.style.display = 'none'; + } + this._multiSelect.clear(); + this._selectedFile = null; + this._selectMode = false; + } + + _clearOverlay() { + if (this._overlay && this._overlay.parentNode) { + this._overlay.parentNode.removeChild(this._overlay); + } + this._overlay = null; + this._selectedFile = null; + this._multiSelect.clear(); + this._selectMode = false; + this._searchFilter = ''; + this._rawMode = false; + } + + _navigateTo(pathParts) { + this.currentPath = pathParts || []; + this._selectedFile = null; + this._multiSelect.clear(); + this._searchFilter = ''; + const searchInput = this._overlay?.querySelector('input[type="text"]'); + if (searchInput) searchInput.value = ''; + this._updateBreadcrumbs(); + this._refreshFileList(); + this._clearPreview(); + this._updateStatus('No file selected'); + } + + _updateBreadcrumbs() { + const breadcrumbs = document.getElementById('filemgr-breadcrumbs'); + if (!breadcrumbs) return; + + // Keep root segment, remove rest + while (breadcrumbs.children.length > 1) { + breadcrumbs.removeChild(breadcrumbs.lastChild); + } + + let accumulated = ''; + this.currentPath.forEach((part, i) => { + const sep = document.createElement('span'); + sep.textContent = ' › '; + sep.style.cssText = 'color:var(--text-muted);margin:0 2px;'; + breadcrumbs.appendChild(sep); + + const seg = document.createElement('span'); + seg.textContent = part; + seg.style.cssText = 'cursor:pointer;padding:2px 4px;border-radius:3px;color:var(--text-color);'; + if (i === this.currentPath.length - 1) { + seg.style.color = 'var(--accent-color)'; + seg.style.fontWeight = 'bold'; + } + seg.onmouseenter = () => { seg.style.background = 'var(--bg-tertiary)'; }; + seg.onmouseleave = () => { seg.style.background = 'transparent'; }; + const pathUpTo = this.currentPath.slice(0, i + 1); + seg.onclick = () => this._navigateTo(pathUpTo); + breadcrumbs.appendChild(seg); + }); + } + + _refreshFileList() { + const sidebar = document.getElementById('filemgr-sidebar'); + if (!sidebar) return; + + // Read directory + const pathStr = '/' + this.currentPath.join('/'); + const entries = this.terminal.fs.ls(pathStr) || []; + + // Separate dirs and files + const dirs = entries.filter(e => e.type === 'dir'); + const files = entries.filter(e => e.type !== 'dir'); + + // Prune selections that no longer exist here (deleted/moved elsewhere) + try { + const _live = new Set(); + entries.forEach(e => _live.add(pathStr === '/' ? '/' + e.name : pathStr + '/' + e.name)); + [...this._multiSelect].forEach(p => { if (!_live.has(p)) this._multiSelect.delete(p); }); + } catch (_e) {} + + // Sort alphabetically + dirs.sort((a, b) => a.name.localeCompare(b.name)); + files.sort((a, b) => a.name.localeCompare(b.name)); + + // Apply search filter + let filteredDirs = dirs; + let filteredFiles = files; + if (this._searchFilter) { + const q = this._searchFilter; + filteredDirs = dirs.filter(e => e.name.toLowerCase().includes(q)); + filteredFiles = files.filter(e => e.name.toLowerCase().includes(q)); + } + + const _scrollTop = sidebar.scrollTop; + sidebar.innerHTML = ''; + + if (filteredDirs.length === 0 && filteredFiles.length === 0) { + const emptyMsg = document.createElement('div'); + emptyMsg.style.cssText = 'padding:20px;text-align:center;color:var(--text-muted);'; + emptyMsg.textContent = this._searchFilter ? 'No matching files' : '(empty)'; + sidebar.appendChild(emptyMsg); + this._itemCount = 0; + this._updateSelectionStatus(); + return; + } + + // Add directory items + filteredDirs.forEach(entry => { + const item = document.createElement('div'); + const fullPath = pathStr === '/' ? '/' + entry.name : pathStr + '/' + entry.name; + item.dataset.path = fullPath; + item.dataset.type = 'dir'; + item.style.cssText = 'display:flex;align-items:center;padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--border-color);gap:6px;border-left:3px solid transparent;'; + item.onmouseenter = () => { if (!item.dataset.selected) item.style.background = 'var(--bg-tertiary)'; }; + item.onmouseleave = () => { if (!item.dataset.selected) item.style.background = 'transparent'; }; + item.onclick = (e) => { + if (e.ctrlKey || e.metaKey || this._selectMode) { e.preventDefault(); this._toggleMulti(fullPath); return; } + const newPath = [...this.currentPath, entry.name]; + this._navigateTo(newPath); + }; + + item.appendChild(this._makeCheckbox(fullPath, entry.name + '/')); + + const icon = document.createElement('span'); + icon.textContent = '📁'; + item.appendChild(icon); + + const nameSpan = document.createElement('span'); + nameSpan.textContent = entry.name + '/'; + nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'; + item.appendChild(nameSpan); + + sidebar.appendChild(item); + }); + + // Add file items + filteredFiles.forEach(entry => { + const item = document.createElement('div'); + const fullPath = pathStr === '/' ? '/' + entry.name : pathStr + '/' + entry.name; + const isSelected = this._selectedFile === fullPath; + const isMulti = this._multiSelect.has(fullPath); + item.dataset.path = fullPath; + item.dataset.type = 'file'; + item.dataset.selected = (isSelected || isMulti) ? 'true' : 'false'; + const bgColor = isMulti ? 'var(--bg-tertiary)' : 'transparent'; + item.style.cssText = 'display:flex;align-items:center;padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--border-color);gap:6px;background:' + bgColor + ';border-left:3px solid ' + (isSelected ? 'var(--accent-color)' : 'transparent') + ';'; + item.onmouseenter = () => { if (!item.dataset.selected) item.style.background = 'var(--bg-tertiary)'; }; + item.onmouseleave = () => { if (!item.dataset.selected) item.style.background = 'transparent'; }; + item.onclick = (e) => { + // Ctrl/Cmd or Select-mode → toggle in the batch set (no full re-render) + if (e.ctrlKey || e.metaKey || this._selectMode) { e.preventDefault(); this._toggleMulti(fullPath); return; } + // Plain click → preview AND make it the single selected item + this._multiSelect.clear(); + this._multiSelect.add(fullPath); + this._selectedFile = fullPath; + this._highlightSelected(); + this._renderPreview(fullPath, this.terminal.fs.readFile(fullPath)); + this._updateSelectionStatus(); + }; + + item.appendChild(this._makeCheckbox(fullPath, entry.name)); + + const icon = document.createElement('span'); + icon.textContent = '📄'; + item.appendChild(icon); + + const nameSpan = document.createElement('span'); + nameSpan.textContent = entry.name; + nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'; + item.appendChild(nameSpan); + + const sizeSpan = document.createElement('span'); + sizeSpan.textContent = this._formatSize(entry.size); + sizeSpan.style.cssText = 'color:var(--text-muted);font-size:0.8em;white-space:nowrap;margin-left:4px;'; + item.appendChild(sizeSpan); + + sidebar.appendChild(item); + }); + + this._itemCount = filteredDirs.length + filteredFiles.length; + sidebar.scrollTop = _scrollTop; + this._highlightSelected(); + this._updateSelectionStatus(); + } + + _makeCheckbox(path, label) { + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.checked = this._multiSelect.has(path); + cb.setAttribute('aria-label', 'Select ' + label); + cb.style.cssText = 'width:20px;height:20px;min-width:20px;margin:0 6px 0 0;accent-color:var(--accent-color);cursor:pointer;touch-action:manipulation;'; + cb.onclick = (e) => e.stopPropagation(); + cb.onchange = () => this._toggleMulti(path); + return cb; + } + + _toggleMulti(path) { + if (this._multiSelect.has(path)) this._multiSelect.delete(path); + else this._multiSelect.add(path); + this._highlightSelected(); + this._updateSelectionStatus(); + } + + _updateSelectionStatus() { + const n = this._multiSelect.size; + if (n) { this._updateStatus(n + ' selected'); return; } + if (this._selectedFile) { this._updateStatus('Preview: ' + this._selectedFile.split('/').pop()); return; } + const total = this._itemCount || 0; + this._updateStatus(total + ' item' + (total === 1 ? '' : 's')); + } + + _exists(path) { + const fs = this.terminal.fs; + try { + if (fs.readFile(path) !== null) return true; + if (typeof fs.readDir === 'function' && fs.readDir(path) !== null) return true; + if (typeof fs.stat === 'function' && fs.stat(path)) return true; + } catch (_e) {} + return false; + } + + _highlightSelected() { + const sidebar = document.getElementById('filemgr-sidebar'); + if (!sidebar) return; + const items = sidebar.querySelectorAll('[data-path]'); + items.forEach(item => { + const path = item.dataset.path; + const inBatch = this._multiSelect.has(path); + const preview = this._selectedFile === path; + item.dataset.selected = (inBatch || preview) ? 'true' : 'false'; + item.style.background = inBatch ? 'var(--bg-tertiary)' : 'transparent'; + item.style.borderLeft = '3px solid ' + (preview ? 'var(--accent-color)' : 'transparent'); + const cb = item.querySelector('input[type="checkbox"]'); + if (cb) cb.checked = inBatch; + }); + } + + // v4.5.78: Batch operations + _getSelectedPaths() { + return [...this._multiSelect]; + } + + _batchCopy() { + const paths = this._getSelectedPaths(); + if (!paths.length) { this._updateStatus('No files selected'); return; } + this._clipboard = { mode: 'copy', paths: [...paths] }; + this._updateStatus('📋 Copied ' + paths.length + ' item(s)'); + } + + _batchCut() { + const paths = this._getSelectedPaths(); + if (!paths.length) { this._updateStatus('No files selected'); return; } + this._clipboard = { mode: 'cut', paths: [...paths] }; + this._updateStatus('✂️ Cut ' + paths.length + ' item(s)'); + } + + _batchPaste() { + if (!this._clipboard || !this._clipboard.paths.length) { this._updateStatus('Clipboard empty'); return; } + const fs = this.terminal.fs; + const dest = this.currentPath.length ? '/' + this.currentPath.join('/') : '/'; + let count = 0, skipped = 0; + const failed = []; + this._clipboard.paths.forEach(src => { + const name = src.split('/').pop(); + const destPath = dest === '/' ? '/' + name : dest + '/' + name; + if (destPath === src || destPath.startsWith(src + '/')) { skipped++; return; } + // Directory → existing directory would REPLACE the whole subtree silently; skip it. + let srcIsDir = false; + try { srcIsDir = typeof fs.readDir === 'function' && fs.readDir(src) !== null; } catch (_e) {} + if (this._exists(destPath) && srcIsDir) { skipped++; return; } + if (this._exists(destPath) && !confirm('Overwrite ' + destPath + '?')) { skipped++; return; } + try { + const ok = this._clipboard.mode === 'copy' ? fs.copy(src, destPath) : fs.move(src, destPath); + if (ok) count++; else failed.push(src); + } catch (_e) { failed.push(src); } + }); + if (this._clipboard.mode === 'cut') { + this._clipboard = failed.length ? { mode: 'cut', paths: failed } : { mode: null, paths: [] }; + } + this._multiSelect.clear(); + this._selectedFile = null; + this._refreshFileList(); + this._updateStatus('📥 Pasted ' + count + ' item(s)' + (skipped ? ' — ' + skipped + ' skipped' : '') + (failed.length ? ' — ' + failed.length + ' failed' : '')); + } + + _batchDelete() { + const paths = this._getSelectedPaths(); + if (!paths.length) { this._updateStatus('No files selected'); return false; } + if (!confirm('Delete ' + paths.length + ' item(s)?')) return false; + const fs = this.terminal.fs; + let count = 0, failed = 0; + paths.forEach(p => { + try { + const ok = typeof fs.rm === 'function' ? fs.rm(p) : fs.deleteFile(p); + if (ok) count++; else failed++; + } catch (_e) { failed++; } + }); + this._multiSelect.clear(); + this._selectedFile = null; + this._refreshFileList(); + this._updateStatus('🗑️ Deleted ' + count + ' item(s)' + (failed ? ' — ' + failed + ' failed' : '')); + return count > 0; + } + + _batchRename() { + const paths = this._getSelectedPaths(); + if (!paths.length) { this._updateStatus('No item selected'); return; } + const fs = this.terminal.fs; + if (paths.length === 1) { + const path = paths[0]; + const oldName = path.split('/').pop(); + const newName = (prompt('Rename to:', oldName) || '').trim(); + if (!newName || newName === oldName) return; + if (newName === '.' || newName === '..' || newName.includes('/') || newName.includes('\\')) { + this._updateStatus('❌ Invalid file name'); return; + } + const dir = path.substring(0, path.lastIndexOf('/')) || '/'; + const newPath = dir === '/' ? '/' + newName : dir + '/' + newName; + if (this._exists(newPath)) { this._updateStatus('⚠ Destination already exists'); return; } + if (!fs.rename(path, newPath)) { this._updateStatus('❌ Rename failed'); return; } + this._selectedFile = newPath; + this._multiSelect.clear(); + this._multiSelect.add(newPath); + this._refreshFileList(); + this._updateStatus('📝 Renamed to ' + newName); + return; + } + const prefix = (prompt('Rename ' + paths.length + ' items — new prefix:', 'file') || '').trim(); + if (!prefix) return; + let i = 0, ok = 0, skip = 0; + paths.forEach(p => { + const ext = (p.match(/\.[^./]+$/) || [''])[0]; + const dir = p.substring(0, p.lastIndexOf('/')) || '/'; + const dest = (dir === '/' ? '' : dir) + '/' + prefix + (++i) + ext; + if (dest === p || this._exists(dest)) { skip++; return; } + try { if (fs.rename(p, dest)) ok++; else skip++; } catch (_e) { skip++; } + }); + this._multiSelect.clear(); + this._selectedFile = null; + this._refreshFileList(); + this._updateStatus('📝 Renamed ' + ok + ' item(s)' + (skip ? ' — ' + skip + ' skipped' : '')); + } + + _toggleSelectAll() { + const sidebar = document.getElementById('filemgr-sidebar'); + if (!sidebar) return; + const paths = [...sidebar.querySelectorAll('[data-path]')].map(i => i.dataset.path); + if (!paths.length) { this._updateSelectionStatus(); return; } + const all = paths.every(p => this._multiSelect.has(p)); + if (all) { + paths.forEach(p => this._multiSelect.delete(p)); + if (this._selectedFile && paths.includes(this._selectedFile)) this._selectedFile = null; + } else { + paths.forEach(p => this._multiSelect.add(p)); + } + this._refreshFileList(); + } + + _renderPreview(path, content) { + const preview = document.getElementById('filemgr-preview'); + if (!preview) return; + + // Re-read content on each preview call + if (content === undefined) { + content = this.terminal.fs.readFile(path); + } + + // For raw mode, show raw text regardless of type + if (this._rawMode && typeof content === 'string') { + preview.innerHTML = this._previewShell(path, '<div style="padding:12px"><div style="color:var(--text-muted);font-size:0.85em;margin-bottom:8px">RAW: ' + this._escapeHtml(path) + '</div><pre style="white-space:pre-wrap;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">' + this._escapeHtml(content.substring(0, 10000)) + '</pre></div>'); + this._wirePreviewToolbar(path); + this._updateStatus('Preview: ' + path); + return; + } + + // Check file size for text files + const type = this._detectType(path); + const textTypes = ['md', 'text', 'json', 'code', 'csv', 'other']; + if (textTypes.includes(type) && typeof content === 'string' && content.length > 51200) { + content = content.substring(0, 51200); + preview.innerHTML = this._previewShell(path, + this._getPreviewHtml(path, content) + + '<div style="color:var(--warning-color);padding:8px 12px;border-top:1px solid var(--border-color);font-size:0.85em;">(truncated — file exceeds 50KB)</div>'); + this._wirePreviewToolbar(path); + this._updateStatus('Preview: ' + path); + return; + } + + preview.innerHTML = this._previewShell(path, this._getPreviewHtml(path, content)); + this._wirePreviewToolbar(path); + this._updateStatus('Preview: ' + path); + } + + // 2026-10-21: preview pane had no fullscreen and no share action. + _previewShell(path, innerHtml) { + return '<div class="fm-preview-toolbar" style="display:flex;gap:6px;justify-content:flex-end;' + + 'padding:6px 8px;border-bottom:1px solid var(--border-color);flex-shrink:0">' + + '<button class="fm-btn-fs" title="Fullscreen preview" aria-label="Fullscreen preview" style="background:var(--bg-tertiary);' + + 'border:1px solid var(--border-color);color:var(--text-color);padding:2px 8px;border-radius:4px;' + + 'cursor:pointer;font-size:0.85em;min-height:24px;">⛶</button>' + + '<button class="fm-btn-share" title="Copy share link" aria-label="Copy share link" style="background:var(--bg-tertiary);' + + 'border:1px solid var(--border-color);color:var(--text-color);padding:2px 8px;border-radius:4px;' + + 'cursor:pointer;font-size:0.85em;min-height:24px;">🔗</button>' + + '</div><div class="fm-preview-body" style="flex:1;min-height:0;overflow:auto">' + innerHtml + '</div>'; + } + + _previewShareUrl(path) { + const wdav = this.terminal && this.terminal.moduleManager && this.terminal.moduleManager.modules + ? this.terminal.moduleManager.modules.WebDAVModule : null; + const space = (wdav && wdav._resolveSpaceName && wdav._resolveSpaceName()) || ''; + const clean = String(path).replace(/^\/+/, ''); + return 'https://webx11.duckdns.org/spaces/' + space + '/' + + clean.split('/').map(encodeURIComponent).join('/'); + } + + // 2026-10-21: publish VFS file(s) to WebDAV (was terminal-only: webdav-push) + async _uploadSelectionToWebdav() { + let targets = this._getSelectedPaths ? this._getSelectedPaths() : []; + if (!targets || !targets.length) targets = this._selectedFile ? [this._selectedFile] : []; + if (!targets.length) { this._updateStatus('☁ Select a file first'); return; } + const wdav = this.terminal && this.terminal.moduleManager && this.terminal.moduleManager.modules + ? this.terminal.moduleManager.modules.WebDAVModule : null; + if (!wdav) { this._updateStatus('☁ WebDAV module not loaded'); return; } + if (!wdav.config || !wdav.config.url) { try { await wdav._autoBootstrap(); } catch (e) {} } + if (!wdav.config || !wdav.config.url) { this._updateStatus('☁ WebDAV not configured — run webdav-config'); return; } + for (const p of targets) { + const clean = String(p).replace(/^\/+/, ''); + try { + await this.terminal.security.checkAndRun('webdav-push ' + clean); + const shareUrl = this._previewShareUrl(clean); + this._updateStatus('☁ Uploaded ' + clean + ' → ' + shareUrl); + if (this.terminal.print) this.terminal.print('☁ WebDAV: ' + shareUrl, 'success'); + } catch (e) { + this._updateStatus('☁ Upload failed: ' + (e && e.message ? e.message : e)); + } + } + } + + _wirePreviewToolbar(path) { + const pane = document.getElementById('filemgr-preview'); + if (!pane) return; + const fsBtn = pane.querySelector('.fm-btn-fs'); + const shBtn = pane.querySelector('.fm-btn-share'); + if (fsBtn) fsBtn.onclick = () => { + const body = pane.querySelector('.fm-preview-body') || pane; + if (document.fullscreenElement) { document.exitFullscreen(); return; } + if (body.requestFullscreen) body.requestFullscreen().catch(() => this._updateStatus('⛶ Fullscreen blocked by this browser')); + else if (body.webkitRequestFullscreen) body.webkitRequestFullscreen(); + else this._updateStatus('⛶ Fullscreen not supported here'); + }; + if (shBtn) shBtn.onclick = () => { + const url = this._previewShareUrl(path); + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(url) + .then(() => this._updateStatus('🔗 Link copied: ' + url)) + .catch(() => prompt('Copy share link:', url)); + } else { + prompt('Copy share link:', url); + } + }; + } + + _renderPreviewError(msg) { + const preview = document.getElementById('filemgr-preview'); + if (!preview) return; + preview.innerHTML = '<div style="color:var(--error-color);padding:20px;font-size:1em">' + this._escapeHtml(msg) + '</div>'; + this._updateStatus('Error'); + } + + _clearPreview() { + const preview = document.getElementById('filemgr-preview'); + if (preview) preview.innerHTML = ''; + } + + _updateStatus(msg) { + const status = document.getElementById('filemgr-status'); + if (status) status.textContent = msg; + } + + _actionDelete() { + // Delegate to the batch path so checkbox selections are honoured, not just the previewed file. + if (!this._getSelectedPaths().length && this._selectedFile) { + this._multiSelect.add(this._selectedFile); + } + // Only clear the preview when something was actually deleted (not on cancel). + if (this._batchDelete()) this._clearPreview(); + } + + _actionDownload() { + if (!this._selectedFile) { + this._updateStatus('No file selected to download'); + return; + } + const content = this.terminal.fs.readFile(this._selectedFile); + if (content === null || content === false) { + this._updateStatus('❌ File not found'); + return; + } + const fileName = this._selectedFile.split('/').pop() || 'file'; + const blob = new Blob([content], { type: 'application/octet-stream' }); + 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); + this._updateStatus('⬇ Downloaded: ' + fileName); + } + + _actionEdit() { + if (!this._selectedFile) { + this._updateStatus('No file selected to edit'); + return; + } + const path = this._selectedFile; + // Close file manager first so code editor modal (z-index:1000) is visible above + this._closeOverlay(); + if (typeof webx11 !== 'undefined' && webx11 && webx11.dispatchCommand) { + setTimeout(() => { + webx11.dispatchCommand('edit', [path]); + }, 100); + this._updateStatus('✏ Opened in editor: ' + path); + } else { + this._updateStatus('❌ Editor not available'); + } + } + + _togglePreviewMode() { + this._rawMode = !this._rawMode; + const btn = document.getElementById('filemgr-toggle-raw'); + if (btn) { + btn.textContent = this._rawMode ? '📄 Rendered' : '📄 Raw'; + btn.style.borderColor = this._rawMode ? 'var(--accent-color)' : 'var(--border-color)'; + btn.style.color = this._rawMode ? 'var(--accent-color)' : 'var(--text-muted)'; + } + if (this._selectedFile) { + const content = this.terminal.fs.readFile(this._selectedFile); + this._renderPreview(this._selectedFile, content); + } + } + + _actionEncrypt() { + if (!this._selectedFile) { + this._updateStatus('No file selected'); + return; + } + if (this._selectedFile.endsWith('.enc')) { + this._updateStatus('⚠ Already encrypted (.enc)'); + return; + } + const passphrase = prompt('Enter passphrase for AES-256-GCM encryption:'); + if (!passphrase || passphrase.length < 4) { + this._updateStatus('❌ Passphrase too short (min 4 chars)'); + return; + } + // Delegate to cryptogzip module's encrypt command + if (typeof webx11 !== 'undefined' && webx11.executeCommand) { + webx11.executeCommand('encrypt ' + this._selectedFile + ' ' + passphrase); + this._updateStatus('🔐 Encrypted: ' + this._selectedFile + '.enc'); + this._refreshFileList(); + } + } + + _actionCompress() { + if (!this._selectedFile) { + this._updateStatus('No file selected'); + return; + } + if (this._selectedFile.endsWith('.gz')) { + this._updateStatus('⚠ Already compressed (.gz)'); + return; + } + if (typeof webx11 !== 'undefined' && webx11.executeCommand) { + webx11.executeCommand('gzip ' + this._selectedFile); + this._updateStatus('📦 Compressed: ' + this._selectedFile + '.gz'); + this._refreshFileList(); + } + } + + _actionNewFile() { + const name = prompt('New file name (e.g. myfile.txt):'); + if (!name) return; + const pathStr = '/' + this.currentPath.join('/'); + const cleanPath = pathStr === '/' ? '/' + name : pathStr + '/' + name; + const content = prompt('Enter file content (optional):') || ''; + const ok = this.terminal.fs.writeFile(cleanPath, content); + if (ok) { + this._updateStatus('✅ Created: ' + cleanPath + ' (' + content.length + ' chars)'); + this._refreshFileList(); + } else { + this._updateStatus('❌ Failed to create file'); + } + } +} + +if (typeof window.webx11 !== 'undefined' && window.webx11) { + webx11.moduleManager.registerModule(new FileManagerModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/filemgr.js'); +} |