aboutsummaryrefslogtreecommitdiff
path: root/.bkp-fix-20261023_143429/filemgr.js
diff options
context:
space:
mode:
Diffstat (limited to '.bkp-fix-20261023_143429/filemgr.js')
-rw-r--r--.bkp-fix-20261023_143429/filemgr.js1150
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, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+ // 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</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, '&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>' : '';
+ 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 || '&nbsp;') + '</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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
+ }
+
+ _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');
+}