live: add a control footer under the grid (paging + play/stop + fullscreen)
The live-view <main> now hosts its own 32px footer bar beneath the grid: prev page on the left, play/stop in the centre, next page + fullscreen on the right — all icon buttons. Pagination is new: cameras are split into pages of dim² cells, prev/next walk the pages (page 0 still honours a saved manual layout), and the arrows disable at the bounds. Fullscreen now targets <main> so the control bar stays visible in fullscreen (the global stats footer is outside <main> and hides as before). The sidebar play/fullscreen items now target <main> too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+63
-15
@@ -483,7 +483,7 @@ class WebCodecsHevcPlayer {
|
||||
// Страница LIVE VIEW
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
const Live = {
|
||||
cameras: [], dim: 2, layout: 4, transcode: false, go2rtcUrl: "", selected: [], singleMain: false, maxDim: 8, playing: true,
|
||||
cameras: [], dim: 2, layout: 4, transcode: false, go2rtcUrl: "", selected: [], singleMain: false, maxDim: 8, playing: true, page: 0,
|
||||
|
||||
async init() {
|
||||
const data = await api("/api/cameras");
|
||||
@@ -535,21 +535,52 @@ const Live = {
|
||||
this.dim = Math.min(this.maxDim || 8, Math.max(2, dim));
|
||||
this.layout = this.dim * this.dim;
|
||||
localStorage.setItem("corevision.grid", String(this.dim));
|
||||
// приоритет — сохранённая раскладка (страница "Раскладка"); иначе автозаполнение
|
||||
const saved = getLayout(this.dim);
|
||||
if (Array.isArray(saved) && saved.length === this.layout) {
|
||||
this.selected = saved.slice();
|
||||
} else {
|
||||
this.selected = this.selected.slice(0, this.layout);
|
||||
while (this.selected.length < this.layout) {
|
||||
const next = this.cameras.find((c) => !this.selected.includes(c.id));
|
||||
if (!next) break;
|
||||
this.selected.push(next.id);
|
||||
}
|
||||
}
|
||||
this.page = 0; // смена размера сетки → первая страница
|
||||
this.fillSelected(); // selected = камеры текущей страницы
|
||||
document.querySelectorAll("#live-subnav .subitem, .subnav .subitem").forEach((a) =>
|
||||
a.classList.toggle("active", +a.dataset.grid === this.dim));
|
||||
if (rerender) this.renderGrid();
|
||||
this.updatePager();
|
||||
},
|
||||
|
||||
// число страниц: все камеры, разбитые по layout ячеек
|
||||
pageCount() {
|
||||
return Math.max(1, Math.ceil(this.cameras.length / this.layout));
|
||||
},
|
||||
|
||||
// заполнить selected камерами текущей страницы; страница 0 уважает сохранённую раскладку
|
||||
fillSelected() {
|
||||
const pages = this.pageCount();
|
||||
this.page = Math.min(Math.max(0, this.page), pages - 1);
|
||||
const saved = getLayout(this.dim);
|
||||
if (this.page === 0 && Array.isArray(saved) && saved.length === this.layout) {
|
||||
this.selected = saved.slice();
|
||||
return;
|
||||
}
|
||||
const start = this.page * this.layout;
|
||||
this.selected = this.cameras.slice(start, start + this.layout).map((c) => c.id);
|
||||
while (this.selected.length < this.layout) this.selected.push(undefined);
|
||||
},
|
||||
|
||||
setPage(p) {
|
||||
const pages = this.pageCount();
|
||||
const np = Math.min(pages - 1, Math.max(0, p));
|
||||
if (np === this.page) { this.updatePager(); return; }
|
||||
this.page = np;
|
||||
this.fillSelected();
|
||||
this.renderGrid();
|
||||
this.updatePager();
|
||||
},
|
||||
|
||||
// состояние стрелок пагинации в футере (выкл на краях)
|
||||
updatePager() {
|
||||
const pages = this.pageCount();
|
||||
const prev = document.getElementById("gb-prev");
|
||||
const next = document.getElementById("gb-next");
|
||||
if (prev) prev.disabled = this.page <= 0;
|
||||
if (next) next.disabled = this.page >= pages - 1;
|
||||
const bar = document.getElementById("grid-bar");
|
||||
if (bar) bar.title = `Страница ${this.page + 1} из ${pages}`;
|
||||
},
|
||||
|
||||
bindSubnav() {
|
||||
@@ -561,9 +592,21 @@ const Live = {
|
||||
};
|
||||
});
|
||||
const fs = document.getElementById("fs-grid");
|
||||
if (fs) fs.onclick = (e) => { e.preventDefault(); toggleFullscreen(document.getElementById("grid")); };
|
||||
if (fs) fs.onclick = (e) => { e.preventDefault(); toggleFullscreen(document.getElementById("live-main") || document.getElementById("grid")); };
|
||||
const pt = document.getElementById("play-toggle");
|
||||
if (pt) { this.updatePlayBtn(); pt.onclick = (e) => { e.preventDefault(); this.setPlaying(!this.playing); }; }
|
||||
// ── панель управления под сеткой (футер фрейма): ⏮ play/stop ⏭ + фулскрин ──
|
||||
const fsMain = () => toggleFullscreen(document.getElementById("live-main") || document.getElementById("grid"));
|
||||
const gbPrev = document.getElementById("gb-prev");
|
||||
if (gbPrev) gbPrev.onclick = (e) => { e.preventDefault(); this.setPage(this.page - 1); };
|
||||
const gbNext = document.getElementById("gb-next");
|
||||
if (gbNext) gbNext.onclick = (e) => { e.preventDefault(); this.setPage(this.page + 1); };
|
||||
const gbPlay = document.getElementById("gb-play");
|
||||
if (gbPlay) gbPlay.onclick = (e) => { e.preventDefault(); this.setPlaying(!this.playing); };
|
||||
const gbFs = document.getElementById("gb-fs");
|
||||
if (gbFs) gbFs.onclick = (e) => { e.preventDefault(); fsMain(); };
|
||||
this.updatePlayBtn();
|
||||
this.updatePager();
|
||||
},
|
||||
|
||||
bindFullscreenResize() {
|
||||
@@ -584,6 +627,11 @@ const Live = {
|
||||
pt.textContent = this.playing ? "⏹ Стоп" : "▶ Просмотр";
|
||||
pt.classList.toggle("active", !this.playing);
|
||||
}
|
||||
const gb = document.getElementById("gb-play");
|
||||
if (gb) {
|
||||
gb.textContent = this.playing ? "⏸" : "▶";
|
||||
gb.title = this.playing ? "Остановить просмотр" : "Начать просмотр";
|
||||
}
|
||||
},
|
||||
|
||||
setPlaying(on) {
|
||||
@@ -774,7 +822,7 @@ const Live = {
|
||||
grid.classList.add("solo");
|
||||
tile.classList.add("solo");
|
||||
// ещё не в полноэкранном — войти, чтобы камера заняла весь экран (выход — ESC)
|
||||
if (!(document.fullscreenElement || document.webkitFullscreenElement)) toggleFullscreen(grid);
|
||||
if (!(document.fullscreenElement || document.webkitFullscreenElement)) toggleFullscreen(document.getElementById("live-main") || grid);
|
||||
} // повторный двойной клик — назад к сетке (фуллскрин сохраняется)
|
||||
this.resizePlayers();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user