feat(ui): render the CCTV frame preview, source badge and summary from the store

The frame lifecycle keeps its request state in an object instead of the
image's dataset and publishes it; the panel binds the image's src, class
and data attributes and the wrapper's loading and has-frame classes from
it, so preload tokens, kept pixels on a failed refresh and clearing on a
camera change are unchanged. The source badge is the pure
presentCctvSourceBadge(), the live switch and the typed summary are
published too. The video surface still draws into the panel's canvas.
This commit is contained in:
Sameh Khamis committed 2026-10-30 21:54:42 -07:00
1 parent 6c7035f617
commit eef3dec759
10 files changed
+306 -177

No files matched your search

+7 -9
View File
@@ -249,9 +249,8 @@ export function _renderCctvState(state) {
this.store.publish(presentCctvPanel(state, enabled));
const liveIntent = enabled && !!activeCamera?.isVideo;
this.store.setLive(liveIntent);
if (this._cctvVideo) {
this._cctvVideo.hidden = !liveIntent;
if (this._cctvFrame) this._cctvFrame.hidden = liveIntent;
const visible =
liveIntent &&
!document.hidden &&
@@ -271,16 +270,15 @@ export function _renderCctvState(state) {
if (this._cctvFrame && !liveIntent) {
const nextSrc = enabled ? activeCamera?.frameUrl : null;
const nextCameraId = enabled ? activeCamera?.id || '' : '';
const cameraChanged = this._cctvFrame.dataset.cameraId !== nextCameraId;
const frameLoading = this._cctvFrame.dataset.loading === 'true';
const cameraChanged = this._frame.cameraId !== nextCameraId;
const frameLoading = this._frame.loading;
// A same-camera refresh waits for the current image to settle. Replacing
// src every 10 seconds can cancel a slow but healthy decode forever and
// leave SNAPSHOT · OK beside a blank/loading preview. Camera changes are
// immediate so navigation never waits on the prior camera's request.
if (
nextSrc &&
(cameraChanged ||
(!frameLoading && this._cctvFrame.dataset.currentSrc !== nextSrc))
(cameraChanged || (!frameLoading && this._frame.currentSrc !== nextSrc))
) {
this._queueCctvFrame(nextSrc, nextCameraId, cameraChanged);
}
@@ -305,18 +303,18 @@ export function _typeCctvSummary(text) {
this._lastCctvSummaryText = nextText;
clearInterval(this._cctvSummaryTypingTimer);
this._cctvSummary.textContent = '';
this.store.setSummary('');
let idx = 0;
this._cctvSummaryTypingTimer = setInterval(() => {
if (this.destroyed) return;
idx += 3;
if (idx >= nextText.length) {
this._cctvSummary.textContent = nextText;
this.store.setSummary(nextText);
clearInterval(this._cctvSummaryTypingTimer);
this._cctvSummaryTypingTimer = null;
return;
}
this._cctvSummary.textContent = nextText.slice(0, idx);
this.store.setSummary(nextText.slice(0, idx));
}, 20);
}