Improve: Web UI - iOS safe-area, icon toolbar buttons, logout confirm
This commit is contained in:
@@ -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-toolbar:8px 基础 + 安全区 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
|
||||
|
||||
Reference in New Issue
Block a user