aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/critical-ext-bundle.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext/critical-ext-bundle.js')
-rw-r--r--modules/ext/critical-ext-bundle.js142
1 files changed, 105 insertions, 37 deletions
diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js
index dc3a858..0c92381 100644
--- a/modules/ext/critical-ext-bundle.js
+++ b/modules/ext/critical-ext-bundle.js
@@ -931,6 +931,38 @@ if (typeof window.webx11 !== 'undefined' && window.webx11) {
}
/* === filemgr.js === */
+// 2026-10-24: pure URL helpers, require-able so modules/test/test-download-url.js tests the
+// shipped code instead of a copy of it. No DOM, no terminal, no module state.
+function fmDetectSpaceName(pathname) {
+ try {
+ const parts = String(pathname || '').split('/').filter(Boolean);
+ const i = parts.indexOf('spaces');
+ return (i >= 0 && i + 1 < parts.length) ? parts[i + 1] : '';
+ } catch (e) { return ''; }
+}
+
+function fmExtractSpaceNameFromUrl(url) {
+ if (!url) return '';
+ try {
+ const parts = new URL(url).pathname.split('/').filter(Boolean);
+ const i = parts.indexOf('spaces');
+ return (i >= 0 && i + 1 < parts.length) ? parts[i + 1] : '';
+ } catch (e) { return ''; }
+}
+
+// Returns null when no space can be resolved — never emits /spaces//<path>.
+function fmBuildShareUrl(space, path) {
+ if (!space) return null;
+ const clean = String(path || '').replace(/^\/+/, '');
+ if (!clean) return null;
+ return 'https://webx11.duckdns.org/spaces/' + space + '/' +
+ clean.split('/').map(encodeURIComponent).join('/');
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = { fmDetectSpaceName, fmExtractSpaceNameFromUrl, fmBuildShareUrl };
+}
+
class FileManagerModule {
constructor() {
this.commands = {};
@@ -1876,24 +1908,25 @@ 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:6px;justify-content:flex-end;' +
+ return '<div class="fm-preview-toolbar" style="display:flex;gap:10px;justify-content:flex-end;' +
'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:2px 8px;border-radius:4px;' +
- 'cursor:pointer;font-size:0.85em;min-height:24px;">⛶</button>' +
+ '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:2px 8px;border-radius:4px;' +
- 'cursor:pointer;font-size:0.85em;min-height:24px;">🔗</button>' +
+ '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>';
}
_previewShareUrl(path) {
const wdav = this.terminal && this.terminal.moduleManager && this.terminal.moduleManager.modules
? this.terminal.moduleManager.modules.WebDAVModule : null;
- const space = (wdav && wdav._resolveSpaceName && wdav._resolveSpaceName()) || '';
- const clean = String(path).replace(/^\/+/, '');
- return 'https://webx11.duckdns.org/spaces/' + space + '/' +
- clean.split('/').map(encodeURIComponent).join('/');
+ let space = (wdav && wdav._resolveSpaceName && wdav._resolveSpaceName()) || '';
+ // T5: the fallback detector already shipped in modules/ext/webdav.js:22 — use it.
+ if (!space && wdav && wdav._detectSpaceName) space = wdav._detectSpaceName() || '';
+ if (!space) space = fmDetectSpaceName(window.location.pathname);
+ return fmBuildShareUrl(space, path); // null when nothing resolves — caller reports it
}
// 2026-10-21: publish VFS file(s) to WebDAV (was terminal-only: webdav-push)
@@ -1933,13 +1966,40 @@ class FileManagerModule {
};
if (shBtn) shBtn.onclick = () => {
const url = this._previewShareUrl(path);
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(url)
- .then(() => this._updateStatus('🔗 Link copied: ' + url))
- .catch(() => prompt('Copy share link:', url));
- } else {
- prompt('Copy share link:', url);
+ 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();
};
}
@@ -1970,26 +2030,31 @@ class FileManagerModule {
}
_actionDownload() {
- if (!this._selectedFile) {
- this._updateStatus('No file selected to download');
- return;
- }
- const content = this.terminal.fs.readFile(this._selectedFile);
- if (content === null || content === false) {
- this._updateStatus('❌ File not found');
- return;
- }
- const fileName = this._selectedFile.split('/').pop() || 'file';
- const blob = new Blob([content], { type: 'application/octet-stream' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = fileName;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- this._updateStatus('⬇ Downloaded: ' + fileName);
+ // 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; }
+ const saved = [], skipped = [];
+ for (const p of targets) {
+ const content = this.terminal.fs.readFile(p);
+ if (content === null || content === false) { skipped.push(p); continue; }
+ const fileName = p.split('/').pop() || 'file';
+ const blob = new Blob([content], { type: 'application/octet-stream' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fileName;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ // T1: iOS Safari / Android Chrome hand blob: to the download manager asynchronously.
+ // Revoking in the same tick aborts the transfer while the UI still said "Downloaded".
+ setTimeout(() => URL.revokeObjectURL(url), 30000);
+ saved.push(fileName);
+ }
+ if (saved.length > 1) this._updateStatus('⬇ Downloaded ' + saved.length + ' files');
+ else if (saved.length === 1) this._updateStatus('⬇ Downloaded: ' + saved[0]);
+ if (skipped.length) this._updateStatus('❌ Skipped ' + skipped.join(', '));
}
_actionEdit() {
@@ -2078,8 +2143,11 @@ class FileManagerModule {
}
}
-if (typeof window.webx11 !== 'undefined' && window.webx11) {
- webx11.moduleManager.registerModule(new FileManagerModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/filemgr.js');
+// 2026-10-24: `typeof window.webx11` throws under Node (member access on an undefined global),
+// which would make the module un-require-able by modules/test/test-download-url.js.
+if (typeof window !== 'undefined' && window.webx11 && window.webx11.moduleManager) {
+ window.webx11.moduleManager.registerModule(new FileManagerModule(),
+ 'https://webx11.duckdns.org/spaces/newos4/modules/ext/filemgr.js');
}
/* === diff.js === */