Files
crossdesk/apps/desktop/src/gui/ui/gallery_view.slint
T

917 lines
60 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { GalleryStrings } from "device_strings.slint";
import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.slint";
import { GalleryMenuButton } from "gallery_menu_button.slint";
import { GroupFilter } from "group_filter.slint";
import { ScrollView, LineEdit } from "std-widgets.slint";
import { CompactButton, SecondaryButton, IconButton, FontAwesomeIcons, ImGuiCheckBox } from "common.slint";
component GalleryToolbarButton inherits CompactButton {
in property <string> alternate-text;
in property <length> horizontal-padding: 12px;
// Reserve both labels so toggling a mode never shifts adjacent controls.
width: max(48px, ceil(max(label-width-probe.preferred-width,
alternate-width-probe.preferred-width) / 1px) * 1px + root.horizontal-padding * 2);
label-width-probe := Text { visible: false; text: root.text; font-size: root.text-size; }
alternate-width-probe := Text { visible: false; text: root.alternate-text; font-size: root.text-size; }
}
export struct GalleryTile {
id: string,
name: string,
host-name: string,
group-names: string,
status: string,
connected: bool,
online: bool,
presence-known: bool,
stale: bool,
selected: bool,
has-frame: bool,
frame: image,
}
export struct GalleryRecentDevice {
id: string,
name: string,
selected: bool,
online: bool,
}
export struct GalleryActionDevice {
id: string,
name: string,
connected: bool,
}
export component GalleryView inherits Rectangle {
in property <bool> controlling: false;
in property <bool> input-suspended: false;
in property <bool> simplified: false;
in property <bool> fullscreen: false;
callback toggle-simplified;
callback toggle-fullscreen;
// Keep long localized actions and the selection summary visible at 640px.
private property <length> toolbar-button-padding: root.width < 720px ? 8px : 12px;
private property <length> view-controls-width: compact-toggle.width + 30px;
private property <length> view-controls-space: root.view-controls-width + 6px;
in property <bool> window-active: true;
in-out property <float> tile-width: 216;
out property <bool> resizing: false;
private property <float> preview-aspect-ratio: 16 / 9;
private property <length> card-inset: 6px;
private property <length> card-chrome-height: root.simplified ? root.card-inset * 2 : 36px;
// Keep the complete cell inside the current grid in both dimensions.
private property <length> max-card-width: max(184px, min(grid.width,
(grid.height - root.card-chrome-height) * root.preview-aspect-ratio + root.card-inset * 2));
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-height: root.card-preview-height + root.card-chrome-height;
private property <length> rail-preview-width: 126px;
private property <length> rail-preview-height: root.rail-preview-width / root.preview-aspect-ratio;
private property <length> rail-card-height: root.rail-preview-height + (root.simplified ? 10px : 24px);
private property <string> focused-id: root.focused.id;
private property <string> device-tooltip-id;
private property <string> device-tooltip-text;
out property <length> device-tooltip-x;
out property <length> device-tooltip-y;
private property <GalleryTile> device-tooltip-tile;
// MainWindow renders the same tooltip panel for the wall and recent list.
out property <GalleryTile> hovered-device: root.device-tooltip-id == root.device-tooltip-tile.id
&& !root.adding && !root.importing && !root.confirming ? root.device-tooltip-tile : {};
private property <bool> device-tooltip-visible: false;
private property <string> drag-source;
private property <GalleryTile> dragged-tile;
private property <bool> dragging: false;
private property <length> drag-start-x;
private property <length> drag-start-y;
private property <length> drag-x;
private property <length> drag-y;
private property <int> drag-target: root.drop-index(root.drag-x, root.drag-y);
private property <int> drag-page: -1;
private property <length> drag-edge-width: 32px;
private property <int> drag-page-direction: root.page-direction(root.drag-x, root.drag-y);
private property <bool> swap-save-failed: false;
changed focused-id => { root.device-tooltip-id = ""; root.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 width => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); }
changed height => { root.device-tooltip-id = ""; root.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 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 card-width => { root.cancel-device-drag(); }
changed card-height => { root.cancel-device-drag(); }
changed device-tooltip-id => {
root.device-tooltip-visible = false;
device-tooltip-timer.running = root.device-tooltip-id == "resize-grid";
if root.device-tooltip-id == "resize-grid" { device-tooltip-timer.restart(); }
}
device-tooltip-timer := Timer {
interval: 450ms; running: false;
triggered => {
self.running = false;
root.device-tooltip-visible = root.device-tooltip-id != "";
}
}
// Pointer mapping must use the exact image viewport, including its inset.
out property <float> video-width: video.width / 1px;
out property <float> video-height: video.height / 1px;
// Non-focused previews use the grid cell or the focused view's rail.
out property <float> preview-width: (root.focused.id == ""
? root.card-width - root.card-inset * 2 : root.rail-preview-width) / 1px;
out property <float> preview-height: (root.focused.id == ""
? root.card-preview-height : root.rail-preview-height) / 1px;
callback pointer-input(PointerEventButton, PointerEventKind, length, length);
callback scroll-input(length, length, length, length);
callback key-input(string, bool, bool, bool, bool, bool);
in property <[GalleryTile]> tiles;
in property <[string]> groups;
in-out property <int> group-index: 0;
// Pagination and rendering share the same fixed-aspect cell geometry.
out property <int> columns: max(1, floor(grid.width / root.card-width));
out property <int> rows: max(1, floor(grid.height / root.card-height));
in property <int> page: 0;
in property <int> pages: 1;
in property <int> count: 0;
in property <int> connected-count: 0;
private property <string> connected-summary: root.connected-count + " / " + root.count
+ GalleryStrings.tr(" 台已连接", " connected", " подключено");
callback network-open;
in property <int> selected-count: 0;
in property <bool> selected-audio-available: false;
in property <bool> selected-audio-muted: false;
in property <int> hidden-selected: 0;
in property <string> notice;
in property <GalleryTile> focused;
in-out property <bool> batch: false;
in-out property <bool> adding: false;
in-out property <bool> importing: false;
in property <[GalleryRecentDevice]> recent-devices;
in property <int> recent-selected: 0;
in property <string> import-group;
private property <bool> confirming: false;
private property <string> action-name;
private property <string> action-code;
in property <[GalleryActionDevice]> selected-devices;
callback action(string, string);
callback swap-devices(string, string) -> bool;
callback add-device(string, string, string, bool) -> bool;
callback import-recent(string) -> bool;
callback edit-alias(string, string);
callback edit-groups(string, string);
callback remove-device(string);
background: white;
changed adding => { if root.adding { root.action("release", ""); } }
changed importing => { root.action(root.importing ? "release" : "recent-close", ""); }
function show-device-details(tile: GalleryTile, x: length, y: length) {
root.device-tooltip-tile = tile;
root.device-tooltip-x = x;
root.device-tooltip-y = y;
root.device-tooltip-id = tile.id;
}
public function reset-transient() {
root.resizing = false;
root.cancel-device-drag();
root.batch = false; root.adding = false; root.importing = false; root.confirming = false;
root.device-tooltip-id = ""; add-menu.close-menu(); batch-menu.close-menu();
}
changed batch => { root.device-tooltip-id = ""; root.cancel-device-drag(); add-menu.close-menu(); batch-menu.close-menu(); }
function cancel-device-drag() {
root.dragging = false; root.drag-source = ""; root.drag-page = -1;
}
function cell-index(x: length, y: length) -> int {
if x < 0px || y < 0px || x >= grid.table-width || y >= grid.table-height { return -1; }
let column = floor(x / root.card-width);
let row = floor(y / root.card-height);
let index = row * root.columns + column;
if index >= root.tiles.length { return -1; }
// Separators remain resize targets; drop on the device inside a cell.
let cell-x = x - column * root.card-width;
let cell-y = y - row * root.card-height;
if cell-x < 6px || cell-x >= root.card-width - 6px || cell-y < 6px || cell-y >= root.card-height - 6px { return -1; }
return index;
}
function drop-index(x: length, y: length) -> int {
let index = root.cell-index(x, y);
return !root.dragging || root.drag-page-direction != 0 || index < 0 || root.tiles[index].id == root.drag-source ? -1 : index;
}
function page-direction(x: length, y: length) -> int {
if !root.dragging { return 0; }
let view-x = grid.x + x;
let view-y = grid.y + y;
if view-x < 0px || view-x >= root.width || view-y < 0px || view-y >= root.height { return 0; }
if root.page > 0 && view-x < root.drag-edge-width { return -1; }
if root.page + 1 < root.pages && view-x >= root.width - root.drag-edge-width { return 1; }
// Keep the pager buttons as additional hover targets.
let pager-x = view-x - footer.x - pager.x;
let pager-y = view-y - footer.y - pager.y;
if pager-y < 0px || pager-y >= pager.height { return 0; }
if root.page > 0 && pager-x >= previous-page.x && pager-x < previous-page.x + previous-page.width { return -1; }
if root.page + 1 < root.pages && pager-x >= next-page.x && pager-x < next-page.x + next-page.width { return 1; }
return 0;
}
changed drag-page-direction => {
if root.drag-page-direction != 0 { drag-page-timer.restart(); }
}
drag-page-timer := Timer {
interval: 700ms;
running: root.dragging && root.drag-page-direction != 0 && root.page == root.drag-page;
triggered => {
if !root.dragging || root.drag-page-direction == 0 || root.page != root.drag-page { return; }
root.drag-page = root.page + root.drag-page-direction;
root.action("page", root.drag-page-direction > 0 ? "next" : "previous");
}
}
function move-device-drag(x: length, y: length) {
if root.drag-source == "" { return; }
root.drag-x = x; root.drag-y = y;
if abs(x - root.drag-start-x) >= 6px || abs(y - root.drag-start-y) >= 6px {
root.dragging = true; root.device-tooltip-id = "";
}
}
function finish-device-drag() {
let source = root.drag-source;
let target = root.drag-target >= 0 ? root.tiles[root.drag-target].id : "";
// Clear transient state before the callback can replace the page model.
root.cancel-device-drag();
if source != "" && target != "" {
root.swap-save-failed = !root.swap-devices(source, target);
if root.swap-save-failed { swap-error-timer.restart(); }
}
}
drag-focus := FocusScope {
x: 0px; y: 0px; width: 0px; height: 0px;
key-pressed(event) => {
if event.text == Key.Escape && root.drag-source != "" {
root.cancel-device-drag(); return accept;
}
reject
}
focus-lost => { root.cancel-device-drag(); }
}
swap-error-timer := Timer {
interval: 4s; running: false;
triggered => { root.swap-save-failed = false; self.running = false; }
}
function confirm-action(code: string, name: string) {
root.action-code = code; root.action-name = name; root.confirming = true;
}
// Swap toolbar contents in place so selection never moves the grid or pager.
toolbar := Rectangle {
x: 29px; y: 12px; width: parent.width - 58px - root.view-controls-space; height: 24px;
visible: root.focused.id == "" && !root.simplified;
HorizontalLayout {
x: 0px; y: 0px; width: parent.width - selection-toggle.width - 6px; height: parent.height;
visible: !root.batch; spacing: 6px;
GroupFilter { width: 155px; height: 24px; system-count: 1; model: root.groups; current-index <=> root.group-index;
selected(index) => { root.action("group", index); }
}
Text {
text: root.count + GalleryStrings.tr(" 台设备", " devices", " устройств");
min-width: 0px; overflow: elide; font-size: 11px; color: #858c96; vertical-alignment: center;
}
Rectangle { horizontal-stretch: 1; min-width: 0px; }
add-menu := GalleryMenuButton {
horizontal-padding: root.toolbar-button-padding;
width: self.preferred-button-width;
text: GalleryStrings.tr("添加", "Add", "Добавить"); menu-width: 232px;
items: [
{label: GalleryStrings.tr("通过设备 ID 添加", "Add by device ID", "Добавить по ID устройства"), enabled: true},
{label: GalleryStrings.tr("从近期连接添加", "Add from recent", "Добавить из недавних"), enabled: true},
];
opened => { root.device-tooltip-id = ""; root.action("release", ""); }
selected(index) => {
if index == 0 { root.action("add-open", ""); root.adding = true; }
else { root.action("recent-open", ""); root.importing = true; }
}
}
}
selection-actions := HorizontalLayout {
x: 0px; y: 0px; height: parent.height;
visible: root.batch; width: parent.width - selection-toggle.width - 6px; spacing: 6px;
Rectangle {
horizontal-stretch: 1; min-width: 0px;
Text {
x: 0px; y: 0px; width: parent.width; height: root.hidden-selected > 0 ? 13px : parent.height;
text: GalleryStrings.tr("已选 ", "Selected ", "Выбрано ") + root.selected-count + GalleryStrings.tr(" 台", "", "");
color: #62666d; vertical-alignment: center; font-size: 11px; overflow: elide;
}
if root.hidden-selected > 0: Text {
y: 14px; width: parent.width; height: 10px;
text: GalleryStrings.tr("跨页 ", "Other pages ", "Вне страницы ") + root.hidden-selected;
color: #80858d; font-size: 10px; overflow: elide;
}
}
GalleryToolbarButton {
horizontal-padding: root.toolbar-button-padding;
enabled: root.count > 0;
text: root.count > 0 && root.selected-count == root.count
? GalleryStrings.tr("取消全选", "Deselect all", "Снять выбор")
: GalleryStrings.tr("全选", "Select all", "Выбрать все");
alternate-text: root.count > 0 && root.selected-count == root.count
? GalleryStrings.tr("全选", "Select all", "Выбрать все")
: GalleryStrings.tr("取消全选", "Deselect all", "Снять выбор");
clicked => { root.action("all-select", ""); }
}
GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; primary: true; enabled: root.selected-count > 0; text: GalleryStrings.tr("连接", "Connect", "Связь"); clicked => { root.action("connect", ""); } }
GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; enabled: root.selected-count > 0; text: GalleryStrings.tr("断开", "Close", "Отключить"); clicked => { root.confirm-action("disconnect", self.text); } }
batch-menu := GalleryMenuButton {
horizontal-padding: root.toolbar-button-padding;
width: self.preferred-button-width; enabled: root.selected-count > 0;
text: GalleryStrings.tr("更多", "More", "Ещё");
items: [
{label: root.selected-audio-muted ? GalleryStrings.tr("取消静音", "Unmute", "Со звуком") : GalleryStrings.tr("静音", "Mute", "Без звука"), enabled: root.selected-audio-available},
{label: GalleryStrings.tr("分发文件", "Send file", "Отправить файл"), enabled: root.selected-count > 0},
];
opened => { root.device-tooltip-id = ""; root.action("release", ""); }
selected(index) => {
if index == 0 { root.action(root.selected-audio-muted ? "unmute" : "mute", ""); }
else { root.confirm-action("file", GalleryStrings.tr("分发文件", "Send file", "Отправить файл")); }
}
}
}
selection-toggle := GalleryToolbarButton {
x: parent.width - self.width; y: 0px;
horizontal-padding: root.toolbar-button-padding;
primary: root.batch;
alternate-text: GalleryStrings.tr("选择", "Select", "Выбрать");
text: root.batch ? GalleryStrings.tr("完成", "Done", "Готово") : GalleryStrings.tr("选择", "Select", "Выбрать");
clicked => { root.batch = !root.batch; root.action("selection", ""); }
}
}
grid := TouchArea {
x: root.simplified ? 12px : toolbar.x;
y: toolbar.y + toolbar.height + 12px;
width: parent.width - self.x * 2;
height: footer.y - 12px - self.y;
visible: root.focused.id == "";
private property <length> table-width: root.columns * root.card-width;
private property <length> table-height: root.rows * root.card-height;
Rectangle {
visible: root.count > 0;
x: 0px; y: 0px;
width: grid.table-width; height: grid.table-height;
background: white;
}
// One stable pointer owner survives page model replacements and cell resizing.
private property <int> column-line: clamp(round(self.mouse-x / root.card-width), 0, root.columns);
private property <int> row-line: clamp(round(self.mouse-y / root.card-height), 0, root.rows);
private property <bool> over-table: root.count > 0
&& self.mouse-x >= 0px && self.mouse-x <= grid.table-width
&& self.mouse-y >= 0px && self.mouse-y <= grid.table-height;
// Both sides of a shared separator must resize in the same direction.
private property <int> edge-x: !self.over-table || abs(self.mouse-x - self.column-line * root.card-width) > 6px
? 0 : self.column-line == 0 ? -1 : 1;
private property <int> edge-y: !self.over-table || abs(self.mouse-y - self.row-line * root.card-height) > 6px
? 0 : self.row-line == 0 ? -1 : 1;
private property <int> drag-x;
private property <int> drag-y;
private property <int> drag-columns;
private property <int> drag-rows;
private property <length> start-x;
private property <length> start-y;
private property <length> start-width;
private property <int> cursor-x: root.resizing ? self.drag-x : self.edge-x;
private property <int> cursor-y: root.resizing ? self.drag-y : self.edge-y;
private property <int> hover-index: root.cell-index(self.mouse-x, self.mouse-y);
mouse-cursor: root.dragging ? (root.drag-target >= 0 || root.drag-page-direction != 0 ? MouseCursor.grabbing : MouseCursor.not-allowed)
: self.cursor-x != 0 && self.cursor-y != 0
? (self.cursor-x == self.cursor-y ? MouseCursor.nwse-resize : MouseCursor.nesw-resize)
: self.cursor-x != 0 ? MouseCursor.ew-resize : self.cursor-y != 0 ? MouseCursor.ns-resize
: self.hover-index >= 0 ? MouseCursor.grab : MouseCursor.default;
function show-hint() {
if self.has-hover && !self.pressed && (self.edge-x != 0 || self.edge-y != 0) {
root.device-tooltip-id = "resize-grid";
root.device-tooltip-text = GalleryStrings.tr("拖动表格分隔线,同步调整所有设备大小", "Drag a table divider to resize all devices", "Потяните за разделитель таблицы, чтобы изменить размер всех устройств");
root.device-tooltip-x = grid.x + self.mouse-x;
root.device-tooltip-y = grid.y + self.mouse-y + 16px;
} else if root.device-tooltip-id == "resize-grid" { root.device-tooltip-id = ""; }
}
function resize-cells() {
if !root.resizing { return; }
// Distribute the movement across preceding cells so the chosen
// divider follows the pointer while every cell stays equal.
let dx = (self.mouse-x - self.start-x) * self.drag-x / self.drag-columns;
let dy = (self.mouse-y - self.start-y) * self.drag-y / self.drag-rows;
let delta = self.drag-x == 0 ? dy * root.preview-aspect-ratio
: self.drag-y == 0 ? dx
: (dx + dy / root.preview-aspect-ratio) / (1 + 1 / (root.preview-aspect-ratio * root.preview-aspect-ratio));
root.tile-width = clamp(self.start-width + delta, 184px, root.max-card-width) / 1px;
}
changed has-hover => { self.show-hint(); }
changed mouse-x => { root.device-tooltip-x = grid.x + self.mouse-x; self.show-hint(); }
changed mouse-y => { root.device-tooltip-y = grid.y + self.mouse-y; self.show-hint(); }
pointer-event(event) => {
if event.button == PointerEventButton.left && event.kind == PointerEventKind.down {
root.cancel-device-drag();
if self.edge-x != 0 || self.edge-y != 0 {
self.drag-x = self.edge-x; self.drag-y = self.edge-y;
self.drag-columns = max(1, self.column-line);
self.drag-rows = max(1, self.row-line);
self.start-x = self.mouse-x; self.start-y = self.mouse-y;
self.start-width = root.card-width;
root.resizing = true; root.device-tooltip-id = "";
} else if self.hover-index >= 0 {
drag-focus.focus();
root.dragged-tile = root.tiles[self.hover-index];
root.drag-source = root.dragged-tile.id; root.drag-page = root.page;
root.drag-start-x = self.mouse-x; root.drag-start-y = self.mouse-y;
root.drag-x = self.mouse-x; root.drag-y = self.mouse-y;
root.swap-save-failed = false; root.device-tooltip-id = "";
}
} else if event.kind == PointerEventKind.cancel {
root.resizing = false; root.cancel-device-drag();
} else if event.button == PointerEventButton.left && event.kind == PointerEventKind.up {
if root.resizing { self.resize-cells(); root.resizing = false; }
else if root.drag-source != "" {
root.move-device-drag(self.mouse-x, self.mouse-y);
if !root.dragging && self.hover-index >= 0 && root.tiles[self.hover-index].id == root.drag-source {
let id = root.drag-source;
root.cancel-device-drag();
root.action(root.batch && !root.simplified ? "select" : "focus", id);
} else { root.finish-device-drag(); }
}
}
}
moved => {
if root.resizing { self.resize-cells(); }
else if root.drag-source != "" { root.move-device-drag(self.mouse-x, self.mouse-y); }
}
for tile[index] in root.tiles: tile-card := Rectangle {
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
: 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;
accessible-role: button; accessible-label: self.details;
accessible-action-default => { root.action(root.batch && !root.simplified ? "select" : "focus", tile.id); }
changed details => { if tile-touch.has-hover { tile-touch.show-details(); } }
visible: index < root.columns * root.rows;
x: mod(index, root.columns) * root.card-width;
y: floor(index / root.columns) * root.card-height;
width: root.card-width;
height: root.card-height;
opacity: root.dragging && root.drag-source == tile.id ? 0.45 : 1;
background: !tile.online ? DeviceCardStyle.offline-background
: tile.selected ? #eaf3ff : root.batch && tile-touch.has-hover ? #f3f8fe : white;
Rectangle {
x: root.card-inset; y: self.x;
width: parent.width - self.x * 2;
height: root.card-preview-height;
background: tile.online ? #24272d : DeviceCardStyle.offline-preview;
if tile.has-frame && tile.online: Image { x: 0px; y: 0px; width: parent.width; height: parent.height; source: tile.frame; image-fit: contain; opacity: tile.stale ? 0.45 : 1; }
if !root.simplified && (!tile.has-frame || !tile.connected || tile.stale): Text {
text: tile.status; color: tile.online ? #ffffff : DeviceCardStyle.offline-placeholder; font-size: 12px;
horizontal-alignment: center; vertical-alignment: center;
wrap: word-wrap;
}
}
Text {
visible: !root.simplified; x: 9px; y: parent.height - 30px;
width: parent.width - self.x - (tile-touch.has-hover && !root.batch ? DeviceCardStyle.toolbar-width + 12px : root.batch ? 35px : 9px);
height: 24px; text: tile.name; color: tile.online ? #24272d : DeviceCardStyle.offline-text; font-size: 12px; overflow: elide; vertical-alignment: center;
}
if root.batch && !root.simplified: Rectangle {
x: parent.width - 27px; y: parent.height - 27px;
width: 18px; height: 18px; border-radius: 5px; border-width: 1px;
background: tile.selected ? (tile-touch.pressed ? #5c9fdf : tile-touch.has-hover ? #659fe0 : #6dabea) : tile-touch.has-hover ? #f3f8fe : white;
border-color: tile.selected ? #6dabea : tile-touch.has-hover ? #76aeea : #b7bec8;
animate background, border-color { duration: 120ms; }
if tile.selected: Text {
text: FontAwesomeIcons.check; color: white;
font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 11px;
horizontal-alignment: center; vertical-alignment: center;
}
}
tile-touch := Rectangle {
x: 6px; y: 6px; width: parent.width - 12px; height: parent.height - 12px;
private property <bool> has-hover: grid.has-hover && grid.hover-index == index;
private property <bool> pressed: grid.pressed && root.drag-source == tile.id;
function show-details() {
// Avoid covering the per-device action buttons with a tooltip.
if root.resizing || root.drag-source != "" || (grid.mouse-y - tile-card.y >= tile-card.height - 30px && !root.simplified) { root.device-tooltip-id = ""; return; }
root.show-device-details(tile, grid.x + grid.mouse-x, grid.y + grid.mouse-y);
}
changed has-hover => {
if self.has-hover { self.show-details(); }
else if root.device-tooltip-id == tile.id { root.device-tooltip-id = ""; }
}
private property <bool> over-actions: grid.mouse-y - tile-card.y >= tile-card.height - 30px;
changed over-actions => { if self.has-hover { self.show-details(); } }
if tile-touch.has-hover && !root.dragging && !root.resizing && !root.batch && !root.simplified: DeviceCardActions {
x: parent.width - self.width; y: parent.height - 23px;
remove-label: GalleryStrings.tr("从桌面墙移除", "Remove from DeskHub", "Убрать из DeskHub");
connected: tile.connected;
edit-alias => { root.action("release", ""); root.edit-alias(tile.id, tile.name); }
edit-groups => { root.edit-groups(tile.id, tile.name); }
remove-device => { root.action("release", ""); root.remove-device(tile.id); }
connect-device => { root.action("connect", tile.id); }
disconnect-device => { root.action("disconnect", tile.id); }
}
}
}
// Draw each table line once, continuously across occupied and empty cells.
for column in root.columns + 1: Rectangle {
visible: root.count > 0;
x: min(column * root.card-width, grid.table-width - 1px);
y: 0px;
width: 1px; height: grid.table-height;
background: root.resizing || (grid.has-hover && grid.edge-x != 0 && grid.column-line == column)
? #76aeea : #c9cdd2;
}
for row in root.rows + 1: Rectangle {
visible: root.count > 0;
x: 0px;
y: min(row * root.card-height, grid.table-height - 1px);
width: grid.table-width; height: 1px;
background: root.resizing || (grid.has-hover && grid.edge-y != 0 && grid.row-line == row)
? #76aeea : #c9cdd2;
}
if root.drag-target >= 0: Rectangle {
x: mod(root.drag-target, root.columns) * root.card-width + 1px;
y: floor(root.drag-target / root.columns) * root.card-height + 1px;
width: root.card-width - 2px; height: root.card-height - 2px;
background: #76aeea26; border-width: 2px; border-color: #4a94de;
}
if root.count == 0: Text {
text: root.simplified
? GalleryStrings.tr("暂无设备,退出精简模式后可添加", "No devices. Exit compact mode to add devices", "Нет устройств. Выйдите из компактного режима, чтобы добавить их")
: GalleryStrings.tr("添加设备,建立你的桌面墙", "Add devices to build your DeskHub", "Добавьте устройства в DeskHub");
color: #808080; horizontal-alignment: center; vertical-alignment: center;
}
}
focus-view := Rectangle {
visible: root.focused.id != "";
x: 29px; y: toolbar.y; width: parent.width - 58px;
height: footer.y - 12px - self.y;
HorizontalLayout {
x: 0px; y: 0px; width: parent.width - root.view-controls-space; height: 24px; spacing: 8px;
CompactButton { width: 52px; text: GalleryStrings.tr("返回", "Back", "Назад"); clicked => { root.action("focus", ""); } }
Rectangle {
horizontal-stretch: 1; min-width: 0px;
Text { x: 0px; width: parent.width; height: parent.height; text: root.focused.name; color: root.focused.online ? #303943 : DeviceCardStyle.offline-text; font-size: 12px; font-weight: 600; overflow: elide; horizontal-alignment: left; vertical-alignment: center; }
}
Rectangle {
width: min(focus-state.preferred-width + 16px, 110px); height: 22px;
border-radius: 5px;
background: root.controlling ? #e7f1fd : #f0f2f5;
focus-state := Text {
x: 8px; width: parent.width - 16px; height: parent.height;
text: root.controlling ? GalleryStrings.tr("正在操控", "Controlling", "Управление")
: root.focused.connected && root.focused.has-frame && !root.focused.stale
? GalleryStrings.tr("只读预览", "View only", "Просмотр") : root.focused.status;
font-size: 10px; color: root.controlling ? #3e77b3 : #6b7480; vertical-alignment: center; overflow: elide;
}
}
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 {
x: 0px; y: rail.visible ? rail.y + rail.height + 10px : 36px;
width: parent.width; height: parent.height - self.y;
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;
video := Rectangle {
x: 4px; y: 4px; width: parent.width - 8px; height: parent.height - 8px;
background: root.focused.online ? #24272d : DeviceCardStyle.offline-preview;
if root.focused.has-frame && root.focused.online: Image {
width: parent.width; height: parent.height; source: root.focused.frame; image-fit: contain;
opacity: root.focused.stale || !root.focused.connected ? 0.4 : 1;
}
video-focus := FocusScope {
property <bool> active-control: root.controlling;
changed active-control => { if self.active-control { self.focus(); } }
width: parent.width; height: parent.height;
focus-on-click: true;
focus-lost => { if root.controlling { root.action("release", ""); } }
key-pressed(event) => {
if root.controlling && !root.input-suspended {
if event.text == Key.Escape && event.modifiers.shift && (event.modifiers.control || event.modifiers.meta) {
root.action("release", "");
} else { root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); }
}
accept
}
key-released(event) => {
if root.controlling && !root.input-suspended { root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); }
accept
}
TouchArea {
pointer-event(event) => {
if !root.input-suspended {
if event.kind == PointerEventKind.down { video-focus.focus(); }
if root.controlling { root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y); }
}
}
scroll-event(event) => {
if root.controlling && !root.input-suspended { root.scroll-input(event.delta-x, event.delta-y, self.mouse-x, self.mouse-y); }
accept
}
}
}
if !root.focused.has-frame || root.focused.stale || !root.focused.connected: Text { text: root.focused.status; color: root.focused.online ? white : DeviceCardStyle.offline-placeholder; horizontal-alignment: center; vertical-alignment: center; }
}
}
rail := ScrollView {
visible: root.tiles.length > 1;
x: 0px; y: 36px; width: parent.width; height: root.rail-card-height + 14px;
// Reserve 14px below the cards for the horizontal scrollbar.
viewport-width: max(self.width, max(0, root.tiles.length - 1) * 144px);
viewport-height: self.height;
horizontal-scrollbar-policy: as-needed;
vertical-scrollbar-policy: always-off;
changed width => {
root.device-tooltip-id = "";
self.viewport-x = clamp(self.viewport-x, min(0px, self.width - self.viewport-width), 0px);
}
changed viewport-width => {
root.device-tooltip-id = "";
self.viewport-x = clamp(self.viewport-x, min(0px, self.width - self.viewport-width), 0px);
}
changed viewport-x => { root.device-tooltip-id = ""; }
HorizontalLayout {
x: 0px; y: 0px; height: root.rail-card-height; spacing: 0px; padding: 0px;
for tile in root.tiles: rail-slot := Rectangle {
// Collapse the focused tile and its gap without changing the page model.
width: tile.id == root.focused.id ? 0px : 144px;
height: root.rail-card-height; visible: tile.id != root.focused.id;
rail-card := Rectangle {
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
: 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;
width: 136px; 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;
accessible-role: button;
accessible-label: self.details;
accessible-action-default => { root.action("focus", tile.id); }
changed details => { if rail-touch.has-hover { rail-touch.show-details(); } }
Rectangle {
x: 5px; y: 5px; width: root.rail-preview-width; height: root.rail-preview-height; background: tile.online ? #24272d : DeviceCardStyle.offline-preview;
if tile.has-frame && tile.online: Image { width: parent.width; height: parent.height; source: tile.frame; image-fit: contain; opacity: tile.stale || !tile.connected ? 0.4 : 1; }
if !root.simplified && (!tile.has-frame || tile.stale || !tile.connected): Text {
text: tile.status; color: tile.online ? #e2e6ed : DeviceCardStyle.offline-placeholder; font-size: 10px; overflow: elide;
horizontal-alignment: center; vertical-alignment: center;
}
}
Text {
visible: !root.simplified;
x: 7px; y: root.rail-preview-height + 6px; width: 122px; height: 16px;
text: tile.name; color: tile.online ? #303943 : DeviceCardStyle.offline-text; font-size: 11px;
overflow: elide; vertical-alignment: center;
}
rail-touch := TouchArea {
function show-details() {
root.show-device-details(tile,
focus-view.x + rail.x + rail.viewport-x + rail-slot.x + rail-card.x + self.mouse-x,
focus-view.y + rail.y + rail.viewport-y + rail-slot.y + rail-card.y + self.mouse-y);
}
changed has-hover => {
if self.has-hover { self.show-details(); }
else if root.device-tooltip-id == tile.id { root.device-tooltip-id = ""; }
}
moved => { if self.has-hover { self.show-details(); } }
clicked => { root.device-tooltip-id = ""; root.action("focus", tile.id); }
}
}
}
}
}
}
Rectangle {
y: parent.height - 44px; height: 44px; background: #fafbfc;
Rectangle { y: 0px; width: parent.width; height: 1px; background: #e1e4e8; }
}
// Connection status and pagination remain available in every selection state.
footer := Rectangle {
x: root.simplified ? 12px : toolbar.x; y: parent.height - 34px;
width: parent.width - self.x * 2; height: 24px;
status-area := Rectangle {
x: 0px; y: 0px; width: parent.width - 124px; height: parent.height;
connection-count := Text {
x: 0px; width: min(self.preferred-width, parent.width - network-button.width - 12px); height: parent.height;
text: root.connected-summary;
color: #62666d; vertical-alignment: center; font-size: 11px; overflow: elide;
}
network-button := SecondaryButton {
x: connection-count.x + connection-count.width + 12px;
min-width: 64px; width: 64px; height: parent.height;
border-color: #c8cbd0; border-radius: 3px;
text: GalleryStrings.tr("网络状态", "Network", "Сеть");
accessible-role: button;
accessible-label: self.text;
accessible-action-default => { self.clicked(); }
clicked => { root.device-tooltip-id = ""; root.network-open(); }
}
}
pager := HorizontalLayout {
x: parent.width - self.width; y: 0px; width: 112px; height: 24px; spacing: 6px;
previous-page := CompactButton { primary: root.drag-page-direction < 0; width: 24px; text: "‹"; text-size: 16px; enabled: root.page > 0; clicked => { root.action("page", "previous"); } }
Text { text: (root.page + 1) + " / " + root.pages; min-width: 0px; overflow: elide; horizontal-alignment: center; vertical-alignment: center; }
next-page := CompactButton { primary: root.drag-page-direction > 0; width: 24px; text: "›"; text-size: 16px; enabled: root.page + 1 < root.pages; clicked => { root.action("page", "next"); } }
}
}
// Paint the fullscreen tooltip above the grid/rail, below modal overlays.
view-controls := HorizontalLayout {
x: parent.width - (root.simplified ? 12px : 29px) - self.width;
y: toolbar.y; width: root.view-controls-width; height: toolbar.height; spacing: 6px;
compact-toggle := GalleryToolbarButton {
horizontal-padding: root.toolbar-button-padding;
primary: root.simplified;
alternate-text: GalleryStrings.tr("退出精简", "Exit compact", "Обычный вид");
text: root.simplified ? GalleryStrings.tr("退出精简", "Exit compact", "Обычный вид")
: GalleryStrings.tr("精简模式", "Compact", "Компактно");
clicked => { root.toggle-simplified(); }
}
IconButton {
width: 24px; height: 24px;
icon: root.fullscreen ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.fullscreen ? GalleryStrings.tr("退出全屏", "Exit fullscreen", "Выйти из полноэкранного режима") : GalleryStrings.tr("全屏", "Fullscreen", "На весь экран");
clicked => { root.toggle-fullscreen(); }
}
}
for edge in 2: Rectangle {
visible: root.dragging && (edge == 0 ? root.page > 0 : root.page + 1 < root.pages);
private property <bool> active: root.drag-page-direction == (edge == 0 ? -1 : 1);
x: edge == 0 ? 0px : parent.width - self.width; y: 0px;
width: root.drag-edge-width; height: parent.height; z: 8;
background: self.active ? #76aeea38 : #76aeea14;
Text {
text: edge == 0 ? "‹" : "›"; font-size: 24px;
color: parent.active ? #357bc0 : #7ba2c9;
horizontal-alignment: center; vertical-alignment: center;
}
}
if root.dragging: Rectangle {
x: clamp(grid.x + root.drag-x + 14px, 8px, parent.width - self.width - 8px);
y: clamp(grid.y + root.drag-y + 14px, 8px, footer.y - self.height - 12px);
width: 224px; height: 54px; z: 10;
background: #fffffff2; border-width: 1px; border-color: #76aeea; border-radius: 4px;
Rectangle {
x: 8px; y: 9px; width: 64px; height: 36px;
background: root.dragged-tile.online ? #24272d : DeviceCardStyle.offline-preview;
if root.dragged-tile.has-frame && root.dragged-tile.online: Image {
x: 0px; y: 0px; width: parent.width; height: parent.height;
source: root.dragged-tile.frame; image-fit: contain;
}
}
Text { x: 80px; y: 8px; width: parent.width - 88px; height: 18px; text: root.dragged-tile.name; font-size: 12px; color: #303943; overflow: elide; }
Text {
x: 80px; y: 29px; width: parent.width - 88px; height: 16px;
text: root.drag-page-direction != 0 ? GalleryStrings.tr("停留翻页,松开取消", "Hold to turn page", "Удерживайте для перелистывания")
: root.drag-target >= 0 ? GalleryStrings.tr("松开鼠标交换位置", "Release to swap", "Отпустите для обмена")
: GalleryStrings.tr("拖到另一台设备上", "Drag onto another device", "На другое устройство");
font-size: 10px; color: #527ca8; overflow: elide;
}
}
if root.swap-save-failed: Rectangle {
x: (parent.width - self.width) / 2; y: footer.y - self.height - 14px;
width: min(420px, parent.width - 32px); height: 36px; z: 10;
background: #fff2f0; border-width: 1px; border-color: #e9b7b0; border-radius: 4px;
Text { x: 12px; y: 0px; width: parent.width - 24px; height: parent.height; font-size: 12px; color: #9d4339; vertical-alignment: center;
text: GalleryStrings.tr("换位未保存,请重试", "Couldn't save positions. Please try again.", "Не удалось сохранить порядок. Повторите попытку."); overflow: elide;
}
}
// Device details are rendered by MainWindow; resize hints stay local.
if root.device-tooltip-visible && root.device-tooltip-id == "resize-grid": Rectangle {
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);
width: min(320px, parent.width - 16px); height: device-tooltip-label.preferred-height + 16px;
background: #22252bf2; border-width: 1px; border-color: #ffffff38; border-radius: 5px;
device-tooltip-label := Text {
x: 8px; y: 8px; width: parent.width - 16px;
text: root.device-tooltip-text; color: white; font-size: 11px; wrap: word-wrap;
}
}
if root.adding: Rectangle {
background: #00000042;
TouchArea {}
add-dialog := Rectangle {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 400px; height: 306px;
background: white; border-radius: 8px; border-width: 1px; border-color: #b2b2b2;
function submit() {
if root.add-device(device-id.text, password.text, group.text, remember.checked) {
password.text = "";
root.adding = false;
}
}
Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.tr("添加设备", "Add device", "Добавить устройство"); font-size: 16px; }
Text { x: 18px; y: 56px; width: 72px; height: 30px; text: GalleryStrings.tr("设备 ID", "Device ID", "ID"); vertical-alignment: center; font-size: 12px; }
device-id := LineEdit {
x: 96px; y: 56px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("9 位设备 ID", "9-digit device ID", "ID из 9 цифр");
input-type: InputType.number;
accepted => { password.focus(); }
}
Text { x: 18px; y: 100px; width: 72px; height: 30px; text: GalleryStrings.tr("密码", "Password", "Пароль"); vertical-alignment: center; font-size: 12px; }
password := LineEdit {
x: 96px; y: 100px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("设备连接密码", "Device password", "Пароль устройства");
input-type: InputType.password;
accepted => { group.focus(); }
}
Text { x: 18px; y: 144px; width: 72px; height: 30px; text: GalleryStrings.tr("分组", "Group", "Группа"); vertical-alignment: center; font-size: 12px; }
group := LineEdit {
x: 96px; y: 144px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("分组名称(可选)", "Group name (optional)", "Группа (необязательно)");
accepted => { add-dialog.submit(); }
}
remember := ImGuiCheckBox { x: 96px; y: 188px; }
Text { x: 123px; y: 188px; height: 21px; text: GalleryStrings.tr("记住密码", "Remember password", "Запомнить пароль"); font-size: 12px; vertical-alignment: center;
TouchArea { clicked => { remember.checked = !remember.checked; } }
}
Text { x: 18px; y: 216px; width: 364px; height: 28px; text: root.notice; color: #8e4740; font-size: 11px; wrap: word-wrap; overflow: elide; }
CompactButton { x: 172px; y: 260px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { password.text = ""; root.adding = false; } }
CompactButton {
x: 254px; y: 260px; width: 128px; height: 28px; primary: true;
text: GalleryStrings.tr("添加并连接", "Add and connect", "Подключить");
enabled: device-id.text != "" && password.text != "";
clicked => { add-dialog.submit(); }
}
}
}
if root.importing: Rectangle {
background: #00000042;
TouchArea {}
Rectangle {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 440px; height: 378px;
background: white; border-radius: 8px; border-width: 1px; border-color: #b2b2b2;
Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.tr("从最近连接添加", "Add from recent connections", "Добавить из недавних"); font-size: 16px; }
Text { x: 18px; y: 44px; width: 404px; height: 18px; text: GalleryStrings.tr("加入桌面墙后,可通过批量选择连接", "Add devices, then use Select to connect them.", "После добавления подключайтесь через выбор."); color: #62666d; font-size: 11px; }
ImGuiCheckBox { x: 18px; y: 74px; enabled: root.recent-devices.length > 0; checked: root.recent-devices.length > 0 && root.recent-selected == root.recent-devices.length; }
Text { x: 45px; y: 74px; height: 21px; text: GalleryStrings.tr("全选", "Select all", "Выбрать все"); font-size: 12px; vertical-alignment: center; }
TouchArea { x: 18px; y: 74px; width: 144px; height: 21px; enabled: root.recent-devices.length > 0; clicked => { root.action("recent-all", ""); } }
Text { x: 224px; y: 74px; width: 198px; height: 21px; text: GalleryStrings.tr("已选 ", "Selected ", "Выбрано ") + root.recent-selected + " / " + root.recent-devices.length; horizontal-alignment: right; vertical-alignment: center; font-size: 12px; color: #62666d; }
Rectangle {
x: 18px; y: 102px; width: 404px; height: 160px;
border-width: 1px; border-color: #d8dadd; border-radius: 4px; clip: true;
recent-list := ScrollView {
x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px;
viewport-width: self.width - 12px;
viewport-height: max(self.height, root.recent-devices.length * 44px);
for device[index] in root.recent-devices: Rectangle {
y: index * 44px; width: recent-list.viewport-width; height: 44px;
background: device.selected ? #edf5fe : row-touch.has-hover ? #f4f5f6 : white;
ImGuiCheckBox { x: 8px; y: 11px; checked: device.selected; }
Text { x: 36px; y: 3px; width: parent.width - 126px; height: 20px; text: device.name; font-size: 12px; overflow: elide; vertical-alignment: center; }
Text { x: 36px; y: 23px; width: parent.width - 126px; height: 16px; text: device.id; font-size: 11px; color: #808080; }
Rectangle { x: parent.width - 74px; y: 19px; width: 6px; height: 6px; border-radius: 3px; background: device.online ? #21b567 : #92969e; }
Text {
x: parent.width - 61px; y: 12px; width: 57px; height: 20px;
text: device.online ? GalleryStrings.tr("在线", "Online", "В сети") : GalleryStrings.tr("离线", "Offline", "Не в сети");
color: device.online ? #17854c : #808080; font-size: 11px; vertical-alignment: center;
}
row-touch := TouchArea { clicked => { root.action("recent-select", device.id); } }
}
}
if root.recent-devices.length == 0: Text { text: GalleryStrings.tr("暂无最近连接", "No recent connections", "Нет недавних подключений"); color: #808080; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
}
Text { x: 18px; y: 274px; width: 60px; height: 30px; text: GalleryStrings.tr("分组", "Group", "Группа"); font-size: 12px; vertical-alignment: center; }
import-group := LineEdit { x: 84px; y: 274px; width: 338px; height: 30px; text: root.import-group; placeholder-text: GalleryStrings.tr("分组名称(可选)", "Group name (optional)", "Группа (необязательно)"); }
Text { x: 18px; y: 309px; width: 404px; height: 20px; text: root.notice; font-size: 11px; color: #8e4740; overflow: elide; }
CompactButton { x: 192px; y: 336px; width: 72px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.importing = false; } }
CompactButton { x: 274px; y: 336px; width: 148px; primary: true; text: GalleryStrings.tr("加入桌面墙", "Add selected", "Добавить"); enabled: root.recent-selected > 0; clicked => { if root.import-recent(import-group.text) { root.importing = false; } } }
}
}
if root.confirming: Rectangle {
background: #00000042; TouchArea {}
Rectangle {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2;
width: 360px; height: confirmation-list.height + 118px;
background: white; border-radius: 8px; border-width: 1px; border-color: #b2b2b2;
Text {
x: 20px; y: 18px; width: parent.width - 132px; height: 24px;
text: root.action-code == "disconnect" ? GalleryStrings.tr("断开连接", "Disconnect devices", "Отключение") : root.action-name;
color: #24272d; font-size: 16px; font-weight: 600; vertical-alignment: center;
}
Rectangle {
x: parent.width - 108px; y: 20px; width: 88px; height: 22px;
background: #eff3f8; border-radius: 11px;
Text { text: root.selected-count + GalleryStrings.tr(" 台设备", " devices", " устр."); color: #626e7d; font-size: 11px; horizontal-alignment: center; vertical-alignment: center; }
}
confirmation-list := Rectangle {
x: 20px; y: 58px; width: parent.width - 40px;
height: clamp(root.selected-devices.length * 52px + 2px, 54px, 210px);
background: #fcfdfe; border-width: 1px; border-color: #e2e6eb; border-radius: 6px; clip: true;
confirmation-scroll := ScrollView {
x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px;
viewport-width: self.width - 12px;
viewport-height: max(self.height, root.selected-devices.length * 52px);
for device[index] in root.selected-devices: Rectangle {
y: index * 52px; width: confirmation-scroll.viewport-width; height: 52px;
Text { x: 12px; y: 8px; width: parent.width - 32px - connection-badge.width; height: 18px; text: device.name; color: #303740; font-size: 12px; font-weight: 600; overflow: elide; vertical-alignment: center; }
Text { x: 12px; y: 29px; width: parent.width - 32px - connection-badge.width; height: 16px; text: "ID " + device.id; color: #8a929e; font-size: 11px; vertical-alignment: center; }
connection-badge := Rectangle {
x: parent.width - self.width - 12px; y: 16px;
width: GalleryStrings.language == 0 ? 68px : 104px; height: 20px; border-radius: 10px;
background: device.connected ? #eaf6ee : #f0f2f5;
Rectangle { x: 9px; y: 7px; width: 6px; height: 6px; border-radius: 3px; background: device.connected ? #27a762 : #a0a6b0; }
Text { x: 21px; width: parent.width - 27px; height: parent.height; text: device.connected ? GalleryStrings.tr("已连接", "Connected", "Подключено") : GalleryStrings.tr("未连接", "Not connected", "Нет связи"); color: device.connected ? #348259 : #858d98; font-size: 10px; vertical-alignment: center; }
}
if index + 1 < root.selected-devices.length: Rectangle { x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; background: #edf0f3; }
}
}
}
CompactButton { x: parent.width - 208px; y: parent.height - 46px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.confirming = false; } }
CompactButton {
x: parent.width - 126px; y: parent.height - 46px; width: 108px; height: 28px; primary: true;
text: root.action-code == "disconnect" ? GalleryStrings.tr("确认断开", "Disconnect", "Отключить") : GalleryStrings.tr("确认执行", "Continue", "Продолжить");
clicked => { root.confirming = false; root.action(root.action-code, ""); }
}
}
}
}