UI: сворачивание активного звонка

This commit is contained in:
AidarKC
2026-07-24 15:00:01 +04:00
parent 5df69d73c7
commit 4d9e42205f
5 changed files with 300 additions and 21 deletions
+153 -17
View File
@@ -6,29 +6,116 @@ import {
subscribeCallState,
} from './call-service.js';
let shellEl = null;
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 (shellEl) return;
if (rootEl) return;
shellEl = document.createElement('section');
shellEl.className = 'call-overlay';
shellEl.hidden = true;
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';
@@ -69,20 +156,17 @@ function ensureUi() {
});
controls.append(muteBtn, acceptBtn, declineBtn, hangupBtn);
panelEl.append(titleEl, statusEl, controls);
shellEl.append(panelEl);
document.body.append(shellEl);
headerEl.append(titleEl, minimizeBtn);
panelEl.append(headerEl, statusEl, controls);
overlayEl.append(panelEl);
rootEl.append(barEl, overlayEl);
resolveAppShell().append(rootEl);
}
function applyCallState(snapshot) {
ensureUi();
function applyExpandedState(snapshot) {
overlayEl.hidden = false;
barEl.hidden = true;
if (!snapshot) {
shellEl.hidden = true;
return;
}
shellEl.hidden = false;
titleEl.textContent = `Звонок: ${snapshot.peerLogin || 'пользователь'}`;
statusEl.textContent = snapshot.statusText || '';
@@ -95,10 +179,62 @@ function applyCallState(snapshot) {
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) {