From 7904f3e9300a4ee02243728c185e8bfce0953bb1 Mon Sep 17 00:00:00 2001 From: Agando Date: Mon, 26 Oct 2026 15:21:14 +0100 Subject: fix(newos4): four phone-test defects — no preview close, single-file share, dead-end fullscreen, silent WebDAV move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- modules/ext/critical-ext-bundle.js | 150 ++++++++++++++++++++++++------------- 1 file changed, 100 insertions(+), 50 deletions(-) (limited to 'modules/ext/critical-ext-bundle.js') 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 '
+ ''; + return '
' + - '' + - '' + - '
' + innerHtml + '
'; + btn('fm-btn-close', '✕', 'Close preview') + + '
' + + btn('fm-btn-fs', '⛶', 'Fullscreen preview') + + btn('fm-btn-copy', '📋', 'Copy link') + + btn('fm-btn-share', '🔗', 'Share link') + + '
' + innerHtml + '
'; + } + + // 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); -- cgit v1.2.3