From dcc4cc046f96f5c6466c1921569d0d166b5db648 Mon Sep 17 00:00:00 2001 From: core Date: Mon, 1 Jun 2026 22:14:59 +0500 Subject: [PATCH] archive: replace toolbar with a 220px split sidebar (camera list + calendar) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The player-frame toolbar (camera - - + +
+
+
Кликните по таймлайну ниже, чтобы смотреть запись
+
+
-
-
Кликните по таймлайну ниже, чтобы смотреть запись
-
-
diff --git a/frontend/static/app.js b/frontend/static/app.js index 37a657e..2e1bb9e 100644 --- a/frontend/static/app.js +++ b/frontend/static/app.js @@ -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) => ``).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) => + `
${c.name}
` + ).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 = `
` + + `` + + `${MN[m]} ${y}` + + `
`; + html += `
` + WD.map((w) => `
${w}
`).join(""); + for (let i = 0; i < startDow; i++) html += `
`; + 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 += `
${dd}
`; + } + box.innerHTML = html + `
`; + }, + async load() { if (!this.cam || !this.date) return; const [y, m, d] = this.date.split("-").map(Number); diff --git a/frontend/static/style.css b/frontend/static/style.css index d834edc..3be97b5 100644 --- a/frontend/static/style.css +++ b/frontend/static/style.css @@ -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); }