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