Add region filter bar and navigation buttons for video clips
This commit is contained in:
1 parent
d12d5e5414
commit
1c7ce1dd0a
4 files changed
+410
-29
No files matched your search
+87
-1
@@ -95,6 +95,50 @@ header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Region filter bar */
|
||||
.region-bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.region-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.region-btn {
|
||||
padding: 5px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s,
|
||||
border-color 0.15s;
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
.region-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.region-btn.active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Video Player Section */
|
||||
.player-panel {
|
||||
width: 100%;
|
||||
@@ -182,7 +226,7 @@ header {
|
||||
color: var(--text-secondary);
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
@@ -194,6 +238,48 @@ header {
|
||||
color: var(--accent);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.now-playing .loc {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.now-playing .loc::before {
|
||||
content: "📍 ";
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Nav buttons (prev/next) */
|
||||
.nav-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s,
|
||||
border-color 0.15s;
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.nav-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.nav-btn:hover {
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.nav-btn:active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
Camera Selector: 3-column grid + car graphic
|
||||
|
||||
+103
-7
@@ -10,6 +10,10 @@
|
||||
const nowPlaying = document.getElementById("nowPlaying");
|
||||
const npName = document.getElementById("npName");
|
||||
const npTime = document.getElementById("npTime");
|
||||
const npLocation = document.getElementById("npLocation");
|
||||
const prevBtn = document.getElementById("prevBtn");
|
||||
const nextBtn = document.getElementById("nextBtn");
|
||||
const regionBar = document.getElementById("regionBar");
|
||||
const csBtns = document.querySelectorAll(".cs-btn[data-cam]");
|
||||
|
||||
// ---- State ----
|
||||
@@ -126,6 +130,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Prev/Next navigation ----
|
||||
function findClipIndex(clip) {
|
||||
const sameCam = (allVideos[clip.cam] && allVideos[clip.cam].videos) || [];
|
||||
return sameCam.findIndex(function (c) {
|
||||
return c.timestamp === clip.timestamp && c.filename === clip.filename;
|
||||
});
|
||||
}
|
||||
|
||||
function prevClip() {
|
||||
if (!currentClip) return;
|
||||
const sameCam =
|
||||
(allVideos[currentClip.cam] && allVideos[currentClip.cam].videos) || [];
|
||||
const idx = findClipIndex(currentClip);
|
||||
if (idx > 0) playClip(sameCam[idx - 1]);
|
||||
}
|
||||
|
||||
function nextClip() {
|
||||
if (!currentClip) return;
|
||||
const sameCam =
|
||||
(allVideos[currentClip.cam] && allVideos[currentClip.cam].videos) || [];
|
||||
const idx = findClipIndex(currentClip);
|
||||
if (idx > -1 && sameCam[idx + 1]) playClip(sameCam[idx + 1]);
|
||||
}
|
||||
|
||||
prevBtn.addEventListener("click", prevClip);
|
||||
nextBtn.addEventListener("click", nextClip);
|
||||
|
||||
// ---- Playback ----
|
||||
function playClip(clip) {
|
||||
if (!clip) return;
|
||||
@@ -160,6 +191,7 @@
|
||||
nowPlaying.hidden = false;
|
||||
npName.textContent = clip.filename;
|
||||
npTime.textContent = prettyTs(clip.timestamp);
|
||||
npLocation.textContent = clip.location || "";
|
||||
|
||||
updateCamButtons(clip.cam);
|
||||
}
|
||||
@@ -180,6 +212,7 @@
|
||||
|
||||
badgeText.textContent = target.camDisplay;
|
||||
npName.textContent = target.filename;
|
||||
npLocation.textContent = target.location || "";
|
||||
|
||||
player.addEventListener("loadedmetadata", function onMeta() {
|
||||
player.removeEventListener("loadedmetadata", onMeta);
|
||||
@@ -206,12 +239,7 @@
|
||||
if (!currentClip) return;
|
||||
const sameCam =
|
||||
(allVideos[currentClip.cam] && allVideos[currentClip.cam].videos) || [];
|
||||
const idx = sameCam.findIndex(function (c) {
|
||||
return (
|
||||
c.timestamp === currentClip.timestamp &&
|
||||
c.filename === currentClip.filename
|
||||
);
|
||||
});
|
||||
const idx = findClipIndex(currentClip);
|
||||
if (idx > -1 && sameCam[idx + 1]) playClip(sameCam[idx + 1]);
|
||||
});
|
||||
|
||||
@@ -234,7 +262,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Keyboard shortcuts: 1-6 for camera angles ----
|
||||
// ---- Keyboard shortcuts: 1-6 for camera angles, ArrowLeft/Right for prev/next ----
|
||||
const camOrder = [
|
||||
"front",
|
||||
"left_repeater",
|
||||
@@ -258,10 +286,78 @@
|
||||
if (videos.length > 0) playClip(videos[0]);
|
||||
}
|
||||
}
|
||||
if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
prevClip();
|
||||
}
|
||||
if (e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
nextClip();
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Region filter ----
|
||||
let regionData = {};
|
||||
|
||||
function fetchRegions() {
|
||||
fetch("/api/regions")
|
||||
.then(function (r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
regionData = data.regions || {};
|
||||
buildRegionButtons();
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("Failed to load regions:", err);
|
||||
});
|
||||
}
|
||||
|
||||
function buildRegionButtons() {
|
||||
// Keep "All" button, remove any existing region buttons
|
||||
var existing = regionBar.querySelectorAll(
|
||||
".region-btn:not([data-region=''])",
|
||||
);
|
||||
existing.forEach(function (b) {
|
||||
b.remove();
|
||||
});
|
||||
|
||||
Object.keys(regionData).forEach(function (name) {
|
||||
var btn = document.createElement("button");
|
||||
btn.className = "region-btn";
|
||||
btn.setAttribute("data-region", name);
|
||||
btn.textContent = name + " (" + regionData[name].count + ")";
|
||||
regionBar.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
regionBar.addEventListener("click", function (e) {
|
||||
var btn = e.target.closest(".region-btn");
|
||||
if (!btn) return;
|
||||
var region = btn.getAttribute("data-region");
|
||||
|
||||
// Update active state
|
||||
regionBar.querySelectorAll(".region-btn").forEach(function (b) {
|
||||
b.classList.toggle("active", b === btn);
|
||||
});
|
||||
|
||||
if (!region) return; // "All" selected — no jump
|
||||
var info = regionData[region];
|
||||
if (!info || !info.first_timestamp) return;
|
||||
|
||||
// Jump to first front-camera clip for this region
|
||||
var frontVideos = (allVideos["front"] && allVideos["front"].videos) || [];
|
||||
var clip = frontVideos.find(function (c) {
|
||||
return c.timestamp === info.first_timestamp;
|
||||
});
|
||||
if (clip) playClip(clip);
|
||||
});
|
||||
|
||||
// ---- Init: fetch full video data, then auto-play front ----
|
||||
function init() {
|
||||
// Fetch regions data for the filter bar
|
||||
fetchRegions();
|
||||
|
||||
fetch("/api/videos")
|
||||
.then(function (r) {
|
||||
return r.json();
|
||||
|
||||
Reference in new issue
Block a user