diff --git a/VERSION.properties b/VERSION.properties
index 04751ddd..167f3309 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.2.348
+client.version=1.2.349
server.version=1.2.319
diff --git a/shine-UI/js/services/call-ui-service.js b/shine-UI/js/services/call-ui-service.js
index c9102eb5..a392e4c7 100644
--- a/shine-UI/js/services/call-ui-service.js
+++ b/shine-UI/js/services/call-ui-service.js
@@ -21,6 +21,9 @@ let hangupBtn = null;
let minimizeBtn = null;
let barEl = null;
let barTextEl = null;
+let barActionsEl = null;
+let barMuteBtn = null;
+let barHangupBtn = null;
let unbind = null;
let tickerId = 0;
let currentSnapshot = null;
@@ -55,6 +58,59 @@ function buildBarText(snapshot) {
return `Звонок с ${peer} · ${elapsed}`;
}
+function stopEvent(event) {
+ event.preventDefault();
+ event.stopPropagation();
+}
+
+function getIconSvg(name) {
+ if (name === 'mic-on') {
+ return '';
+ }
+ if (name === 'mic-off') {
+ return '';
+ }
+ if (name === 'hangup') {
+ return '';
+ }
+ if (name === 'minimize') {
+ return '';
+ }
+ return '';
+}
+
+function setButtonIcon(button, iconName, label) {
+ if (!button) return;
+ button.innerHTML = getIconSvg(iconName);
+ button.setAttribute('aria-label', label);
+ button.title = label;
+}
+
+function updateMuteButtons(muted) {
+ const iconName = muted ? 'mic-off' : 'mic-on';
+ const label = muted ? 'Включить микрофон' : 'Выключить микрофон';
+ setButtonIcon(muteBtn, iconName, label);
+ setButtonIcon(barMuteBtn, iconName, label);
+ if (muteBtn) muteBtn.dataset.muted = muted ? '1' : '0';
+ if (barMuteBtn) barMuteBtn.dataset.muted = muted ? '1' : '0';
+}
+
+async function handleMuteClick(event) {
+ stopEvent(event);
+ const nowMuted = event.currentTarget?.dataset?.muted === '1';
+ await setMicMuted(!nowMuted);
+}
+
+async function handleEndCallClick(event) {
+ stopEvent(event);
+ if (!currentSnapshot) return;
+ if (currentSnapshot.canDecline && !currentSnapshot.canHangup) {
+ await declineIncomingCall();
+ return;
+ }
+ await hangupActiveCall();
+}
+
function setMinimized(nextValue) {
minimized = Boolean(nextValue);
render();
@@ -81,18 +137,33 @@ function ensureUi() {
rootEl.className = 'call-ui-root';
rootEl.hidden = true;
- barEl = document.createElement('button');
- barEl.type = 'button';
+ barEl = document.createElement('div');
barEl.className = 'call-minimized-bar';
barEl.hidden = true;
- barEl.addEventListener('click', () => {
+
+ barTextEl = document.createElement('button');
+ barTextEl.type = 'button';
+ barTextEl.className = 'call-minimized-bar-text';
+ barTextEl.addEventListener('click', () => {
if (!currentSnapshot) return;
setMinimized(false);
});
- barTextEl = document.createElement('span');
- barTextEl.className = 'call-minimized-bar-text';
- barEl.append(barTextEl);
+ barActionsEl = document.createElement('div');
+ barActionsEl.className = 'call-minimized-bar-actions';
+
+ barMuteBtn = document.createElement('button');
+ barMuteBtn.type = 'button';
+ barMuteBtn.className = 'call-icon-btn call-icon-btn--bar call-icon-btn--secondary';
+ barMuteBtn.addEventListener('click', handleMuteClick);
+
+ barHangupBtn = document.createElement('button');
+ barHangupBtn.type = 'button';
+ barHangupBtn.className = 'call-icon-btn call-icon-btn--bar call-icon-btn--danger';
+ barHangupBtn.addEventListener('click', handleEndCallClick);
+
+ barActionsEl.append(barMuteBtn, barHangupBtn);
+ barEl.append(barTextEl, barActionsEl);
overlayEl = document.createElement('div');
overlayEl.className = 'call-overlay';
@@ -109,9 +180,10 @@ function ensureUi() {
minimizeBtn = document.createElement('button');
minimizeBtn.type = 'button';
- minimizeBtn.className = 'secondary-btn call-overlay-minimize-btn';
- minimizeBtn.textContent = 'Свернуть';
- minimizeBtn.addEventListener('click', () => {
+ minimizeBtn.className = 'call-icon-btn call-icon-btn--secondary call-overlay-minimize-btn';
+ setButtonIcon(minimizeBtn, 'minimize', 'Свернуть звонок');
+ minimizeBtn.addEventListener('click', (event) => {
+ stopEvent(event);
if (!currentSnapshot) return;
setMinimized(true);
});
@@ -124,12 +196,8 @@ function ensureUi() {
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);
- });
+ muteBtn.className = 'call-icon-btn call-icon-btn--secondary';
+ muteBtn.addEventListener('click', handleMuteClick);
acceptBtn = document.createElement('button');
acceptBtn.type = 'button';
@@ -141,19 +209,17 @@ function ensureUi() {
declineBtn = document.createElement('button');
declineBtn.type = 'button';
- declineBtn.className = 'destructive-btn';
- declineBtn.textContent = 'Сбросить';
+ declineBtn.className = 'call-icon-btn call-icon-btn--danger';
+ setButtonIcon(declineBtn, 'hangup', 'Отклонить звонок');
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();
- });
+ hangupBtn.className = 'call-icon-btn call-icon-btn--danger';
+ setButtonIcon(hangupBtn, 'hangup', 'Положить трубку');
+ hangupBtn.addEventListener('click', handleEndCallClick);
controls.append(muteBtn, acceptBtn, declineBtn, hangupBtn);
headerEl.append(titleEl, minimizeBtn);
@@ -171,8 +237,7 @@ function applyExpandedState(snapshot) {
statusEl.textContent = snapshot.statusText || '';
const muted = Boolean(snapshot.muted);
- muteBtn.dataset.muted = muted ? '1' : '0';
- muteBtn.textContent = muted ? 'Включить микрофон' : 'Выключить микрофон';
+ updateMuteButtons(muted);
muteBtn.hidden = !snapshot.canMute;
muteBtn.disabled = !snapshot.canMute;
@@ -186,6 +251,14 @@ function applyMinimizedState(snapshot) {
overlayEl.hidden = true;
barEl.hidden = false;
barTextEl.textContent = buildBarText(snapshot);
+ const muted = Boolean(snapshot.muted);
+ updateMuteButtons(muted);
+ barMuteBtn.hidden = !snapshot.canMute;
+ barMuteBtn.disabled = !snapshot.canMute;
+ const canEnd = snapshot.canHangup || snapshot.canDecline;
+ barHangupBtn.hidden = !canEnd;
+ barHangupBtn.disabled = !canEnd;
+ setButtonIcon(barHangupBtn, 'hangup', snapshot.canDecline && !snapshot.canHangup ? 'Отклонить звонок' : 'Положить трубку');
}
function syncShellState() {
diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css
index 7f65c562..ea060e6a 100644
--- a/shine-UI/styles/components.css
+++ b/shine-UI/styles/components.css
@@ -1749,29 +1749,106 @@
top: 0;
left: 0;
right: 0;
+ display: flex;
+ align-items: center;
+ gap: 10px;
min-height: 44px;
padding: calc(8px + env(safe-area-inset-top)) 14px 8px;
- border: 0;
border-bottom: 1px solid rgba(159, 255, 196, 0.28);
background: linear-gradient(180deg, rgba(30, 138, 79, 0.98), rgba(18, 106, 60, 0.98));
box-shadow: 0 10px 24px rgba(5, 30, 16, 0.28);
+ pointer-events: auto;
+}
+
+.call-minimized-bar-text {
+ flex: 1 1 auto;
+ min-width: 0;
+ border: 0;
+ background: transparent;
color: #f3fff8;
font-size: 13px;
font-weight: 700;
line-height: 1.2;
text-align: left;
- pointer-events: auto;
+ padding: 0;
cursor: pointer;
overflow: hidden;
- white-space: nowrap;
text-overflow: ellipsis;
+ white-space: nowrap;
}
-.call-minimized-bar-text {
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.call-minimized-bar-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 0 0 auto;
+}
+
+.call-icon-btn {
+ width: 40px;
+ min-width: 40px;
+ height: 40px;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 12px;
+ border: 1px solid rgba(132, 162, 228, 0.42);
+ background: linear-gradient(180deg, rgba(19, 35, 63, 0.9), rgba(14, 26, 50, 0.95));
+ color: #e7efff;
+ cursor: pointer;
+ transition: 0.2s ease;
+}
+
+.call-icon-btn:hover {
+ transform: translateY(-1px);
+}
+
+.call-icon-btn:disabled {
+ opacity: 1;
+ color: #93a6cc;
+ background: linear-gradient(180deg, rgba(18, 30, 54, 0.8), rgba(11, 20, 39, 0.86));
+ border-color: rgba(124, 145, 189, 0.28);
+ box-shadow: none;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.call-icon-btn svg {
+ width: 20px;
+ height: 20px;
+}
+
+.call-icon-btn--secondary {
+ border-color: rgba(132, 162, 228, 0.42);
+ background: linear-gradient(180deg, rgba(19, 35, 63, 0.9), rgba(14, 26, 50, 0.95));
+ color: #e7efff;
+}
+
+.call-icon-btn--secondary:hover {
+ border-color: rgba(167, 195, 255, 0.76);
+}
+
+.call-icon-btn--danger {
+ border-color: rgba(255, 129, 147, 0.52);
+ background: linear-gradient(120deg, rgba(188, 42, 65, 0.96), rgba(142, 28, 47, 0.98));
+ color: #fff2f5;
+}
+
+.call-icon-btn--danger:hover {
+ border-color: rgba(255, 163, 176, 0.82);
+}
+
+.call-icon-btn--bar {
+ width: 34px;
+ min-width: 34px;
+ height: 34px;
+ border-radius: 10px;
+}
+
+.call-icon-btn--bar svg {
+ width: 18px;
+ height: 18px;
}
.call-overlay {
@@ -1823,6 +1900,7 @@
display: flex;
flex-wrap: wrap;
gap: 8px;
+ align-items: center;
}
.call-accept-btn {