aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main.html6
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/ext/critical-ext-bundle.js9
-rw-r--r--modules/ext/filemgr.js9
-rw-r--r--modules/ext/webdav.js59
5 files changed, 80 insertions, 5 deletions
diff --git a/main.html b/main.html
index 99f6324..86c034c 100644
--- a/main.html
+++ b/main.html
@@ -1349,13 +1349,13 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=v4.6.99"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.101"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=v4.6.99"></script>
+<script src="modules/core/module-loader.js?v=v4.6.101"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.99"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.101"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 5056cdb..252c34b 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = 'v4.6.99';
+ const LATEST_VER = 'v4.6.101';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js
index 8e968fb..d1a2b83 100644
--- a/modules/ext/critical-ext-bundle.js
+++ b/modules/ext/critical-ext-bundle.js
@@ -1520,6 +1520,15 @@ class FileManagerModule {
}
const overlay = this._createOverlay();
document.body.appendChild(overlay);
+ // 2026-10-26 (Issue D2): the WebDAV pane lives in another module and needs a read-only view of
+ // this clipboard to implement "cut/copy here, paste there".
+ try {
+ if (typeof window !== 'undefined' && window.webx11) {
+ window.webx11.fileClipboard = () => ({ mode: this._clipboard.mode, paths: [...this._clipboard.paths] });
+ window.webx11.fileClipboardClear = () => { this._clipboard = { mode: null, paths: [] }; };
+ window.webx11.fileClipboardSet = (paths, mode) => { this._clipboard = { mode: mode || 'copy', paths: [...(paths || [])] }; };
+ }
+ } catch (_e) {}
this._navigateTo(this.currentPath);
}
diff --git a/modules/ext/filemgr.js b/modules/ext/filemgr.js
index 7412578..b813815 100644
--- a/modules/ext/filemgr.js
+++ b/modules/ext/filemgr.js
@@ -587,6 +587,15 @@ class FileManagerModule {
}
const overlay = this._createOverlay();
document.body.appendChild(overlay);
+ // 2026-10-26 (Issue D2): the WebDAV pane lives in another module and needs a read-only view of
+ // this clipboard to implement "cut/copy here, paste there".
+ try {
+ if (typeof window !== 'undefined' && window.webx11) {
+ window.webx11.fileClipboard = () => ({ mode: this._clipboard.mode, paths: [...this._clipboard.paths] });
+ window.webx11.fileClipboardClear = () => { this._clipboard = { mode: null, paths: [] }; };
+ window.webx11.fileClipboardSet = (paths, mode) => { this._clipboard = { mode: mode || 'copy', paths: [...(paths || [])] }; };
+ }
+ } catch (_e) {}
this._navigateTo(this.currentPath);
}
diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js
index 88f1f59..7f3e7a7 100644
--- a/modules/ext/webdav.js
+++ b/modules/ext/webdav.js
@@ -1654,7 +1654,8 @@ class WebDAVModule {
bulkBar.innerHTML = '<span id="bulk-count" style="font-size:0.95em;color:var(--text-color);flex:1;">0 selected</span>' +
'<button class="btn" id="bulk-delete" style="font-size:0.9em;padding:5px 12px;">🗑 Delete</button>' +
'<button class="btn" id="bulk-move" style="font-size:0.9em;padding:5px 12px;">📦 Move</button>' +
- '<button class="btn" id="bulk-download" style="font-size:0.9em;padding:5px 12px;">⬇ Download</button>';
+ '<button class="btn" id="bulk-download" style="font-size:0.9em;padding:5px 12px;">⬇ Download</button>' +
+ '<button class="btn" id="bulk-paste" style="font-size:0.9em;padding:5px 12px;display:none;">📥 Paste</button>';
content.appendChild(bulkBar);
// Footer buttons
@@ -2001,6 +2002,62 @@ class WebDAVModule {
dest.focus();
dest.select();
});
+ // ── 2026-10-26 (Issue D2): paste the VFS clipboard into this WebDAV directory ──────────
+ const refreshPaste = function() {
+ const btn = document.getElementById('bulk-paste');
+ if (!btn) return;
+ const clip = (typeof webx11 !== 'undefined' && webx11.fileClipboard) ? webx11.fileClipboard() : null;
+ const n = clip && clip.paths ? clip.paths.length : 0;
+ btn.style.display = n ? '' : 'none';
+ btn.textContent = '📥 Paste ' + n;
+ btn.title = n ? 'Upload ' + n + ' file(s) from 📁 Files (' + (clip.mode || 'copy') + ')' : '';
+ };
+ refreshPaste();
+ document.getElementById('bulk-paste')?.addEventListener('click', async function() {
+ const clip = (typeof webx11 !== 'undefined' && webx11.fileClipboard) ? webx11.fileClipboard() : null;
+ if (!clip || !clip.paths || !clip.paths.length) {
+ self.terminal.print('📥 Clipboard empty — copy or cut files in 📁 Files first', 'error');
+ return;
+ }
+ const destDir = (path || '').replace(/\/+$/, '');
+ let ok = 0, failed = 0;
+ for (const src of clip.paths) {
+ const name = src.split('/').pop();
+ const content = webx11.fs.readFile(src);
+ if (content === null || content === false) {
+ failed++;
+ self.terminal.print('📥 Skipped (no longer in VFS): ' + name, 'error');
+ continue;
+ }
+ try {
+ const r = await self._webdavRequest('PUT', destDir + '/' + name, { body: content });
+ if (!(r.ok || r.status === 201 || r.status === 204)) throw new Error('HTTP ' + r.status);
+ ok++;
+ // cut = move: delete locally only after the server accepted the bytes
+ if (clip.mode === 'cut') {
+ const fs = webx11.fs;
+ const del = typeof fs.rm === 'function' ? fs.rm(src) : fs.deleteFile(src);
+ if (del === false) self.terminal.print('📥 Uploaded but local delete failed: ' + name, 'error');
+ }
+ } catch (e) {
+ failed++;
+ self.terminal.print('📥 Paste failed: ' + name + ' — ' + (e && e.message ? e.message : e), 'error');
+ }
+ }
+ self.terminal.print('📥 Pasted ' + ok + ' item(s) to ' + (destDir || '/') +
+ (failed ? ' — ' + failed + ' failed' : ''), failed ? 'error' : 'success');
+ // cut = move: only the files that actually moved leave the clipboard; a failed upload
+ // stays in it so the operator can retry instead of hunting for the file again.
+ if (clip.mode === 'cut' && ok) {
+ const left = clip.paths.filter(function(p) {
+ const c = webx11.fs.readFile(p);
+ return c !== null && c !== false;
+ });
+ if (left.length && typeof webx11.fileClipboardSet === 'function') webx11.fileClipboardSet(left, 'cut');
+ else if (typeof webx11.fileClipboardClear === 'function') webx11.fileClipboardClear();
+ }
+ setTimeout(function() { self.openWebdavBrowser(path); }, 600);
+ });
document.getElementById('bulk-download')?.addEventListener('click', function() {
const names = Object.entries(self._selected).filter(function(e) { return e[1]; }).map(function(e) { return e[0]; });
names.forEach(function(name) {