diff --git a/VERSION.properties b/VERSION.properties index 9e7bd58d..f83edfa4 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.5.29 +client.version=1.5.31 server.version=1.4.10 diff --git a/shine-UI/index.html b/shine-UI/index.html index 7a10f904..a392a36e 100644 --- a/shine-UI/index.html +++ b/shine-UI/index.html @@ -2,7 +2,10 @@
- +Маленькие файлы и аватары через Turbo пока загружаются бесплатно.
Turbo использует Solana-ключ пользователя. Файлы до 100 KB пока бесплатно. Если файл больше, пополните Turbo со своего SOL-кошелька.
+Turbo использует Solana-ключ пользователя. Маленькие файлы и аватары пока бесплатно. Если файл больше, пополните Turbo со своего SOL-кошелька.
` : ` diff --git a/shine-UI/js/components/toolbar.js b/shine-UI/js/components/toolbar.js index da954c24..38f15286 100644 --- a/shine-UI/js/components/toolbar.js +++ b/shine-UI/js/components/toolbar.js @@ -78,7 +78,6 @@ export function renderToolbar(currentPageId, navigate) { ${item.label} - connected diff --git a/shine-UI/js/pages/arweave-uploads-view.js b/shine-UI/js/pages/arweave-uploads-view.js index c9c1fc87..286ebe6f 100644 --- a/shine-UI/js/pages/arweave-uploads-view.js +++ b/shine-UI/js/pages/arweave-uploads-view.js @@ -157,7 +157,7 @@ export function render({ navigate }) { const menu = controls.querySelector('[data-menu="true"]'); if (menu) menu.hidden = true; window.alert( - 'Здесь вы можете заранее добавить файл в Arweave или через Turbo. Кнопка «Загрузить через Turbo» находится внутри стандартного окна загрузки.\n\n' + 'Здесь вы можете заранее добавить файл в Arweave или через Turbo. По умолчанию сразу выбрана загрузка через Turbo, а маленькие файлы пока загружаются там бесплатно.\n\n' + 'Потом при создании сообщения можно открыть историю загрузок и выбрать уже загруженный файл по txId. ' + 'Если файл только что загружен, gateway может несколько минут его не отдавать, поэтому статус может быть “ещё обновляется”.' ); diff --git a/shine-UI/js/pages/channels-list.js b/shine-UI/js/pages/channels-list.js index 76c1d417..93a50226 100644 --- a/shine-UI/js/pages/channels-list.js +++ b/shine-UI/js/pages/channels-list.js @@ -1147,11 +1147,12 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed container.append(list); } -function updateBottomCta({ button, listState, navigate, isTabEmpty = false }) { - const baseClass = `primary-btn channels-bottom-action${isTabEmpty ? ' is-empty-lift' : ''}`; - button.textContent = 'Поиск каналов'; - button.className = baseClass; - button.onclick = () => openChannelFinderModal({ navigate }); +function updateBottomCta({ button }) { + if (!button) return; + button.hidden = true; + button.textContent = ''; + button.className = 'channels-bottom-action'; + button.onclick = null; } async function loadFeedAndRender({ screen, listState, contentEl, navigate }) { @@ -1306,12 +1307,7 @@ export function render({ navigate, route }) { createInMyBtn.style.display = ''; if (topTitle.parentElement !== topBarLeft) topBarLeft.append(topTitle); - updateBottomCta({ - button: bottomCta, - listState, - navigate, - isTabEmpty, - }); + updateBottomCta({ button: bottomCta }); }; screen.append(topBarEl, contentEl, bottomCta); @@ -1320,12 +1316,7 @@ export function render({ navigate, route }) { showToast(createSuccessFlash); } - updateBottomCta({ - button: bottomCta, - listState, - navigate, - isTabEmpty: true, - }); + updateBottomCta({ button: bottomCta }); // Применяем корректное состояние хедера сразу на первом рендере, // чтобы не показывать лишние кнопки до первой перерисовки. diff --git a/shine-UI/js/services/call-ui-service.js b/shine-UI/js/services/call-ui-service.js index 2a912267..219fe9f6 100644 --- a/shine-UI/js/services/call-ui-service.js +++ b/shine-UI/js/services/call-ui-service.js @@ -7,6 +7,8 @@ import { subscribeCallState, toggleSpeakerMode, } from './call-service.js'; +import { renderUserAvatar } from '../components/avatar-image.js'; +import { loadProfileSnapshot } from './user-profile-params.js'; const MINIMIZED_BAR_HEIGHT_PX = 44; @@ -18,9 +20,13 @@ let titleEl = null; let statusEl = null; let mediaStageEl = null; let remoteVideoEl = null; -let remoteVideoPlaceholderEl = null; let localPreviewWrapEl = null; let localVideoEl = null; +let heroEl = null; +let avatarSlotEl = null; +let peerLoginEl = null; +let kindEl = null; +let topStatusEl = null; let incomingActionsEl = null; let activeActionsEl = null; let speakerBtn = null; @@ -41,6 +47,16 @@ let unbind = null; let tickerId = 0; let currentSnapshot = null; let minimized = false; +let activeAvatarLogin = ''; +let activeAvatarLoadToken = 0; +let localPreviewDragPointerId = null; +let localPreviewDragRect = null; +let localPreviewDragOffsetX = 0; +let localPreviewDragOffsetY = 0; +let localPreviewPosition = null; + +const avatarSnapshotCache = new Map(); +const avatarPendingByLogin = new Map(); function resolveAppShell() { return document.querySelector('.app-shell') || document.body; @@ -72,11 +88,75 @@ function buildBarText(snapshot) { return `${kind} с ${peer} · ${elapsed}`; } +async function loadAvatarSnapshot(login) { + const cleanLogin = String(login || '').trim(); + if (!cleanLogin) return null; + const key = cleanLogin.toLowerCase(); + if (avatarSnapshotCache.has(key)) return avatarSnapshotCache.get(key); + if (avatarPendingByLogin.has(key)) return avatarPendingByLogin.get(key); + const pending = loadProfileSnapshot(cleanLogin) + .then((snapshot) => { + avatarSnapshotCache.set(key, snapshot || null); + avatarPendingByLogin.delete(key); + return snapshot || null; + }) + .catch(() => { + avatarSnapshotCache.set(key, null); + avatarPendingByLogin.delete(key); + return null; + }); + avatarPendingByLogin.set(key, pending); + return pending; +} + +function renderPeerAvatar(login, avatar = null) { + const hasAvatar = Boolean(String(avatar?.ar || '').trim()); + const avatarEl = renderUserAvatar({ + login: String(login || '').trim() || 'unknown', + avatar, + size: 'large', + className: 'call-peer-avatar', + title: login ? `Профиль ${login}` : 'Профиль собеседника', + }); + if (!hasAvatar) { + avatarEl.classList.add('call-peer-avatar--empty'); + const fallbackEl = avatarEl.querySelector('.avatar-fallback'); + if (fallbackEl) fallbackEl.textContent = ''; + } + return avatarEl; +} + +function updatePeerAvatar(login) { + if (!avatarSlotEl) return; + const cleanLogin = String(login || '').trim(); + if (activeAvatarLogin === cleanLogin && avatarSlotEl.childElementCount > 0) return; + activeAvatarLogin = cleanLogin; + const loadToken = ++activeAvatarLoadToken; + avatarSlotEl.innerHTML = ''; + avatarSlotEl.append(renderPeerAvatar(cleanLogin)); + if (!cleanLogin) return; + void loadAvatarSnapshot(cleanLogin).then((snapshot) => { + if (!avatarSlotEl || loadToken !== activeAvatarLoadToken || activeAvatarLogin !== cleanLogin) return; + const avatar = snapshot?.avatar?.txId + ? { + ar: String(snapshot.avatar.txId || '').trim(), + sha256Hex: String(snapshot.avatar.sha256Hex || '').trim().toLowerCase(), + } + : null; + const upgraded = renderPeerAvatar(cleanLogin, avatar); + avatarSlotEl.replaceChildren(upgraded); + }); +} + function stopEvent(event) { event.preventDefault(); event.stopPropagation(); } +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + function getIconSvg(name) { if (name === 'mic-on') { return ''; @@ -185,6 +265,60 @@ function ensureTicker() { } } +function applyLocalPreviewPosition() { + if (!localPreviewWrapEl) return; + if (!localPreviewPosition) { + localPreviewWrapEl.style.left = ''; + localPreviewWrapEl.style.top = ''; + localPreviewWrapEl.style.right = ''; + localPreviewWrapEl.style.bottom = ''; + return; + } + localPreviewWrapEl.style.left = `${localPreviewPosition.leftPx}px`; + localPreviewWrapEl.style.top = `${localPreviewPosition.topPx}px`; + localPreviewWrapEl.style.right = 'auto'; + localPreviewWrapEl.style.bottom = 'auto'; +} + +function updateLocalPreviewPositionFromPointer(clientX, clientY) { + if (!mediaStageEl || !localPreviewWrapEl || !localPreviewDragRect) return; + const stageRect = mediaStageEl.getBoundingClientRect(); + const width = localPreviewDragRect.width; + const height = localPreviewDragRect.height; + const nextLeft = clamp(clientX - stageRect.left - localPreviewDragOffsetX, 8, Math.max(8, stageRect.width - width - 8)); + const nextTop = clamp(clientY - stageRect.top - localPreviewDragOffsetY, 8, Math.max(8, stageRect.height - height - 8)); + localPreviewPosition = { + leftPx: Math.round(nextLeft), + topPx: Math.round(nextTop), + }; + applyLocalPreviewPosition(); +} + +function handleLocalPreviewPointerDown(event) { + if (!localPreviewWrapEl || event.pointerType === 'mouse' && event.button !== 0) return; + localPreviewDragPointerId = event.pointerId; + localPreviewDragRect = localPreviewWrapEl.getBoundingClientRect(); + localPreviewDragOffsetX = event.clientX - localPreviewDragRect.left; + localPreviewDragOffsetY = event.clientY - localPreviewDragRect.top; + try { localPreviewWrapEl.setPointerCapture(event.pointerId); } catch {} + stopEvent(event); +} + +function handleLocalPreviewPointerMove(event) { + if (localPreviewDragPointerId !== event.pointerId) return; + updateLocalPreviewPositionFromPointer(event.clientX, event.clientY); + stopEvent(event); +} + +function stopLocalPreviewDrag(event = null) { + if (event && localPreviewDragPointerId !== event.pointerId) return; + if (event && localPreviewWrapEl) { + try { localPreviewWrapEl.releasePointerCapture(event.pointerId); } catch {} + } + localPreviewDragPointerId = null; + localPreviewDragRect = null; +} + function ensureUi() { if (rootEl) return; @@ -240,7 +374,7 @@ function ensureUi() { headerEl = document.createElement('div'); headerEl.className = 'call-overlay-head'; - titleEl = document.createElement('h2'); + titleEl = document.createElement('div'); titleEl.className = 'call-overlay-title'; minimizeBtn = document.createElement('button'); @@ -265,12 +399,12 @@ function ensureUi() { remoteVideoEl.playsInline = true; remoteVideoEl.muted = true; - remoteVideoPlaceholderEl = document.createElement('div'); - remoteVideoPlaceholderEl.className = 'call-video-placeholder'; - remoteVideoPlaceholderEl.textContent = 'Собеседник пока без видео'; - localPreviewWrapEl = document.createElement('div'); localPreviewWrapEl.className = 'call-local-preview'; + localPreviewWrapEl.addEventListener('pointerdown', handleLocalPreviewPointerDown); + localPreviewWrapEl.addEventListener('pointermove', handleLocalPreviewPointerMove); + localPreviewWrapEl.addEventListener('pointerup', stopLocalPreviewDrag); + localPreviewWrapEl.addEventListener('pointercancel', stopLocalPreviewDrag); localVideoEl = document.createElement('video'); localVideoEl.className = 'call-local-video'; @@ -278,8 +412,24 @@ function ensureUi() { localVideoEl.playsInline = true; localVideoEl.muted = true; + heroEl = document.createElement('div'); + heroEl.className = 'call-hero'; + + avatarSlotEl = document.createElement('div'); + avatarSlotEl.className = 'call-peer-avatar-slot'; + + peerLoginEl = document.createElement('div'); + peerLoginEl.className = 'call-peer-login'; + + kindEl = document.createElement('div'); + kindEl.className = 'call-kind'; + + topStatusEl = document.createElement('div'); + topStatusEl.className = 'call-hero-status'; + + heroEl.append(avatarSlotEl, peerLoginEl, kindEl, topStatusEl); localPreviewWrapEl.append(localVideoEl); - mediaStageEl.append(remoteVideoEl, remoteVideoPlaceholderEl, localPreviewWrapEl); + mediaStageEl.append(remoteVideoEl, heroEl, localPreviewWrapEl); incomingActionsEl = document.createElement('div'); incomingActionsEl.className = 'call-overlay-controls call-overlay-controls--incoming'; @@ -327,7 +477,7 @@ function ensureUi() { incomingActionsEl.append(acceptBtn, declineBtn); activeActionsEl.append(speakerBtn, muteBtn, cameraBtn, hangupBtn); headerEl.append(titleEl, minimizeBtn); - panelEl.append(headerEl, statusEl, mediaStageEl, incomingActionsEl, activeActionsEl); + panelEl.append(mediaStageEl, headerEl, statusEl, incomingActionsEl, activeActionsEl); overlayEl.append(panelEl); rootEl.append(barEl, overlayEl); resolveAppShell().append(rootEl); @@ -337,14 +487,21 @@ function applyExpandedState(snapshot) { overlayEl.hidden = false; barEl.hidden = true; - titleEl.textContent = `${snapshot.titleText || 'Звонок'}: ${snapshot.peerLogin || 'пользователь'}`; - statusEl.textContent = snapshot.statusText || ''; + titleEl.textContent = ''; + statusEl.textContent = ''; + peerLoginEl.textContent = snapshot.peerLogin || 'пользователь'; + kindEl.textContent = snapshot.titleText || 'Звонок'; + topStatusEl.textContent = snapshot.statusText || ''; + updatePeerAvatar(snapshot.peerLogin || ''); const incomingStage = Boolean(snapshot.canAnswer || snapshot.canDecline); const muted = Boolean(snapshot.muted); updateMuteButtons(muted); updateSpeakerButtons(Boolean(snapshot.speakerEnabled)); updateCameraButtons(Boolean(snapshot.cameraEnabled)); + panelEl.classList.toggle('call-overlay-panel--remote-video', Boolean(snapshot.hasRemoteVideo)); + mediaStageEl.classList.toggle('call-video-stage--remote-video', Boolean(snapshot.hasRemoteVideo)); + heroEl.hidden = Boolean(snapshot.hasRemoteVideo); minimizeBtn.hidden = incomingStage; minimizeBtn.disabled = incomingStage; @@ -365,15 +522,18 @@ function applyExpandedState(snapshot) { declineBtn.hidden = !incomingStage || !snapshot.canDecline; hangupBtn.hidden = !snapshot.canHangup; - mediaStageEl.hidden = incomingStage; + mediaStageEl.hidden = false; const remoteStream = snapshot.remoteMediaStream || null; const localStream = snapshot.localPreviewStream || null; const remoteVideoSource = snapshot.hasRemoteVideo ? remoteStream : null; if (remoteVideoEl.srcObject !== remoteVideoSource) remoteVideoEl.srcObject = remoteVideoSource; if (localVideoEl.srcObject !== localStream) localVideoEl.srcObject = localStream; remoteVideoEl.hidden = !snapshot.hasRemoteVideo; - remoteVideoPlaceholderEl.hidden = snapshot.hasRemoteVideo; localPreviewWrapEl.hidden = !snapshot.hasLocalVideo; + if (!snapshot.hasLocalVideo) { + localPreviewPosition = null; + applyLocalPreviewPosition(); + } } function applyMinimizedState(snapshot) { diff --git a/shine-UI/manifest.webmanifest b/shine-UI/manifest.webmanifest index 4ec6a5d7..30db939c 100644 --- a/shine-UI/manifest.webmanifest +++ b/shine-UI/manifest.webmanifest @@ -2,13 +2,12 @@ "name": "СИЯНИЕ", "short_name": "СИЯНИЕ", "start_url": "./index.html", - "display": "fullscreen", + "display": "standalone", "display_override": [ - "fullscreen", "standalone", "minimal-ui" ], - "orientation": "any", + "orientation": "portrait", "background_color": "#0b1020", "theme_color": "#0b1020", "icons": [ @@ -21,6 +20,12 @@ "src": "./img/logo.jpg", "sizes": "512x512", "type": "image/jpeg" + }, + { + "src": "./img/shine-logo-transparent-final.png", + "sizes": "1280x1280", + "type": "image/png", + "purpose": "any maskable" } ] } diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index 3155e947..8f18cbad 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -10,6 +10,7 @@ justify-content: space-between; margin-bottom: 14px; gap: 8px; + padding-top: max(0px, env(safe-area-inset-top)); } .page-title { @@ -45,8 +46,10 @@ } .chat-header-icon-btn { - min-width: 42px; - width: 42px; + min-width: 44px; + width: 44px; + min-height: 44px; + height: 44px; padding: 0; display: inline-flex; align-items: center; @@ -92,6 +95,22 @@ transition: 0.2s ease; } +.icon-btn, +.text-btn, +.primary-btn, +.secondary-btn, +.destructive-btn, +.ghost-btn, +.badge.profile-badge-trigger, +.entrypoint-history-item, +.toolbar-item, +.toolbar-action, +.toolbar-button { + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; +} + .icon-btn:hover, .text-btn:hover, .primary-btn:hover, @@ -102,6 +121,18 @@ transform: translateY(-1px); } +.icon-btn:active, +.text-btn:active, +.primary-btn:active, +.secondary-btn:active, +.destructive-btn:active, +.ghost-btn:active, +.badge.profile-badge-trigger:active, +.entrypoint-history-item:active { + transform: scale(0.98); + filter: brightness(0.96); +} + .primary-btn { background: linear-gradient(120deg, rgba(218, 179, 87, 0.95), rgba(184, 137, 54, 0.92)); border-color: rgba(242, 210, 129, 0.72); @@ -1471,8 +1502,8 @@ .toolbar { display: grid; grid-template-columns: repeat(5, 1fr); - gap: 8px; - padding: 8px; + gap: 4px; + padding: 4px 4px 2px; background: rgba(20, 28, 44, 0.95); border: 1px solid rgba(255, 255, 255, 0.07); border-radius: 16px; @@ -1484,12 +1515,14 @@ border-radius: 12px; background: transparent; color: var(--text-muted); - padding: 8px 4px; + padding: 4px 3px 2px; cursor: pointer; display: grid; justify-items: center; - gap: 4px; + gap: 2px; font-size: 11px; + align-content: end; + min-height: 48px; } .toolbar-label-wrap { @@ -1528,13 +1561,17 @@ } .toolbar-btn-profile .toolbar-label-wrap { - padding-bottom: 10px; + padding-bottom: 6px; +} + +.toolbar-btn-profile .toolbar-connection-indicator { + display: none; } .toolbar-connection-indicator { position: absolute; left: 50%; - bottom: 2px; + bottom: 0; transform: translateX(-50%); display: inline-flex; align-items: center; @@ -1543,6 +1580,7 @@ } .toolbar-connection-text { + display: none; font-size: 8px; letter-spacing: 0.02em; text-transform: lowercase; @@ -1959,58 +1997,75 @@ .call-overlay { position: absolute; inset: 0; - display: flex; - align-items: flex-end; - justify-content: center; - padding: 16px; - background: rgba(6, 10, 16, 0.55); - backdrop-filter: blur(4px); + background: rgba(4, 8, 16, 0.88); + backdrop-filter: blur(8px); pointer-events: auto; } .call-overlay-panel { - width: min(520px, 100%); - border-radius: 18px; - padding: 16px; - border: 1px solid rgba(112, 146, 214, 0.38); - background: linear-gradient(180deg, rgba(15, 27, 51, 0.96), rgba(8, 15, 31, 0.98)); - box-shadow: 0 18px 40px rgba(0, 0, 0, 0.46); - display: grid; - gap: 10px; + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + background: + radial-gradient(circle at 50% 18%, rgba(79, 124, 208, 0.24), transparent 34%), + linear-gradient(180deg, rgba(11, 20, 38, 0.98), rgba(3, 7, 15, 1)); +} + +.call-overlay-panel--remote-video { + background: #010203; } .call-overlay-head { + position: absolute; + top: calc(12px + env(safe-area-inset-top)); + left: 14px; + right: 14px; + z-index: 4; display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; - gap: 12px; + gap: 10px; + pointer-events: none; } .call-overlay-title { margin: 0; - font-size: 20px; - color: #eff4ff; + min-height: 22px; + font-size: 14px; + font-weight: 700; + color: rgba(236, 242, 255, 0.94); + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.44); } .call-overlay-minimize-btn { flex: 0 0 auto; + pointer-events: auto; } .call-overlay-status { - color: #c8d7f6; - font-size: 14px; + position: absolute; + left: 18px; + right: 18px; + bottom: calc(12px + env(safe-area-inset-bottom)); + z-index: 4; + text-align: center; + color: rgba(207, 219, 246, 0.84); + font-size: 13px; + pointer-events: none; } .call-video-stage { - position: relative; - width: 100%; - min-height: 220px; - border-radius: 16px; + position: absolute; + inset: 0; overflow: hidden; background: - radial-gradient(circle at top, rgba(41, 72, 118, 0.48), transparent 58%), - linear-gradient(180deg, rgba(7, 14, 27, 0.98), rgba(3, 7, 14, 1)); - border: 1px solid rgba(118, 154, 228, 0.2); + radial-gradient(circle at 50% 24%, rgba(54, 88, 151, 0.48), transparent 30%), + linear-gradient(180deg, rgba(9, 17, 32, 0.98), rgba(2, 5, 12, 1)); +} + +.call-video-stage--remote-video { + background: #000; } .call-remote-video, @@ -2023,39 +2078,117 @@ } .call-remote-video[hidden], -.call-local-preview[hidden] { +.call-local-preview[hidden], +.call-hero[hidden] { display: none !important; } -.call-video-placeholder { +.call-hero { position: absolute; - inset: 0; + left: 0; + right: 0; + top: clamp(8vh, 16vh, 19vh); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + padding: 0 24px; + text-align: center; + z-index: 2; +} + +.call-peer-avatar-slot { + width: min(42vw, 168px); + height: min(42vw, 168px); display: grid; place-items: center; - padding: 18px; - color: rgba(223, 234, 255, 0.72); - font-size: 15px; - text-align: center; +} + +.call-peer-avatar-slot .call-peer-avatar { + width: 100%; + height: 100%; + min-width: 100%; + min-height: 100%; + border-radius: 999px; + border: 2px solid rgba(221, 233, 255, 0.2); + background: linear-gradient(180deg, rgba(18, 31, 58, 0.96), rgba(9, 15, 29, 0.98)); + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.34); +} + +.call-peer-avatar-slot .call-peer-avatar.call-peer-avatar--empty { + border-color: rgba(221, 233, 255, 0.28); + background: radial-gradient(circle at 35% 30%, rgba(68, 92, 136, 0.68), rgba(15, 24, 43, 0.98)); +} + +.call-peer-avatar-slot .call-peer-avatar .avatar-fallback { + font-size: clamp(34px, 9vw, 58px); + font-weight: 800; +} + +.call-peer-avatar-slot .call-peer-avatar.call-peer-avatar--empty .avatar-fallback { + display: none; +} + +.call-peer-login { + max-width: 86vw; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #f4f8ff; + font-size: clamp(20px, 5.8vw, 28px); + font-weight: 700; + letter-spacing: 0.02em; +} + +.call-kind { + color: #ffffff; + font-size: clamp(27px, 8vw, 40px); + font-weight: 800; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.call-hero-status { + max-width: 88vw; + color: rgba(211, 223, 246, 0.9); + font-size: 14px; + line-height: 1.35; } .call-local-preview { position: absolute; - right: 12px; - bottom: 12px; - width: min(32vw, 128px); + right: 16px; + bottom: calc(18px + env(safe-area-inset-bottom)); + z-index: 4; + width: min(30vw, 130px); aspect-ratio: 3 / 4; border-radius: 14px; overflow: hidden; border: 1px solid rgba(176, 205, 255, 0.38); background: rgba(7, 12, 22, 0.94); box-shadow: 0 12px 28px rgba(0, 0, 0, 0.32); + cursor: grab; + touch-action: none; + user-select: none; +} + +.call-local-preview:active { + cursor: grabbing; } .call-overlay-controls { + position: absolute; + left: 50%; + transform: translateX(-50%); + bottom: calc(env(safe-area-inset-bottom) + clamp(54px, 16vh, 112px)); display: flex; - flex-wrap: wrap; - gap: 8px; + flex-wrap: nowrap; + gap: 18px; align-items: center; + justify-content: center; + width: min(92vw, 520px); + z-index: 4; } .call-overlay-controls[hidden], @@ -2069,15 +2202,45 @@ } .call-overlay-controls--incoming { - justify-content: flex-start; + justify-content: center; } .call-overlay-controls--active { - justify-content: flex-start; + justify-content: center; +} + +.call-overlay .call-icon-btn { + background: rgba(4, 9, 18, 0.18); + border-width: 1.5px; + backdrop-filter: blur(3px); + box-shadow: none; +} + +.call-overlay .call-icon-btn--secondary { + border-color: rgba(229, 237, 255, 0.64); + background: rgba(4, 9, 18, 0.12); + color: rgba(244, 248, 255, 0.96); +} + +.call-overlay .call-icon-btn--secondary:hover { + border-color: rgba(255, 255, 255, 0.96); + background: rgba(10, 20, 36, 0.22); +} + +.call-overlay .call-icon-btn--danger { + border-color: rgba(255, 98, 123, 0.92); + background: rgba(60, 8, 18, 0.12); + color: #ff5b76; +} + +.call-overlay .call-icon-btn--danger:hover { + border-color: rgba(255, 131, 150, 0.98); + background: rgba(82, 10, 24, 0.2); + color: #ff788d; } .call-decline-btn { - min-width: 120px; + min-width: 132px; } .call-accept-btn { @@ -2087,6 +2250,7 @@ color: #052113; padding: 9px 12px; min-height: 38px; + min-width: 132px; font-weight: 700; cursor: pointer; transition: 0.2s ease; @@ -2107,6 +2271,30 @@ transform: none; } +@media (max-width: 420px) { + .call-overlay-controls { + gap: 16px; + bottom: calc(env(safe-area-inset-bottom) + clamp(48px, 14vh, 96px)); + } + + .call-icon-btn { + width: 46px; + min-width: 46px; + height: 46px; + border-radius: 14px; + } + + .call-icon-btn svg { + width: 22px; + height: 22px; + } + + .call-accept-btn, + .call-decline-btn { + min-width: 124px; + } +} + .pwa-diag-list { gap: 8px; } diff --git a/shine-UI/styles/layout.css b/shine-UI/styles/layout.css index 00d0ddef..34448a7c 100644 --- a/shine-UI/styles/layout.css +++ b/shine-UI/styles/layout.css @@ -43,7 +43,7 @@ body::before { top: var(--call-minimized-bar-height, 0px); left: 0; right: 0; - bottom: 74px; + bottom: 62px; overflow-y: auto; padding: 14px 14px 24px; } @@ -68,7 +68,7 @@ body::before { left: 0; right: 0; bottom: 0; - padding: 10px 12px calc(10px + env(safe-area-inset-bottom)); + padding: 2px 10px calc(4px + env(safe-area-inset-bottom)); background: linear-gradient(180deg, rgba(7, 12, 23, 0) 0%, rgba(6, 11, 22, 0.96) 44%); transition: opacity 0.18s ease, transform 0.18s ease; } diff --git a/shine-UI/styles/main.css b/shine-UI/styles/main.css index 3439fbdb..024dbf06 100644 --- a/shine-UI/styles/main.css +++ b/shine-UI/styles/main.css @@ -26,7 +26,11 @@ html, body { margin: 0; padding: 0; + width: 100%; + height: 100%; min-height: 100%; + overflow: hidden; + overscroll-behavior: none; background: radial-gradient(circle at 12% -8%, rgba(214, 176, 90, 0.24), transparent 35%), radial-gradient(circle at 84% 4%, rgba(43, 78, 148, 0.42), transparent 38%),