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.js150
-rw-r--r--modules/ext/filemgr.js150
-rw-r--r--modules/ext/webdav.js51
5 files changed, 242 insertions, 117 deletions
diff --git a/main.html b/main.html
index 1249536..99f6324 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.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]; });