import { acceptIncomingCall, declineIncomingCall, hangupActiveCall, setMicMuted, subscribeCallState, } from './call-service.js'; const MINIMIZED_BAR_HEIGHT_PX = 44; let rootEl = null; let overlayEl = null; let panelEl = null; let headerEl = null; let titleEl = null; let statusEl = null; let muteBtn = null; let acceptBtn = null; let declineBtn = null; let hangupBtn = null; let minimizeBtn = null; let barEl = null; let barTextEl = null; let unbind = null; let tickerId = 0; let currentSnapshot = null; let minimized = false; function resolveAppShell() { return document.querySelector('.app-shell') || document.body; } function pad2(value) { return String(Math.max(0, Number(value) || 0)).padStart(2, '0'); } function formatElapsedMs(ms) { const totalSec = Math.max(0, Math.floor(Number(ms || 0) / 1000)); const hours = Math.floor(totalSec / 3600); const minutes = Math.floor((totalSec % 3600) / 60); const seconds = totalSec % 60; if (hours > 0) return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`; return `${pad2(minutes)}:${pad2(seconds)}`; } function getSnapshotElapsedMs(snapshot) { const baseMs = Number(snapshot?.connectedAtMs || snapshot?.startedAtMs || 0); if (!baseMs || !Number.isFinite(baseMs)) return 0; return Math.max(0, Date.now() - baseMs); } function buildBarText(snapshot) { const peer = String(snapshot?.peerLogin || '').trim() || 'пользователь'; const elapsed = formatElapsedMs(getSnapshotElapsedMs(snapshot)); return `Звонок с ${peer} · ${elapsed}`; } function setMinimized(nextValue) { minimized = Boolean(nextValue); render(); } function ensureTicker() { const shouldTick = Boolean(currentSnapshot); if (shouldTick && !tickerId) { tickerId = window.setInterval(() => { if (!currentSnapshot) return; render(); }, 1000); } if (!shouldTick && tickerId) { window.clearInterval(tickerId); tickerId = 0; } } function ensureUi() { if (rootEl) return; rootEl = document.createElement('section'); rootEl.className = 'call-ui-root'; rootEl.hidden = true; barEl = document.createElement('button'); barEl.type = 'button'; barEl.className = 'call-minimized-bar'; barEl.hidden = true; barEl.addEventListener('click', () => { if (!currentSnapshot) return; setMinimized(false); }); barTextEl = document.createElement('span'); barTextEl.className = 'call-minimized-bar-text'; barEl.append(barTextEl); overlayEl = document.createElement('div'); overlayEl.className = 'call-overlay'; overlayEl.hidden = true; panelEl = document.createElement('div'); panelEl.className = 'call-overlay-panel'; headerEl = document.createElement('div'); headerEl.className = 'call-overlay-head'; titleEl = document.createElement('h2'); titleEl.className = 'call-overlay-title'; minimizeBtn = document.createElement('button'); minimizeBtn.type = 'button'; minimizeBtn.className = 'secondary-btn call-overlay-minimize-btn'; minimizeBtn.textContent = 'Свернуть'; minimizeBtn.addEventListener('click', () => { if (!currentSnapshot) return; setMinimized(true); }); statusEl = document.createElement('div'); statusEl.className = 'call-overlay-status'; const controls = document.createElement('div'); controls.className = 'call-overlay-controls'; muteBtn = document.createElement('button'); muteBtn.type = 'button'; muteBtn.className = 'secondary-btn'; muteBtn.textContent = 'Микрофон'; muteBtn.addEventListener('click', async () => { const nowMuted = muteBtn.dataset.muted === '1'; await setMicMuted(!nowMuted); }); acceptBtn = document.createElement('button'); acceptBtn.type = 'button'; acceptBtn.className = 'call-accept-btn'; acceptBtn.textContent = 'Ответить'; acceptBtn.addEventListener('click', async () => { await acceptIncomingCall(); }); declineBtn = document.createElement('button'); declineBtn.type = 'button'; declineBtn.className = 'destructive-btn'; declineBtn.textContent = 'Сбросить'; declineBtn.addEventListener('click', async () => { await declineIncomingCall(); }); hangupBtn = document.createElement('button'); hangupBtn.type = 'button'; hangupBtn.className = 'destructive-btn'; hangupBtn.textContent = 'Положить трубку'; hangupBtn.addEventListener('click', async () => { await hangupActiveCall(); }); controls.append(muteBtn, acceptBtn, declineBtn, hangupBtn); headerEl.append(titleEl, minimizeBtn); panelEl.append(headerEl, statusEl, controls); overlayEl.append(panelEl); rootEl.append(barEl, overlayEl); resolveAppShell().append(rootEl); } function applyExpandedState(snapshot) { overlayEl.hidden = false; barEl.hidden = true; titleEl.textContent = `Звонок: ${snapshot.peerLogin || 'пользователь'}`; statusEl.textContent = snapshot.statusText || ''; const muted = Boolean(snapshot.muted); muteBtn.dataset.muted = muted ? '1' : '0'; muteBtn.textContent = muted ? 'Включить микрофон' : 'Выключить микрофон'; muteBtn.hidden = !snapshot.canMute; muteBtn.disabled = !snapshot.canMute; acceptBtn.hidden = !snapshot.canAnswer; declineBtn.hidden = !snapshot.canDecline; hangupBtn.hidden = !snapshot.canHangup; } function applyMinimizedState(snapshot) { overlayEl.hidden = true; barEl.hidden = false; barTextEl.textContent = buildBarText(snapshot); } function syncShellState() { const appShell = resolveAppShell(); if (!appShell) return; if (currentSnapshot && minimized) { appShell.classList.add('has-minimized-call'); const measuredHeight = Math.max( MINIMIZED_BAR_HEIGHT_PX, Math.ceil(Number(barEl?.offsetHeight || 0)), ); appShell.style.setProperty('--call-minimized-bar-height', `${measuredHeight}px`); } else { appShell.classList.remove('has-minimized-call'); appShell.style.removeProperty('--call-minimized-bar-height'); } } function render() { ensureUi(); rootEl.hidden = !currentSnapshot; ensureTicker(); if (!currentSnapshot) { syncShellState(); overlayEl.hidden = true; barEl.hidden = true; return; } if (minimized) { applyMinimizedState(currentSnapshot); syncShellState(); return; } applyExpandedState(currentSnapshot); syncShellState(); } function applyCallState(snapshot) { ensureUi(); currentSnapshot = snapshot || null; if (!currentSnapshot) { minimized = false; } render(); } export function initCallUiOverlay() { ensureUi(); if (unbind) { unbind(); unbind = null; } unbind = subscribeCallState(applyCallState); }