Files
SHiNE-server/shine-UI/js/services/call-ui-service.js
T

246 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}