diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-26 15:25:58 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-26 15:25:58 +0100 |
| commit | 258b32cd89135cb0948f9df7eca164ae4146a73d (patch) | |
| tree | 765e8cedf302f0c2aef059ca53e9758315009ff0 | |
| parent | ac3f0e4b94f9b029091a19471405ca5b3449f0e5 (diff) | |
feat(newos4): cut/copy in Files, paste into WebDAV (Issue D2)
The two panes kept separate clipboards (filemgr._clipboard vs webdav._selected), so "cut here, paste
there" did not exist — the operator's last phone-test item.
filemgr.js _openOverlay() publishes a read-only view of its clipboard on the terminal object:
webx11.fileClipboard() / fileClipboardClear() / fileClipboardSet(paths, mode).
webdav.js the bulk bar gains "📥 Paste N" (hidden when the clipboard is empty). Handler: for each
path readFile -> _webdavRequest('PUT', dest + '/' + name, {body}); only 200/201/204 counts.
For mode "cut" the local file is deleted AFTER the server accepted the bytes, never
before. A failed PUT keeps the local copy, prints "📥 Paste failed: <name> — HTTP n",
and leaves that path in the clipboard so "📥 Paste 1" is a retry. Successes leave it.
Summary "📥 Pasted N item(s) to <path>" (+ "— M failed"), then the pane reopens there.
Semantics: cut moves (upload then delete local), copy leaves the local file and keeps the clipboard so
the same files can go into several folders.
Verified with the transport stubbed in the live page (this browser has no WebDAV credential):
button "📥 Paste 2" for a 2-path cut
PUT ok (201) -> local file null (moved)
PUT fail(500) -> local file "BBB" (kept)
clipboard after -> {mode:"cut", paths:["/fail-d.txt"]}, button "📥 Paste 1"
node --check both modules, no-undef clean (55 files), v4.6.101 x3 in main.html, syntax OK, help 362/362.
| -rw-r--r-- | main.html | 6 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/ext/critical-ext-bundle.js | 9 | ||||
| -rw-r--r-- | modules/ext/filemgr.js | 9 | ||||
| -rw-r--r-- | modules/ext/webdav.js | 59 |
5 files changed, 80 insertions, 5 deletions
@@ -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) { |