aboutsummaryrefslogtreecommitdiff
path: root/modules/ext
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext')
-rw-r--r--modules/ext/critical-ext-bundle.js94
-rw-r--r--modules/ext/filemgr.js94
-rw-r--r--modules/ext/webdav.js48
3 files changed, 209 insertions, 27 deletions
diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js
index 8f63081..dc3a858 100644
--- a/modules/ext/critical-ext-bundle.js
+++ b/modules/ext/critical-ext-bundle.js
@@ -1372,6 +1372,16 @@ class FileManagerModule {
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
@@ -1840,7 +1850,8 @@ class FileManagerModule {
// For raw mode, show raw text regardless of type
if (this._rawMode && typeof content === 'string') {
- preview.innerHTML = '<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>';
+ 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;
}
@@ -1850,19 +1861,88 @@ class FileManagerModule {
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._getPreviewHtml(path, content);
- const truncatedMsg = document.createElement('div');
- truncatedMsg.style.cssText = 'color:var(--warning-color);padding:8px 12px;border-top:1px solid var(--border-color);font-size:0.85em;';
- truncatedMsg.textContent = '(truncated — file exceeds 50KB)';
- preview.appendChild(truncatedMsg);
+ 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._getPreviewHtml(path, content);
+ 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;
diff --git a/modules/ext/filemgr.js b/modules/ext/filemgr.js
index e099a34..01d1da9 100644
--- a/modules/ext/filemgr.js
+++ b/modules/ext/filemgr.js
@@ -439,6 +439,16 @@ class FileManagerModule {
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
@@ -907,7 +917,8 @@ class FileManagerModule {
// For raw mode, show raw text regardless of type
if (this._rawMode && typeof content === 'string') {
- preview.innerHTML = '<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>';
+ 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;
}
@@ -917,19 +928,88 @@ class FileManagerModule {
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._getPreviewHtml(path, content);
- const truncatedMsg = document.createElement('div');
- truncatedMsg.style.cssText = 'color:var(--warning-color);padding:8px 12px;border-top:1px solid var(--border-color);font-size:0.85em;';
- truncatedMsg.textContent = '(truncated — file exceeds 50KB)';
- preview.appendChild(truncatedMsg);
+ 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._getPreviewHtml(path, content);
+ 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;
diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js
index 310d6f3..ed312a5 100644
--- a/modules/ext/webdav.js
+++ b/modules/ext/webdav.js
@@ -1722,20 +1722,42 @@ class WebDAVModule {
if (window.webx11 && window.webx11.inputField) window.webx11.inputField.focus();
};
- // Share: copy direct folder URL to clipboard
- document.getElementById('wdav-share').onclick = function() {
- const spaceName = self._resolveSpaceName();
- const cleanPath = (path || '/').replace(/\/+$/, '') + '/';
- const shareUrl = 'https://webx11.duckdns.org/spaces/' + (spaceName || '') + cleanPath;
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(shareUrl).then(function() {
- self.terminal.print('🔗 Shared URL copied: ' + shareUrl, 'success');
- }, function() {
- self.terminal.print('🔗 Share URL: ' + shareUrl, 'info');
- });
- } else {
- self.terminal.print('🔗 Share URL: ' + shareUrl, 'info');
+ // Share: copy the public URL of the selected FILE(s). Folder listings need the
+ // owner session, so a folder URL is a guaranteed 403 for the recipient (2026-10-21).
+ document.getElementById('wdav-share').onclick = async function() {
+ const space = self._resolveSpaceName() || '';
+ const sel = Object.keys(self._selected || {}).filter(function(n) { return self._selected[n]; });
+ const files = sel.filter(function(n) {
+ const e = (allEntries || []).find(function(x) { return x.name === n; });
+ return e && e.type === 'file';
+ });
+ if (!files.length) {
+ const dirName = String(path || '/').replace(/^\/+/, '').replace(/\/+$/, '');
+ const _mods = self.terminal && self.terminal.moduleManager && self.terminal.moduleManager.modules;
+ const gz = _mods && _mods.CryptoGzipModule;
+ if (gz && dirName) {
+ self.terminal.print('🔗 Folders cannot be shared directly — archiving ' + dirName + ' …', 'info');
+ try { await self.terminal.security.checkAndRun('tar -c ' + dirName); } catch (e) {}
+ self.terminal.print('Now select the resulting .tar and press 🔗 (it is a file).', 'info');
+ } else {
+ self.terminal.print('🔗 Select one or more FILES to share — folder listings require the owner session and return 403 for everyone else.', 'warning');
+ }
+ return;
}
+ const dir = String(path || '/').replace(/^\/+/, '').replace(/\/+$/, '');
+ const prefix = 'https://webx11.duckdns.org/spaces/' + space + '/' + (dir ? dir + '/' : '');
+ const urls = files.map(function(n) { return prefix + encodeURIComponent(n); });
+ const verified = [];
+ for (let i = 0; i < urls.length; i++) {
+ let st = null;
+ try { const h = await fetch(urls[i], { method: 'HEAD', credentials: 'omit' }); st = h.status; } catch (e) {}
+ if (st === 200) verified.push(urls[i]);
+ else self.terminal.print('⚠️ Not shareable: ' + urls[i] + ' → HTTP ' + (st === null ? 'no network' : st), 'warning');
+ }
+ if (!verified.length) return;
+ const text = verified.join('\n');
+ try { await navigator.clipboard.writeText(text); } catch (e) { prompt('Copy share link(s):', text); }
+ self.terminal.print('🔗 Copied ' + verified.length + ' verified link(s): ' + verified.join(' , '), 'success');
};
function renderList() {