aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/filemgr.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-05-28 18:01:22 +0200
committerAgando <roduccomm@gmail.com>2026-05-28 18:01:22 +0200
commitf8a04e76ea9aa5156e6e80a7534f1aa5a336429b (patch)
tree35f08d7228f1da9493e45d2fcd64e7277f008589 /modules/ext/filemgr.js
parentd1af24deb8ad344994c0a06b909b7a6f4bac4c94 (diff)
auto: 2026-05-28_18:00 changes
Diffstat (limited to 'modules/ext/filemgr.js')
-rw-r--r--modules/ext/filemgr.js169
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, '&quot;').replace(/'/g, '&#39;')) + '" ' +
+ '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');
}