// filePreview.js import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}'; import { t } from './i18n.js?v={{APP_QVER}}'; import { fileData, setFileProgressBadge, setFileWatchedBadge } from './fileListView.js?v={{APP_QVER}}'; // Build a preview URL that always goes through the API layer (respects ACLs/UPLOAD_DIR) export function buildPreviewUrl(folder, name) { const f = (!folder || folder === '') ? 'root' : String(folder); return `/api/file/download.php?folder=${encodeURIComponent(f)}&file=${encodeURIComponent(name)}&inline=1&t=${Date.now()}`; } // New: build a download URL (attachment) export function buildDownloadUrl(folder, name) { const f = (!folder || folder === '') ? 'root' : String(folder); const params = new URLSearchParams({ folder: f, file: name, inline: '0', t: String(Date.now()) }); return `/api/file/download.php?${params.toString()}`; } const MEDIA_VOLUME_KEY = 'frMediaVolume'; const MEDIA_MUTED_KEY = 'frMediaMuted'; function loadSavedMediaVolume(el) { if (!el) return; try { const v = localStorage.getItem(MEDIA_VOLUME_KEY); if (v !== null) { const vol = parseFloat(v); if (!Number.isNaN(vol)) { el.volume = Math.max(0, Math.min(1, vol)); } } const m = localStorage.getItem(MEDIA_MUTED_KEY); if (m !== null) { el.muted = (m === '1'); } } catch { // ignore storage errors } } function attachVolumePersistence(el) { if (!el) return; try { el.addEventListener('volumechange', () => { try { localStorage.setItem(MEDIA_VOLUME_KEY, String(el.volume)); localStorage.setItem(MEDIA_MUTED_KEY, el.muted ? '1' : '0'); } catch { // ignore storage errors } }); } catch { // ignore } } /* -------------------------------- Share modal (existing) -------------------------------- */ export function openShareModal(file, folder) { const existing = document.getElementById("shareModal"); if (existing) existing.remove(); const modal = document.createElement("div"); modal.id = "shareModal"; modal.classList.add("modal"); modal.innerHTML = ` `; document.body.appendChild(modal); modal.style.display = "block"; document.getElementById("closeShareModal").addEventListener("click", () => modal.remove()); document.getElementById("shareExpiration").addEventListener("change", e => { const container = document.getElementById("customExpirationContainer"); container.style.display = e.target.value === "custom" ? "block" : "none"; }); document.getElementById("generateShareLinkBtn").addEventListener("click", () => { const sel = document.getElementById("shareExpiration"); let value, unit; if (sel.value === "custom") { value = parseInt(document.getElementById("customExpirationValue").value, 10); unit = document.getElementById("customExpirationUnit").value; } else { value = parseInt(sel.value, 10); unit = "minutes"; } const password = document.getElementById("sharePassword").value; fetch("/api/file/createShareLink.php", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, body: JSON.stringify({ folder, file: file.name, expirationValue: value, expirationUnit: unit, password }) }) .then(res => res.json()) .then(data => { if (data.token) { const url = `${window.location.origin}/api/file/share.php?token=${encodeURIComponent(data.token)}`; document.getElementById("shareLinkInput").value = url; document.getElementById("shareLinkDisplay").style.display = "block"; } else { showToast(t("error_generating_share") + ": " + (data.error || "Unknown")); } }) .catch(err => { console.error(err); showToast(t("error_generating_share")); }); }); document.getElementById("copyShareLinkBtn").addEventListener("click", () => { const input = document.getElementById("shareLinkInput"); input.select(); document.execCommand("copy"); showToast(t("link_copied")); }); } /* -------------------------------- Media modal viewer -------------------------------- */ // Images that are safe to inline in tags: const IMG_RE = /\.(jpg|jpeg|png|gif|bmp|webp|ico)$/i; // SVG handled separately so we *don’t* inline it const SVG_RE = /\.svg$/i; const VID_RE = /\.(mp4|mkv|webm|mov|ogv)$/i; const AUD_RE = /\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i; const ARCH_RE = /\.(zip|rar|7z|gz|bz2|xz|tar)$/i; const CODE_RE = /\.(js|mjs|ts|tsx|json|yml|yaml|xml|html?|css|scss|less|php|py|rb|go|rs|c|cpp|h|hpp|java|cs|sh|bat|ps1)$/i; const TXT_RE = /\.(txt|rtf|md|log)$/i; function getIconForFile(name) { const lower = (name || '').toLowerCase(); if (IMG_RE.test(lower)) return 'image'; if (VID_RE.test(lower)) return 'ondemand_video'; if (AUD_RE.test(lower)) return 'audiotrack'; if (lower.endsWith('.pdf')) return 'picture_as_pdf'; if (ARCH_RE.test(lower)) return 'archive'; if (CODE_RE.test(lower)) return 'code'; if (TXT_RE.test(lower)) return 'description'; return 'insert_drive_file'; } function ensureMediaModal() { let overlay = document.getElementById("filePreviewModal"); if (overlay) return overlay; overlay = document.createElement("div"); overlay.id = "filePreviewModal"; Object.assign(overlay.style, { position: "fixed", inset: "0", width: "100vw", height: "100vh", backgroundColor: "rgba(0,0,0,0.7)", display: "flex", justifyContent: "center", alignItems: "center", zIndex: "1000" }); const root = document.documentElement; const styles = getComputedStyle(root); const isDark = root.classList.contains('dark-mode'); const panelBg = styles.getPropertyValue('--panel-bg').trim() || styles.getPropertyValue('--bg-color').trim() || (isDark ? '#2c2c2c' : '#ffffff'); const textCol = styles.getPropertyValue('--text-color').trim() || (isDark ? '#eaeaea' : '#111111'); const navBg = isDark ? 'rgba(255,255,255,.28)' : 'rgba(0,0,0,.45)'; const navFg = '#fff'; const navBorder = isDark ? 'rgba(255,255,255,.35)' : 'rgba(0,0,0,.25)'; // fixed top bar; pad-right to avoid overlap with absolute close “×” overlay.innerHTML = ` `; document.body.appendChild(overlay); // Ensure a container for tags next to the title (created once) (function ensureTitleTagsContainer() { const titleRow = overlay.querySelector('.media-title'); if (!titleRow) return; let tagsEl = overlay.querySelector('.title-tags'); if (!tagsEl) { tagsEl = document.createElement('div'); tagsEl.className = 'title-tags'; Object.assign(tagsEl.style, { display: 'flex', flexWrap: 'wrap', gap: '4px', marginLeft: '6px', maxHeight: '32px', overflow: 'hidden', }); titleRow.appendChild(tagsEl); } })(); // theme the close “×” for visibility + hover rules that match your site: const closeBtn = overlay.querySelector("#closeFileModal"); function paintCloseBase() { closeBtn.style.backgroundColor = 'transparent'; closeBtn.style.color = '#e11d48'; // base red X closeBtn.style.boxShadow = 'none'; } function onCloseHoverEnter() { const dark = document.documentElement.classList.contains('dark-mode'); closeBtn.style.backgroundColor = '#ef4444'; // red fill closeBtn.style.color = dark ? '#000' : '#fff'; // X: black in dark / white in light closeBtn.style.boxShadow = '0 0 6px rgba(239,68,68,.6)'; } function onCloseHoverLeave() { paintCloseBase(); } paintCloseBase(); closeBtn.addEventListener('mouseenter', onCloseHoverEnter); closeBtn.addEventListener('mouseleave', onCloseHoverLeave); function closeModal() { try { overlay.querySelectorAll("video,audio").forEach(m => { try{m.pause()}catch(_){}}); } catch {} if (overlay._onKey) window.removeEventListener('keydown', overlay._onKey); overlay.remove(); } closeBtn.addEventListener("click", closeModal); overlay.addEventListener("click", (e) => { if (e.target === overlay) closeModal(); }); return overlay; } function setTitle(overlay, name) { const textEl = overlay.querySelector('.title-text'); const iconEl = overlay.querySelector('.title-icon'); const tagsEl = overlay.querySelector('.title-tags'); // File name + tooltip if (textEl) { textEl.textContent = name || ''; textEl.setAttribute('title', name || ''); } // File type icon if (iconEl) { iconEl.textContent = getIconForFile(name); const dark = document.documentElement.classList.contains('dark-mode'); iconEl.style.color = dark ? '#f5f5f5' : '#111111'; iconEl.style.opacity = dark ? '0.96' : '0.9'; } // Tag badges next to the title if (tagsEl) { tagsEl.innerHTML = ''; let fileObj = null; if (Array.isArray(fileData)) { fileObj = fileData.find(f => f.name === name); } if (fileObj && Array.isArray(fileObj.tags) && fileObj.tags.length) { fileObj.tags.forEach(tag => { const badge = document.createElement('span'); badge.textContent = tag.name; badge.style.backgroundColor = tag.color || '#444'; badge.style.color = '#fff'; badge.style.padding = '2px 6px'; badge.style.borderRadius = '999px'; badge.style.fontSize = '0.75rem'; badge.style.lineHeight = '1.2'; badge.style.whiteSpace = 'nowrap'; tagsEl.appendChild(badge); }); } } } // New: Download icon that uses current file name function makeDownloadButton(folder, getName) { const btn = makeTopIcon('download', t('download') || 'Download'); btn.addEventListener('click', (e) => { e.stopPropagation(); const nm = getName && getName(); if (!nm) return; const url = buildDownloadUrl(folder, nm); // Use a temporary with download attribute for nicer behavior const a = document.createElement('a'); a.href = url; a.download = nm; document.body.appendChild(a); a.click(); a.remove(); }); return btn; } // Topbar icon (theme-aware) used for image tools + video actions function makeTopIcon(name, title) { const b = document.createElement('button'); b.className = 'material-icons'; b.textContent = name; b.title = title; const dark = document.documentElement.classList.contains('dark-mode'); Object.assign(b.style, { width: '32px', height: '32px', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: dark ? '1px solid rgba(255,255,255,.25)' : '1px solid rgba(0,0,0,.15)', background: dark ? 'rgba(255,255,255,.14)' : 'rgba(0,0,0,.08)', cursor: 'pointer', fontSize: '20px', lineHeight: '1', color: dark ? '#f5f5f5' : '#111', boxShadow: dark ? '0 1px 2px rgba(0,0,0,.6)' : '0 1px 1px rgba(0,0,0,.08)' }); b.addEventListener('mouseenter', () => { const darkNow = document.documentElement.classList.contains('dark-mode'); b.style.background = darkNow ? 'rgba(255,255,255,.22)' : 'rgba(0,0,0,.14)'; }); b.addEventListener('mouseleave', () => { const darkNow = document.documentElement.classList.contains('dark-mode'); b.style.background = darkNow ? 'rgba(255,255,255,.14)' : 'rgba(0,0,0,.08)'; }); return b; } function setNavVisibility(overlay, showPrev, showNext) { const prev = overlay.querySelector('.nav-left'); const next = overlay.querySelector('.nav-right'); prev.style.display = showPrev ? 'flex' : 'none'; next.style.display = showNext ? 'flex' : 'none'; } function setRowWatchedBadge(name, watched) { try { const cell = document.querySelector(`tr[data-file-name="${CSS.escape(name)}"] .name-cell`); if (!cell) return; const old = cell.querySelector('.status-badge.watched'); if (watched) { if (!old) { const b = document.createElement('span'); b.className = 'status-badge watched'; b.textContent = t("watched") || t("viewed") || "Watched"; b.style.marginLeft = "6px"; cell.appendChild(b); } } else if (old) { old.remove(); } } catch {} } /* -------------------------------- Entry -------------------------------- */ export function previewFile(fileUrl, fileName) { const overlay = ensureMediaModal(); const container = overlay.querySelector(".file-preview-container"); const actionWrap = overlay.querySelector(".media-right .action-group"); const statusChip = overlay.querySelector(".media-right .status-chip"); // replace nav buttons to clear old listeners let prevBtn = overlay.querySelector('.nav-left'); let nextBtn = overlay.querySelector('.nav-right'); const newPrev = prevBtn.cloneNode(true); const newNext = nextBtn.cloneNode(true); prevBtn.replaceWith(newPrev); nextBtn.replaceWith(newNext); prevBtn = newPrev; nextBtn = newNext; // reset container.innerHTML = ""; actionWrap.innerHTML = ""; if (statusChip) statusChip.style.display = 'none'; if (overlay._onKey) window.removeEventListener('keydown', overlay._onKey); overlay._onKey = null; const folder = window.currentFolder || 'root'; const name = fileName; const lower = (name || '').toLowerCase(); const isSvg = SVG_RE.test(lower); const isImage = IMG_RE.test(lower); const isVideo = VID_RE.test(lower); const isAudio = AUD_RE.test(lower); // Base preview URL from the link we clicked const baseUrl = fileUrl; // Use the same preview endpoint, just swap the "file" param. function siblingPreviewUrl(newName) { try { const u = new URL(baseUrl, window.location.origin); u.searchParams.set('file', newName); // cache-bust so we don’t get stale frames u.searchParams.set('t', String(Date.now())); return u.toString(); } catch { // Fallback: go through generic download/inline endpoint return buildPreviewUrl(folder, newName); } } setTitle(overlay, name); if (isSvg) { const downloadBtn = makeDownloadButton(folder, () => name); actionWrap.appendChild(downloadBtn); container.textContent = t("svg_preview_disabled") || "SVG preview is disabled for security. Use Download to view this file."; overlay.style.display = "flex"; return; } /* -------------------- IMAGES -------------------- */ if (isImage) { const img = document.createElement("img"); img.src = fileUrl; img.className = "image-modal-img"; img.style.maxWidth = "88vw"; img.style.maxHeight = "88vh"; img.style.transition = "transform 0.3s ease"; img.dataset.scale = 1; img.dataset.rotate = 0; container.appendChild(img); let currentName = name; // topbar-aligned, theme-aware icons const zoomInBtn = makeTopIcon('zoom_in', t('zoom_in') || 'Zoom In'); const zoomOutBtn = makeTopIcon('zoom_out', t('zoom_out') || 'Zoom Out'); const rotateLeft = makeTopIcon('rotate_left', t('rotate_left') || 'Rotate Left'); const rotateRight = makeTopIcon('rotate_right', t('rotate_right') || 'Rotate Right'); const downloadBtn = makeDownloadButton(folder, () => currentName); actionWrap.appendChild(downloadBtn); actionWrap.appendChild(zoomInBtn); actionWrap.appendChild(zoomOutBtn); actionWrap.appendChild(rotateLeft); actionWrap.appendChild(rotateRight); zoomInBtn.addEventListener('click', (e) => { e.stopPropagation(); let s = parseFloat(img.dataset.scale) || 1; s += 0.1; img.dataset.scale = s; img.style.transform = `scale(${s}) rotate(${img.dataset.rotate}deg)`; }); zoomOutBtn.addEventListener('click', (e) => { e.stopPropagation(); let s = parseFloat(img.dataset.scale) || 1; s = Math.max(0.1, s - 0.1); img.dataset.scale = s; img.style.transform = `scale(${s}) rotate(${img.dataset.rotate}deg)`; }); rotateLeft.addEventListener('click', (e) => { e.stopPropagation(); let r = parseFloat(img.dataset.rotate) || 0; r = (r - 90 + 360) % 360; img.dataset.rotate = r; img.style.transform = `scale(${img.dataset.scale}) rotate(${r}deg)`; }); rotateRight.addEventListener('click', (e) => { e.stopPropagation(); let r = parseFloat(img.dataset.rotate) || 0; r = (r + 90) % 360; img.dataset.rotate = r; img.style.transform = `scale(${img.dataset.scale}) rotate(${r}deg)`; }); const images = (Array.isArray(fileData) ? fileData : []).filter(f => IMG_RE.test(f.name)); overlay.mediaType = 'image'; overlay.mediaList = images; overlay.mediaIndex = Math.max(0, images.findIndex(f => f.name === name)); setNavVisibility(overlay, images.length > 1, images.length > 1); const navigate = (dir) => { if (!overlay.mediaList || overlay.mediaList.length < 2) return; overlay.mediaIndex = (overlay.mediaIndex + dir + overlay.mediaList.length) % overlay.mediaList.length; const newFile = overlay.mediaList[overlay.mediaIndex].name; currentName = newFile; // keep download button pointing to the right file setTitle(overlay, newFile); img.dataset.scale = 1; img.dataset.rotate = 0; img.style.transform = 'scale(1) rotate(0deg)'; img.src = siblingPreviewUrl(newFile); // <-- changed }; if (images.length > 1) { prevBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(-1); }); nextBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(+1); }); const onKey = (e) => { if (!document.body.contains(overlay)) { window.removeEventListener("keydown", onKey); return; } if (e.key === "ArrowLeft") navigate(-1); if (e.key === "ArrowRight") navigate(+1); }; window.addEventListener("keydown", onKey); overlay._onKey = onKey; } overlay.style.display = "flex"; return; } /* -------------------- PDF => new tab -------------------- */ if (lower.endsWith('.pdf')) { const separator = fileUrl.includes('?') ? '&' : '?'; const urlWithTs = fileUrl + separator + 't=' + Date.now(); window.open(urlWithTs, "_blank"); overlay.remove(); return; } /* -------------------- VIDEOS -------------------- */ if (isVideo) { let video = document.createElement("video"); video.controls = true; video.preload = 'auto'; // hint browser to start fetching quickly video.style.maxWidth = "88vw"; video.style.maxHeight = "88vh"; video.style.objectFit = "contain"; container.appendChild(video); // Apply last-used volume/mute, and persist future changes loadSavedMediaVolume(video); attachVolumePersistence(video); // Top-right action icons (Material icons, theme-aware) const markBtnIcon = makeTopIcon('check_circle', t("mark_as_viewed") || "Mark as viewed"); const clearBtnIcon = makeTopIcon('restart_alt', t("clear_progress") || "Clear progress"); // Track which file is currently active let currentName = name; // Use the URL we were passed in (old behavior) for the *first* video, // fall back to API URL if for some reason it's empty. const initialUrl = fileUrl && fileUrl.trim() ? fileUrl : buildPreviewUrl(folder, name); const downloadBtn = makeDownloadButton(folder, () => currentName); // Order: Download | Mark | Reset actionWrap.appendChild(downloadBtn); actionWrap.appendChild(markBtnIcon); actionWrap.appendChild(clearBtnIcon); const videos = (Array.isArray(fileData) ? fileData : []).filter(f => VID_RE.test(f.name)); overlay.mediaType = 'video'; overlay.mediaList = videos; overlay.mediaIndex = Math.max(0, videos.findIndex(f => f.name === name)); setNavVisibility(overlay, videos.length > 1, videos.length > 1); // Helper: set src for a given video name const setVideoSrc = (nm) => { currentName = nm; // For the current file, reuse the original working URL. // For other files (next/prev), go through the API. const url = (nm === name) ? initialUrl : buildPreviewUrl(folder, nm); video.src = url; video.src = siblingPreviewUrl(nm); setTitle(overlay, nm); }; const SAVE_INTERVAL_MS = 5000; let lastSaveAt = 0; let pending = false; async function getProgress(nm) { try { const res = await fetch(`/api/media/getProgress.php?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(nm)}&t=${Date.now()}`, { credentials: "include" }); const data = await res.json(); return data && data.state ? data.state : null; } catch { return null; } } async function sendProgress({nm, seconds, duration, completed, clear}) { try { pending = true; const res = await fetch("/api/media/updateProgress.php", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, body: JSON.stringify({ folder, file: nm, seconds, duration, completed, clear }) }); const data = await res.json(); pending = false; return data; } catch (e) { pending = false; console.error(e); return null; } } const lsKey = (nm) => `videoProgress-${folder}/${nm}`; function renderStatus(state) { if (!statusChip) return; // Completed if (state && state.completed) { statusChip.textContent = (t('viewed') || 'Viewed') + ' ✓'; statusChip.style.display = 'inline-block'; statusChip.style.borderColor = 'rgba(34,197,94,.45)'; statusChip.style.background = 'rgba(34,197,94,.15)'; statusChip.style.color = '#22c55e'; markBtnIcon.style.display = 'none'; clearBtnIcon.style.display = ''; clearBtnIcon.title = t('reset_progress') || t('clear_progress') || 'Reset'; return; } // In progress if (state && Number.isFinite(state.seconds) && Number.isFinite(state.duration) && state.duration > 0) { const pct = Math.max(1, Math.min(99, Math.round((state.seconds / state.duration) * 100))); statusChip.textContent = `${pct}%`; statusChip.style.display = 'inline-block'; const dark = document.documentElement.classList.contains('dark-mode'); const ORANGE_HEX = '#ea580c'; statusChip.style.color = ORANGE_HEX; statusChip.style.borderColor = dark ? 'rgba(234,88,12,.55)' : 'rgba(234,88,12,.45)'; statusChip.style.background = dark ? 'rgba(234,88,12,.18)' : 'rgba(234,88,12,.12)'; markBtnIcon.style.display = ''; clearBtnIcon.style.display = ''; clearBtnIcon.title = t('reset_progress') || t('clear_progress') || 'Reset'; return; } // No progress statusChip.style.display = 'none'; markBtnIcon.style.display = ''; clearBtnIcon.style.display = 'none'; } // ---- Event handlers (use currentName instead of rebinding per file) ---- video.addEventListener("loadedmetadata", async () => { const nm = currentName; try { const state = await getProgress(nm); if (state && Number.isFinite(state.seconds) && state.seconds > 0 && state.seconds < (video.duration || Infinity)) { video.currentTime = state.seconds; const seconds = Math.floor(video.currentTime || 0); const duration = Math.floor(video.duration || 0); setFileProgressBadge(nm, seconds, duration); showToast((t("resumed_from") || "Resumed from") + " " + Math.floor(state.seconds) + "s"); } else { const ls = localStorage.getItem(lsKey(nm)); if (ls) video.currentTime = parseFloat(ls); } renderStatus(state || null); } catch { renderStatus(null); } }); video.addEventListener("timeupdate", async () => { const now = Date.now(); if ((now - lastSaveAt) < SAVE_INTERVAL_MS || pending) return; lastSaveAt = now; const nm = currentName; const seconds = Math.floor(video.currentTime || 0); const duration = Math.floor(video.duration || 0); sendProgress({ nm, seconds, duration }); setFileProgressBadge(nm, seconds, duration); try { localStorage.setItem(lsKey(nm), String(seconds)); } catch {} renderStatus({ seconds, duration, completed: false }); }); video.addEventListener("ended", async () => { const nm = currentName; const duration = Math.floor(video.duration || 0); await sendProgress({ nm, seconds: duration, duration, completed: true }); try { localStorage.removeItem(lsKey(nm)); } catch {} showToast(t("marked_viewed") || "Marked as viewed"); setFileWatchedBadge(nm, true); renderStatus({ seconds: duration, duration, completed: true }); }); markBtnIcon.onclick = async () => { const nm = currentName; const duration = Math.floor(video.duration || 0); await sendProgress({ nm, seconds: duration, duration, completed: true }); showToast(t("marked_viewed") || "Marked as viewed"); setFileWatchedBadge(nm, true); renderStatus({ seconds: duration, duration, completed: true }); }; clearBtnIcon.onclick = async () => { const nm = currentName; await sendProgress({ nm, seconds: 0, duration: null, completed: false, clear: true }); try { localStorage.removeItem(lsKey(nm)); } catch {} showToast(t("progress_cleared") || "Progress cleared"); setFileWatchedBadge(nm, false); renderStatus(null); }; const navigate = (dir) => { if (!overlay.mediaList || overlay.mediaList.length < 2) return; overlay.mediaIndex = (overlay.mediaIndex + dir + overlay.mediaList.length) % overlay.mediaList.length; const nm = overlay.mediaList[overlay.mediaIndex].name; setVideoSrc(nm); renderStatus(null); }; if (videos.length > 1) { prevBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(-1); }); nextBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(+1); }); const onKey = (e) => { if (!document.body.contains(overlay)) { window.removeEventListener("keydown", onKey); return; } if (e.key === "ArrowLeft") navigate(-1); if (e.key === "ArrowRight") navigate(+1); }; window.addEventListener("keydown", onKey); overlay._onKey = onKey; } // Kick off first video using the original working URL setVideoSrc(name); renderStatus(null); overlay.style.display = "flex"; return; } /* -------------------- AUDIO / OTHER -------------------- */ if (isAudio) { const audio = document.createElement("audio"); audio.src = fileUrl; audio.controls = true; audio.className = "audio-modal"; audio.style.maxWidth = "88vw"; container.appendChild(audio); // Share the same volume/mute behavior with videos loadSavedMediaVolume(audio); attachVolumePersistence(audio); const downloadBtn = makeDownloadButton(folder, () => name); actionWrap.appendChild(downloadBtn); overlay.style.display = "flex"; } else { const downloadBtn = makeDownloadButton(folder, () => name); actionWrap.appendChild(downloadBtn); container.textContent = t("preview_not_available") || "Preview not available for this file type."; overlay.style.display = "flex"; } } /* -------------------------------- Small display helper -------------------------------- */ export function displayFilePreview(file, container) { const actualFile = file.file || file; if (!(actualFile instanceof File)) { console.error("displayFilePreview called with an invalid file object"); return; } container.style.display = "inline-block"; while (container.firstChild) container.removeChild(container.firstChild); if (IMG_RE.test(actualFile.name)) { const img = document.createElement("img"); img.src = URL.createObjectURL(actualFile); img.classList.add("file-preview-img"); container.appendChild(img); } else { const iconSpan = document.createElement("span"); iconSpan.classList.add("material-icons", "file-icon"); iconSpan.textContent = "insert_drive_file"; container.appendChild(iconSpan); } } // expose for HTML onclick usage window.previewFile = previewFile; window.openShareModal = openShareModal;