-
This commit is contained in:
parent
0f7b410ede
commit
87670de0a7
238
assets/app.css
238
assets/app.css
@ -143,6 +143,15 @@ textarea {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.content-header__back {
|
||||
display: none;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.content-header > div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content-header__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -1031,6 +1040,10 @@ textarea {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body.is-mobile-ui #camera-modal {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.camera-modal {
|
||||
position: relative;
|
||||
width: calc(100vw - 24px);
|
||||
@ -1219,9 +1232,22 @@ textarea {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.entity-modal__cover {
|
||||
grid-template-columns: minmax(0, 1fr) 118px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.entity-modal__rail {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.entity-modal__rail--cover {
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
justify-items: center;
|
||||
align-content: stretch;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.entity-modal__cover-meta {
|
||||
@ -1248,22 +1274,75 @@ textarea {
|
||||
}
|
||||
|
||||
.entity-modal__cover-track {
|
||||
height: 10px;
|
||||
position: relative;
|
||||
justify-self: center;
|
||||
width: 56px;
|
||||
min-height: 280px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
background:
|
||||
radial-gradient(circle at 50% 6%, rgba(255,255,255,0.10), transparent 28%),
|
||||
linear-gradient(180deg, rgba(18, 20, 27, 0.96), rgba(10, 12, 17, 0.98));
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
inset 0 -18px 28px rgba(0, 0, 0, 0.26);
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.entity-modal__cover-fill {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #67d6ff, #88f0c7);
|
||||
width: 0%;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(103, 214, 255, 0.96) 0%, rgba(126, 236, 220, 0.94) 58%, rgba(188, 255, 242, 0.96) 100%);
|
||||
box-shadow:
|
||||
0 10px 26px rgba(103, 214, 255, 0.14),
|
||||
inset 0 1px 0 rgba(255,255,255,0.20);
|
||||
width: 100%;
|
||||
height: 0%;
|
||||
}
|
||||
|
||||
.entity-modal__slider {
|
||||
.entity-modal__cover-handle {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -10px;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255,255,255,0.18);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.98), rgba(228,236,244,0.9));
|
||||
box-shadow:
|
||||
0 10px 22px rgba(0, 0, 0, 0.26),
|
||||
inset 0 1px 0 rgba(255,255,255,0.7);
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.entity-modal__actions--vertical {
|
||||
display: grid;
|
||||
grid-auto-rows: minmax(52px, auto);
|
||||
gap: 10px;
|
||||
align-content: center;
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.entity-modal__actions--vertical .mushroom-button--square {
|
||||
width: 100%;
|
||||
accent-color: var(--accent);
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.entity-modal__actions--vertical .mushroom-button__body {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.entity-modal__actions--vertical .mushroom-button__icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.entity-modal__actions {
|
||||
@ -1281,6 +1360,16 @@ textarea {
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.entity-modal__actions.entity-modal__actions--vertical {
|
||||
grid-template-columns: 1fr;
|
||||
grid-auto-rows: minmax(52px, auto);
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.entity-modal__actions.entity-modal__actions--vertical .mushroom-button--square {
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.entity-modal__climate-summary {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@ -1826,18 +1915,84 @@ textarea {
|
||||
|
||||
@media (max-width: 920px) {
|
||||
body {
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
height: 100dvh;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
.app-shell.is-mobile {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell.is-mobile .sidebar,
|
||||
.app-shell.is-mobile .content {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app-shell.is-mobile .sidebar {
|
||||
overflow: auto;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
padding: 18px 16px calc(18px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.app-shell.is-mobile .content {
|
||||
overflow: auto;
|
||||
padding: 18px 16px calc(18px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.app-shell.is-mobile.mobile-view-spaces .content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-shell.is-mobile.mobile-view-room .sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-shell.is-mobile .content-header {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.app-shell.is-mobile .content-header__back {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.app-shell.is-mobile .content-header__title {
|
||||
margin-top: 0;
|
||||
font-size: clamp(26px, 7vw, 36px);
|
||||
}
|
||||
|
||||
.app-shell.is-mobile .content-header__meta {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.app-shell.is-mobile .room-item.is-selected {
|
||||
background: linear-gradient(180deg, rgba(28, 31, 39, 0.92), rgba(20, 23, 30, 0.92));
|
||||
border-color: var(--border);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
.app-shell.is-mobile .room-item.is-selected .room-item__icon {
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--accent);
|
||||
--icon-node-img-filter: brightness(0) saturate(100%) invert(72%) sepia(45%) saturate(1190%) hue-rotate(165deg) brightness(102%) contrast(101%);
|
||||
}
|
||||
|
||||
.app-shell.is-mobile .room-item.is-selected .room-item__count {
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--text-subtle);
|
||||
}
|
||||
|
||||
.app-shell.is-mobile .content-top {
|
||||
margin-top: -12px;
|
||||
}
|
||||
|
||||
.main-dashboard__hero {
|
||||
@ -1849,7 +2004,8 @@ textarea {
|
||||
}
|
||||
|
||||
.main-dashboard__actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.main-quick-action {
|
||||
@ -1860,16 +2016,26 @@ textarea {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.grid-card--auto,
|
||||
.grid-card--entity,
|
||||
.grid-card--entity-wide,
|
||||
.grid-card--cover,
|
||||
.grid-card--climate {
|
||||
grid-column: 1 / -1;
|
||||
.main-dashboard__cards {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.main-dashboard__actions {
|
||||
grid-template-columns: 1fr;
|
||||
.main-dashboard__cards .grid-card {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.room-entities-section__grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.room-entities-section__grid .grid-card--entity,
|
||||
.room-entities-section__grid .grid-card--entity-wide,
|
||||
.room-entities-section__grid .grid-card--cover,
|
||||
.room-entities-section__grid .grid-card--climate,
|
||||
.room-entities-section__grid .grid-card--auto {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.main-quick-action {
|
||||
@ -1890,4 +2056,36 @@ textarea {
|
||||
height: calc(100vh - 16px);
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.entity-modal {
|
||||
width: calc(100vw - 20px);
|
||||
max-height: calc(100dvh - 20px);
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.entity-modal__body {
|
||||
padding: 16px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.entity-modal__cover {
|
||||
grid-template-columns: minmax(0, 1fr) 88px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.entity-modal__cover-track {
|
||||
width: 44px;
|
||||
min-height: 240px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.entity-modal__cover-handle {
|
||||
width: 30px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.entity-modal__actions--vertical {
|
||||
grid-auto-rows: minmax(48px, auto);
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
240
assets/app.js
240
assets/app.js
@ -1,8 +1,11 @@
|
||||
(function () {
|
||||
const bootstrap = window.APP_BOOTSTRAP || {};
|
||||
const MOBILE_BREAKPOINT = 920;
|
||||
const state = {
|
||||
snapshot: bootstrap,
|
||||
selectedRoomId: 'main',
|
||||
isMobileViewport: false,
|
||||
mobileView: 'spaces',
|
||||
editMode: Boolean(bootstrap?.settings?.edit_mode),
|
||||
clockTimer: null,
|
||||
hlsInstance: null,
|
||||
@ -47,6 +50,48 @@
|
||||
return Array.from(root.querySelectorAll(sel));
|
||||
}
|
||||
|
||||
function mobileViewportQuery() {
|
||||
return window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`);
|
||||
}
|
||||
|
||||
function isMobileViewport() {
|
||||
return Boolean(state.isMobileViewport);
|
||||
}
|
||||
|
||||
function isMobileRoomView() {
|
||||
return isMobileViewport() && state.mobileView === 'room';
|
||||
}
|
||||
|
||||
function setMobileView(nextView) {
|
||||
if (!isMobileViewport()) {
|
||||
state.mobileView = 'room';
|
||||
return;
|
||||
}
|
||||
|
||||
state.mobileView = nextView === 'room' ? 'room' : 'spaces';
|
||||
}
|
||||
|
||||
function syncViewportState() {
|
||||
const query = mobileViewportQuery();
|
||||
const nextIsMobile = Boolean(query.matches);
|
||||
const changed = nextIsMobile !== state.isMobileViewport;
|
||||
|
||||
state.isMobileViewport = nextIsMobile;
|
||||
if (nextIsMobile) {
|
||||
state.mobileView = changed ? 'spaces' : (state.mobileView || 'spaces');
|
||||
} else {
|
||||
state.mobileView = 'room';
|
||||
clearRoomAutoReturnTimer();
|
||||
scheduleRoomAutoReturn(state.selectedRoomId || 'main');
|
||||
}
|
||||
|
||||
if (nextIsMobile) {
|
||||
clearRoomAutoReturnTimer();
|
||||
}
|
||||
|
||||
return nextIsMobile;
|
||||
}
|
||||
|
||||
function iconClass(icon) {
|
||||
if (!icon) return 'mdi mdi-help-circle-outline';
|
||||
return icon.startsWith('mdi:') ? `mdi ${icon.replace('mdi:', 'mdi-')}` : icon;
|
||||
@ -402,12 +447,30 @@
|
||||
});
|
||||
}
|
||||
|
||||
function sortMainEntities(entities) {
|
||||
return (Array.isArray(entities) ? entities : [])
|
||||
.slice()
|
||||
.sort((left, right) => {
|
||||
const leftTime = entitySortTime(left);
|
||||
const rightTime = entitySortTime(right);
|
||||
if (leftTime !== rightTime) return leftTime - rightTime;
|
||||
|
||||
const leftOrder = Number(left?.order ?? 9999);
|
||||
const rightOrder = Number(right?.order ?? 9999);
|
||||
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
||||
|
||||
return String(left?.name || '').localeCompare(String(right?.name || ''), 'ru');
|
||||
});
|
||||
}
|
||||
|
||||
function roomEntities(snapshot, roomId) {
|
||||
return sortRoomEntities(roomEntityCollection(snapshot, roomId).filter((entity) => entity.visible !== false));
|
||||
const collection = roomEntityCollection(snapshot, roomId).filter((entity) => entity.visible !== false);
|
||||
return roomId === 'main' ? sortMainEntities(collection) : sortRoomEntities(collection);
|
||||
}
|
||||
|
||||
function roomEntitiesIncludingHidden(snapshot, roomId) {
|
||||
return sortRoomEntities(roomEntityCollection(snapshot, roomId));
|
||||
const collection = roomEntityCollection(snapshot, roomId);
|
||||
return roomId === 'main' ? sortMainEntities(collection) : sortRoomEntities(collection);
|
||||
}
|
||||
|
||||
function entityKindLabel(entity) {
|
||||
@ -520,6 +583,21 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function syncLayoutState() {
|
||||
if (!els.appShell) return;
|
||||
|
||||
const mobile = isMobileViewport();
|
||||
document.body.classList.toggle('is-mobile-ui', mobile);
|
||||
els.appShell.classList.toggle('is-mobile', mobile);
|
||||
els.appShell.classList.toggle('is-desktop', !mobile);
|
||||
els.appShell.classList.toggle('mobile-view-spaces', mobile && state.mobileView !== 'room');
|
||||
els.appShell.classList.toggle('mobile-view-room', mobile && state.mobileView === 'room');
|
||||
|
||||
if (els.selectedRoomBack) {
|
||||
els.selectedRoomBack.hidden = !isMobileRoomView();
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePositionValue(value) {
|
||||
const next = Number(value);
|
||||
if (!Number.isFinite(next)) return null;
|
||||
@ -563,7 +641,8 @@
|
||||
|
||||
function sortMainCardsBySnapshot(container) {
|
||||
const snapshot = state.snapshot || {};
|
||||
const order = new Map((snapshot.main_entities || []).map((entity, index) => [entity.entity_id, index]));
|
||||
const orderedMainIds = sortMainEntities(snapshot.main_entities || []).map((entity) => entity.entity_id);
|
||||
const order = new Map(orderedMainIds.map((entityId, index) => [entityId, index]));
|
||||
const cards = Array.from(container?.querySelectorAll('.grid-card[data-entity-id]') || []);
|
||||
cards.sort((left, right) => {
|
||||
const leftId = left.dataset.entityId || '';
|
||||
@ -607,7 +686,7 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
const orderedMainIds = (snapshot.main_entities || []).map((item) => item.entity_id);
|
||||
const orderedMainIds = sortMainEntities(snapshot.main_entities || []).map((item) => item.entity_id);
|
||||
const nextIndex = orderedMainIds.indexOf(entityId);
|
||||
const cards = Array.from(container.querySelectorAll('.grid-card[data-entity-id]'));
|
||||
for (const card of cards) {
|
||||
@ -1172,7 +1251,7 @@
|
||||
function scheduleRoomAutoReturn(roomId) {
|
||||
const nextRoomId = roomId || 'main';
|
||||
clearRoomAutoReturnTimer();
|
||||
if (nextRoomId === 'main') {
|
||||
if (nextRoomId === 'main' || isMobileViewport()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1189,6 +1268,9 @@
|
||||
const token = ++state.roomSelectionToken;
|
||||
clearRoomAutoReturnTimer();
|
||||
patchSnapshotSelection(nextRoomId);
|
||||
if (isMobileViewport()) {
|
||||
setMobileView('room');
|
||||
}
|
||||
render();
|
||||
scheduleRoomAutoReturn(nextRoomId);
|
||||
try {
|
||||
@ -1944,7 +2026,7 @@
|
||||
wrap.className = 'entity-modal__cover';
|
||||
|
||||
const rail = document.createElement('div');
|
||||
rail.className = 'entity-modal__rail';
|
||||
rail.className = 'entity-modal__rail entity-modal__rail--cover';
|
||||
|
||||
const currentPosition = Number(entity.attributes?.current_position);
|
||||
const initialValue = Number.isFinite(currentPosition)
|
||||
@ -1968,43 +2050,71 @@
|
||||
|
||||
const fill = document.createElement('div');
|
||||
fill.className = 'entity-modal__cover-fill';
|
||||
fill.style.width = `${initialValue}%`;
|
||||
fill.style.height = `${initialValue}%`;
|
||||
fill.style.top = '0';
|
||||
fill.style.width = '100%';
|
||||
progress.appendChild(fill);
|
||||
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
slider.min = '0';
|
||||
slider.max = '100';
|
||||
slider.step = '1';
|
||||
slider.value = String(initialValue);
|
||||
slider.className = 'entity-modal__slider';
|
||||
const handle = document.createElement('div');
|
||||
handle.className = 'entity-modal__cover-handle';
|
||||
progress.appendChild(handle);
|
||||
|
||||
const syncSlider = () => {
|
||||
const nextValue = Number(slider.value || 0);
|
||||
fill.style.width = `${nextValue}%`;
|
||||
const syncValue = (nextValue) => {
|
||||
fill.style.height = `${nextValue}%`;
|
||||
handle.style.top = nextValue <= 0 ? 'calc(100% - 10px)' : `calc(${nextValue}% - 10px)`;
|
||||
value.textContent = `${nextValue}%`;
|
||||
};
|
||||
slider.addEventListener('input', syncSlider);
|
||||
slider.addEventListener('change', () => {
|
||||
const nextValue = Math.max(0, Math.min(100, Number(slider.value || 0)));
|
||||
syncValue(initialValue);
|
||||
|
||||
const updateFromPointer = (clientY) => {
|
||||
const rect = progress.getBoundingClientRect();
|
||||
const ratio = 1 - ((clientY - rect.top) / rect.height);
|
||||
const nextValue = Math.max(0, Math.min(100, Math.round(ratio * 100)));
|
||||
syncValue(nextValue);
|
||||
handleCoverPosition(entity, nextValue);
|
||||
};
|
||||
|
||||
let dragPointerId = null;
|
||||
const onPointerMove = (event) => {
|
||||
if (dragPointerId !== event.pointerId) return;
|
||||
event.preventDefault();
|
||||
updateFromPointer(event.clientY);
|
||||
};
|
||||
const onPointerUp = (event) => {
|
||||
if (dragPointerId !== event.pointerId) return;
|
||||
event.preventDefault();
|
||||
progress.releasePointerCapture?.(dragPointerId);
|
||||
dragPointerId = null;
|
||||
window.removeEventListener('pointermove', onPointerMove);
|
||||
window.removeEventListener('pointerup', onPointerUp);
|
||||
window.removeEventListener('pointercancel', onPointerUp);
|
||||
};
|
||||
|
||||
progress.addEventListener('pointerdown', (event) => {
|
||||
if (event.button !== 0) return;
|
||||
dragPointerId = event.pointerId;
|
||||
progress.setPointerCapture?.(dragPointerId);
|
||||
updateFromPointer(event.clientY);
|
||||
window.addEventListener('pointermove', onPointerMove, { passive: false });
|
||||
window.addEventListener('pointerup', onPointerUp, { passive: false });
|
||||
window.addEventListener('pointercancel', onPointerUp, { passive: false });
|
||||
});
|
||||
|
||||
rail.append(valueRow, progress, slider);
|
||||
rail.append(valueRow, progress);
|
||||
|
||||
const buttons = document.createElement('div');
|
||||
buttons.className = 'entity-modal__actions';
|
||||
|
||||
const closeBtn = createButton('Закрыть', null, 'mdi:arrow-down', 'mushroom-button mushroom-button--small mushroom-button--square');
|
||||
closeBtn.addEventListener('click', () => handleEntityService(entity, 'close'));
|
||||
|
||||
const stopBtn = createButton('Стоп', null, 'mdi:stop', 'mushroom-button mushroom-button--small mushroom-button--square');
|
||||
stopBtn.addEventListener('click', () => handleEntityService(entity, 'stop'));
|
||||
buttons.className = 'entity-modal__actions entity-modal__actions--vertical';
|
||||
|
||||
const openBtn = createButton('Открыть', null, 'mdi:arrow-up', 'mushroom-button mushroom-button--small mushroom-button--square');
|
||||
openBtn.addEventListener('click', () => handleEntityService(entity, 'open'));
|
||||
|
||||
buttons.append(closeBtn, stopBtn, openBtn);
|
||||
const stopBtn = createButton('Стоп', null, 'mdi:stop', 'mushroom-button mushroom-button--small mushroom-button--square');
|
||||
stopBtn.addEventListener('click', () => handleEntityService(entity, 'stop'));
|
||||
|
||||
const closeBtn = createButton('Закрыть', null, 'mdi:arrow-down', 'mushroom-button mushroom-button--small mushroom-button--square');
|
||||
closeBtn.addEventListener('click', () => handleEntityService(entity, 'close'));
|
||||
|
||||
buttons.append(openBtn, stopBtn, closeBtn);
|
||||
wrap.append(rail, buttons);
|
||||
return wrap;
|
||||
}
|
||||
@ -2326,6 +2436,10 @@
|
||||
function renderCoverCard(entity, options = {}) {
|
||||
const card = document.createElement('article');
|
||||
const isOpen = ['open', 'opening'].includes(String(entity.state).toLowerCase());
|
||||
const currentPosition = Number(entity.attributes?.current_position);
|
||||
const hasVisiblePosition = Number.isFinite(currentPosition)
|
||||
? currentPosition > 0
|
||||
: isOpen;
|
||||
card.className = `grid-card grid-card--cover ${!options.isMain && isOpen ? 'is-active' : ''}`;
|
||||
card.dataset.entityId = entity.entity_id;
|
||||
card.tabIndex = 0;
|
||||
@ -2350,22 +2464,29 @@
|
||||
const rail = document.createElement('div');
|
||||
rail.className = 'cover-card__rail';
|
||||
|
||||
const progress = document.createElement('div');
|
||||
progress.className = 'cover-progress';
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'cover-progress__value';
|
||||
const pos = Number(entity.attributes?.current_position);
|
||||
bar.style.width = Number.isFinite(pos) ? `${Math.max(0, Math.min(100, pos))}%` : (isOpen ? '100%' : '0%');
|
||||
progress.appendChild(bar);
|
||||
if (hasVisiblePosition) {
|
||||
const progress = document.createElement('div');
|
||||
progress.className = 'cover-progress';
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'cover-progress__value';
|
||||
const pos = Number.isFinite(currentPosition) ? currentPosition : 100;
|
||||
bar.style.width = `${Math.max(0, Math.min(100, pos))}%`;
|
||||
progress.appendChild(bar);
|
||||
|
||||
if (!options.isMain) {
|
||||
rail.append(progress);
|
||||
inner.append(left, rail);
|
||||
if (!options.isMain) {
|
||||
rail.append(progress);
|
||||
inner.append(left, rail);
|
||||
if (state.editMode) {
|
||||
inner.appendChild(renderEditActions(entity));
|
||||
}
|
||||
} else {
|
||||
inner.append(left, progress);
|
||||
}
|
||||
} else {
|
||||
inner.append(left);
|
||||
if (state.editMode) {
|
||||
inner.appendChild(renderEditActions(entity));
|
||||
}
|
||||
} else {
|
||||
inner.append(left, progress);
|
||||
}
|
||||
|
||||
card.appendChild(inner);
|
||||
@ -2702,7 +2823,7 @@
|
||||
els.contentTop.classList.toggle('is-main', room.id === 'main');
|
||||
}
|
||||
if (els.contentHeader) {
|
||||
els.contentHeader.classList.toggle('hidden', room.id === 'main');
|
||||
els.contentHeader.classList.toggle('hidden', room.id === 'main' && !isMobileRoomView());
|
||||
}
|
||||
updateMainPrintStrip(snapshot);
|
||||
if (room.id !== 'main') {
|
||||
@ -2808,6 +2929,11 @@
|
||||
}
|
||||
|
||||
function renderPopup(snapshot) {
|
||||
if (isMobileViewport()) {
|
||||
hidePopup({ preserveSnapshot: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const popup = mergePopupWithCamera(snapshot.popup || {});
|
||||
const signature = JSON.stringify([
|
||||
popup.active,
|
||||
@ -2883,13 +3009,13 @@
|
||||
}
|
||||
|
||||
function hidePopup(options = {}) {
|
||||
const { suppressAutoOpen = false } = options;
|
||||
const { suppressAutoOpen = false, preserveSnapshot = false } = options;
|
||||
if (suppressAutoOpen) {
|
||||
state.popupAutoOpenBlockedUntil = Date.now() + 60000;
|
||||
}
|
||||
state.lastPopupSignature = '';
|
||||
state.snapshot = state.snapshot || bootstrap;
|
||||
if (state.snapshot.popup) {
|
||||
if (!preserveSnapshot && state.snapshot.popup) {
|
||||
state.snapshot.popup = {
|
||||
...state.snapshot.popup,
|
||||
active: false,
|
||||
@ -3052,6 +3178,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
syncLayoutState();
|
||||
renderRoomButtons(snapshot.spaces || snapshot.rooms);
|
||||
renderSelectedRoom(snapshot);
|
||||
renderDashboard(snapshot);
|
||||
@ -3067,6 +3194,7 @@
|
||||
function renderDashboardOnly() {
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||
syncLayoutState();
|
||||
renderSelectedRoom(snapshot);
|
||||
renderDashboard(snapshot);
|
||||
renderPopup(snapshot);
|
||||
@ -3119,6 +3247,7 @@
|
||||
function renderSelectionOnly() {
|
||||
const snapshot = state.snapshot || bootstrap;
|
||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||
syncLayoutState();
|
||||
renderSelectedRoom(snapshot);
|
||||
}
|
||||
|
||||
@ -3241,6 +3370,15 @@
|
||||
}
|
||||
|
||||
function wireEvents() {
|
||||
els.selectedRoomBack?.addEventListener('click', () => {
|
||||
if (!isMobileViewport()) return;
|
||||
closeEntityPopup();
|
||||
setMobileView('spaces');
|
||||
syncLayoutState();
|
||||
renderSidebarOnly();
|
||||
renderSelectionOnly();
|
||||
});
|
||||
|
||||
els.cameraBackdrop.addEventListener('click', (event) => {
|
||||
if (event.target === els.cameraBackdrop) {
|
||||
apiPost('popup', { command: 'close' }).catch(() => {});
|
||||
@ -3312,11 +3450,13 @@
|
||||
}
|
||||
|
||||
function initRefs() {
|
||||
els.appShell = q('.app-shell');
|
||||
els.clockTime = $('clock-time');
|
||||
els.clockDate = $('clock-date');
|
||||
els.roomsCount = $('rooms-count');
|
||||
els.roomList = $('room-list');
|
||||
els.editModeToggle = $('edit-mode-toggle');
|
||||
els.selectedRoomBack = $('selected-room-back');
|
||||
els.contentTop = q('.content-top');
|
||||
els.mainPrintStripSlot = $('main-print-strip-slot');
|
||||
els.contentHeader = q('.content-header');
|
||||
@ -3551,11 +3691,23 @@
|
||||
|
||||
async function start() {
|
||||
initRefs();
|
||||
syncViewportState();
|
||||
updateClock();
|
||||
clearInterval(state.clockTimer);
|
||||
state.clockTimer = setInterval(updateClock, 1000);
|
||||
wireEvents();
|
||||
|
||||
const viewportQuery = mobileViewportQuery();
|
||||
const handleViewportChange = () => {
|
||||
syncViewportState();
|
||||
render();
|
||||
};
|
||||
if (typeof viewportQuery.addEventListener === 'function') {
|
||||
viewportQuery.addEventListener('change', handleViewportChange);
|
||||
} else if (typeof viewportQuery.addListener === 'function') {
|
||||
viewportQuery.addListener(handleViewportChange);
|
||||
}
|
||||
|
||||
const initial = window.APP_BOOTSTRAP || {};
|
||||
state.snapshot = initial;
|
||||
render();
|
||||
|
||||
@ -83,7 +83,9 @@
|
||||
"poster_url": "http://10.0.6.110:1984/api/frame.jpeg?src=doorbell_main",
|
||||
"popup_timeout_minutes": 3,
|
||||
"trigger_entities": [
|
||||
"binary_sensor.doorbell_person_occupancy"
|
||||
"binary_sensor.doorbell_person_occupancy",
|
||||
"binary_sensor.barn_all_occupancy",
|
||||
"binary_sensor.doorbell_all_occupancy"
|
||||
]
|
||||
},
|
||||
"rooms": [
|
||||
|
||||
@ -23,8 +23,8 @@ $appTitle = htmlspecialchars((string)($config['app']['title'] ?? 'Wall Panel'),
|
||||
<script>
|
||||
window.APP_BOOTSTRAP = <?= json_encode($bootstrap, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/app.css?v=0.16">
|
||||
<script src="assets/app.js?v=0.16" defer></script>
|
||||
<link rel="stylesheet" href="assets/app.css?v=0.19">
|
||||
<script src="assets/app.js?v=0.19" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
@ -53,6 +53,9 @@ $appTitle = htmlspecialchars((string)($config['app']['title'] ?? 'Wall Panel'),
|
||||
<div class="main-print-strip-slot" id="main-print-strip-slot"></div>
|
||||
</div>
|
||||
<header class="content-header">
|
||||
<button class="icon-button icon-button--ghost content-header__back" id="selected-room-back" type="button" aria-label="Back">
|
||||
<i class="mdi mdi-arrow-left"></i>
|
||||
</button>
|
||||
<div>
|
||||
<div class="content-header__eyebrow" id="selected-room-eyebrow"></div>
|
||||
<h1 class="content-header__title" id="selected-room-title">Загрузка</h1>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user