rdp-proxy: replace corner status with centered fullscreen overlay
- Remove Ожидайте corner badge - Phase 1 (connecting): centered spinner + Подключение к рабочему столу... + elapsed timer - Phase 2 (desktop ready): progress bar fills over 5.8s then fades out - Errors (reconnect) shown in small top-left box only
This commit is contained in:
+97
-22
@@ -26,16 +26,32 @@ cat > /opt/portal/index.html <<HTML
|
|||||||
<title>RDP Session</title>
|
<title>RDP Session</title>
|
||||||
<style>
|
<style>
|
||||||
html,body,#screen{margin:0;height:100%;background:#111}
|
html,body,#screen{margin:0;height:100%;background:#111}
|
||||||
.status{
|
#co{
|
||||||
position:fixed;left:12px;top:12px;z-index:50;padding:8px 10px;border-radius:8px;
|
position:fixed;inset:0;z-index:50;background:rgba(8,14,24,.95);
|
||||||
background:rgba(16,22,32,.86);border:1px solid rgba(255,255,255,.18);
|
display:flex;align-items:center;justify-content:center;
|
||||||
color:#dce8f5;font:600 13px/1.25 sans-serif;max-width:min(92vw,560px);
|
transition:opacity .5s;
|
||||||
}
|
}
|
||||||
.status.error{background:rgba(85,20,20,.9);border-color:rgba(255,130,130,.36);color:#ffe3e3}
|
#co.hidden{opacity:0;pointer-events:none}
|
||||||
.status.hidden{display:none}
|
.co-inner{display:flex;flex-direction:column;align-items:center;gap:20px;padding:40px}
|
||||||
.spinner{display:inline-block;width:12px;height:12px;border:2px solid rgba(220,232,245,.3);
|
.co-ring{
|
||||||
border-top-color:#dce8f5;border-radius:50%;animation:spin .8s linear infinite;margin-right:7px;vertical-align:middle}
|
width:64px;height:64px;border-radius:50%;
|
||||||
|
border:4px solid rgba(220,232,245,.12);
|
||||||
|
border-top-color:#4a9de0;
|
||||||
|
animation:spin .9s linear infinite;
|
||||||
|
}
|
||||||
|
.co-title{color:#dce8f5;font:600 1.1rem/1.4 sans-serif;text-align:center;letter-spacing:.01em}
|
||||||
|
.co-timer{color:#6a8aaa;font:400 .85rem sans-serif;min-height:1.2em}
|
||||||
|
.co-bar-wrap{
|
||||||
|
width:280px;height:4px;background:rgba(220,232,245,.1);
|
||||||
|
border-radius:99px;overflow:hidden;
|
||||||
|
}
|
||||||
|
.co-bar{height:100%;width:0%;border-radius:99px;background:linear-gradient(90deg,#2a8cd6,#4fc3f7)}
|
||||||
@keyframes spin{to{transform:rotate(360deg)}}
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
#status{
|
||||||
|
position:fixed;left:12px;top:12px;z-index:60;padding:8px 12px;border-radius:8px;
|
||||||
|
background:rgba(85,20,20,.9);border:1px solid rgba(255,130,130,.36);
|
||||||
|
color:#ffe3e3;font:600 13px/1.25 sans-serif;max-width:min(92vw,560px);display:none;
|
||||||
|
}
|
||||||
.nav-panel{
|
.nav-panel{
|
||||||
position:fixed;left:16px;top:64px;z-index:99;display:flex;gap:10px;
|
position:fixed;left:16px;top:64px;z-index:99;display:flex;gap:10px;
|
||||||
background:linear-gradient(180deg,rgba(15,24,36,.78),rgba(9,14,22,.86));
|
background:linear-gradient(180deg,rgba(15,24,36,.78),rgba(9,14,22,.86));
|
||||||
@@ -55,7 +71,17 @@ cat > /opt/portal/index.html <<HTML
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="screen"></div>
|
<div id="screen"></div>
|
||||||
<div id="status" class="status"><span class="spinner"></span>Ожидайте...</div>
|
<div id="co">
|
||||||
|
<div class="co-inner">
|
||||||
|
<div class="co-ring"></div>
|
||||||
|
<div class="co-title" id="co-title">Подключение к рабочему столу...</div>
|
||||||
|
<div class="co-timer" id="co-timer"></div>
|
||||||
|
<div class="co-bar-wrap" id="co-bar-wrap" style="display:none">
|
||||||
|
<div class="co-bar" id="co-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="status"></div>
|
||||||
<div class="nav-panel">
|
<div class="nav-panel">
|
||||||
<button class="nav-btn" id="btn-back" type="button" title="Назад">←</button>
|
<button class="nav-btn" id="btn-back" type="button" title="Назад">←</button>
|
||||||
<button class="nav-btn" id="btn-home" type="button" title="Главная">⌂</button>
|
<button class="nav-btn" id="btn-home" type="button" title="Главная">⌂</button>
|
||||||
@@ -65,9 +91,14 @@ cat > /opt/portal/index.html <<HTML
|
|||||||
import RFB from './core/rfb.js';
|
import RFB from './core/rfb.js';
|
||||||
const wsBase = location.pathname.replace(/\/+$/, '');
|
const wsBase = location.pathname.replace(/\/+$/, '');
|
||||||
const wsUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + wsBase + '/websockify';
|
const wsUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + wsBase + '/websockify';
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
const XK_ALT_L = 0xffe9;
|
const coEl = document.getElementById('co');
|
||||||
const XK_LEFT = 0xff51;
|
const coTitleEl = document.getElementById('co-title');
|
||||||
|
const coTimerEl = document.getElementById('co-timer');
|
||||||
|
const coBarWrap = document.getElementById('co-bar-wrap');
|
||||||
|
const coBar = document.getElementById('co-bar');
|
||||||
|
const XK_ALT_L = 0xffe9;
|
||||||
|
const XK_LEFT = 0xff51;
|
||||||
|
|
||||||
let rfb = null;
|
let rfb = null;
|
||||||
let connected = false;
|
let connected = false;
|
||||||
@@ -76,31 +107,76 @@ cat > /opt/portal/index.html <<HTML
|
|||||||
const MAX_RECONNECT = 12;
|
const MAX_RECONNECT = 12;
|
||||||
const DELAYS = [1000,2000,3000,5000,8000];
|
const DELAYS = [1000,2000,3000,5000,8000];
|
||||||
let manualDisconnect = false;
|
let manualDisconnect = false;
|
||||||
|
let timerInterval = null;
|
||||||
|
let timerStart = 0;
|
||||||
|
|
||||||
function showStatus(text, isError) {
|
function startTimer() {
|
||||||
const spinner = isError ? '' : '<span class="spinner"></span>';
|
timerStart = Date.now();
|
||||||
statusEl.innerHTML = spinner + text;
|
clearInterval(timerInterval);
|
||||||
statusEl.className = 'status' + (isError ? ' error' : '');
|
timerInterval = setInterval(() => {
|
||||||
|
const s = Math.floor((Date.now() - timerStart) / 1000);
|
||||||
|
coTimerEl.textContent = s + ' сек';
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideStatus() { statusEl.className = 'status hidden'; }
|
function showOverlayWaiting() {
|
||||||
|
coTitleEl.textContent = 'Подключение к рабочему столу...';
|
||||||
|
coTimerEl.textContent = '0 сек';
|
||||||
|
coBarWrap.style.display = 'none';
|
||||||
|
coBar.style.width = '0%';
|
||||||
|
coEl.style.opacity = '1';
|
||||||
|
coEl.style.display = 'flex';
|
||||||
|
coEl.classList.remove('hidden');
|
||||||
|
statusEl.style.display = 'none';
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOverlayConnecting() {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
coTitleEl.textContent = 'Устанавливается соединение с рабочим столом...';
|
||||||
|
coTimerEl.textContent = '';
|
||||||
|
coBarWrap.style.display = 'block';
|
||||||
|
coBar.style.transition = 'none';
|
||||||
|
coBar.style.width = '0%';
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||||
|
coBar.style.transition = 'width 5.8s cubic-bezier(0.25,0.46,0.45,0.94)';
|
||||||
|
coBar.style.width = '100%';
|
||||||
|
}));
|
||||||
|
setTimeout(hideOverlay, 6200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOverlay() {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
coEl.classList.add('hidden');
|
||||||
|
setTimeout(() => { coEl.style.display = 'none'; }, 520);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showStatus(text) {
|
||||||
|
statusEl.textContent = text;
|
||||||
|
statusEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideStatus() { statusEl.style.display = 'none'; }
|
||||||
|
|
||||||
function scheduleReconnect(reason) {
|
function scheduleReconnect(reason) {
|
||||||
if (manualDisconnect) return;
|
if (manualDisconnect) return;
|
||||||
if (reconnectAttempts >= MAX_RECONNECT) {
|
if (reconnectAttempts >= MAX_RECONNECT) {
|
||||||
showStatus('Соединение потеряно. Переподключение не удалось. Откройте сервис заново.', true);
|
hideOverlay();
|
||||||
|
showStatus('Соединение потеряно. Переподключение не удалось. Откройте сервис заново.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const n = ++reconnectAttempts;
|
const n = ++reconnectAttempts;
|
||||||
const delay = DELAYS[Math.min(n-1, DELAYS.length-1)];
|
const delay = DELAYS[Math.min(n-1, DELAYS.length-1)];
|
||||||
showStatus(\`\${reason} Повтор \${n}/\${MAX_RECONNECT} через \${Math.ceil(delay/1000)} сек.\`, true);
|
hideOverlay();
|
||||||
|
showStatus(\`\${reason} Повтор \${n}/\${MAX_RECONNECT} через \${Math.ceil(delay/1000)} сек.\`);
|
||||||
reconnectTimer = setTimeout(connect, delay);
|
reconnectTimer = setTimeout(connect, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
if (manualDisconnect) return;
|
if (manualDisconnect) return;
|
||||||
connected = false;
|
connected = false;
|
||||||
showStatus('Ожидайте...');
|
hideStatus();
|
||||||
|
showOverlayWaiting();
|
||||||
if (rfb) { try { rfb.disconnect(); } catch(e){} }
|
if (rfb) { try { rfb.disconnect(); } catch(e){} }
|
||||||
rfb = new RFB(document.getElementById('screen'), wsUrl);
|
rfb = new RFB(document.getElementById('screen'), wsUrl);
|
||||||
rfb.viewOnly = false;
|
rfb.viewOnly = false;
|
||||||
@@ -110,8 +186,7 @@ cat > /opt/portal/index.html <<HTML
|
|||||||
connected = true;
|
connected = true;
|
||||||
reconnectAttempts = 0;
|
reconnectAttempts = 0;
|
||||||
clearTimeout(reconnectTimer);
|
clearTimeout(reconnectTimer);
|
||||||
showStatus('Устанавливается соединение с рабочим столом...');
|
showOverlayConnecting();
|
||||||
setTimeout(hideStatus, 6000);
|
|
||||||
});
|
});
|
||||||
rfb.addEventListener('disconnect', () => {
|
rfb.addEventListener('disconnect', () => {
|
||||||
connected = false;
|
connected = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user