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
This commit is contained in:
1 parent
bbb3d8c2fa
commit
cd4f3675fd
1 file changed
+120
-17
+120
-17
@@ -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 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generic alert / confirmation dialog. Replaces native alert()/confirm():
|
||||
their button labels come from the browser locale (e.g. German on a
|
||||
German system) and do not match the app. -->
|
||||
<div class="modal-overlay" id="app-dialog" onclick="if(event.target===this)closeAppDialog(false)"
|
||||
onkeydown="if(event.key==='Escape')closeAppDialog(false)">
|
||||
<div class="confirm-modal-content" id="app-dialog-content" role="dialog" aria-modal="true"
|
||||
aria-labelledby="app-dialog-title" aria-describedby="app-dialog-message">
|
||||
<div class="confirm-modal-icon" id="app-dialog-icon"></div>
|
||||
<h3 id="app-dialog-title">Notice</h3>
|
||||
<p id="app-dialog-message"></p>
|
||||
<div class="confirm-modal-actions">
|
||||
<button class="cancel-btn" id="app-dialog-cancel" onclick="closeAppDialog(false)">Cancel</button>
|
||||
<button class="danger-btn" id="app-dialog-ok" onclick="closeAppDialog(true)">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="__BASE_PATH__/static/xterm.js"></script>
|
||||
<script src="__BASE_PATH__/static/xterm-fit.js"></script>
|
||||
|
||||
@@ -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: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
||||
'<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>' +
|
||||
'<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||||
info: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
||||
'<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/>' +
|
||||
'<line x1="12" y1="8" x2="12.01" y2="8"/></svg>'
|
||||
};
|
||||
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<boolean> 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
|
||||
|
||||
Reference in new issue
Block a user