/* Put a video file on the device itself (Photos / Files / Downloads). * Pure helpers + the three ways a browser can do it; the app decides which * source (saved copy, server copy, other devices) the bytes come from. * DeviceExport.fileName(title, ext) → safe "Title.ext" * DeviceExport.canShareFile(file) → Web Share with files works here * DeviceExport.canPickLocation() → "Save as…" dialog exists (desktop Chromium) * DeviceExport.share(file, name, title) → share sheet ("Save Video" → Photos / Files) * DeviceExport.pick(file, name) → stream into a user-chosen file * DeviceExport.download(urlOrFile, name) → plain browser download */ (function (root) { 'use strict'; const BAD = /[\u0000-\u001f\\/:*?"<>|]+/g; function fileName(title, ext, fallback) { const base = String(title || '').replace(BAD, ' ').replace(/\s+/g, ' ').trim().slice(0, 120) || String(fallback || 'video'); const e = String(ext || 'mp4').replace(/^\./, '').toLowerCase().replace(/[^a-z0-9]/g, '') || 'mp4'; return `${base}.${e}`; } const mimeFor = (name) => (/\.(m4a|mp3|aac|opus|ogg|flac|wav)$/i.test(name) ? 'audio/mp4' : /\.webm$/i.test(name) ? 'video/webm' : 'video/mp4'); const typed = (file, name) => (file.type ? file : new File([file], name, { type: mimeFor(name) })); function canShareFile(file) { try { if (!navigator.canShare || !navigator.share || !file) return false; return navigator.canShare({ files: [typed(file, file.name || 'video.mp4')] }); } catch { return false; } } const canPickLocation = () => typeof root.showSaveFilePicker === 'function' && !/Android|iPhone|iPad/i.test(navigator.userAgent || ''); // Resolves 'shared' | 'cancelled'; rejects on a real failure. async function share(file, name, title) { // new File() over an OPFS file does not copy it — it stays disk-backed. const f = new File([file], name, { type: file.type || mimeFor(name) }); try { await navigator.share({ files: [f], title: title || name }); return 'shared'; } catch (e) { if (e && e.name === 'AbortError') return 'cancelled'; throw e; } } async function pick(file, name) { let handle; try { handle = await root.showSaveFilePicker({ suggestedName: name, types: [{ description: 'Video', accept: { [mimeFor(name)]: ['.' + name.split('.').pop()] } }] }); } catch (e) { if (e && e.name === 'AbortError') return 'cancelled'; throw e; } const w = await handle.createWritable(); await file.stream().pipeTo(w); // chunked: a multi-GB file is never held in memory return 'saved'; } // A File/Blob → object URL; a string → used as the href directly (server copy). function download(src, name) { const isStr = typeof src === 'string'; const href = isStr ? src : URL.createObjectURL(src); const a = document.createElement('a'); a.href = href; a.download = name; a.rel = 'noopener'; document.body.appendChild(a); a.click(); a.remove(); if (!isStr) setTimeout(() => URL.revokeObjectURL(href), 10 * 60 * 1000); return 'started'; } root.DeviceExport = { fileName, canShareFile, canPickLocation, share, pick, download }; if (typeof module !== 'undefined' && module.exports) module.exports = root.DeviceExport; })(typeof window !== 'undefined' ? window : globalThis);