Improve: Web UI - iOS safe-area, icon toolbar buttons, logout confirm

This commit is contained in:
yuanyuanxiang
2026-05-14 22:18:37 +02:00
parent 571ec7d80c
commit 84a52b9dcf
2 changed files with 173 additions and 46 deletions

View File

@@ -9,6 +9,8 @@
//
// 编码要求:此文件必须保存为 UTF-8 with BOMMSVC 要求)
// 注意:此文件中的配置会编译到程序中,运行时无法修改。
// 修改原则最小化原则。如果是UI文案修改通常没有问题
// 如果修改项涉及数据存储、程序配置、代码逻辑,可能导致程序异常。
//
// ============================================================
//
@@ -107,6 +109,7 @@
// 注册表键名 [仅ASCII无空格]
// 存储位置HKCU\Software\{此键名}
// 不能修改,修改会隐藏授权
#define BRAND_REGISTRY_KEY "YAMA"
// 网络通信前缀 [仅ASCII无空格]
@@ -262,7 +265,7 @@
//
// 如果配置值以 "http" 开头,则作为 URL 打开浏览器。
// 否则直接显示为文本消息。
//
// 链接从上级同步而来,修改无效;建议设置这些菜单为隐藏
// 反馈链接(帮助菜单 → 反馈)
#define BRAND_URL_FEEDBACK "https://t.me/SimpleRemoter"

View File

@@ -32,7 +32,17 @@ inline std::string GetWebPageHTML() {
min-height: 100vh;
overflow-x: hidden;
}
.page { display: none !important; padding: 20px; min-height: 100vh; }
.page {
display: none !important;
min-height: 100vh;
/* iOS notch / Dynamic Island: viewport-fit=cover 让 viewport 顶到物理边缘,
这里用 env(safe-area-inset-*) 把内容推回到安全区内,避免被前摄/底部 Home 条遮挡。
旧设备 / 非全屏环境下 env() 解析为 0等价于纯 20px 内边距。 */
padding: calc(20px + env(safe-area-inset-top))
calc(20px + env(safe-area-inset-right))
calc(20px + env(safe-area-inset-bottom))
calc(20px + env(safe-area-inset-left));
}
.page.active { display: block !important; }
#login-page.active {
display: flex !important;
@@ -252,44 +262,32 @@ inline std::string GetWebPageHTML() {
.view-btn:first-child { border-right: 1px solid rgba(255,255,255,0.1); }
.view-btn:hover { color: #fff; }
.view-btn.active { background: rgba(233, 69, 96, 0.3); color: #e94560; }
.refresh-btn {
padding: 10px 20px;
/* Unified icon button (Refresh / Users / Logout). 40x40 方块更紧凑,留位置给未来按钮。
颜色身份通过修饰类(.refresh / .users / .logout保留hover 高光与原来一致。 */
.icon-btn {
width: 40px;
height: 40px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #0f3460 0%, #1a4a7a 100%);
color: #fff;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
margin-left: 8px;
}
.refresh-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(15, 52, 96, 0.4); }
.logout-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #c0392b 0%, #e74c3c 100%);
color: #fff;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
margin-left: 10px;
}
.logout-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); }
.users-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
margin-left: 10px;
display: none;
}
.users-btn.visible { display: inline-block; }
.users-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(142, 68, 173, 0.4); }
.icon-btn svg { display: block; width: 20px; height: 20px; }
.icon-btn:hover { transform: translateY(-1px); }
.icon-btn:active { transform: translateY(0); }
.icon-btn.refresh { background: linear-gradient(135deg, #0f3460 0%, #1a4a7a 100%); }
.icon-btn.refresh:hover { box-shadow: 0 4px 12px rgba(15, 52, 96, 0.4); }
.icon-btn.users { background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); display: none; }
.icon-btn.users.visible { display: inline-flex; }
.icon-btn.users:hover { box-shadow: 0 4px 12px rgba(142, 68, 173, 0.4); }
.icon-btn.logout { background: linear-gradient(135deg, #c0392b 0%, #e74c3c 100%); }
.icon-btn.logout:hover { box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); }
/* User Management Modal */
.modal-overlay {
display: none;
@@ -387,6 +385,54 @@ inline std::string GetWebPageHTML() {
.user-msg { padding: 10px; border-radius: 6px; margin-bottom: 12px; font-size: 13px; }
.user-msg.success { background: rgba(39, 174, 96, 0.2); color: #2ecc71; }
.user-msg.error { background: rgba(231, 76, 60, 0.2); color: #e74c3c; }
/* Generic confirmation modal (compact yes/no dialog, e.g. logout) —— 复用 .modal-overlay
做遮罩,自带一套紧凑布局 + 危险/取消双按钮风格。 */
.confirm-modal-content {
background: rgba(22, 33, 62, 0.98);
border-radius: 16px;
padding: 28px;
width: 90%;
max-width: 380px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
border: 1px solid rgba(231, 76, 60, 0.25);
text-align: center;
}
.confirm-modal-icon {
width: 56px;
height: 56px;
margin: 0 auto 16px;
border-radius: 50%;
background: rgba(231, 76, 60, 0.15);
color: #e74c3c;
display: flex;
align-items: center;
justify-content: center;
}
.confirm-modal-icon svg { width: 28px; height: 28px; }
.confirm-modal-content h3 { color: #fff; margin: 0 0 8px; font-size: 18px; }
.confirm-modal-content p { color: #aaa; margin: 0 0 24px; font-size: 14px; line-height: 1.5; }
.confirm-modal-actions { display: flex; gap: 12px; justify-content: center; }
.confirm-modal-actions button {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.confirm-modal-actions .cancel-btn {
background: rgba(255,255,255,0.08);
color: #ccc;
}
.confirm-modal-actions .cancel-btn:hover { background: rgba(255,255,255,0.15); }
.confirm-modal-actions .danger-btn {
background: linear-gradient(135deg, #c0392b 0%, #e74c3c 100%);
color: #fff;
}
.confirm-modal-actions .danger-btn:hover { box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); transform: translateY(-1px); }
)HTML";
// Part 3: Device card styles
@@ -558,7 +604,11 @@ inline std::string GetWebPageHTML() {
#screen-canvas { max-width: 100%; max-height: 100%; }
.screen-toolbar {
background: linear-gradient(180deg, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.85) 100%);
padding: 12px 20px;
/* 远程桌面顶部工具栏也避开 notch / Dynamic Island —— 顶 padding + 安全区 */
padding: calc(12px + env(safe-area-inset-top))
calc(20px + env(safe-area-inset-right))
12px
calc(20px + env(safe-area-inset-left));
display: flex;
justify-content: space-between;
align-items: center;
@@ -633,13 +683,14 @@ inline std::string GetWebPageHTML() {
height: 100dvh !important;
max-height: none !important;
}
/* Portrait fullscreen: align canvas to top */
/* Portrait fullscreen: align canvas to top. 56px 是给浮动工具栏留出的空间;
竖屏 iPhone 全屏时再加上 safe-area-inset-top 把内容推到 notch / 灵动岛之下。 */
@media (orientation: portrait) {
#screen-page:fullscreen .canvas-container,
#screen-page:-webkit-full-screen .canvas-container,
#screen-page.pseudo-fullscreen .canvas-container {
align-items: flex-start !important;
padding-top: 56px !important;
padding-top: calc(56px + env(safe-area-inset-top)) !important;
}
}
#screen-page.pseudo-fullscreen #screen-canvas {
@@ -666,7 +717,8 @@ inline std::string GetWebPageHTML() {
/* Floating toolbar menu - minimal icon style */
.floating-toolbar {
position: fixed;
top: 8px;
/* 8px 基础留白 + safe-area-inset-top 避开 iPhone 刘海/灵动岛 */
top: calc(8px + env(safe-area-inset-top));
left: 50%;
transform: translateX(-50%);
z-index: 1001;
@@ -701,7 +753,8 @@ inline std::string GetWebPageHTML() {
.toolbar-btn:disabled:active { transform: none; }
.toolbar-toggle {
position: fixed;
top: 8px;
/* 同 .floating-toolbar8px 基础 + 安全区 inset 避开刘海/灵动岛 */
top: calc(8px + env(safe-area-inset-top));
left: 50%;
transform: translateX(-50%);
z-index: 1000;
@@ -779,6 +832,10 @@ inline std::string GetWebPageHTML() {
/* Portrait: horizontal at top center */
@media (orientation: portrait) {
.quick-controls {
/* 非全屏screen-toolbar 已经吃了一份 safe-area这里再叠加会让快捷按钮
离 title 远到 ~2x 图标高度的空白,且 canvas-container 因此被推得过低,
底部 input-shortcuts 在键盘弹出时会被遮挡。
全屏:另在 fullscreen 选择器里补回 safe-area-inset-top 避开前置摄像头。 */
top: 4px;
left: 50%;
transform: translateX(-50%);
@@ -791,12 +848,23 @@ inline std::string GetWebPageHTML() {
height: 40px;
font-size: 18px;
}
/* Portrait: align canvas to top, leave space for controls */
/* Portrait: align canvas to top, leave space for controls.
同上:非全屏 screen-toolbar 已避开刘海,这里只留固定 56px 给快捷按钮,
全屏路径在下方有独立 !important 覆写补回 safe-area。 */
.canvas-container {
align-items: flex-start;
padding-top: 56px;
}
}
/* 全屏(无 screen-toolbar 兜底)才需要 quick-controls 自己加 safe-area
避开 iPhone 刘海/灵动岛/前置摄像头。 */
@media (orientation: portrait) {
#screen-page:fullscreen .quick-controls,
#screen-page:-webkit-full-screen .quick-controls,
#screen-page.pseudo-fullscreen .quick-controls {
top: calc(4px + env(safe-area-inset-top));
}
}
/* Landscape: vertical at right center */
@media (orientation: landscape) {
.quick-controls {
@@ -852,7 +920,12 @@ inline std::string GetWebPageHTML() {
.input-shortcuts .shortcut-btn:active { transform: scale(0.95); background: rgba(128,128,128,0.6); }
/* Mobile responsive */
@media (max-width: 768px) {
.page { padding: 10px; }
.page {
padding: calc(10px + env(safe-area-inset-top))
calc(10px + env(safe-area-inset-right))
calc(10px + env(safe-area-inset-bottom))
calc(10px + env(safe-area-inset-left));
}
.header { flex-direction: column; gap: 12px; padding: 12px; }
.header h1 { font-size: 20px; }
.search-box { width: 100%; }
@@ -863,7 +936,12 @@ inline std::string GetWebPageHTML() {
.device-card .ip { font-size: 12px; }
.device-card .meta-row { flex-wrap: wrap; gap: 8px; }
.device-card .active-window { font-size: 11px; }
.screen-toolbar { padding: 8px 12px; }
.screen-toolbar {
padding: calc(8px + env(safe-area-inset-top))
calc(12px + env(safe-area-inset-right))
8px
calc(12px + env(safe-area-inset-left));
}
.back-btn { padding: 6px 12px; font-size: 13px; }
.toolbar-info .device-name { font-size: 13px; }
.toolbar-info .conn-info { font-size: 11px; }
@@ -925,9 +1003,28 @@ inline std::string GetWebPageHTML() {
<button id="view-grid" class="view-btn active" onclick="setViewMode('grid')" title="Grid View">Grid</button>
<button id="view-list" class="view-btn" onclick="setViewMode('list')" title="List View">List</button>
</div>
<button class="refresh-btn" onclick="getDevices()">Refresh</button>
<button class="users-btn" id="users-btn" onclick="openUsersModal()">Users</button>
<button class="logout-btn" onclick="logout()">Logout</button>
<button class="icon-btn refresh" onclick="getDevices()" title="Refresh" aria-label="Refresh">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"/>
<polyline points="1 20 1 14 7 14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
</button>
<button class="icon-btn users" id="users-btn" onclick="openUsersModal()" title="User Management" aria-label="User Management">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</button>
<button class="icon-btn logout" onclick="logout()" title="Logout" aria-label="Logout">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
</button>
</div>
</div>
<div id="stats-bar" class="stats-bar">
@@ -1013,6 +1110,25 @@ inline std::string GetWebPageHTML() {
</div>
</div>
</div>
<!-- Logout Confirmation Modal -->
<div class="modal-overlay" id="logout-confirm-modal" onclick="if(event.target===this)cancelLogout()">
<div class="confirm-modal-content">
<div class="confirm-modal-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
</div>
<h3>Confirm Logout</h3>
<p>You will be returned to the login screen.<br>Continue?</p>
<div class="confirm-modal-actions">
<button class="cancel-btn" onclick="cancelLogout()">Cancel</button>
<button class="danger-btn" onclick="confirmLogout()">Logout</button>
</div>
</div>
</div>
)HTML";
// Part 7: JavaScript - State and WebSocket
@@ -1648,7 +1764,15 @@ inline std::string GetWebPageHTML() {
ws.send(JSON.stringify({ cmd: 'login', username, response, nonce: challengeNonce }));
}
// Logout 二次确认onclick="logout()" 仅打开确认弹窗;用户点 "Logout" 才真正退出。
function logout() {
document.getElementById('logout-confirm-modal').classList.add('active');
}
function cancelLogout() {
document.getElementById('logout-confirm-modal').classList.remove('active');
}
function confirmLogout() {
document.getElementById('logout-confirm-modal').classList.remove('active');
// Close and reconnect WebSocket to get new challenge
if (ws) {
ws.onclose = null; // Prevent auto-reconnect delay