[feat] add resizable DeskHub focus previews

This commit is contained in:
dijunkun
2026-10-14 04:17:31 +08:00
parent 4855b2d3b6
commit 61ff9b2787
4 changed files with 101 additions and 19 deletions
@@ -1278,6 +1278,9 @@ void GuiApplication::InitializeUi() {
if (std::isfinite(tile_width)) if (std::isfinite(tile_width))
// The rendered grid constrains the restored width to available space. // The rendered grid constrains the restored width to available space.
ui_->wall->set_gallery_tile_width(std::max(tile_width, 184.0f)); ui_->wall->set_gallery_tile_width(std::max(tile_width, 184.0f));
const float focus_rail_height = saved.value("focus_rail_height", 70.875f);
if (std::isfinite(focus_rail_height))
ui_->wall->set_gallery_focus_rail_height(std::max(focus_rail_height, 54.0f));
ui_->connection_window.BeginRestore(); ui_->connection_window.BeginRestore();
} }
} catch (...) { } catch (...) {
@@ -4526,6 +4529,7 @@ void GuiApplication::SaveGalleryWindowState() {
const float connection_scale = std::max(ui_->main->window().scale_factor(), 1.0f); const float connection_scale = std::max(ui_->main->window().scale_factor(), 1.0f);
nlohmann::json state{{"version", 2}, nlohmann::json state{{"version", 2},
{"tile_width", ui_->wall->get_gallery_tile_width()}, {"tile_width", ui_->wall->get_gallery_tile_width()},
{"focus_rail_height", ui_->wall->get_gallery_focus_rail_height()},
{"fullscreen", ui_->gallery_window.wall_fullscreen}, {"fullscreen", ui_->gallery_window.wall_fullscreen},
{"maximized", ui_->gallery_window.wall_maximized}}; {"maximized", ui_->gallery_window.wall_maximized}};
if (ui_->gallery_window.wall_size) { if (ui_->gallery_window.wall_size) {
+93 -17
View File
@@ -55,6 +55,8 @@ export component GalleryView inherits Rectangle {
in property <bool> window-active: true; in property <bool> window-active: true;
in-out property <float> tile-width: 216; in-out property <float> tile-width: 216;
out property <bool> resizing: false; out property <bool> resizing: false;
in-out property <float> focus-rail-height: 70.875;
out property <bool> focus-resizing: false;
private property <float> preview-aspect-ratio: 16 / 9; private property <float> preview-aspect-ratio: 16 / 9;
private property <length> card-inset: 6px; private property <length> card-inset: 6px;
private property <length> card-chrome-height: root.simplified ? root.card-inset * 2 : 36px; private property <length> card-chrome-height: root.simplified ? root.card-inset * 2 : 36px;
@@ -64,9 +66,19 @@ export component GalleryView inherits Rectangle {
private property <length> card-width: clamp(root.tile-width * 1px, 184px, root.max-card-width); private property <length> card-width: clamp(root.tile-width * 1px, 184px, root.max-card-width);
private property <length> card-preview-height: (root.card-width - root.card-inset * 2) / root.preview-aspect-ratio; private property <length> card-preview-height: (root.card-width - root.card-inset * 2) / root.preview-aspect-ratio;
private property <length> card-height: root.card-preview-height + root.card-chrome-height; private property <length> card-height: root.card-preview-height + root.card-chrome-height;
private property <length> rail-preview-width: 126px; private property <bool> focus-rail-visible: root.focused.id != "" && root.tiles.length > 1;
private property <length> rail-preview-height: root.rail-preview-width / root.preview-aspect-ratio; private property <length> rail-chrome-height: root.simplified ? 10px : 24px;
private property <length> rail-card-height: root.rail-preview-height + (root.simplified ? 10px : 24px); private property <length> min-rail-preview-height: 54px;
// Leave at least 160px for the main preview and keep each thumbnail inside
// the window. Clamp the rendered size without losing the saved preference.
private property <length> max-rail-preview-height: max(root.min-rail-preview-height,
min((focus-view.width - 10px) / root.preview-aspect-ratio,
focus-view.height - 36px - root.rail-chrome-height - 14px - 12px - 160px));
private property <length> rail-preview-height: clamp(root.focus-rail-height * 1px,
root.min-rail-preview-height, root.max-rail-preview-height);
private property <length> rail-preview-width: root.rail-preview-height * root.preview-aspect-ratio;
private property <length> rail-card-height: root.rail-preview-height + root.rail-chrome-height;
private property <length> rail-slot-width: root.rail-preview-width + 18px;
private property <string> focused-id: root.focused.id; private property <string> focused-id: root.focused.id;
private property <string> device-tooltip-id; private property <string> device-tooltip-id;
private property <string> device-tooltip-text; private property <string> device-tooltip-text;
@@ -89,19 +101,20 @@ export component GalleryView inherits Rectangle {
private property <length> drag-edge-width: 32px; private property <length> drag-edge-width: 32px;
private property <int> drag-page-direction: root.page-direction(root.drag-x, root.drag-y); private property <int> drag-page-direction: root.page-direction(root.drag-x, root.drag-y);
private property <bool> swap-save-failed: false; private property <bool> swap-save-failed: false;
changed focused-id => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); } changed focused-id => { root.device-tooltip-id = ""; root.resizing = false; root.focus-resizing = false; root.cancel-device-drag(); }
changed visible => { root.device-tooltip-id = ""; if !root.visible { root.resizing = false; root.cancel-device-drag(); add-menu.close-menu(); batch-menu.close-menu(); } } changed focus-rail-visible => { root.focus-resizing = false; }
changed width => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); } changed visible => { root.device-tooltip-id = ""; if !root.visible { root.resizing = false; root.focus-resizing = false; root.cancel-device-drag(); add-menu.close-menu(); batch-menu.close-menu(); } }
changed height => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); } changed width => { root.device-tooltip-id = ""; root.resizing = false; root.focus-resizing = false; root.cancel-device-drag(); }
changed window-active => { if !root.window-active { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); } } changed height => { root.device-tooltip-id = ""; root.resizing = false; root.focus-resizing = false; root.cancel-device-drag(); }
changed window-active => { if !root.window-active { root.device-tooltip-id = ""; root.resizing = false; root.focus-resizing = false; root.cancel-device-drag(); } }
changed page => { root.device-tooltip-id = ""; if root.page != root.drag-page { root.cancel-device-drag(); } } changed page => { root.device-tooltip-id = ""; if root.page != root.drag-page { root.cancel-device-drag(); } }
changed group-index => { root.device-tooltip-id = ""; root.cancel-device-drag(); } changed group-index => { root.device-tooltip-id = ""; root.cancel-device-drag(); }
changed card-width => { root.cancel-device-drag(); } changed card-width => { root.cancel-device-drag(); }
changed card-height => { root.cancel-device-drag(); } changed card-height => { root.cancel-device-drag(); }
changed device-tooltip-id => { changed device-tooltip-id => {
root.device-tooltip-visible = false; root.device-tooltip-visible = false;
device-tooltip-timer.running = root.device-tooltip-id == "resize-grid"; device-tooltip-timer.running = root.device-tooltip-id == "resize-grid" || root.device-tooltip-id == "resize-focus";
if root.device-tooltip-id == "resize-grid" { device-tooltip-timer.restart(); } if device-tooltip-timer.running { device-tooltip-timer.restart(); }
} }
device-tooltip-timer := Timer { device-tooltip-timer := Timer {
interval: 450ms; running: false; interval: 450ms; running: false;
@@ -168,6 +181,7 @@ export component GalleryView inherits Rectangle {
} }
public function reset-transient() { public function reset-transient() {
root.resizing = false; root.resizing = false;
root.focus-resizing = false;
root.cancel-device-drag(); root.cancel-device-drag();
root.batch = false; root.adding = false; root.importing = false; root.confirming = false; root.batch = false; root.adding = false; root.importing = false; root.confirming = false;
root.device-tooltip-id = ""; add-menu.close-menu(); batch-menu.close-menu(); root.device-tooltip-id = ""; add-menu.close-menu(); batch-menu.close-menu();
@@ -561,7 +575,7 @@ export component GalleryView inherits Rectangle {
CompactButton { width: 100px; primary: true; enabled: !root.focused.connected || root.controlling || (root.focused.has-frame && !root.focused.stale); text: root.focused.connected ? (root.controlling ? GalleryStrings.tr("释放操控", "Release", "Отпустить") : GalleryStrings.tr("接管操控", "Control", "Управлять")) : GalleryStrings.tr("连接", "Connect", "Подключить"); clicked => { root.action(root.focused.connected ? (root.controlling ? "release" : "control") : "connect", root.focused.id); } } CompactButton { width: 100px; primary: true; enabled: !root.focused.connected || root.controlling || (root.focused.has-frame && !root.focused.stale); text: root.focused.connected ? (root.controlling ? GalleryStrings.tr("释放操控", "Release", "Отпустить") : GalleryStrings.tr("接管操控", "Control", "Управлять")) : GalleryStrings.tr("连接", "Connect", "Подключить"); clicked => { root.action(root.focused.connected ? (root.controlling ? "release" : "control") : "connect", root.focused.id); } }
} }
Rectangle { Rectangle {
x: 0px; y: rail.visible ? rail.y + rail.height + 10px : 36px; x: 0px; y: rail.visible ? focus-divider.y + focus-divider.height : 36px;
width: parent.width; height: parent.height - self.y; width: parent.width; height: parent.height - self.y;
background: root.focused.online ? #eff1f4 : DeviceCardStyle.offline-background; border-radius: 6px; background: root.focused.online ? #eff1f4 : DeviceCardStyle.offline-background; border-radius: 6px;
border-width: root.controlling ? 2px : 1px; border-color: root.controlling ? #76aeea : root.focused.online ? #d5dbe3 : DeviceCardStyle.offline-border; border-width: root.controlling ? 2px : 1px; border-color: root.controlling ? #76aeea : root.focused.online ? #d5dbe3 : DeviceCardStyle.offline-border;
@@ -607,10 +621,10 @@ export component GalleryView inherits Rectangle {
} }
} }
rail := ScrollView { rail := ScrollView {
visible: root.tiles.length > 1; visible: root.focus-rail-visible;
x: 0px; y: 36px; width: parent.width; height: root.rail-card-height + 14px; x: 0px; y: 36px; width: parent.width; height: root.rail-card-height + 14px;
// Reserve 14px below the cards for the horizontal scrollbar. // Reserve 14px below the cards for the horizontal scrollbar.
viewport-width: max(self.width, max(0, root.tiles.length - 1) * 144px); viewport-width: max(self.width, max(0, root.tiles.length - 1) * root.rail-slot-width);
viewport-height: self.height; viewport-height: self.height;
horizontal-scrollbar-policy: as-needed; horizontal-scrollbar-policy: as-needed;
vertical-scrollbar-policy: always-off; vertical-scrollbar-policy: always-off;
@@ -627,13 +641,13 @@ export component GalleryView inherits Rectangle {
x: 0px; y: 0px; height: root.rail-card-height; spacing: 0px; padding: 0px; x: 0px; y: 0px; height: root.rail-card-height; spacing: 0px; padding: 0px;
for tile in root.tiles: rail-slot := Rectangle { for tile in root.tiles: rail-slot := Rectangle {
// Collapse the focused tile and its gap without changing the page model. // Collapse the focused tile and its gap without changing the page model.
width: tile.id == root.focused.id ? 0px : 144px; width: tile.id == root.focused.id ? 0px : root.rail-slot-width;
height: root.rail-card-height; visible: tile.id != root.focused.id; height: root.rail-card-height; visible: tile.id != root.focused.id;
rail-card := Rectangle { rail-card := Rectangle {
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети") private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно"); : tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
private property <string> details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence; private property <string> details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence;
width: 136px; height: root.rail-card-height; border-radius: 5px; width: root.rail-preview-width + 10px; height: root.rail-card-height; border-radius: 5px;
background: tile.online ? #f5f6f8 : DeviceCardStyle.offline-background; border-width: 1px; border-color: tile.online ? #d5dbe3 : DeviceCardStyle.offline-border; background: tile.online ? #f5f6f8 : DeviceCardStyle.offline-background; border-width: 1px; border-color: tile.online ? #d5dbe3 : DeviceCardStyle.offline-border;
accessible-role: button; accessible-role: button;
accessible-label: self.details; accessible-label: self.details;
@@ -649,7 +663,7 @@ export component GalleryView inherits Rectangle {
} }
Text { Text {
visible: !root.simplified; visible: !root.simplified;
x: 7px; y: root.rail-preview-height + 6px; width: 122px; height: 16px; x: 7px; y: root.rail-preview-height + 6px; width: parent.width - 14px; height: 16px;
text: tile.name; color: tile.online ? #303943 : DeviceCardStyle.offline-text; font-size: 11px; text: tile.name; color: tile.online ? #303943 : DeviceCardStyle.offline-text; font-size: 11px;
overflow: elide; vertical-alignment: center; overflow: elide; vertical-alignment: center;
} }
@@ -670,6 +684,68 @@ export component GalleryView inherits Rectangle {
} }
} }
} }
focus-divider := Rectangle {
visible: root.focus-rail-visible;
x: 0px; y: rail.y + rail.height; width: parent.width; height: 12px;
private property <bool> highlighted: divider-touch.has-hover || root.focus-resizing || divider-focus.has-focus;
Rectangle {
x: 0px; y: 5px; width: parent.width; height: 1px;
background: focus-divider.highlighted ? #a6c9ed : #e6e9ed;
}
Rectangle {
x: (parent.width - self.width) / 2; y: 4px; width: 36px; height: 3px; border-radius: 1.5px;
background: focus-divider.highlighted ? #6dabea : #bcc4ce;
}
divider-focus := FocusScope {
width: parent.width; height: parent.height;
focus-lost => { root.focus-resizing = false; }
key-pressed(event) => {
if event.text == Key.Escape { root.focus-resizing = false; return accept; }
if event.text == Key.UpArrow || event.text == Key.DownArrow {
root.focus-rail-height = clamp(root.rail-preview-height
+ (event.text == Key.UpArrow ? -8px : 8px),
root.min-rail-preview-height, root.max-rail-preview-height) / 1px;
return accept;
}
reject
}
divider-touch := TouchArea {
mouse-cursor: MouseCursor.ns-resize;
private property <length> start-y;
private property <length> start-height;
function show-hint() {
if self.has-hover && !root.focus-resizing {
root.device-tooltip-id = "resize-focus";
root.device-tooltip-text = GalleryStrings.tr("上下拖动,调整缩略图与主画面大小", "Drag up or down to resize the thumbnails and main preview", "Потяните вверх или вниз, чтобы изменить размер миниатюр и основного экрана");
root.device-tooltip-x = focus-view.x + self.mouse-x;
root.device-tooltip-y = focus-view.y + focus-divider.y + self.mouse-y + 16px;
} else if root.device-tooltip-id == "resize-focus" { root.device-tooltip-id = ""; }
}
function resize-preview() {
if !root.focus-resizing { return; }
// Use coordinates in the fixed parent: the divider itself
// moves while it retains the pointer grab.
root.focus-rail-height = clamp(self.start-height
+ focus-divider.y + self.mouse-y - self.start-y,
root.min-rail-preview-height, root.max-rail-preview-height) / 1px;
}
changed has-hover => { self.show-hint(); }
pointer-event(event) => {
if event.button == PointerEventButton.left && event.kind == PointerEventKind.down {
divider-focus.focus();
self.start-y = focus-divider.y + self.mouse-y;
self.start-height = root.rail-preview-height;
root.device-tooltip-id = ""; root.focus-resizing = true;
} else if event.kind == PointerEventKind.cancel {
root.focus-resizing = false;
} else if event.button == PointerEventButton.left && event.kind == PointerEventKind.up {
self.resize-preview(); root.focus-resizing = false;
}
}
moved => { self.resize-preview(); self.show-hint(); }
}
}
}
} }
Rectangle { Rectangle {
y: parent.height - 44px; height: 44px; background: #fafbfc; y: parent.height - 44px; height: 44px; background: #fafbfc;
@@ -766,7 +842,7 @@ export component GalleryView inherits Rectangle {
} }
} }
// Device details are rendered by MainWindow; resize hints stay local. // Device details are rendered by MainWindow; resize hints stay local.
if root.device-tooltip-visible && root.device-tooltip-id == "resize-grid": Rectangle { if root.device-tooltip-visible && (root.device-tooltip-id == "resize-grid" || root.device-tooltip-id == "resize-focus"): Rectangle {
x: clamp(root.device-tooltip-x, 8px, parent.width - self.width - 8px); x: clamp(root.device-tooltip-x, 8px, parent.width - self.width - 8px);
y: clamp(root.device-tooltip-y + self.height > footer.y ? root.device-tooltip-y - self.height - 8px : root.device-tooltip-y, 4px, parent.height - self.height - 4px); y: clamp(root.device-tooltip-y + self.height > footer.y ? root.device-tooltip-y - self.height - 8px : root.device-tooltip-y, 4px, parent.height - self.height - 4px);
width: min(320px, parent.width - 16px); height: device-tooltip-label.preferred-height + 16px; width: min(320px, parent.width - 16px); height: device-tooltip-label.preferred-height + 16px;
+3 -1
View File
@@ -322,7 +322,8 @@ export component MainWindow inherits Window {
out property <int> gallery-columns: gallery.columns; out property <int> gallery-columns: gallery.columns;
out property <int> gallery-rows: gallery.rows; out property <int> gallery-rows: gallery.rows;
in-out property <float> gallery-tile-width: 216; in-out property <float> gallery-tile-width: 216;
out property <bool> gallery-resizing: gallery.resizing; in-out property <float> gallery-focus-rail-height: 70.875;
out property <bool> gallery-resizing: gallery.resizing || gallery.focus-resizing;
in property <int> gallery-page: 0; in property <int> gallery-page: 0;
in property <int> gallery-pages: 1; in property <int> gallery-pages: 1;
in property <int> gallery-count: 0; in property <int> gallery-count: 0;
@@ -1317,6 +1318,7 @@ export component MainWindow inherits Window {
toggle-fullscreen => { root.toggle-gallery-fullscreen(); } toggle-fullscreen => { root.toggle-gallery-fullscreen(); }
window-active: root.window-active; window-active: root.window-active;
tile-width <=> root.gallery-tile-width; tile-width <=> root.gallery-tile-width;
focus-rail-height <=> root.gallery-focus-rail-height;
network-open => { root.gallery-network-open = true; } network-open => { root.gallery-network-open = true; }
controlling: root.gallery-controlling; controlling: root.gallery-controlling;
input-suspended: root.gallery-transitioning; input-suspended: root.gallery-transitioning;
+1 -1
View File
@@ -27,7 +27,7 @@
- 服务器连接状态、指示灯和重连入口仅在主窗口显示,桌面墙的普通、精简、展开、批量选择和全屏视图均不显示服务器状态栏。桌面墙底部操作栏左侧以普通文字显示已连接设备数量,旁边是紧凑的“网络状态”按钮;点击按钮可打开当前已建立连接的设备列表,空白区域不响应点击,也不显示整条悬停提示。弹窗采用紧凑宽度,高度随设备数量调整,最多同时显示 6 行,更多设备通过滚动查看。网络列表不受桌面墙分组和分页限制;离线、未连接和连接中的设备不列入,断开后自动移除。列表显示设备名称、ID、直连/中继、实际协商的 SRTP 加密状态、往返延迟(RTT)、控制端接收与发送速率、视频丢包率、实际接收分辨率与帧率,打开期间每秒刷新。加密状态紧随连接方式显示为“已加密/未加密”,尚无统计或报告超过 3 秒未更新时显示“待确认”。分辨率与帧率在解码交付时统计,墙内预览和后台会话均可用;尚未收到画面时显示“—”,画面停止更新超过 1 秒时帧率显示 0 FPS。网络数值尚无统计或报告超过 3 秒未更新时显示“—”。可通过关闭按钮、Esc 或点击列表外关闭;打开列表会释放远程操控。 - 服务器连接状态、指示灯和重连入口仅在主窗口显示,桌面墙的普通、精简、展开、批量选择和全屏视图均不显示服务器状态栏。桌面墙底部操作栏左侧以普通文字显示已连接设备数量,旁边是紧凑的“网络状态”按钮;点击按钮可打开当前已建立连接的设备列表,空白区域不响应点击,也不显示整条悬停提示。弹窗采用紧凑宽度,高度随设备数量调整,最多同时显示 6 行,更多设备通过滚动查看。网络列表不受桌面墙分组和分页限制;离线、未连接和连接中的设备不列入,断开后自动移除。列表显示设备名称、ID、直连/中继、实际协商的 SRTP 加密状态、往返延迟(RTT)、控制端接收与发送速率、视频丢包率、实际接收分辨率与帧率,打开期间每秒刷新。加密状态紧随连接方式显示为“已加密/未加密”,尚无统计或报告超过 3 秒未更新时显示“待确认”。分辨率与帧率在解码交付时统计,墙内预览和后台会话均可用;尚未收到画面时显示“—”,画面停止更新超过 1 秒时帧率显示 0 FPS。网络数值尚无统计或报告超过 3 秒未更新时显示“—”。可通过关闭按钮、Esc 或点击列表外关闭;打开列表会释放远程操控。
- 点卡片进入只读放大视图,点“接管操控”才开始发送键鼠。点“释放操控”或按 Ctrl/⌘ + Shift + Esc 可以释放。切换设备、翻页、打开本地操作面板、切换模式及窗口失焦也会释放。 - 点卡片进入只读放大视图,点“接管操控”才开始发送键鼠。点“释放操控”或按 Ctrl/⌘ + Shift + Esc 可以释放。切换设备、翻页、打开本地操作面板、切换模式及窗口失焦也会释放。
- 桌面墙连接默认静音,包含新建连接、重连、复用已有连接、添加或导入已有会话,以及切入桌面墙时已有的连接。展开、全屏或接管操控不会自动开启声音;需要声音时,可通过批量“取消静音”或在独立串流窗口主动开启。两个入口共用同一静音状态,主动开启的声音在桌面墙可见时播放,翻页、展开及释放操控保留声音设置;每台设备使用独立音频流同时播放,静音一台不会清空其他设备的声音。新版本对端按会话关闭音频订阅;旧版本发送的音频由控制端丢弃,不通过停止对端的全局声音采集影响其他控制端。 - 桌面墙连接默认静音,包含新建连接、重连、复用已有连接、添加或导入已有会话,以及切入桌面墙时已有的连接。展开、全屏或接管操控不会自动开启声音;需要声音时,可通过批量“取消静音”或在独立串流窗口主动开启。两个入口共用同一静音状态,主动开启的声音在桌面墙可见时播放,翻页、展开及释放操控保留声音设置;每台设备使用独立音频流同时播放,静音一台不会清空其他设备的声音。新版本对端按会话关闭音频订阅;旧版本发送的音频由控制端丢弃,不通过停止对端的全局声音采集影响其他控制端。
- 放大视图使用一行设备操作栏,显示返回、设备名称、状态和接管/释放操控按钮,右侧保留精简模式及全屏入口;选中的桌面在下方放大显示,同页其他桌面的缩略图统一在上方横向排列,超出宽度时可横向滚动。点击上方缩略图可切换下方桌面,当前桌面不在缩略图中重复显示;同页只有一台设备时隐藏缩略图栏。普通模式缩略图下方以一行显示设备名,长名称省略,不显示状态圆点;不在线设备的缩略图和下方放大预览统一使用灰色,精简模式同样生效;悬停可查看设备名称、ID 和在线状态,精简模式也支持悬停查看。缺少画面或画面停滞时保留状态占位;滚动条单独留出空间。普通和精简模式的缩略图预览区域均固定为 16:9,普通模式的设备名称单独显示在画面下方。普通、精简、宽窗口及全屏均保持上下布局,调整尺寸后仍保留当前设备所在页。主预览和缩略图均完整等比显示,鼠标坐标按实际预览区域映射。 - 放大视图使用一行设备操作栏,显示返回、设备名称、状态和接管/释放操控按钮,右侧保留精简模式及全屏入口;选中的桌面在下方放大显示,同页其他桌面的缩略图统一在上方横向排列,超出宽度时可横向滚动。点击上方缩略图可切换下方桌面,当前桌面不在缩略图中重复显示;同页只有一台设备时隐藏缩略图栏。普通模式缩略图下方以一行显示设备名,长名称省略,不显示状态圆点;不在线设备的缩略图和下方放大预览统一使用灰色,精简模式同样生效;悬停可查看设备名称、ID 和在线状态,精简模式也支持悬停查看。缺少画面或画面停滞时保留状态占位;滚动条单独留出空间。普通和精简模式的缩略图预览区域均固定为 16:9,普通模式的设备名称单独显示在画面下方。普通、精简、宽窗口及全屏均保持上下布局,上下区域之间提供带抓手的分隔条:向上拖动扩大下方主画面,向下拖动同步放大上方缩略图;选中分隔条后也可用上下方向键微调。缩略图始终保持 16:9,并保留主画面的最小可用高度;窗口缩小时临时限制尺寸,恢复窗口后沿用原先尺寸。普通和精简模式共用此尺寸,切换设备后保持,松开鼠标后保存并在重启时恢复;与网格的单元格尺寸独立。同页只有一台设备时同时隐藏缩略图栏和分隔条。拖动分隔条不会切换设备或发送远程键鼠,Esc、窗口失焦及窗口大小变化会结束拖动。调整尺寸后仍保留当前设备所在页。主预览和缩略图均完整等比显示,鼠标坐标按实际预览区域映射。
- 在“近期连接”中点击已连接设备的卡片画面,可打开原有独立串流窗口,使用声音、显示器切换、隐私屏、文件传输等完整单会话工具。关闭墙内设备的独立窗口不会断开墙中的会话。 - 在“近期连接”中点击已连接设备的卡片画面,可打开原有独立串流窗口,使用声音、显示器切换、隐私屏、文件传输等完整单会话工具。关闭墙内设备的独立窗口不会断开墙中的会话。
- 点击“选择”进入批量模式,“完成”在同一位置退出;顶部设备管理栏原位切换为批量操作栏,从左到右显示已选数量(包含跨页数量)、“全选/取消全选”、“连接”、“断开”、“更多”和“完成”,无需往返底栏。批量模式暂时收起分组筛选和“添加”入口,退出后恢复;工具栏高度不变,设备卡片的位置、尺寸及分页容量保持不变。已选数量占用剩余空间,批量操作按钮同样按文字宽度和统一内边距自适应;“全选/取消全选”使用与“断开”、“更多”一致的浅灰背景和边框,并预留两种文字中较宽的一种,切换选中数量或按钮文字时,后续按钮保持原位。“连接”保留主操作的蓝色强调,“完成”以蓝色表示当前选择模式。点击“全选”会选中当前筛选下的所有设备,跨页生效;全部选中后显示“取消全选”。翻页保留选择,切换分组清空选择;底栏始终保留已连接数量、网络状态入口和分页;静音和文件分发收进顶部向下展开的“更多”菜单。声音菜单项根据已选且已连接设备的状态切换:全部静音时显示“取消静音”,有设备开启声音时显示“静音”,无已连接设备时禁用。点击后统一设置所有已选的已连接设备,包含跨页选择,跳过未连接设备,保留当前选择。断开和文件分发会先列出完整目标;提交后的操作目标不会跟随之后的选择变化。 - 点击“选择”进入批量模式,“完成”在同一位置退出;顶部设备管理栏原位切换为批量操作栏,从左到右显示已选数量(包含跨页数量)、“全选/取消全选”、“连接”、“断开”、“更多”和“完成”,无需往返底栏。批量模式暂时收起分组筛选和“添加”入口,退出后恢复;工具栏高度不变,设备卡片的位置、尺寸及分页容量保持不变。已选数量占用剩余空间,批量操作按钮同样按文字宽度和统一内边距自适应;“全选/取消全选”使用与“断开”、“更多”一致的浅灰背景和边框,并预留两种文字中较宽的一种,切换选中数量或按钮文字时,后续按钮保持原位。“连接”保留主操作的蓝色强调,“完成”以蓝色表示当前选择模式。点击“全选”会选中当前筛选下的所有设备,跨页生效;全部选中后显示“取消全选”。翻页保留选择,切换分组清空选择;底栏始终保留已连接数量、网络状态入口和分页;静音和文件分发收进顶部向下展开的“更多”菜单。声音菜单项根据已选且已连接设备的状态切换:全部静音时显示“取消静音”,有设备开启声音时显示“静音”,无已连接设备时禁用。点击后统一设置所有已选的已连接设备,包含跨页选择,跳过未连接设备,保留当前选择。断开和文件分发会先列出完整目标;提交后的操作目标不会跟随之后的选择变化。