-
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;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-header__back {
|
||||||
|
display: none;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header > div {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.content-header__actions {
|
.content-header__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1031,6 +1040,10 @@ textarea {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.is-mobile-ui #camera-modal {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.camera-modal {
|
.camera-modal {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: calc(100vw - 24px);
|
width: calc(100vw - 24px);
|
||||||
@ -1219,9 +1232,22 @@ textarea {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entity-modal__cover {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 118px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
.entity-modal__rail {
|
.entity-modal__rail {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
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 {
|
.entity-modal__cover-meta {
|
||||||
@ -1248,22 +1274,75 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.entity-modal__cover-track {
|
.entity-modal__cover-track {
|
||||||
height: 10px;
|
position: relative;
|
||||||
|
justify-self: center;
|
||||||
|
width: 56px;
|
||||||
|
min-height: 280px;
|
||||||
border-radius: 999px;
|
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;
|
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 {
|
.entity-modal__cover-fill {
|
||||||
height: 100%;
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
background: linear-gradient(90deg, #67d6ff, #88f0c7);
|
background:
|
||||||
width: 0%;
|
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%;
|
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 {
|
.entity-modal__actions {
|
||||||
@ -1281,6 +1360,16 @@ textarea {
|
|||||||
line-height: 1.05;
|
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 {
|
.entity-modal__climate-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@ -1826,18 +1915,84 @@ textarea {
|
|||||||
|
|
||||||
@media (max-width: 920px) {
|
@media (max-width: 920px) {
|
||||||
body {
|
body {
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
height: auto;
|
height: 100dvh;
|
||||||
min-height: 100vh;
|
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-right: 0;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
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 {
|
.main-dashboard__hero {
|
||||||
@ -1849,7 +2004,8 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main-dashboard__actions {
|
.main-dashboard__actions {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-quick-action {
|
.main-quick-action {
|
||||||
@ -1860,16 +2016,26 @@ textarea {
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-card--auto,
|
.main-dashboard__cards {
|
||||||
.grid-card--entity,
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
.grid-card--entity-wide,
|
gap: 10px;
|
||||||
.grid-card--cover,
|
|
||||||
.grid-card--climate {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-dashboard__actions {
|
.main-dashboard__cards .grid-card {
|
||||||
grid-template-columns: 1fr;
|
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 {
|
.main-quick-action {
|
||||||
@ -1890,4 +2056,36 @@ textarea {
|
|||||||
height: calc(100vh - 16px);
|
height: calc(100vh - 16px);
|
||||||
border-radius: 18px;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
220
assets/app.js
220
assets/app.js
@ -1,8 +1,11 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const bootstrap = window.APP_BOOTSTRAP || {};
|
const bootstrap = window.APP_BOOTSTRAP || {};
|
||||||
|
const MOBILE_BREAKPOINT = 920;
|
||||||
const state = {
|
const state = {
|
||||||
snapshot: bootstrap,
|
snapshot: bootstrap,
|
||||||
selectedRoomId: 'main',
|
selectedRoomId: 'main',
|
||||||
|
isMobileViewport: false,
|
||||||
|
mobileView: 'spaces',
|
||||||
editMode: Boolean(bootstrap?.settings?.edit_mode),
|
editMode: Boolean(bootstrap?.settings?.edit_mode),
|
||||||
clockTimer: null,
|
clockTimer: null,
|
||||||
hlsInstance: null,
|
hlsInstance: null,
|
||||||
@ -47,6 +50,48 @@
|
|||||||
return Array.from(root.querySelectorAll(sel));
|
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) {
|
function iconClass(icon) {
|
||||||
if (!icon) return 'mdi mdi-help-circle-outline';
|
if (!icon) return 'mdi mdi-help-circle-outline';
|
||||||
return icon.startsWith('mdi:') ? `mdi ${icon.replace('mdi:', 'mdi-')}` : icon;
|
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) {
|
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) {
|
function roomEntitiesIncludingHidden(snapshot, roomId) {
|
||||||
return sortRoomEntities(roomEntityCollection(snapshot, roomId));
|
const collection = roomEntityCollection(snapshot, roomId);
|
||||||
|
return roomId === 'main' ? sortMainEntities(collection) : sortRoomEntities(collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
function entityKindLabel(entity) {
|
function entityKindLabel(entity) {
|
||||||
@ -520,6 +583,21 @@
|
|||||||
return true;
|
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) {
|
function normalizePositionValue(value) {
|
||||||
const next = Number(value);
|
const next = Number(value);
|
||||||
if (!Number.isFinite(next)) return null;
|
if (!Number.isFinite(next)) return null;
|
||||||
@ -563,7 +641,8 @@
|
|||||||
|
|
||||||
function sortMainCardsBySnapshot(container) {
|
function sortMainCardsBySnapshot(container) {
|
||||||
const snapshot = state.snapshot || {};
|
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]') || []);
|
const cards = Array.from(container?.querySelectorAll('.grid-card[data-entity-id]') || []);
|
||||||
cards.sort((left, right) => {
|
cards.sort((left, right) => {
|
||||||
const leftId = left.dataset.entityId || '';
|
const leftId = left.dataset.entityId || '';
|
||||||
@ -607,7 +686,7 @@
|
|||||||
return true;
|
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 nextIndex = orderedMainIds.indexOf(entityId);
|
||||||
const cards = Array.from(container.querySelectorAll('.grid-card[data-entity-id]'));
|
const cards = Array.from(container.querySelectorAll('.grid-card[data-entity-id]'));
|
||||||
for (const card of cards) {
|
for (const card of cards) {
|
||||||
@ -1172,7 +1251,7 @@
|
|||||||
function scheduleRoomAutoReturn(roomId) {
|
function scheduleRoomAutoReturn(roomId) {
|
||||||
const nextRoomId = roomId || 'main';
|
const nextRoomId = roomId || 'main';
|
||||||
clearRoomAutoReturnTimer();
|
clearRoomAutoReturnTimer();
|
||||||
if (nextRoomId === 'main') {
|
if (nextRoomId === 'main' || isMobileViewport()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1189,6 +1268,9 @@
|
|||||||
const token = ++state.roomSelectionToken;
|
const token = ++state.roomSelectionToken;
|
||||||
clearRoomAutoReturnTimer();
|
clearRoomAutoReturnTimer();
|
||||||
patchSnapshotSelection(nextRoomId);
|
patchSnapshotSelection(nextRoomId);
|
||||||
|
if (isMobileViewport()) {
|
||||||
|
setMobileView('room');
|
||||||
|
}
|
||||||
render();
|
render();
|
||||||
scheduleRoomAutoReturn(nextRoomId);
|
scheduleRoomAutoReturn(nextRoomId);
|
||||||
try {
|
try {
|
||||||
@ -1944,7 +2026,7 @@
|
|||||||
wrap.className = 'entity-modal__cover';
|
wrap.className = 'entity-modal__cover';
|
||||||
|
|
||||||
const rail = document.createElement('div');
|
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 currentPosition = Number(entity.attributes?.current_position);
|
||||||
const initialValue = Number.isFinite(currentPosition)
|
const initialValue = Number.isFinite(currentPosition)
|
||||||
@ -1968,43 +2050,71 @@
|
|||||||
|
|
||||||
const fill = document.createElement('div');
|
const fill = document.createElement('div');
|
||||||
fill.className = 'entity-modal__cover-fill';
|
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);
|
progress.appendChild(fill);
|
||||||
|
|
||||||
const slider = document.createElement('input');
|
const handle = document.createElement('div');
|
||||||
slider.type = 'range';
|
handle.className = 'entity-modal__cover-handle';
|
||||||
slider.min = '0';
|
progress.appendChild(handle);
|
||||||
slider.max = '100';
|
|
||||||
slider.step = '1';
|
|
||||||
slider.value = String(initialValue);
|
|
||||||
slider.className = 'entity-modal__slider';
|
|
||||||
|
|
||||||
const syncSlider = () => {
|
const syncValue = (nextValue) => {
|
||||||
const nextValue = Number(slider.value || 0);
|
fill.style.height = `${nextValue}%`;
|
||||||
fill.style.width = `${nextValue}%`;
|
handle.style.top = nextValue <= 0 ? 'calc(100% - 10px)' : `calc(${nextValue}% - 10px)`;
|
||||||
value.textContent = `${nextValue}%`;
|
value.textContent = `${nextValue}%`;
|
||||||
};
|
};
|
||||||
slider.addEventListener('input', syncSlider);
|
syncValue(initialValue);
|
||||||
slider.addEventListener('change', () => {
|
|
||||||
const nextValue = Math.max(0, Math.min(100, Number(slider.value || 0)));
|
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);
|
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');
|
const buttons = document.createElement('div');
|
||||||
buttons.className = 'entity-modal__actions';
|
buttons.className = 'entity-modal__actions entity-modal__actions--vertical';
|
||||||
|
|
||||||
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'));
|
|
||||||
|
|
||||||
const openBtn = createButton('Открыть', null, 'mdi:arrow-up', 'mushroom-button mushroom-button--small mushroom-button--square');
|
const openBtn = createButton('Открыть', null, 'mdi:arrow-up', 'mushroom-button mushroom-button--small mushroom-button--square');
|
||||||
openBtn.addEventListener('click', () => handleEntityService(entity, 'open'));
|
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);
|
wrap.append(rail, buttons);
|
||||||
return wrap;
|
return wrap;
|
||||||
}
|
}
|
||||||
@ -2326,6 +2436,10 @@
|
|||||||
function renderCoverCard(entity, options = {}) {
|
function renderCoverCard(entity, options = {}) {
|
||||||
const card = document.createElement('article');
|
const card = document.createElement('article');
|
||||||
const isOpen = ['open', 'opening'].includes(String(entity.state).toLowerCase());
|
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.className = `grid-card grid-card--cover ${!options.isMain && isOpen ? 'is-active' : ''}`;
|
||||||
card.dataset.entityId = entity.entity_id;
|
card.dataset.entityId = entity.entity_id;
|
||||||
card.tabIndex = 0;
|
card.tabIndex = 0;
|
||||||
@ -2350,12 +2464,13 @@
|
|||||||
const rail = document.createElement('div');
|
const rail = document.createElement('div');
|
||||||
rail.className = 'cover-card__rail';
|
rail.className = 'cover-card__rail';
|
||||||
|
|
||||||
|
if (hasVisiblePosition) {
|
||||||
const progress = document.createElement('div');
|
const progress = document.createElement('div');
|
||||||
progress.className = 'cover-progress';
|
progress.className = 'cover-progress';
|
||||||
const bar = document.createElement('div');
|
const bar = document.createElement('div');
|
||||||
bar.className = 'cover-progress__value';
|
bar.className = 'cover-progress__value';
|
||||||
const pos = Number(entity.attributes?.current_position);
|
const pos = Number.isFinite(currentPosition) ? currentPosition : 100;
|
||||||
bar.style.width = Number.isFinite(pos) ? `${Math.max(0, Math.min(100, pos))}%` : (isOpen ? '100%' : '0%');
|
bar.style.width = `${Math.max(0, Math.min(100, pos))}%`;
|
||||||
progress.appendChild(bar);
|
progress.appendChild(bar);
|
||||||
|
|
||||||
if (!options.isMain) {
|
if (!options.isMain) {
|
||||||
@ -2367,6 +2482,12 @@
|
|||||||
} else {
|
} else {
|
||||||
inner.append(left, progress);
|
inner.append(left, progress);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
inner.append(left);
|
||||||
|
if (state.editMode) {
|
||||||
|
inner.appendChild(renderEditActions(entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
card.appendChild(inner);
|
card.appendChild(inner);
|
||||||
card.addEventListener('click', async (event) => {
|
card.addEventListener('click', async (event) => {
|
||||||
@ -2702,7 +2823,7 @@
|
|||||||
els.contentTop.classList.toggle('is-main', room.id === 'main');
|
els.contentTop.classList.toggle('is-main', room.id === 'main');
|
||||||
}
|
}
|
||||||
if (els.contentHeader) {
|
if (els.contentHeader) {
|
||||||
els.contentHeader.classList.toggle('hidden', room.id === 'main');
|
els.contentHeader.classList.toggle('hidden', room.id === 'main' && !isMobileRoomView());
|
||||||
}
|
}
|
||||||
updateMainPrintStrip(snapshot);
|
updateMainPrintStrip(snapshot);
|
||||||
if (room.id !== 'main') {
|
if (room.id !== 'main') {
|
||||||
@ -2808,6 +2929,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPopup(snapshot) {
|
function renderPopup(snapshot) {
|
||||||
|
if (isMobileViewport()) {
|
||||||
|
hidePopup({ preserveSnapshot: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const popup = mergePopupWithCamera(snapshot.popup || {});
|
const popup = mergePopupWithCamera(snapshot.popup || {});
|
||||||
const signature = JSON.stringify([
|
const signature = JSON.stringify([
|
||||||
popup.active,
|
popup.active,
|
||||||
@ -2883,13 +3009,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hidePopup(options = {}) {
|
function hidePopup(options = {}) {
|
||||||
const { suppressAutoOpen = false } = options;
|
const { suppressAutoOpen = false, preserveSnapshot = false } = options;
|
||||||
if (suppressAutoOpen) {
|
if (suppressAutoOpen) {
|
||||||
state.popupAutoOpenBlockedUntil = Date.now() + 60000;
|
state.popupAutoOpenBlockedUntil = Date.now() + 60000;
|
||||||
}
|
}
|
||||||
state.lastPopupSignature = '';
|
state.lastPopupSignature = '';
|
||||||
state.snapshot = state.snapshot || bootstrap;
|
state.snapshot = state.snapshot || bootstrap;
|
||||||
if (state.snapshot.popup) {
|
if (!preserveSnapshot && state.snapshot.popup) {
|
||||||
state.snapshot.popup = {
|
state.snapshot.popup = {
|
||||||
...state.snapshot.popup,
|
...state.snapshot.popup,
|
||||||
active: false,
|
active: false,
|
||||||
@ -3052,6 +3178,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncLayoutState();
|
||||||
renderRoomButtons(snapshot.spaces || snapshot.rooms);
|
renderRoomButtons(snapshot.spaces || snapshot.rooms);
|
||||||
renderSelectedRoom(snapshot);
|
renderSelectedRoom(snapshot);
|
||||||
renderDashboard(snapshot);
|
renderDashboard(snapshot);
|
||||||
@ -3067,6 +3194,7 @@
|
|||||||
function renderDashboardOnly() {
|
function renderDashboardOnly() {
|
||||||
const snapshot = state.snapshot || bootstrap;
|
const snapshot = state.snapshot || bootstrap;
|
||||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||||
|
syncLayoutState();
|
||||||
renderSelectedRoom(snapshot);
|
renderSelectedRoom(snapshot);
|
||||||
renderDashboard(snapshot);
|
renderDashboard(snapshot);
|
||||||
renderPopup(snapshot);
|
renderPopup(snapshot);
|
||||||
@ -3119,6 +3247,7 @@
|
|||||||
function renderSelectionOnly() {
|
function renderSelectionOnly() {
|
||||||
const snapshot = state.snapshot || bootstrap;
|
const snapshot = state.snapshot || bootstrap;
|
||||||
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
if (!snapshot || !(snapshot.spaces || snapshot.rooms)) return;
|
||||||
|
syncLayoutState();
|
||||||
renderSelectedRoom(snapshot);
|
renderSelectedRoom(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3241,6 +3370,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function wireEvents() {
|
function wireEvents() {
|
||||||
|
els.selectedRoomBack?.addEventListener('click', () => {
|
||||||
|
if (!isMobileViewport()) return;
|
||||||
|
closeEntityPopup();
|
||||||
|
setMobileView('spaces');
|
||||||
|
syncLayoutState();
|
||||||
|
renderSidebarOnly();
|
||||||
|
renderSelectionOnly();
|
||||||
|
});
|
||||||
|
|
||||||
els.cameraBackdrop.addEventListener('click', (event) => {
|
els.cameraBackdrop.addEventListener('click', (event) => {
|
||||||
if (event.target === els.cameraBackdrop) {
|
if (event.target === els.cameraBackdrop) {
|
||||||
apiPost('popup', { command: 'close' }).catch(() => {});
|
apiPost('popup', { command: 'close' }).catch(() => {});
|
||||||
@ -3312,11 +3450,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initRefs() {
|
function initRefs() {
|
||||||
|
els.appShell = q('.app-shell');
|
||||||
els.clockTime = $('clock-time');
|
els.clockTime = $('clock-time');
|
||||||
els.clockDate = $('clock-date');
|
els.clockDate = $('clock-date');
|
||||||
els.roomsCount = $('rooms-count');
|
els.roomsCount = $('rooms-count');
|
||||||
els.roomList = $('room-list');
|
els.roomList = $('room-list');
|
||||||
els.editModeToggle = $('edit-mode-toggle');
|
els.editModeToggle = $('edit-mode-toggle');
|
||||||
|
els.selectedRoomBack = $('selected-room-back');
|
||||||
els.contentTop = q('.content-top');
|
els.contentTop = q('.content-top');
|
||||||
els.mainPrintStripSlot = $('main-print-strip-slot');
|
els.mainPrintStripSlot = $('main-print-strip-slot');
|
||||||
els.contentHeader = q('.content-header');
|
els.contentHeader = q('.content-header');
|
||||||
@ -3551,11 +3691,23 @@
|
|||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
initRefs();
|
initRefs();
|
||||||
|
syncViewportState();
|
||||||
updateClock();
|
updateClock();
|
||||||
clearInterval(state.clockTimer);
|
clearInterval(state.clockTimer);
|
||||||
state.clockTimer = setInterval(updateClock, 1000);
|
state.clockTimer = setInterval(updateClock, 1000);
|
||||||
wireEvents();
|
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 || {};
|
const initial = window.APP_BOOTSTRAP || {};
|
||||||
state.snapshot = initial;
|
state.snapshot = initial;
|
||||||
render();
|
render();
|
||||||
|
|||||||
@ -83,7 +83,9 @@
|
|||||||
"poster_url": "http://10.0.6.110:1984/api/frame.jpeg?src=doorbell_main",
|
"poster_url": "http://10.0.6.110:1984/api/frame.jpeg?src=doorbell_main",
|
||||||
"popup_timeout_minutes": 3,
|
"popup_timeout_minutes": 3,
|
||||||
"trigger_entities": [
|
"trigger_entities": [
|
||||||
"binary_sensor.doorbell_person_occupancy"
|
"binary_sensor.doorbell_person_occupancy",
|
||||||
|
"binary_sensor.barn_all_occupancy",
|
||||||
|
"binary_sensor.doorbell_all_occupancy"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"rooms": [
|
"rooms": [
|
||||||
|
|||||||
@ -23,8 +23,8 @@ $appTitle = htmlspecialchars((string)($config['app']['title'] ?? 'Wall Panel'),
|
|||||||
<script>
|
<script>
|
||||||
window.APP_BOOTSTRAP = <?= json_encode($bootstrap, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
window.APP_BOOTSTRAP = <?= json_encode($bootstrap, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="assets/app.css?v=0.16">
|
<link rel="stylesheet" href="assets/app.css?v=0.19">
|
||||||
<script src="assets/app.js?v=0.16" defer></script>
|
<script src="assets/app.js?v=0.19" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-shell">
|
<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 class="main-print-strip-slot" id="main-print-strip-slot"></div>
|
||||||
</div>
|
</div>
|
||||||
<header class="content-header">
|
<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>
|
||||||
<div class="content-header__eyebrow" id="selected-room-eyebrow"></div>
|
<div class="content-header__eyebrow" id="selected-room-eyebrow"></div>
|
||||||
<h1 class="content-header__title" id="selected-room-title">Загрузка</h1>
|
<h1 class="content-header__title" id="selected-room-title">Загрузка</h1>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user