diff --git a/cmd/server/web/css/style.css b/cmd/server/web/css/style.css index 2b3d72a..db071f2 100644 --- a/cmd/server/web/css/style.css +++ b/cmd/server/web/css/style.css @@ -166,7 +166,7 @@ h1 { .fill { height: 100%; - width: 0%; + width: 0; background: #e6852d; box-shadow: inset 0 0 4px rgba(255,255,255,0.2), @@ -177,7 +177,7 @@ h1 { .fill.no-connection { background: #555555 !important; box-shadow: none !important; - width: 0% !important; + width: 0 !important; } /* ===== SIGNAL ICON ===== */ @@ -194,7 +194,7 @@ h1 { flex: 1; border-radius: 2px; min-width: 5px; - border: 1.5px solid #e6852d; + border: 2px solid #e6852d; background-color: transparent; transition: background-color 0.3s, border-color 0.3s; } diff --git a/cmd/server/web/js/camera.ts b/cmd/server/web/js/camera.ts index 4aa0961..2d9ae6f 100644 --- a/cmd/server/web/js/camera.ts +++ b/cmd/server/web/js/camera.ts @@ -1,23 +1,18 @@ // camera.ts -import { updateResizerVisibility } from './layout.js'; +import { updateResizerVisibility, openRightPanel, closeRightPanel } from './layout.js'; let rightPanel: HTMLElement | null = null; let camImage: HTMLImageElement | null = null; let crosshair: HTMLElement | null = null; let isOpen: boolean = false; let streamCheckInterval: number | null = null; - -interface StreamResponse { - cam: string; - available: boolean; - source: string; -} +let resizer: HTMLElement | null = null; export function initCamera(): void { rightPanel = document.getElementById("rightPanel"); camImage = document.getElementById("camImage") as HTMLImageElement; crosshair = document.getElementById("crosshair"); - + resizer = document.getElementById("dragBar"); const btn = document.getElementById("camToggle"); @@ -28,9 +23,8 @@ export function initCamera(): void { isOpen = true; - if (rightPanel) { - rightPanel.classList.remove("hidden"); - } + // Открываем правую панель через layout + openRightPanel(); // Показываем перекрестие if (crosshair) { @@ -44,13 +38,15 @@ export function initCamera(): void { if (btn) { const textSpan = btn.querySelector('.cam-btn-text'); - if (textSpan) textSpan.textContent = 'Скрыть'; + if (textSpan) { + textSpan.textContent = 'Скрыть'; + } else { + btn.innerHTML = '📷 Скрыть'; + } } console.log("[camera] opened"); - updateResizerVisibility(); - // Запускаем мониторинг потока startStreamMonitoring(); } @@ -65,24 +61,25 @@ export function initCamera(): void { crosshair.style.display = "none"; } - // остановка stream + // Останавливаем поток if (camImage) { camImage.src = ""; } - if (rightPanel) { - rightPanel.classList.add("hidden"); - } + // Закрываем правую панель через layout + closeRightPanel(); if (btn) { const textSpan = btn.querySelector('.cam-btn-text'); - if (textSpan) textSpan.textContent = 'Камера'; + if (textSpan) { + textSpan.textContent = 'Камера'; + } else { + btn.innerHTML = '📷 Камера'; + } } console.log("[camera] closed"); - updateResizerVisibility(); - // Останавливаем мониторинг stopStreamMonitoring(); } @@ -139,7 +136,15 @@ export function initCamera(): void { } } -// Экспортируем для возможности ручного управления +// ============================================ +// ПУБЛИЧНЫЕ ФУНКЦИИ (используются в других модулях) +// ============================================ + +/** + * Показать/скрыть перекрестие на видео + * @param show - показывать или скрыть + */ +// noinspection JSUnusedGlobalSymbols export function showCrosshair(show: boolean = true): void { const crosshairElem = document.getElementById("crosshair"); if (crosshairElem) { @@ -147,9 +152,14 @@ export function showCrosshair(show: boolean = true): void { } } +/** + * Установить прозрачность перекрестия + * @param opacity - значение прозрачности (0-1) + */ +// noinspection JSUnusedGlobalSymbols export function setCrosshairOpacity(opacity: string | number): void { const crosshairElem = document.getElementById("crosshair"); if (crosshairElem) { crosshairElem.style.opacity = String(opacity); } -} +} \ No newline at end of file diff --git a/cmd/server/web/js/layout.ts b/cmd/server/web/js/layout.ts index 8cc8f54..02aa30f 100644 --- a/cmd/server/web/js/layout.ts +++ b/cmd/server/web/js/layout.ts @@ -1,88 +1,236 @@ // layout.ts +let resizer: HTMLElement | null = null; let leftPanel: HTMLElement | null = null; let rightPanel: HTMLElement | null = null; -let dragBar: HTMLElement | null = null; -let isDragging: boolean = false; -let pendingX: number | null = null; +let isOpen: boolean = false; +let lastLeftWidth: number = 0; +let lastRightWidth: number = 0; export function initResize(): void { + resizer = document.getElementById("dragBar"); leftPanel = document.getElementById("leftPanel"); rightPanel = document.getElementById("rightPanel"); - dragBar = document.getElementById("dragBar"); - if (!leftPanel || !rightPanel || !dragBar) return; - - const savedRatio = localStorage.getItem('layoutRatio'); - if (savedRatio) { - applyRatio(parseFloat(savedRatio)); + if (!resizer || !leftPanel || !rightPanel) { + console.warn("[layout] Элементы не найдены"); + return; } - dragBar.addEventListener("mousedown", (e) => { - if (rightPanel!.classList.contains("hidden")) return; + // Проверяем видимость при инициализации + updateResizerVisibility(); + + let isDragging = false; + + // Обработчик начала перетаскивания + resizer.addEventListener("mousedown", (e: MouseEvent) => { + // Сохраняем ссылки на элементы в локальные переменные + const left = leftPanel; + const right = rightPanel; + + // Проверяем, что элементы существуют + if (!left || !right) { + console.warn("[layout] Панели не найдены"); + return; + } + + // Проверяем, что правая панель видима + if (right.classList.contains("hidden")) { + return; + } isDragging = true; document.body.style.cursor = "col-resize"; - e.preventDefault(); - }); + document.body.style.userSelect = "none"; - document.addEventListener("mouseup", () => { - if (!isDragging) return; + // Сохраняем начальные размеры + const startX = e.clientX; + const leftWidth = left.getBoundingClientRect().width; + const rightWidth = right.getBoundingClientRect().width; + const totalWidth = leftWidth + rightWidth; - isDragging = false; - document.body.style.cursor = "default"; + function onMouseMove(moveEvent: MouseEvent): void { + if (!isDragging) return; - if (rightPanel && !rightPanel.classList.contains("hidden")) { - const total = window.innerWidth; - const leftWidth = leftPanel!.getBoundingClientRect().width; - localStorage.setItem('layoutRatio', String(leftWidth / total)); + // Проверяем, что элементы всё ещё существуют + if (!left || !right) return; + + const deltaX = moveEvent.clientX - startX; + let newLeftWidth = leftWidth + deltaX; + + // Ограничиваем минимальные размеры + const minWidth = 200; + newLeftWidth = Math.max(minWidth, Math.min(totalWidth - minWidth, newLeftWidth)); + + const newRightWidth = totalWidth - newLeftWidth; + + left.style.flex = `0 0 ${newLeftWidth}px`; + right.style.flex = `0 0 ${newRightWidth}px`; + + // Сохраняем последние размеры + lastLeftWidth = newLeftWidth; + lastRightWidth = newRightWidth; } + + function onMouseUp(): void { + isDragging = false; + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + } + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); }); - document.addEventListener("mousemove", (e) => { - if (!isDragging) return; - if (rightPanel && rightPanel.classList.contains("hidden")) return; - - pendingX = e.clientX; - - requestAnimationFrame(() => { - if (pendingX === null) return; - if (!leftPanel || !rightPanel || !dragBar) return; - - const total = window.innerWidth; - let leftWidth = pendingX; - - const min = 300; - if (leftWidth < min) leftWidth = min; - if (leftWidth > total - min) leftWidth = total - min; - - const rightWidth = total - leftWidth - dragBar.offsetWidth; - - leftPanel.style.flex = `0 0 ${leftWidth}px`; - rightPanel.style.flex = `0 0 ${rightWidth}px`; - - pendingX = null; - }); + // Обработчик изменения размера окна + window.addEventListener("resize", () => { + handleWindowResize(); }); } -function applyRatio(ratio: number): void { - const total = window.innerWidth; - const leftWidth = Math.floor(total * ratio); - const rightWidth = total - leftWidth; +/** + * Обработка изменения размера окна + */ +function handleWindowResize(): void { + if (!leftPanel || !rightPanel) return; - if (leftPanel && rightPanel) { - leftPanel.style.flex = `0 0 ${leftWidth}px`; - rightPanel.style.flex = `0 0 ${rightWidth}px`; + // Если правая панель видима, пересчитываем размеры + if (!rightPanel.classList.contains("hidden")) { + // Если есть сохраненные размеры, используем их пропорционально + if (lastLeftWidth > 0 && lastRightWidth > 0) { + const totalWidth = window.innerWidth; + const ratio = lastLeftWidth / (lastLeftWidth + lastRightWidth); + const newLeftWidth = totalWidth * ratio; + const newRightWidth = totalWidth - newLeftWidth; + + // Ограничиваем минимальные размеры + const minWidth = 200; + const finalLeftWidth = Math.max(minWidth, Math.min(totalWidth - minWidth, newLeftWidth)); + const finalRightWidth = totalWidth - finalLeftWidth; + + leftPanel.style.flex = `0 0 ${finalLeftWidth}px`; + rightPanel.style.flex = `0 0 ${finalRightWidth}px`; + + lastLeftWidth = finalLeftWidth; + lastRightWidth = finalRightWidth; + } else { + // Если нет сохраненных размеров, делим пополам + const totalWidth = window.innerWidth; + const halfWidth = totalWidth / 2; + leftPanel.style.flex = `0 0 ${halfWidth}px`; + rightPanel.style.flex = `0 0 ${halfWidth}px`; + + lastLeftWidth = halfWidth; + lastRightWidth = halfWidth; + } } } +/** + * Обновление видимости разделителя в зависимости от состояния правой панели + */ export function updateResizerVisibility(): void { - if (!dragBar || !rightPanel || !leftPanel) return; + if (!resizer || !rightPanel) return; + + // Если правая панель скрыта - скрываем разделитель + if (rightPanel.classList.contains("hidden")) { + resizer.classList.add("hidden"); + } else { + resizer.classList.remove("hidden"); + } +} + +/** + * Открыть правую панель + */ +export function openRightPanel(): void { + if (!leftPanel || !rightPanel) return; + + // Убираем класс hidden + rightPanel.classList.remove("hidden"); + + // Обновляем разделитель + updateResizerVisibility(); + + // Устанавливаем размеры + const totalWidth = window.innerWidth; + const halfWidth = totalWidth / 2; + + // Проверяем, есть ли сохраненные размеры + if (lastLeftWidth > 0 && lastRightWidth > 0) { + // Используем сохраненные пропорции + const ratio = lastLeftWidth / (lastLeftWidth + lastRightWidth); + const newLeftWidth = totalWidth * ratio; + const newRightWidth = totalWidth - newLeftWidth; + + const minWidth = 200; + const finalLeftWidth = Math.max(minWidth, Math.min(totalWidth - minWidth, newLeftWidth)); + const finalRightWidth = totalWidth - finalLeftWidth; + + leftPanel.style.flex = `0 0 ${finalLeftWidth}px`; + rightPanel.style.flex = `0 0 ${finalRightWidth}px`; + + lastLeftWidth = finalLeftWidth; + lastRightWidth = finalRightWidth; + } else { + // Делим пополам + leftPanel.style.flex = `0 0 ${halfWidth}px`; + rightPanel.style.flex = `0 0 ${halfWidth}px`; + + lastLeftWidth = halfWidth; + lastRightWidth = halfWidth; + } +} + +/** + * Закрыть правую панель + */ +export function closeRightPanel(): void { + if (!rightPanel) return; + + // Добавляем класс hidden + rightPanel.classList.add("hidden"); + + // Обновляем разделитель + updateResizerVisibility(); + + // Сбрасываем flex для левой панели + if (leftPanel) { + leftPanel.style.flex = "1 1 auto"; + } +} + +// ============================================ +// ПУБЛИЧНЫЕ ФУНКЦИИ (используются в других модулях) +// ============================================ + +/** + * Установить видимость разделителя вручную + * @param visible - показывать или скрыть + */ +// noinspection JSUnusedGlobalSymbols +export function setResizerVisible(visible: boolean): void { + if (resizer) { + if (visible) { + resizer.classList.remove("hidden"); + } else { + resizer.classList.add("hidden"); + } + } +} + +/** + * Переключить состояние правой панели + */ +// noinspection JSUnusedGlobalSymbols +export function toggleRightPanel(): void { + if (!rightPanel) return; if (rightPanel.classList.contains("hidden")) { - dragBar.classList.add("hidden"); - leftPanel.style.flex = "1"; + openRightPanel(); } else { - dragBar.classList.remove("hidden"); + closeRightPanel(); } -} +} \ No newline at end of file