(function () { "use strict"; const summary = document.getElementById("local-video-summary"); const body = document.querySelector("#local-video-list [data-data-list-body]"); const refreshButton = document.getElementById("local-videos-refresh-btn"); let playingPath = null; // ── Modal player ────────────────────────────────────────────────────────── const modalEl = document.getElementById("video-player-modal"); const modalTitle = document.getElementById("video-player-modal-title"); const player = document.getElementById("local-video-player"); const bsModal = new bootstrap.Modal(modalEl); modalEl.addEventListener("hidden.bs.modal", function () { player.pause(); player.removeAttribute("src"); player.load(); playingPath = null; }); function playVideo(path) { playingPath = path; modalTitle.textContent = path.split("/").pop(); player.src = `/api/local-video/file?path=${encodeURIComponent(path)}`; bsModal.show(); player.load(); player.play().catch(function () {}); } // ── Thumbnail loading with retry on 202 ────────────────────────────────── const THUMB_RETRY_MS = 2000; const THUMB_MAX_RETRIES = 15; function loadThumbnail(image) { const url = `/api/local-video/thumbnail?path=${encodeURIComponent(image.dataset.path)}`; let attempts = 0; function attempt() { fetch(url) .then(function (resp) { if (resp.status === 202 && attempts < THUMB_MAX_RETRIES) { attempts++; setTimeout(attempt, THUMB_RETRY_MS); return; } if (!resp.ok) { image.hidden = true; return; } return resp.blob().then(function (blob) { image.src = URL.createObjectURL(blob); }); }) .catch(function () { image.hidden = true; }); } attempt(); } const thumbnailObserver = new IntersectionObserver(function (entries, observer) { entries.forEach(function (entry) { if (!entry.isIntersecting) return; observer.unobserve(entry.target); loadThumbnail(entry.target); }); }, { rootMargin: "200px" }); // ── Utilities ───────────────────────────────────────────────────────────── function fetchJson(url, options) { return fetch(url, options).then(function (response) { return response.json().then(function (data) { if (!response.ok) { throw new Error(data.message || data.error || data.status || `HTTP ${response.status}`); } return data; }); }); } function textCell(text) { const cell = document.createElement("td"); cell.textContent = text; return cell; } // ── File list rendering ─────────────────────────────────────────────────── function renderFiles(result) { thumbnailObserver.disconnect(); body.replaceChildren(); const files = result.files || []; let uploadedCount = 0; let convertingCount = 0; files.forEach(function (file) { const row = document.createElement("tr"); row.className = "dash-row"; row.dataset.path = file.path; // Thumbnail cell — also contains filename for mobile view const preview = document.createElement("td"); preview.className = "dash-thumb-td"; const image = document.createElement("img"); image.className = "local-video-thumbnail"; image.alt = ""; image.dataset.path = file.path; preview.appendChild(image); // Filename shown only on mobile (hidden on desktop via CSS) const mobileLabel = document.createElement("div"); mobileLabel.className = "dash-thumb-name text-truncate small mt-1"; mobileLabel.style.maxWidth = "5rem"; mobileLabel.textContent = file.path.split("/").pop(); preview.appendChild(mobileLabel); // Date shown only on mobile (desktop has its own column) const mobileMeta = document.createElement("div"); mobileMeta.className = "dash-thumb-meta text-muted"; mobileMeta.textContent = new Date(file.modified_at * 1000).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); preview.appendChild(mobileMeta); // Converting badge shown on mobile inside thumbnail cell if (file.converting) { const p = file.convert_progress; const elapsed = p ? `${Math.floor(p.elapsed_s / 60)}m${p.elapsed_s % 60}s` : '…'; const pct = p ? ` ${p.pct}%` : ''; const mobileBadge = document.createElement("span"); mobileBadge.className = "badge bg-info text-dark dash-thumb-converting mt-1"; mobileBadge.innerHTML = `${pct || elapsed}`; preview.appendChild(mobileBadge); } row.appendChild(preview); thumbnailObserver.observe(image); // File name const nameCell = document.createElement("td"); nameCell.className = "dash-col-file"; nameCell.textContent = file.path; row.appendChild(nameCell); // Size const sizeCell = textCell(YarisUI.formatBytes(file.size)); sizeCell.className = "dash-col-size"; row.appendChild(sizeCell); // Date const dateCell = textCell(new Date(file.modified_at * 1000).toLocaleString()); dateCell.className = "dash-col-date"; row.appendChild(dateCell); // Upload / convert badge (desktop) const upload = document.createElement("td"); upload.className = "dash-col-upload"; const badge = document.createElement("span"); if (file.converting) { const p = file.convert_progress; const pct = p && p.pct > 0 ? ` ${p.pct}%` : ''; const elapsed = p ? ` ${Math.floor(p.elapsed_s / 60)}m${p.elapsed_s % 60}s` : ''; badge.className = "badge bg-info text-dark"; badge.innerHTML = `Converting${pct}${elapsed}`; convertingCount++; } else { badge.className = file.uploaded ? "badge bg-success" : "badge bg-warning text-dark"; badge.textContent = file.uploaded ? "✓" : "…"; } upload.appendChild(badge); row.appendChild(upload); if (file.uploaded) uploadedCount++; // Actions — wrap in div so td stays a table cell (not flex) const actionsTd = document.createElement("td"); actionsTd.className = "dash-actions-td"; const actionsDiv = document.createElement("div"); actionsDiv.className = "data-list-actions"; const playButton = document.createElement("button"); playButton.type = "button"; playButton.className = "btn btn-sm btn-outline-primary btn-icon-mobile"; playButton.dataset.playPath = file.path; playButton.innerHTML = ' Play'; actionsDiv.appendChild(playButton); // Convert button — only for non-MP4 files not already converting if (!file.converting && !file.path.toLowerCase().endsWith(".mp4")) { const convertButton = document.createElement("button"); convertButton.type = "button"; convertButton.className = "btn btn-sm btn-outline-secondary btn-icon-mobile"; convertButton.dataset.convertPath = file.path; convertButton.innerHTML = ' Convert'; actionsDiv.appendChild(convertButton); } const deleteButton = document.createElement("button"); deleteButton.type = "button"; deleteButton.className = "btn btn-sm btn-outline-danger btn-icon-mobile"; deleteButton.dataset.deleteLocalPath = file.path; deleteButton.dataset.confirm = `Delete the local Pi copy of "${file.path}"?`; deleteButton.innerHTML = ' Del'; actionsDiv.appendChild(deleteButton); actionsTd.appendChild(actionsDiv); row.appendChild(actionsTd); body.appendChild(row); }); summary.textContent = `${files.length} video(s), ${YarisUI.formatBytes(result.total_size || 0)} total, ${uploadedCount} uploaded`; if (convertingCount > 0) summary.textContent += `; ${convertingCount} converting…`; if (result.retention_days > 0) { summary.textContent += `; > ${result.retention_days}d uploaded auto-deleted`; } if (result.errors && result.errors.length) { summary.textContent += ` (${result.errors.length} error(s))`; } // Auto-refresh every 10s while conversions are running if (convertingCount > 0) { setTimeout(refreshFiles, 10000); } } function refreshFiles() { refreshButton.disabled = true; summary.textContent = "Loading..."; return fetchJson("/api/local-videos") .then(renderFiles) .catch(function (error) { body.replaceChildren(); summary.textContent = `Unable to list local videos: ${error.message}`; }) .finally(function () { refreshButton.disabled = false; }); } function deleteVideo(path) { return fetchJson("/api/local-video", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path }), }).then(function () { if (playingPath === path) bsModal.hide(); return refreshFiles(); }).catch(function (error) { summary.textContent = `Local delete failed: ${error.message}`; }); } body.addEventListener("click", function (event) { const playButton = event.target.closest("[data-play-path]"); if (playButton) { playVideo(playButton.dataset.playPath); return; } const deleteButton = event.target.closest("[data-delete-local-path]"); if (deleteButton) { if (!confirm(deleteButton.dataset.confirm)) return; deleteButton.disabled = true; deleteVideo(deleteButton.dataset.deleteLocalPath).finally(function () { deleteButton.disabled = false; }); return; } const convertButton = event.target.closest("[data-convert-path]"); if (convertButton) { convertButton.disabled = true; convertButton.innerHTML = ''; fetch("/api/local-video/convert", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: convertButton.dataset.convertPath }), }).then(function () { setTimeout(refreshFiles, 1000); }).catch(function (err) { summary.textContent = `Convert failed: ${err.message}`; convertButton.disabled = false; }); } }); refreshButton.addEventListener("click", refreshFiles); refreshFiles(); }()); function fetchJson(url, options) { return fetch(url, options).then(function (response) { return response.json().then(function (data) { if (!response.ok) { throw new Error(data.message || data.error || data.status || `HTTP ${response.status}`); } return data; }); }); } function textCell(text) { const cell = document.createElement("td"); cell.textContent = text; return cell; }