(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; files.forEach(function (file) { const row = document.createElement("tr"); row.dataset.path = file.path; // Thumbnail cell const preview = document.createElement("td"); const image = document.createElement("img"); image.className = "local-video-thumbnail"; image.alt = ""; image.dataset.path = file.path; preview.appendChild(image); 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 badge const upload = document.createElement("td"); upload.className = "dash-col-upload"; const badge = document.createElement("span"); 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 const actions = document.createElement("td"); actions.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'; actions.appendChild(playButton); 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'; actions.appendChild(deleteButton); row.appendChild(actions); body.appendChild(row); }); summary.textContent = `${files.length} video(s), ${YarisUI.formatBytes(result.total_size || 0)} total, ${uploadedCount} uploaded`; 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))`; } } 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; }); } }); 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; } function closePlayer() { player.pause(); player.removeAttribute("src"); player.load(); playerPanel.hidden = true; playingPath = null; } function playVideo(path) { playingPath = path; playerTitle.textContent = path; player.src = `/api/local-video/file?path=${encodeURIComponent(path)}`; playerPanel.hidden = false; player.load(); player.play().catch(function () { // Browser autoplay policy may require the user to press play. }); playerPanel.scrollIntoView({ behavior: "smooth", block: "start" }); } function renderFiles(result) { thumbnailObserver.disconnect(); body.replaceChildren(); const files = result.files || []; let uploadedCount = 0; files.forEach(function (file) { const row = document.createElement("tr"); row.dataset.path = file.path; row.dataset.size = file.size; row.dataset.modified = file.modified_at; row.dataset.uploaded = file.uploaded ? "1" : "0"; const preview = document.createElement("td"); const image = document.createElement("img"); image.className = "local-video-thumbnail"; image.alt = `Preview of ${file.path}`; image.loading = "lazy"; image.dataset.path = file.path; image.addEventListener("error", function () { image.hidden = true; }); preview.appendChild(image); row.appendChild(preview); thumbnailObserver.observe(image); row.appendChild(textCell(file.path)); row.appendChild(textCell(YarisUI.formatBytes(file.size))); row.appendChild(textCell(new Date(file.modified_at * 1000).toLocaleString())); const upload = document.createElement("td"); const badge = document.createElement("span"); badge.className = file.uploaded ? "badge bg-success" : "badge bg-warning text-dark"; badge.textContent = file.uploaded ? "Uploaded" : "Pending"; upload.appendChild(badge); row.appendChild(upload); if (file.uploaded) { uploadedCount += 1; } const actions = document.createElement("td"); actions.className = "data-list-actions"; const playButton = document.createElement("button"); playButton.type = "button"; playButton.className = "btn btn-sm btn-outline-primary"; playButton.dataset.playPath = file.path; playButton.innerHTML = 'Play'; actions.appendChild(playButton); const deleteButton = document.createElement("button"); deleteButton.type = "button"; deleteButton.className = "btn btn-sm btn-outline-danger"; deleteButton.dataset.deleteLocalPath = file.path; deleteButton.dataset.confirm = `Delete the local Pi copy of "${file.path}"? The remote archive will not be changed.`; deleteButton.innerHTML = 'Delete local'; actions.appendChild(deleteButton); row.appendChild(actions); body.appendChild(row); }); summary.textContent = `${files.length} video(s), ${YarisUI.formatBytes(result.total_size || 0)} total, ${uploadedCount} uploaded`; if (result.retention_days > 0) { summary.textContent += `; uploaded copies older than ${result.retention_days} day(s) are auto-deleted`; } else { summary.textContent += "; automatic retention cleanup is off"; } if (result.errors && result.errors.length) { summary.textContent += ` (${result.errors.length} file error(s))`; } } 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) { closePlayer(); } 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) { deleteButton.disabled = true; deleteVideo(deleteButton.dataset.deleteLocalPath).finally(function () { deleteButton.disabled = false; }); } }); refreshButton.addEventListener("click", refreshFiles); closePlayerButton.addEventListener("click", closePlayer); refreshFiles(); }());