Add local dashcam video library
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
+2
-11
@@ -25,15 +25,6 @@ function fetchJson(url, options) {
|
||||
}));
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) {
|
||||
return '0 B';
|
||||
}
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const unit = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
||||
return `${(bytes / (1024 ** unit)).toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
|
||||
}
|
||||
|
||||
function createCell(text) {
|
||||
const cell = document.createElement('td');
|
||||
cell.textContent = text;
|
||||
@@ -93,7 +84,7 @@ function renderDashcamFiles(summary) {
|
||||
row.appendChild(selection);
|
||||
|
||||
row.appendChild(createCell(file.path));
|
||||
row.appendChild(createCell(formatBytes(file.size)));
|
||||
row.appendChild(createCell(YarisUI.formatBytes(file.size)));
|
||||
row.appendChild(createCell(new Date(file.modified_at * 1000).toLocaleString()));
|
||||
|
||||
const actions = document.createElement('td');
|
||||
@@ -116,7 +107,7 @@ function renderDashcamFiles(summary) {
|
||||
fileListBody.appendChild(row);
|
||||
});
|
||||
|
||||
fileSummary.textContent = `${files.length} video(s), ${formatBytes(summary.total_size || 0)} total`;
|
||||
fileSummary.textContent = `${files.length} video(s), ${YarisUI.formatBytes(summary.total_size || 0)} total`;
|
||||
if (summary.errors && summary.errors.length) {
|
||||
fileSummary.textContent += ` (${summary.errors.length} file error(s))`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) {
|
||||
return "0 B";
|
||||
}
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
const unit = Math.min(
|
||||
Math.floor(Math.log(bytes) / Math.log(1024)),
|
||||
units.length - 1
|
||||
);
|
||||
return `${(bytes / (1024 ** unit)).toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
|
||||
}
|
||||
|
||||
function compareRows(left, right, key, direction) {
|
||||
const leftValue = left.dataset[key] || "";
|
||||
const rightValue = right.dataset[key] || "";
|
||||
@@ -66,4 +78,5 @@
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-data-list]").forEach(initializeList);
|
||||
window.YarisUI = Object.assign(window.YarisUI || {}, { formatBytes });
|
||||
}());
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
(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");
|
||||
const playerPanel = document.getElementById("local-video-player-panel");
|
||||
const playerTitle = document.getElementById("local-video-player-title");
|
||||
const player = document.getElementById("local-video-player");
|
||||
const closePlayerButton = document.getElementById("local-video-player-close");
|
||||
let playingPath = null;
|
||||
|
||||
const thumbnailObserver = new IntersectionObserver(function (entries, observer) {
|
||||
entries.forEach(function (entry) {
|
||||
if (!entry.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
const image = entry.target;
|
||||
image.src = `/api/local-video/thumbnail?path=${encodeURIComponent(image.dataset.path)}`;
|
||||
observer.unobserve(image);
|
||||
});
|
||||
}, { rootMargin: "200px" });
|
||||
|
||||
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 = '<i class="fas fa-play me-1"></i>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 = '<i class="fas fa-trash me-1"></i>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();
|
||||
}());
|
||||
Reference in New Issue
Block a user