From cd4f3675fd494f4319e5224f8e1beb64b2526bd6 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Mon, 19 Oct 2026 10:27:06 +0200 Subject: [PATCH] Improve: Replace native browser prompts with themed in-app dialogs Native alert() and confirm() render their buttons in the browser or OS locale, so on a German system prompts such as the Clear Screen Wall confirmation showed German labels that disagreed with the rest of the web UI. Both are now served by a single themed modal that reuses the logout confirmation styling: an info variant with one OK button for notices, and a danger variant with Cancel/OK for destructive actions. The alert helper is fire-and-forget, while the confirm helper returns a Promise, so the two call sites that need an answer (clearWall and deleteUser) are now async. Dialog title and message are written with textContent so device names, user names and server error strings can never be interpreted as markup. A pending dialog is resolved as cancelled when another one supersedes it, keeping callers from hanging, and clicking the backdrop or pressing Escape closes the dialog. Remote keyboard forwarding now ignores keys while a dialog is open. Co-Authored-By: deepseek-flash --- server/web/index.html | 137 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 120 insertions(+), 17 deletions(-) diff --git a/server/web/index.html b/server/web/index.html index 4e7fbb8..357b01d 100644 --- a/server/web/index.html +++ b/server/web/index.html @@ -574,6 +574,14 @@ color: #fff; } .confirm-modal-actions .danger-btn:hover { box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); transform: translateY(-1px); } + /* Info variant for the generic alert dialog (single OK button). */ + .confirm-modal-content.info { border-color: rgba(230, 126, 34, 0.25); } + .confirm-modal-icon.info { background: rgba(230, 126, 34, 0.15); color: #e67e22; } + .confirm-modal-actions .primary-btn { + background: linear-gradient(135deg, #b9770e 0%, #e67e22 100%); + color: #fff; + } + .confirm-modal-actions .primary-btn:hover { box-shadow: 0 4px 12px rgba(230, 126, 34, 0.4); transform: translateY(-1px); } .device-grid { display: grid; @@ -1974,6 +1982,23 @@ + + + @@ -2703,7 +2728,7 @@ // mode=removed:历史记录已删除,刷新后卡片消失。 getDevices(); } else { - alert(msg.msg || 'Failed to delete device'); + appAlert(msg.msg || 'Failed to delete device', 'Unable to Delete Device'); } break; case 'list_users_result': @@ -3289,7 +3314,10 @@ const existing = wall.sessions.get(l32); if (existing && existing.fullId !== id) { // 两台设备的低 32 位相同:绝不能共用一个解码器。 - if (!restoring) alert('Device ID collision (low 32 bits). Cannot open both on the same wall.'); + if (!restoring) { + appAlert('Device ID collision (low 32 bits). Cannot open both on the same wall.', + 'Device ID Collision'); + } return null; } const uid = 'w' + (wall.uidSeq++); @@ -3311,11 +3339,11 @@ function addWallTile(deviceId) { if (!serverCaps.multiWatch) { - alert('Screen Wall is not supported by this server'); + appAlert('Screen Wall is not supported by this server.', 'Screen Wall Unavailable'); return; } if (wall.tiles.length >= WALL_MAX_TILES) { - alert('Wall is full (max ' + WALL_MAX_TILES + ' tiles)'); + appAlert('Wall is full (max ' + WALL_MAX_TILES + ' tiles).', 'Screen Wall Full'); return; } const dev = devices.find(d => String(d.id) === String(deviceId)); @@ -3355,9 +3383,11 @@ wallUpdateSidebarBadges(); } - function clearWall() { + async function clearWall() { if (!wall.tiles.length) return; - if (!confirm('Close all tiles on the wall?')) return; + const ok = await appConfirm('Close all tiles on the wall?', + { title: 'Clear Screen Wall', okText: 'Clear' }); + if (!ok) return; const tiles = wall.tiles.slice(); wall.tiles = []; for (const tile of tiles) { @@ -3569,8 +3599,8 @@ const tile = wall.tiles.find(t => t.uid === uid); if (!tile) return; const dev = devices.find(d => String(d.id) === tile.deviceId); - if (!dev) { alert('Device not found'); return; } - if (!dev.online) { alert('Device is offline'); return; } + if (!dev) { appAlert('Device not found.', 'Device Not Found'); return; } + if (!dev.online) { appAlert('Device is offline.', 'Device Offline'); return; } const s = wall.sessions.get(tile.low32); if (s && s.status === 'offline') { // 设备重新上线:离线时服务端已移除 watch,且 streams 条目被整体删除, @@ -3609,12 +3639,13 @@ function showWall() { if (!serverCaps.multiWatch) { - alert('Screen Wall requires a newer server with multi-view support.'); + appAlert('Screen Wall requires a newer server with multi-view support.', + 'Screen Wall Unavailable'); return; } const compat = checkWebCodecs(); if (!compat.supported) { - alert('Browser does not support H.264: ' + compat.reason); + appAlert('Browser does not support H.264: ' + compat.reason, 'H.264 Not Supported'); return; } if (!wallRestored) wallRestoreState(); @@ -4450,6 +4481,74 @@ updateAdminButtons(); } + // ===== In-app alert / confirmation dialog ===== + // Native alert()/confirm() use the browser locale for their buttons + // (e.g. "Abbrechen"/"OK" on a German system) and ignore the app theme. + // Both now go through the same themed modal as the logout prompt. + const APP_DIALOG_ICONS = { + danger: '' + + '' + + '', + info: '' + + '' + + '' + }; + let appDialogResolve = null; + + function openAppDialog(opts) { + // A pending dialog is superseded (e.g. an async error arrives while + // a confirm is open): resolve it as cancelled so no caller hangs. + if (appDialogResolve) { + const previous = appDialogResolve; + appDialogResolve = null; + previous(false); + } + const danger = opts.danger !== false; + const content = document.getElementById('app-dialog-content'); + const icon = document.getElementById('app-dialog-icon'); + const cancel = document.getElementById('app-dialog-cancel'); + const ok = document.getElementById('app-dialog-ok'); + content.className = 'confirm-modal-content' + (danger ? '' : ' info'); + icon.className = 'confirm-modal-icon' + (danger ? '' : ' info'); + icon.innerHTML = danger ? APP_DIALOG_ICONS.danger : APP_DIALOG_ICONS.info; + document.getElementById('app-dialog-title').textContent = + opts.title || (danger ? 'Confirm' : 'Notice'); + // textContent: server/device/user supplied strings must never be + // interpreted as markup. + document.getElementById('app-dialog-message').textContent = opts.message || ''; + cancel.style.display = opts.cancelText ? '' : 'none'; + cancel.textContent = opts.cancelText || 'Cancel'; + ok.className = danger ? 'danger-btn' : 'primary-btn'; + ok.textContent = opts.okText || 'OK'; + document.getElementById('app-dialog').classList.add('active'); + setTimeout(() => ok.focus(), 0); + return new Promise(resolve => { appDialogResolve = resolve; }); + } + + function closeAppDialog(result) { + document.getElementById('app-dialog').classList.remove('active'); + const resolve = appDialogResolve; + appDialogResolve = null; + if (resolve) resolve(!!result); + } + + // Fire-and-forget replacement for alert(). + function appAlert(message, title) { + return openAppDialog({ title: title || 'Notice', message, okText: 'OK', danger: false }); + } + + // Promise replacement for confirm(). + function appConfirm(message, opts) { + opts = opts || {}; + return openAppDialog({ + title: opts.title || 'Confirm', + message, + okText: opts.okText || 'OK', + cancelText: opts.cancelText || 'Cancel', + danger: opts.danger !== false + }); + } + // ===== 删除设备 ===== // 两种情况都删除服务端 devices.json 中的历史记录;在线设备额外按 MFC // 的做法发送单字节 TOKEN_UNINSTALL(卸载主机)。都先弹二次确认。 @@ -4463,7 +4562,7 @@ function askDeleteDevice(id) { if (!canDeleteDevices()) return; const d = devices.find(x => String(x.id) === String(id)); - if (!d) { alert('Device not found'); return; } + if (!d) { appAlert('Device not found.', 'Device Not Found'); return; } const offline = d.online === false; const name = displayName(d); pendingDeleteDeviceId = String(id); @@ -4559,8 +4658,10 @@ } } - function deleteUser(username) { - if (!confirm('Delete user "' + username + '"?')) return; + async function deleteUser(username) { + const ok = await appConfirm('Delete user "' + username + '"?', + { title: 'Delete User', okText: 'Delete' }); + if (!ok) return; if (ws && ws.readyState === WebSocket.OPEN && token) { ws.send(JSON.stringify({ cmd: 'delete_user', token, username })); } @@ -4792,7 +4893,7 @@ function connectDevice(id, fromWall) { const dev = devices.find(d => d.id === id || d.id === String(id)); if (!dev || !dev.online) { - alert('Device is offline'); + appAlert('Device is offline.', 'Device Offline'); return; } // 无显示器(screen 字段格式 "n:WxH",n=0 表示无显示器)→ 远程桌面没有意义, @@ -4804,7 +4905,7 @@ return; } const compat = checkWebCodecs(); - if (!compat.supported) { alert('Browser does not support H264: ' + compat.reason); return; } + if (!compat.supported) { appAlert('Browser does not support H264: ' + compat.reason, 'H.264 Not Supported'); return; } const sid = String(id); // 同页直接切设备(正常 UI 不会发生):先释放旧 focus 引用。 if (currentDevice && String(currentDevice.id) !== sid) { @@ -4842,11 +4943,11 @@ function openTerminal(id) { if (typeof Terminal === 'undefined') { - alert('Terminal library not loaded yet, please retry'); + appAlert('Terminal library not loaded yet, please retry.', 'Terminal Unavailable'); return; } const dev = devices.find(d => d.id === id || d.id === String(id)); - if (!dev || !dev.online) { alert('Device is offline'); return; } + if (!dev || !dev.online) { appAlert('Device is offline.', 'Device Offline'); return; } // 已经有终端在跑:直接 show(重连同设备视为重置) if (termState.deviceId && termState.deviceId !== String(id)) { closeTerminal(); @@ -6392,6 +6493,8 @@ // Desktop keyboard handling document.addEventListener('keydown', function(e) { if (!document.getElementById('screen-page').classList.contains('active')) return; + // Keys belong to an open in-app dialog (buttons are not INPUT/TEXTAREA). + if (document.getElementById('app-dialog').classList.contains('active')) return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; // F11 belongs to the local fullscreen toggle UNLESS we're actively // controlling the remote — see the F11 handler above. Skip here