(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;
}