SHA256
246 lines
6.5 KiB
JavaScript
246 lines
6.5 KiB
JavaScript
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);
|
||
}
|