archive: replace toolbar with a 220px split sidebar (camera list + calendar)

The player-frame toolbar (camera <select> + date input) is removed. Archive
main is now a row: a 220px sidebar split in half by height — the top half is
a scrollable clickable camera list, the bottom half holds the date input plus
a new inline month calendar (prev/next month, today + selected highlight,
Monday-first). Picking a camera or a day loads that day's recordings. The
player and DVR timeline move into an .arch-content column on the right.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 22:14:59 +05:00
parent ab817e0d31
commit dcc4cc046f
4 changed files with 133 additions and 20 deletions
+12 -9
View File
@@ -16,16 +16,19 @@
</aside>
<main class="archive-main">
<div class="toolbar">
<label class="muted">Камера:</label>
<select id="cam-select"></select>
<label class="muted">Дата:</label>
<input type="date" id="date">
<aside class="arch-side">
<div class="arch-cams" id="arch-cams"></div>
<div class="arch-date">
<input type="date" id="date" class="arch-dateinput">
<div class="arch-cal" id="calendar"></div>
</div>
</aside>
<div class="arch-content">
<div class="player-wrap" id="player">
<div class="hint">Кликните по таймлайну ниже, чтобы смотреть запись</div>
</div>
<div class="timeline" id="timeline"></div>
</div>
<div class="player-wrap" id="player">
<div class="hint">Кликните по таймлайну ниже, чтобы смотреть запись</div>
</div>
<div class="timeline" id="timeline"></div>
</main>
</div>
+79 -9
View File
@@ -1025,7 +1025,7 @@ class ArchivePlayer {
}
const Archive = {
cameras: [], cam: null, date: null, segments: [], transcode: false,
cameras: [], cam: null, date: null, segments: [], transcode: false, calYear: 0, calMonth: 0,
centerTime: 0, dayStart: 0, dragging: false, following: false, track: null, _curSeg: null, _playTok: 0,
async init() {
@@ -1033,16 +1033,19 @@ const Archive = {
this.cameras = data.cameras;
this.transcode = profileMode("a") === "transcode"; // профиль архива (этот браузер)
this.cam = this.cameras[0] ? this.cameras[0].id : null;
this.buildCamList();
const sel = document.getElementById("cam-select");
sel.innerHTML = this.cameras.map((c) => `<option value="${c.id}">${c.name}</option>`).join("");
sel.onchange = () => { this.cam = sel.value; this.load(); };
const dp = document.getElementById("date");
const today = new Date();
dp.value = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
this.date = dp.value;
dp.onchange = () => { this.date = dp.value; this.load(); };
this.date = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
this.calYear = today.getFullYear();
this.calMonth = today.getMonth();
const dp = document.getElementById("date");
if (dp) {
dp.value = this.date;
dp.onchange = () => { this.date = dp.value; this.syncCalToDate(); this.renderCalendar(); this.load(); };
}
this.bindCalendar();
this.renderCalendar();
// при ресайзе окна пересчитать смещение ленты (центр зависит от ширины вьюпорта)
window.addEventListener("resize", () => { if (this.track) this._applyTl(); });
@@ -1050,6 +1053,73 @@ const Archive = {
this.load();
},
// верхняя половина сайдбара: список камер (клик — выбор)
buildCamList() {
const box = document.getElementById("arch-cams");
if (!box) return;
box.innerHTML = this.cameras.map((c) =>
`<div class="arch-cam${c.id === this.cam ? " active" : ""}" data-cam="${c.id}">${c.name}</div>`
).join("");
box.onclick = (e) => {
const it = e.target.closest(".arch-cam[data-cam]");
if (!it) return;
this.cam = it.dataset.cam;
box.querySelectorAll(".arch-cam").forEach((x) => x.classList.toggle("active", x.dataset.cam === this.cam));
this.load();
};
},
syncCalToDate() {
const [y, m] = (this.date || "").split("-").map(Number);
if (y && m) { this.calYear = y; this.calMonth = m - 1; }
},
bindCalendar() {
const box = document.getElementById("calendar");
if (!box) return;
box.onclick = (e) => {
const nav = e.target.closest("[data-cal]");
if (nav) {
if (nav.dataset.cal === "prev") { if (--this.calMonth < 0) { this.calMonth = 11; this.calYear--; } }
else { if (++this.calMonth > 11) { this.calMonth = 0; this.calYear++; } }
this.renderCalendar();
return;
}
const day = e.target.closest(".cal-day[data-day]");
if (!day) return;
this.date = `${this.calYear}-${pad(this.calMonth + 1)}-${pad(+day.dataset.day)}`;
const dp = document.getElementById("date");
if (dp) dp.value = this.date;
this.renderCalendar();
this.load();
};
},
// нижняя половина сайдбара: месячный календарь
renderCalendar() {
const box = document.getElementById("calendar");
if (!box) return;
const MN = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"];
const WD = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
const y = this.calYear, m = this.calMonth;
const [sy, sm, sd] = (this.date || "").split("-").map(Number);
const startDow = (new Date(y, m, 1).getDay() + 6) % 7; // 0 = понедельник
const days = new Date(y, m + 1, 0).getDate();
const t = new Date();
let html = `<div class="cal-head">` +
`<button class="cal-nav" data-cal="prev" title="Предыдущий месяц"></button>` +
`<span class="cal-title">${MN[m]} ${y}</span>` +
`<button class="cal-nav" data-cal="next" title="Следующий месяц"></button></div>`;
html += `<div class="cal-grid">` + WD.map((w) => `<div class="cal-wd">${w}</div>`).join("");
for (let i = 0; i < startDow; i++) html += `<div class="cal-day empty"></div>`;
for (let dd = 1; dd <= days; dd++) {
const sel = (y === sy && m === sm - 1 && dd === sd) ? " sel" : "";
const today = (y === t.getFullYear() && m === t.getMonth() && dd === t.getDate()) ? " today" : "";
html += `<div class="cal-day${sel}${today}" data-day="${dd}">${dd}</div>`;
}
box.innerHTML = html + `</div>`;
},
async load() {
if (!this.cam || !this.date) return;
const [y, m, d] = this.date.split("-").map(Number);
+41 -1
View File
@@ -247,7 +247,47 @@ footer.grid-bar {
}
/* ── Архив ── */
.archive-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.archive-main { flex: 1; display: flex; overflow: hidden; } /* сайдбар + контент */
/* левый сайдбар архива: 220px, поделён по высоте пополам */
.arch-side {
width: 220px; flex: none; display: flex; flex-direction: column;
background: var(--panel); border-right: 1px solid var(--border); overflow: hidden;
}
.arch-cams { flex: 1 1 50%; min-height: 0; overflow-y: auto; border-bottom: 1px solid var(--border); }
.arch-cam {
padding: 6px 12px; font-size: 13px; color: var(--text); cursor: pointer;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.arch-cam:hover { background: var(--panel-2); }
.arch-cam.active { background: #c0392b; color: #fff; font-weight: 600; }
.arch-date {
flex: 1 1 50%; min-height: 0; overflow-y: auto;
display: flex; flex-direction: column; gap: 8px; padding: 10px;
}
.arch-dateinput {
width: 100%; background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
border-radius: 6px; padding: 5px 8px; font-size: 13px;
}
.arch-cal { user-select: none; }
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.cal-title { font-size: 13px; font-weight: 600; color: var(--text); }
.cal-nav {
background: var(--panel-2); border: 1px solid var(--border); color: var(--text);
width: 24px; height: 24px; border-radius: 6px; cursor: pointer; padding: 0; line-height: 1; font-size: 14px;
}
.cal-nav:hover { border-color: #c0392b; color: #fff; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.cal-wd { font-size: 10px; color: var(--text-dim); text-align: center; padding: 2px 0; }
.cal-day {
text-align: center; font-size: 12px; padding: 4px 0; border-radius: 5px; cursor: pointer;
color: var(--text); font-variant-numeric: tabular-nums;
}
.cal-day.empty { visibility: hidden; cursor: default; }
.cal-day:hover:not(.empty) { background: var(--panel-2); }
.cal-day.today { box-shadow: inset 0 0 0 1px var(--text-dim); }
.cal-day.sel { background: #c0392b; color: #fff; font-weight: 600; }
/* контент архива (плеер + таймлайн) */
.arch-content { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
.player-wrap { flex: 1; background: #000; display: flex; align-items: center; justify-content: center; min-height: 0; }
.player-wrap video { max-width: 100%; max-height: 100%; }
.player-wrap .hint { color: var(--text-dim); }