diff options
Diffstat (limited to 'modules/ext/filemgr.js')
| -rw-r--r-- | modules/ext/filemgr.js | 169 |
1 files changed, 134 insertions, 35 deletions
diff --git a/modules/ext/filemgr.js b/modules/ext/filemgr.js index 5532cee..4074f06 100644 --- a/modules/ext/filemgr.js +++ b/modules/ext/filemgr.js @@ -7,6 +7,7 @@ class FileManagerModule { this._selectedFile = null; this._searchFilter = ''; this._rawMode = false; + this._inlineHtml = false; } requirements() { @@ -15,8 +16,8 @@ class FileManagerModule { getHelpInfo() { return { - 'files': 'Open the file manager overlay with VFS directory listing and preview', - 'preview': 'Preview a specific file (usage: preview <path>)', + 'files': 'files — Open the file manager overlay with VFS directory listing, preview, encrypt, zip', + 'preview': 'preview <path> — Preview a specific file', }; } @@ -52,15 +53,16 @@ class FileManagerModule { _detectType(filePath) { const ext = filePath.split('.').pop().toLowerCase(); if (!ext || ext === filePath) return 'other'; - const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp']; - const videoExts = ['mp4', 'webm']; - const audioExts = ['mp3', 'wav', 'ogg']; - const codeExts = ['js', 'py', 'html', 'css', 'yaml', 'yml', 'toml']; + 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'; @@ -113,7 +115,10 @@ class FileManagerModule { const fileName = path.split('/').pop() || path; // Handle binary detection for media types - const isLikelyBinary = typeof content === 'string' && 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; + 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))); @@ -121,6 +126,17 @@ class FileManagerModule { 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>' : ''; @@ -158,11 +174,14 @@ class FileManagerModule { } if (type === 'image') { - if (isLikelyBinary && !isTextContent) { - return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📷 Image — <span style="color:var(--info-color);cursor:pointer;text-decoration:underline" onclick="' + this._escapeHtml("var a=document.createElement('a');a.href='data:image/" + ext + ";base64," + btoa(content) + "';a.download='" + fileName + "';a.click()") + '">download to view</span></div>'; + // 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>'; } - const base64 = btoa(unescape(encodeURIComponent(content))); - return '<div style="padding:12px;text-align:center"><img src="data:image/' + ext + ';base64,' + base64 + '" style="max-width:80%;max-height:60vh;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') { @@ -187,28 +206,31 @@ class FileManagerModule { } if (type === 'video') { - if (isLikelyBinary && !isTextContent) { - return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📷 Video — <span style="color:var(--info-color);cursor:pointer;text-decoration:underline" onclick="var a=document.createElement(\'a\');a.href=\'data:video/' + ext + ';base64,' + btoa(content) + '\';a.download=\'' + fileName + '\';a.click()">download to view</span></div>'; + 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>'; } - const base64 = btoa(unescape(encodeURIComponent(content))); - return '<div style="padding:12px;text-align:center"><video controls style="max-width:80%;max-height:60vh;border-radius:4px"><source src="data:video/' + ext + ';base64,' + base64 + '" type="video/' + ext + '"></video></div>'; + return '<div style="padding:20px;text-align:center;color:var(--text-muted)">🎬 Video — cannot render</div>'; } if (type === 'audio') { - if (isLikelyBinary && !isTextContent) { - return '<div style="padding:20px;text-align:center;color:var(--text-muted)">🎵 Audio — <span style="color:var(--info-color);cursor:pointer;text-decoration:underline" onclick="var a=document.createElement(\'a\');a.href=\'data:audio/' + ext + ';base64,' + btoa(content) + '\';a.download=\'' + fileName + '\';a.click()">download to play</span></div>'; + 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>'; } - const base64 = btoa(unescape(encodeURIComponent(content))); - return '<div style="padding:12px;text-align:center"><audio controls style="max-width:80%"><source src="data:audio/' + ext + ';base64,' + base64 + '" type="audio/' + ext + '"></audio></div>'; + return '<div style="padding:20px;text-align:center;color:var(--text-muted)">🎵 Audio — cannot render</div>'; } if (type === 'pdf') { - try { - const base64 = btoa(unescape(encodeURIComponent(content))); - return '<div style="padding:12px;text-align:center;height:70vh"><embed src="data:application/pdf;base64,' + base64 + '" type="application/pdf" style="width:100%;height:100%;border:1px solid var(--border-color);border-radius:4px"></div>'; - } catch (e) { - return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📄 PDF — <span style="color:var(--info-color);cursor:pointer;text-decoration:underline" onclick="var a=document.createElement(\'a\');a.href=\'data:application/pdf;base64,' + btoa(content) + '\';a.download=\'' + fileName + '\';a.click()">download to view</span></div>'; + 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') { @@ -285,7 +307,7 @@ class FileManagerModule { 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;'; + 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'; @@ -318,10 +340,11 @@ class FileManagerModule { border-right: none !important; border-bottom: 1px solid var(--border-color); } - #filemgr-overlay #filemgr-preview { - width: 100% !important; - min-height: 40vh; - } + #filemgr-overlay #filemgr-preview { + width: 100% !important; + flex: 1 !important; + min-height: 0; + } #filemgr-action-bar { flex-wrap: wrap; gap: 6px; @@ -384,12 +407,12 @@ class FileManagerModule { const mainArea = document.createElement('div'); mainArea.id = 'filemgr-main-container'; mainArea.className = 'filemgr-main'; - mainArea.style.cssText = 'overflow:hidden;'; + 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:30%;min-width:200px;overflow-y:auto;background:var(--bg-secondary);border-right:1px solid var(--border-color);'; + 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); @@ -413,6 +436,18 @@ class FileManagerModule { 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;'; @@ -431,6 +466,12 @@ class FileManagerModule { 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'; @@ -727,9 +768,14 @@ class FileManagerModule { this._updateStatus('No file selected to edit'); return; } - if (typeof webx11 !== 'undefined' && webx11 && webx11.executeCommand) { - webx11.executeCommand('edit ' + this._selectedFile); - this._updateStatus('✏ Opened in editor: ' + this._selectedFile); + 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'); } @@ -748,8 +794,61 @@ class FileManagerModule { 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/chatstudio/newos4/modules/ext/filemgr.js'); + webx11.moduleManager.registerModule(new FileManagerModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/filemgr.js'); } |