From 0159dd9074b13f26a3a8f6cc995debf1fe707dc62aa3fc61339d2d1192509c03 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 22 Apr 2026 16:53:36 +0300 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=D0=B8=D0=BD=D0=B4=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D1=80=20connected=20=D0=B2=20=D1=82=D1=83?= =?UTF-8?q?=D0=BB=D0=B1=D0=B0=D1=80=D0=B5=20=D0=B8=20=D0=BF=D0=B0=D0=BD?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shine-UI/js/app.js | 146 ++++++++++++++++++++++++------ shine-UI/js/components/toolbar.js | 18 +++- shine-UI/styles/components.css | 56 ++++++++++++ shine-UI/styles/layout.css | 15 +-- 4 files changed, 198 insertions(+), 37 deletions(-) diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 92d3a68..79d46ea 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -113,53 +113,126 @@ let versionCheckIntervalId = null; let versionCheckInFlight = false; let reconnectIntervalId = null; let sessionRuntimeStarted = false; -let connectionStatusEl = null; let connectionState = ''; +let connectionStatusText = ''; +let connectionRetryBannerEl = null; +let connectionStatusCountdownId = null; +let connectionNextRetryAtMs = 0; +let connectionCheckInFlight = false; setClientErrorTransport((payload) => authService.reportClientError(payload)); initPwaInstallPromptHandling(); initCallUiOverlay(); setCallDebugReporter((payload) => authService.reportClientDebug(payload)); -function ensureConnectionStatusEl() { - if (connectionStatusEl) return connectionStatusEl; +function ensureConnectionIndicatorEl() { + return document.getElementById('toolbar-connection-indicator'); +} + +function ensureConnectionRetryBannerEl() { + if (connectionRetryBannerEl) return connectionRetryBannerEl; if (!appShellEl) return null; const el = document.createElement('div'); - el.id = 'connection-status-slot'; - el.className = 'connection-status-slot is-connecting'; - el.textContent = 'Подключение к серверу...'; + el.id = 'connection-retry-banner'; + el.className = 'connection-retry-banner is-connecting'; + el.textContent = 'Соединяюсь…'; + el.addEventListener('click', () => { + void triggerImmediateConnectionRetry(); + }); appShellEl.append(el); - connectionStatusEl = el; + connectionRetryBannerEl = el; return el; } +function stopConnectionCountdown() { + if (connectionStatusCountdownId) { + window.clearInterval(connectionStatusCountdownId); + connectionStatusCountdownId = null; + } +} + +function getConnectionRetrySecondsLeft() { + const leftMs = Math.max(0, Number(connectionNextRetryAtMs || 0) - Date.now()); + return Math.ceil(leftMs / 1000); +} + +function refreshConnectionUi() { + const state = String(connectionState || 'connecting').trim(); + + const indicatorEl = ensureConnectionIndicatorEl(); + if (indicatorEl) { + indicatorEl.classList.remove('is-connected', 'is-disconnected', 'is-connecting', 'is-updating', 'is-unknown'); + indicatorEl.classList.add(`is-${state || 'unknown'}`); + } + + const bannerEl = ensureConnectionRetryBannerEl(); + if (!bannerEl) return; + + if (state === 'connected' || state === 'updating') { + bannerEl.hidden = true; + stopConnectionCountdown(); + return; + } + + bannerEl.hidden = false; + bannerEl.classList.remove('is-connected', 'is-disconnected', 'is-connecting', 'is-updating'); + bannerEl.classList.add(`is-${state}`); + + if (connectionStatusText) { + bannerEl.textContent = connectionStatusText; + return; + } + + if (state === 'connecting') { + bannerEl.textContent = 'Соединяюсь… Нажмите, чтобы попробовать сразу'; + return; + } + + if (state === 'disconnected') { + const secs = getConnectionRetrySecondsLeft(); + bannerEl.textContent = `Нет соединения. Повтор через ${secs}с. Нажмите для попытки сейчас`; + return; + } + + bannerEl.textContent = 'Проблема с соединением. Нажмите для повтора'; +} + +function startConnectionCountdown() { + if (connectionStatusCountdownId) return; + connectionStatusCountdownId = window.setInterval(() => { + if (connectionState !== 'disconnected') return; + const secs = getConnectionRetrySecondsLeft(); + if (secs <= 0 && !connectionCheckInFlight) { + connectionStatusText = ''; + setConnectionStatus('connecting', 'Соединяюсь…'); + void checkConnectionHealth(); + return; + } + refreshConnectionUi(); + }, 1000); +} + function setConnectionStatus(nextState, text = '') { - const el = ensureConnectionStatusEl(); - if (!el) return; const state = String(nextState || '').trim(); if (!state) return; - if (state === connectionState && !text) return; connectionState = state; - el.classList.remove('is-connected', 'is-connecting', 'is-disconnected', 'is-updating'); - el.classList.add(`is-${state}`); - - if (text) { - el.textContent = text; - return; - } - if (state === 'connected') { - el.textContent = 'Подключено к серверу'; - return; - } + connectionStatusText = String(text || '').trim(); if (state === 'disconnected') { - el.textContent = 'Нет соединения с сервером'; - return; + if (!connectionNextRetryAtMs || connectionNextRetryAtMs <= Date.now()) { + connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS; + } + startConnectionCountdown(); + } else { + stopConnectionCountdown(); } - if (state === 'updating') { - el.textContent = 'Найдена новая версия, обновляю приложение...'; - return; - } - el.textContent = 'Подключение к серверу...'; + refreshConnectionUi(); +} + +async function triggerImmediateConnectionRetry() { + if (connectionCheckInFlight) return; + connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS; + setConnectionStatus('connecting', 'Соединяюсь…'); + await checkConnectionHealth(); } function parseBuildHashFromHtml(html) { @@ -207,14 +280,22 @@ function startVersionMonitor() { } async function checkConnectionHealth() { + if (connectionCheckInFlight) return; + connectionCheckInFlight = true; if (connectionState !== 'connected') { setConnectionStatus('connecting'); } try { + if (!wsIsOpen()) { + await authService.ws.open(); + } await authService.ws.request('Ping', { ts: Date.now() }, 7000); setConnectionStatus('connected'); } catch { + connectionStatusText = ''; setConnectionStatus('disconnected'); + } finally { + connectionCheckInFlight = false; } } @@ -223,8 +304,15 @@ function startConnectionMonitor() { window.clearInterval(pingIntervalId); pingIntervalId = null; } + + connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS; void checkConnectionHealth(); + pingIntervalId = window.setInterval(() => { + connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS; + if (connectionState === 'disconnected') { + refreshConnectionUi(); + } void checkConnectionHealth(); }, CONNECTION_CHECK_INTERVAL_MS); } @@ -355,6 +443,7 @@ function renderPageFailureFallback(pageId, error) { screenEl.classList.toggle('no-app-chrome', false); toolbarEl.innerHTML = ''; + refreshConnectionUi(); } function renderApp() { @@ -395,6 +484,7 @@ function renderApp() { if (showAppChrome) { toolbarEl.append(renderToolbar(page.pageMeta.id, navigate)); } + refreshConnectionUi(); } catch (error) { console.error('[renderApp] controlled fallback', error); renderPageFailureFallback(pageId, error); diff --git a/shine-UI/js/components/toolbar.js b/shine-UI/js/components/toolbar.js index 5a1920e..ee71320 100644 --- a/shine-UI/js/components/toolbar.js +++ b/shine-UI/js/components/toolbar.js @@ -15,8 +15,22 @@ export function renderToolbar(currentPageId, navigate) { ITEMS.forEach((item) => { const btn = document.createElement('button'); - btn.className = `toolbar-btn${item.pageId === active ? ' active' : ''}`; - btn.innerHTML = `${item.icon}${item.label}`; + const isProfile = item.pageId === 'profile-view'; + btn.className = `toolbar-btn${item.pageId === active ? ' active' : ''}${isProfile ? ' toolbar-btn-profile' : ''}`; + if (isProfile) { + btn.innerHTML = ` + ${item.icon} + + ${item.label} + + connected + + + + `; + } else { + btn.innerHTML = `${item.icon}${item.label}`; + } btn.addEventListener('click', () => navigate(item.pageId)); root.append(btn); }); diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index 0060830..2e48629 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -611,6 +611,62 @@ font-size: 11px; } +.toolbar-label-wrap { + display: grid; + justify-items: center; + line-height: 1.05; +} + +.toolbar-btn-profile { + position: relative; +} + +.toolbar-connection-indicator { + display: inline-flex; + align-items: center; + gap: 3px; + margin-top: 1px; + min-height: 9px; +} + +.toolbar-connection-text { + font-size: 8px; + letter-spacing: 0.02em; + text-transform: lowercase; + color: rgba(191, 213, 255, 0.8); + opacity: 0; + transition: opacity 0.2s ease; +} + +.toolbar-connection-dot { + width: 7px; + height: 7px; + border-radius: 999px; + background: rgba(148, 162, 195, 0.65); + box-shadow: 0 0 0 2px rgba(97, 116, 156, 0.25); + transition: 0.2s ease; +} + +.toolbar-connection-indicator.is-connected .toolbar-connection-text { + opacity: 1; + color: rgba(145, 255, 192, 0.9); +} + +.toolbar-connection-indicator.is-connected .toolbar-connection-dot { + background: #71e9a5; + box-shadow: 0 0 0 2px rgba(72, 201, 134, 0.28); +} + +.toolbar-connection-indicator.is-connecting .toolbar-connection-dot { + background: #f0c56b; + box-shadow: 0 0 0 2px rgba(240, 197, 107, 0.22); +} + +.toolbar-connection-indicator.is-disconnected .toolbar-connection-dot { + background: #e48792; + box-shadow: 0 0 0 2px rgba(228, 135, 146, 0.24); +} + .toolbar-btn.active { background: rgba(83, 216, 251, 0.14); color: var(--text); diff --git a/shine-UI/styles/layout.css b/shine-UI/styles/layout.css index c7efd84..9b2c4c9 100644 --- a/shine-UI/styles/layout.css +++ b/shine-UI/styles/layout.css @@ -41,11 +41,11 @@ body { background: linear-gradient(180deg, rgba(7, 12, 23, 0) 0%, rgba(6, 11, 22, 0.96) 44%); } -.connection-status-slot { +.connection-retry-banner { position: absolute; left: 12px; right: 12px; - bottom: calc(62px + env(safe-area-inset-bottom)); + bottom: calc(74px + env(safe-area-inset-bottom)); z-index: 5; border-radius: 11px; border: 1px solid rgba(133, 156, 201, 0.3); @@ -55,26 +55,27 @@ body { line-height: 1.2; text-align: center; padding: 7px 10px; - pointer-events: none; + pointer-events: auto; + cursor: pointer; backdrop-filter: blur(10px); } -.connection-status-slot.is-connected { +.connection-retry-banner.is-connected { border-color: rgba(124, 235, 171, 0.4); color: #d8ffe9; } -.connection-status-slot.is-connecting { +.connection-retry-banner.is-connecting { border-color: rgba(238, 196, 107, 0.42); color: #ffe8bb; } -.connection-status-slot.is-disconnected { +.connection-retry-banner.is-disconnected { border-color: rgba(228, 127, 145, 0.44); color: #ffdce3; } -.connection-status-slot.is-updating { +.connection-retry-banner.is-updating { border-color: rgba(144, 201, 255, 0.44); color: #d9eeff; }