diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-26 15:21:14 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-26 15:21:14 +0100 |
| commit | ac3f0e4b94f9b029091a19471405ca5b3449f0e5 (patch) | |
| tree | ba63a9c28b8ed47df3765295ac470cc5b36385bc | |
| parent | 979254cf303af562574e6a6a2c86416fe2d1e7e8 (diff) | |
fix(newos4): four phone-test defects — no preview close, single-file share, dead-end fullscreen, silent WebDAV move
All four came from the operator's phone pass on v4.6.95 and each collapsed to a line of code.
A "No file selected to download": the preview pane had no close control; the only ✕ was the file
manager's own, and it calls _closeOverlay() which wipes _multiSelect/_selectedFile/_selectMode.
On a phone the preview covers the list, so closing it before pressing Download always emptied the
selection. The toolbar now starts with ✕ -> _clearPreview(), which keeps the selection and leaves
fullscreen first; Download with no targets names the fix instead of stating a dead end.
B share/copy: 🔗 existed only in the preview toolbar and took a single path, and after the Android
share sheet there was no copy affordance. Added _sharePaths(paths, mode) used by both the toolbar
(1 path, 📋 copy and 🔗 share) and a new action-bar 🔗 Share (whole selection), plus
_inlineLinkBox() which renders a selectable input/textarea with all URLs when neither the share
sheet nor the clipboard is available. No prompt() anywhere on this path.
C fullscreen: requestFullscreen targeted .fm-preview-body, which sits INSIDE the pane but excludes
the toolbar, so ⛶ and ✗ disappeared and only the OS back gesture got out. Now fullscreens
#filemgr-preview — toolbar, ✕ and ⛶ stay on screen and ⛶ toggles.
D webdav move: bulk Move asked for the destination with prompt(), which returns null in iOS
standalone PWAs and most Android WebViews, so it silently did nothing and looked unimplemented.
Destination is now an inline row in the bulk bar (44px input + "Move here"/"Cancel", prefilled).
Verified on the served code: ext bundle md5 5f440c77b6ab5529729aee8019a6c351 (served == local),
webdav.js carries wdav-move-dest x2, and a headless browser probe on the real page shows the toolbar
as ["✕","⛶","📋","🔗"], closing the preview prints "Preview closed — selection kept" with the
selection intact, selecting 3 files and copying yields a textarea with the 3 real WebDAV URLs, and
Download with nothing selected explains what to do. 6 PASS on the URL test, no-undef clean,
v4.6.99, help 362/362.
Not done, deliberately: cut/copy + paste across the VFS/WebDAV border. Recorded as Issue D2 in the
plan with the exact insertion points, the upload-then-delete ordering rule and the failure case.
| -rw-r--r-- | main.html | 6 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/ext/critical-ext-bundle.js | 150 | ||||
| -rw-r--r-- | modules/ext/filemgr.js | 150 | ||||
| -rw-r--r-- | modules/ext/webdav.js | 51 |
5 files changed, 242 insertions, 117 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.95"></script> +<script src="modules/core/core-bundle.js?v=v4.6.99"></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.95"></script> +<script src="modules/core/module-loader.js?v=v4.6.99"></script> <!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work --> -<script src="modules/ext/critical-ext-bundle.js?v=v4.6.95"></script> +<script src="modules/ext/critical-ext-bundle.js?v=v4.6.99"></script> <script> // ═══ ONE TRUE VERSION SOURCE ═══ diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 92f08fa..5056cdb 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.95'; + const LATEST_VER = 'v4.6.99'; 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 0c92381..8e968fb 100644 --- a/modules/ext/critical-ext-bundle.js +++ b/modules/ext/critical-ext-bundle.js @@ -1473,6 +1473,20 @@ class FileManagerModule { downloadBtn.onclick = () => this._actionDownload(); actionBar.appendChild(downloadBtn); + // 2026-10-26 (phone test): share the whole selection — the preview toolbar could only ever + // share the one previewed file, so three selected files produced one link. + const shareSelBtn = document.createElement('button'); + shareSelBtn.textContent = '🔗 Share'; + shareSelBtn.title = 'Share or copy the selected files'; + shareSelBtn.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;'; + shareSelBtn.onclick = () => { + let t = this._getSelectedPaths ? this._getSelectedPaths() : []; + if (!t.length && this._selectedFile) t = [this._selectedFile]; + if (!t.length) { this._updateStatus('🔗 Nothing selected — tap ☐ Select then tap files'); return; } + this._sharePaths(t); + }; + actionBar.appendChild(shareSelBtn); + 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;'; @@ -1908,15 +1922,77 @@ class FileManagerModule { // 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:10px;justify-content:flex-end;' + + // 2026-10-26 (phone test): the pane had no close control at all — the only ✕ belonged to the + // whole file manager and wiped _multiSelect on the way out, so "select 3 files → Download" + // ended in "No file selected to download". + const btn = (cls, label, title) => + '<button class="' + cls + '" title="' + title + '" aria-label="' + title + '" style="background:var(--bg-tertiary);' + + 'border:1px solid var(--border-color);color:var(--text-color);padding:0 10px;border-radius:4px;' + + 'cursor:pointer;font-size:1em;min-height:44px;min-width:44px;touch-action:manipulation;">' + label + '</button>'; + return '<div class="fm-preview-toolbar" style="display:flex;gap:10px;justify-content:space-between;align-items:center;' + '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:0 10px;border-radius:4px;' + - 'cursor:pointer;font-size:1em;min-height:44px;min-width:44px;touch-action:manipulation;">⛶</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:0 10px;border-radius:4px;' + - 'cursor:pointer;font-size:1em;min-height:44px;min-width:44px;touch-action:manipulation;">🔗</button>' + - '</div><div class="fm-preview-body" style="flex:1;min-height:0;overflow:auto">' + innerHtml + '</div>'; + btn('fm-btn-close', '✕', 'Close preview') + + '<div style="display:flex;gap:10px;align-items:center">' + + btn('fm-btn-fs', '⛶', 'Fullscreen preview') + + btn('fm-btn-copy', '📋', 'Copy link') + + btn('fm-btn-share', '🔗', 'Share link') + + '</div></div><div class="fm-preview-body" style="flex:1;min-height:0;overflow:auto">' + innerHtml + '</div>'; + } + + // 2026-10-26: one entry point for both the preview toolbar (1 path) and the action bar (N paths). + _sharePaths(paths, mode) { + if (!paths || !paths.length) { this._updateStatus('🔗 Nothing selected — tap ☐ Select then tap files'); return; } + const urls = (paths || []).map(p => this._previewShareUrl(p)).filter(Boolean); + if (!urls.length) { this._updateStatus('🔗 Share unavailable — no space resolved'); return; } + const n = urls.length, text = urls.join('\n'); + const copy = () => { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text) + .then(() => this._updateStatus('📋 Copied ' + n + (n === 1 ? ' link' : ' links'))) + .catch(() => this._inlineLinkBox(text, n)); + } else { this._inlineLinkBox(text, n); } + }; + if (mode === 'copy') { copy(); return; } + if (navigator.share) { + const payload = n === 1 ? { url: urls[0] } : { title: n + ' newOS4 files', text }; + navigator.share(payload) + .then(() => this._updateStatus('🔗 Shared ' + n + (n === 1 ? ' link' : ' links'))) + .catch(copy); + return; + } + copy(); + } + + // Visible, selectable fallback: prompt() cannot appear in iOS standalone PWAs or most WebViews. + _inlineLinkBox(text, n) { + const pane = document.getElementById('filemgr-preview'); + const bar = pane ? pane.querySelector('.fm-preview-toolbar') : null; + let box = document.querySelector('.fm-share-input'); + if (!box) { + box = document.createElement(n > 1 ? 'textarea' : 'input'); + box.className = 'fm-share-input'; + box.readOnly = true; + box.setAttribute('aria-label', n > 1 ? 'Share links' : 'Share link'); + box.style.cssText = 'flex:1;min-width:0;min-height:44px;padding:6px 10px;font-size:1em;box-sizing:border-box;' + + 'border:1px solid var(--border-color);border-radius:4px;background:var(--bg-primary);color:var(--text-color)'; + if (bar) { + bar.insertBefore(box, bar.firstChild); + } else { + // 2026-10-26: sharing from the action bar happens with no preview open — the fallback + // box needs its own row above the buttons, otherwise the URL is only in the status line. + const anchor = document.getElementById('filemgr-action-bar'); + const host = anchor && anchor.parentNode ? anchor.parentNode + : document.getElementById('filemgr-main-container'); + if (!host) { this._updateStatus('📋 ' + text); return; } + const row = document.createElement('div'); + row.className = 'fm-share-row'; + row.style.cssText = 'display:flex;gap:8px;padding:6px 8px;flex-shrink:0'; + row.appendChild(box); + host.insertBefore(row, anchor || null); + } + } + if (box) { box.value = text; box.focus(); box.select(); } + this._updateStatus('📋 Select and copy ' + n + (n === 1 ? ' link' : ' links')); } _previewShareUrl(path) { @@ -1958,49 +2034,19 @@ class FileManagerModule { 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(); + // 2026-10-26 (phone test): fullscreening .fm-preview-body hid the toolbar, so the only way + // out was the OS back gesture. Fullscreen the pane instead — ✕ and ⛶ stay on screen. + if (document.fullscreenElement) { document.exitFullscreen().catch(() => {}); return; } + const target = pane; + if (target.requestFullscreen) target.requestFullscreen().catch(() => this._updateStatus('⛶ Fullscreen blocked by this browser')); + else if (target.webkitRequestFullscreen) target.webkitRequestFullscreen(); else this._updateStatus('⛶ Fullscreen not supported here'); }; - if (shBtn) shBtn.onclick = () => { - const url = this._previewShareUrl(path); - if (!url) { this._updateStatus('🔗 Share unavailable — no space resolved'); return; } - // T3: prompt() is suppressed in iOS standalone PWAs and most Android WebViews, so the - // last resort is a real input the user can select — never a dialog that cannot appear. - const inlineFallback = () => { - const bar = pane.querySelector('.fm-preview-toolbar'); - let inp = pane.querySelector('.fm-share-input'); - if (!inp && bar) { - inp = document.createElement('input'); - inp.className = 'fm-share-input'; - inp.readOnly = true; - inp.setAttribute('aria-label', 'Share link'); - inp.style.cssText = 'flex:1;min-width:0;min-height:44px;padding:0 10px;font-size:1em;' + - 'border:1px solid var(--border-color);border-radius:4px;' + - 'background:var(--bg-primary);color:var(--text-color)'; - inp.value = url; - bar.insertBefore(inp, bar.firstChild); - } - if (inp) { inp.focus(); inp.select(); } - this._updateStatus('🔗 Select and copy'); - }; - const copyThen = () => { - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(url) - .then(() => this._updateStatus('🔗 Link copied: ' + url)) - .catch(inlineFallback); - } else { inlineFallback(); } - }; - if (navigator.share) { - navigator.share({ title: 'newOS4 file', url }) - .then(() => this._updateStatus('🔗 Shared')) - .catch(copyThen); - return; - } - copyThen(); - }; + const closeBtn = pane.querySelector('.fm-btn-close'); + if (closeBtn) closeBtn.onclick = () => { this._clearPreview(); this._updateStatus('Preview closed — selection kept'); }; + const copyBtn = pane.querySelector('.fm-btn-copy'); + if (copyBtn) copyBtn.onclick = () => this._sharePaths([path], 'copy'); + if (shBtn) shBtn.onclick = () => this._sharePaths([path]); } _renderPreviewError(msg) { @@ -2012,7 +2058,11 @@ class FileManagerModule { _clearPreview() { const preview = document.getElementById('filemgr-preview'); + if (document.fullscreenElement) { + document.exitFullscreen().catch(() => {}); + } if (preview) preview.innerHTML = ''; + this._updateSelectionStatus(); // selection survives closing the preview } _updateStatus(msg) { @@ -2033,7 +2083,7 @@ class FileManagerModule { // T2: honour the multi-selection the way every sibling action does (_getSelectedPaths). let targets = this._getSelectedPaths ? this._getSelectedPaths() : []; if (!targets || !targets.length) targets = this._selectedFile ? [this._selectedFile] : []; - if (!targets.length) { this._updateStatus('No file selected to download'); return; } + if (!targets.length) { this._updateStatus('⬇ Nothing selected — tap ☐ Select then tap files, or tap one file first'); return; } const saved = [], skipped = []; for (const p of targets) { const content = this.terminal.fs.readFile(p); diff --git a/modules/ext/filemgr.js b/modules/ext/filemgr.js index 120e07e..7412578 100644 --- a/modules/ext/filemgr.js +++ b/modules/ext/filemgr.js @@ -540,6 +540,20 @@ class FileManagerModule { downloadBtn.onclick = () => this._actionDownload(); actionBar.appendChild(downloadBtn); + // 2026-10-26 (phone test): share the whole selection — the preview toolbar could only ever + // share the one previewed file, so three selected files produced one link. + const shareSelBtn = document.createElement('button'); + shareSelBtn.textContent = '🔗 Share'; + shareSelBtn.title = 'Share or copy the selected files'; + shareSelBtn.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;'; + shareSelBtn.onclick = () => { + let t = this._getSelectedPaths ? this._getSelectedPaths() : []; + if (!t.length && this._selectedFile) t = [this._selectedFile]; + if (!t.length) { this._updateStatus('🔗 Nothing selected — tap ☐ Select then tap files'); return; } + this._sharePaths(t); + }; + actionBar.appendChild(shareSelBtn); + 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;'; @@ -975,15 +989,77 @@ class FileManagerModule { // 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:10px;justify-content:flex-end;' + + // 2026-10-26 (phone test): the pane had no close control at all — the only ✕ belonged to the + // whole file manager and wiped _multiSelect on the way out, so "select 3 files → Download" + // ended in "No file selected to download". + const btn = (cls, label, title) => + '<button class="' + cls + '" title="' + title + '" aria-label="' + title + '" style="background:var(--bg-tertiary);' + + 'border:1px solid var(--border-color);color:var(--text-color);padding:0 10px;border-radius:4px;' + + 'cursor:pointer;font-size:1em;min-height:44px;min-width:44px;touch-action:manipulation;">' + label + '</button>'; + return '<div class="fm-preview-toolbar" style="display:flex;gap:10px;justify-content:space-between;align-items:center;' + '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:0 10px;border-radius:4px;' + - 'cursor:pointer;font-size:1em;min-height:44px;min-width:44px;touch-action:manipulation;">⛶</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:0 10px;border-radius:4px;' + - 'cursor:pointer;font-size:1em;min-height:44px;min-width:44px;touch-action:manipulation;">🔗</button>' + - '</div><div class="fm-preview-body" style="flex:1;min-height:0;overflow:auto">' + innerHtml + '</div>'; + btn('fm-btn-close', '✕', 'Close preview') + + '<div style="display:flex;gap:10px;align-items:center">' + + btn('fm-btn-fs', '⛶', 'Fullscreen preview') + + btn('fm-btn-copy', '📋', 'Copy link') + + btn('fm-btn-share', '🔗', 'Share link') + + '</div></div><div class="fm-preview-body" style="flex:1;min-height:0;overflow:auto">' + innerHtml + '</div>'; + } + + // 2026-10-26: one entry point for both the preview toolbar (1 path) and the action bar (N paths). + _sharePaths(paths, mode) { + if (!paths || !paths.length) { this._updateStatus('🔗 Nothing selected — tap ☐ Select then tap files'); return; } + const urls = (paths || []).map(p => this._previewShareUrl(p)).filter(Boolean); + if (!urls.length) { this._updateStatus('🔗 Share unavailable — no space resolved'); return; } + const n = urls.length, text = urls.join('\n'); + const copy = () => { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text) + .then(() => this._updateStatus('📋 Copied ' + n + (n === 1 ? ' link' : ' links'))) + .catch(() => this._inlineLinkBox(text, n)); + } else { this._inlineLinkBox(text, n); } + }; + if (mode === 'copy') { copy(); return; } + if (navigator.share) { + const payload = n === 1 ? { url: urls[0] } : { title: n + ' newOS4 files', text }; + navigator.share(payload) + .then(() => this._updateStatus('🔗 Shared ' + n + (n === 1 ? ' link' : ' links'))) + .catch(copy); + return; + } + copy(); + } + + // Visible, selectable fallback: prompt() cannot appear in iOS standalone PWAs or most WebViews. + _inlineLinkBox(text, n) { + const pane = document.getElementById('filemgr-preview'); + const bar = pane ? pane.querySelector('.fm-preview-toolbar') : null; + let box = document.querySelector('.fm-share-input'); + if (!box) { + box = document.createElement(n > 1 ? 'textarea' : 'input'); + box.className = 'fm-share-input'; + box.readOnly = true; + box.setAttribute('aria-label', n > 1 ? 'Share links' : 'Share link'); + box.style.cssText = 'flex:1;min-width:0;min-height:44px;padding:6px 10px;font-size:1em;box-sizing:border-box;' + + 'border:1px solid var(--border-color);border-radius:4px;background:var(--bg-primary);color:var(--text-color)'; + if (bar) { + bar.insertBefore(box, bar.firstChild); + } else { + // 2026-10-26: sharing from the action bar happens with no preview open — the fallback + // box needs its own row above the buttons, otherwise the URL is only in the status line. + const anchor = document.getElementById('filemgr-action-bar'); + const host = anchor && anchor.parentNode ? anchor.parentNode + : document.getElementById('filemgr-main-container'); + if (!host) { this._updateStatus('📋 ' + text); return; } + const row = document.createElement('div'); + row.className = 'fm-share-row'; + row.style.cssText = 'display:flex;gap:8px;padding:6px 8px;flex-shrink:0'; + row.appendChild(box); + host.insertBefore(row, anchor || null); + } + } + if (box) { box.value = text; box.focus(); box.select(); } + this._updateStatus('📋 Select and copy ' + n + (n === 1 ? ' link' : ' links')); } _previewShareUrl(path) { @@ -1025,49 +1101,19 @@ class FileManagerModule { 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(); + // 2026-10-26 (phone test): fullscreening .fm-preview-body hid the toolbar, so the only way + // out was the OS back gesture. Fullscreen the pane instead — ✕ and ⛶ stay on screen. + if (document.fullscreenElement) { document.exitFullscreen().catch(() => {}); return; } + const target = pane; + if (target.requestFullscreen) target.requestFullscreen().catch(() => this._updateStatus('⛶ Fullscreen blocked by this browser')); + else if (target.webkitRequestFullscreen) target.webkitRequestFullscreen(); else this._updateStatus('⛶ Fullscreen not supported here'); }; - if (shBtn) shBtn.onclick = () => { - const url = this._previewShareUrl(path); - if (!url) { this._updateStatus('🔗 Share unavailable — no space resolved'); return; } - // T3: prompt() is suppressed in iOS standalone PWAs and most Android WebViews, so the - // last resort is a real input the user can select — never a dialog that cannot appear. - const inlineFallback = () => { - const bar = pane.querySelector('.fm-preview-toolbar'); - let inp = pane.querySelector('.fm-share-input'); - if (!inp && bar) { - inp = document.createElement('input'); - inp.className = 'fm-share-input'; - inp.readOnly = true; - inp.setAttribute('aria-label', 'Share link'); - inp.style.cssText = 'flex:1;min-width:0;min-height:44px;padding:0 10px;font-size:1em;' + - 'border:1px solid var(--border-color);border-radius:4px;' + - 'background:var(--bg-primary);color:var(--text-color)'; - inp.value = url; - bar.insertBefore(inp, bar.firstChild); - } - if (inp) { inp.focus(); inp.select(); } - this._updateStatus('🔗 Select and copy'); - }; - const copyThen = () => { - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(url) - .then(() => this._updateStatus('🔗 Link copied: ' + url)) - .catch(inlineFallback); - } else { inlineFallback(); } - }; - if (navigator.share) { - navigator.share({ title: 'newOS4 file', url }) - .then(() => this._updateStatus('🔗 Shared')) - .catch(copyThen); - return; - } - copyThen(); - }; + const closeBtn = pane.querySelector('.fm-btn-close'); + if (closeBtn) closeBtn.onclick = () => { this._clearPreview(); this._updateStatus('Preview closed — selection kept'); }; + const copyBtn = pane.querySelector('.fm-btn-copy'); + if (copyBtn) copyBtn.onclick = () => this._sharePaths([path], 'copy'); + if (shBtn) shBtn.onclick = () => this._sharePaths([path]); } _renderPreviewError(msg) { @@ -1079,7 +1125,11 @@ class FileManagerModule { _clearPreview() { const preview = document.getElementById('filemgr-preview'); + if (document.fullscreenElement) { + document.exitFullscreen().catch(() => {}); + } if (preview) preview.innerHTML = ''; + this._updateSelectionStatus(); // selection survives closing the preview } _updateStatus(msg) { @@ -1100,7 +1150,7 @@ class FileManagerModule { // T2: honour the multi-selection the way every sibling action does (_getSelectedPaths). let targets = this._getSelectedPaths ? this._getSelectedPaths() : []; if (!targets || !targets.length) targets = this._selectedFile ? [this._selectedFile] : []; - if (!targets.length) { this._updateStatus('No file selected to download'); return; } + if (!targets.length) { this._updateStatus('⬇ Nothing selected — tap ☐ Select then tap files, or tap one file first'); return; } const saved = [], skipped = []; for (const p of targets) { const content = this.terminal.fs.readFile(p); diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js index ed312a5..88f1f59 100644 --- a/modules/ext/webdav.js +++ b/modules/ext/webdav.js @@ -1962,19 +1962,44 @@ class WebDAVModule { document.getElementById('bulk-move')?.addEventListener('click', function() { const names = Object.entries(self._selected).filter(function(e) { return e[1]; }).map(function(e) { return e[0]; }); if (!names.length) return; - let target = prompt('Move ' + names.length + ' item(s) to directory:', '/'); - if (!target) return; - target = target.replace(/\/+$/, ''); - names.forEach(function(name) { - const oldFull = (path.replace(/\/+$/, '') || '') + '/' + name; - const newFull = target + '/' + name; - self._webdavRequest('MOVE', oldFull, { headers: { 'Destination': self._absoluteUrl(newFull) } }) - .then(function(r) { - if (r.ok || r.status === 201 || r.status === 204) self.terminal.print('Moved: ' + name, 'success'); - else self.terminal.print('Move failed: ' + name + ' (HTTP ' + r.status + ')', 'error'); - }); - }); - setTimeout(function() { self.openWebdavBrowser(path); }, 500); + // 2026-10-26 (phone test): prompt() is suppressed in iOS standalone PWAs and most + // Android WebViews — it returns null, so "Move" looked like a missing feature. + // Ask for the destination inline in the bulk bar instead. + const doMove = function(rawTarget) { + const target = String(rawTarget || '').trim().replace(/\/+$/, ''); + if (!target) { self.terminal.print('Move cancelled — no destination', 'error'); return; } + names.forEach(function(name) { + const oldFull = (path.replace(/\/+$/, '') || '') + '/' + name; + const newFull = target + '/' + name; + self._webdavRequest('MOVE', oldFull, { headers: { 'Destination': self._absoluteUrl(newFull) } }) + .then(function(r) { + if (r.ok || r.status === 201 || r.status === 204) self.terminal.print('Moved: ' + name + ' → ' + newFull, 'success'); + else self.terminal.print('Move failed: ' + name + ' (HTTP ' + r.status + ')', 'error'); + }) + .catch(function(e) { self.terminal.print('Move failed: ' + name + ' — ' + (e && e.message ? e.message : e), 'error'); }); + }); + setTimeout(function() { self.openWebdavBrowser(target); }, 600); + }; + const bar = document.getElementById('wdav-bulk-bar'); + let row = document.getElementById('wdav-move-row'); + if (row) { row.remove(); row = null; } + if (!bar) { doMove(path || '/'); return; } + row = document.createElement('div'); + row.id = 'wdav-move-row'; + row.style.cssText = 'display:flex;gap:8px;align-items:center;width:100%;padding-top:8px;flex-wrap:wrap'; + row.innerHTML = '<span style="font-size:0.9em;color:var(--text-color)">Move ' + names.length + + ' to:</span><input id="wdav-move-dest" type="text" aria-label="Destination directory" ' + + 'style="flex:1;min-width:120px;min-height:44px;padding:6px 10px;font-size:1em;border-radius:4px;' + + 'border:1px solid var(--border);background:var(--bg-primary);color:var(--text-color)">' + + '<button class="btn" id="wdav-move-go" style="min-height:44px;padding:0 14px">Move here</button>' + + '<button class="btn" id="wdav-move-cancel" style="min-height:44px;padding:0 14px">Cancel</button>'; + bar.appendChild(row); + const dest = row.querySelector('#wdav-move-dest'); + dest.value = (path || '/').replace(/\/+$/, '') || '/'; + row.querySelector('#wdav-move-go').onclick = function() { const v = dest.value; row.remove(); doMove(v); }; + row.querySelector('#wdav-move-cancel').onclick = function() { row.remove(); }; + dest.focus(); + dest.select(); }); 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]; }); |