import { UiRadius } from "ui_radius.slint"; import { GalleryStrings } from "device_strings.slint"; import { RemoteCursor } from "remote_cursor.slint"; import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.slint"; import { GalleryMenuButton } from "gallery_menu_button.slint"; import { GroupFilter } from "group_filter.slint"; import { ScrollView } from "std-widgets.slint"; import { RoundedLineEdit } from "rounded_line_edit.slint"; import { CompactButton, SecondaryButton, IconButton, FontAwesomeIcons, ImGuiCheckBox } from "common.slint"; component GalleryButton inherits CompactButton { border-radius: UiRadius.control; } component GalleryToolbarButton inherits GalleryButton { in property alternate-text; in property 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, added: bool, online: bool, } export struct GalleryActionDevice { id: string, name: string, connected: bool, } export component GalleryView inherits Rectangle { in property controlling: false; in property remote-cursor-shape: 0; in property group-controlling: false; in property input-suspended: false; in property simplified: false; in property fullscreen: false; callback toggle-simplified; callback toggle-fullscreen; // Keep long localized actions and the selection summary visible at 640px. private property toolbar-button-padding: root.width < 720px ? 8px : 12px; private property view-controls-width: compact-toggle.width + 30px; private property view-controls-space: root.view-controls-width + 6px; in property window-active: true; in-out property tile-width: 216; out property resizing: false; in-out property focus-rail-height: 70.875; out property focus-resizing: false; private property preview-aspect-ratio: 16 / 9; private property card-inset: 6px; private property card-chrome-height: root.simplified ? root.card-inset * 2 : 36px; // Keep the complete cell inside the current grid in both dimensions. private property max-card-width: max(184px, min(grid.width, (grid.height - root.card-chrome-height) * root.preview-aspect-ratio + root.card-inset * 2)); private property card-width: clamp(root.tile-width * 1px, 184px, root.max-card-width); private property card-preview-height: (root.card-width - root.card-inset * 2) / root.preview-aspect-ratio; private property card-height: root.card-preview-height + root.card-chrome-height; private property focus-rail-visible: root.focused.id != "" && root.tiles.length > 1; private property rail-chrome-height: root.simplified ? 10px : 24px; private property 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 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 rail-preview-height: clamp(root.focus-rail-height * 1px, root.min-rail-preview-height, root.max-rail-preview-height); private property rail-preview-width: root.rail-preview-height * root.preview-aspect-ratio; private property rail-card-height: root.rail-preview-height + root.rail-chrome-height; private property rail-slot-width: root.rail-preview-width + 18px; private property focused-id: root.focused.id; private property device-tooltip-id; private property device-tooltip-text; out property device-tooltip-x; out property device-tooltip-y; private property device-tooltip-tile; // MainWindow renders the same tooltip panel for the wall and recent list. out property hovered-device: root.device-tooltip-id == root.device-tooltip-tile.id && !root.adding && !root.importing && !root.confirming ? root.device-tooltip-tile : {}; private property device-tooltip-visible: false; private property drag-source; private property dragged-tile; private property dragging: false; private property drag-start-x; private property drag-start-y; private property drag-x; private property drag-y; private property drag-target: root.drop-index(root.drag-x, root.drag-y); private property drag-page: -1; private property drag-edge-width: 32px; private property drag-page-direction: root.page-direction(root.drag-x, root.drag-y); private property swap-save-failed: false; changed focused-id => { root.device-tooltip-id = ""; root.resizing = false; root.focus-resizing = false; root.cancel-device-drag(); } changed focus-rail-visible => { root.focus-resizing = false; } 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 width => { root.device-tooltip-id = ""; root.resizing = false; root.focus-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 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" || root.device-tooltip-id == "resize-focus"; if device-tooltip-timer.running { 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 video-width: video.width / 1px; out property video-height: video.height / 1px; // Non-focused previews use the grid cell or the focused view's rail. out property preview-width: (root.focused.id == "" ? root.card-width - root.card-inset * 2 : root.rail-preview-width) / 1px; out property 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 group-index: 0; // Pagination and rendering share the same fixed-aspect cell geometry. out property columns: max(1, floor(grid.width / root.card-width)); out property rows: max(1, floor(grid.height / root.card-height)); in property page: 0; in property pages: 1; in property count: 0; in property connected-count: 0; private property connected-summary: root.connected-count + " / " + root.count + GalleryStrings.tr(" 台已连接", " connected", " подключено"); callback network-open; in property selected-count: 0; in property selected-audio-available: false; in property selected-audio-muted: false; in property hidden-selected: 0; in property notice; in property focused; in-out property batch: false; in-out property adding: false; in-out property importing: false; in property <[GalleryRecentDevice]> recent-devices; in property recent-selected: 0; in property recent-available: 0; in property import-group; private property confirming: false; private property action-name; private property 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.focus-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 table-width: root.columns * root.card-width; private property 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 column-line: clamp(round(self.mouse-x / root.card-width), 0, root.columns); private property row-line: clamp(round(self.mouse-y / root.card-height), 0, root.rows); private property 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 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 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 drag-x; private property drag-y; private property drag-columns; private property drag-rows; private property start-x; private property start-y; private property start-width; private property cursor-x: root.resizing ? self.drag-x : self.edge-x; private property cursor-y: root.resizing ? self.drag-y : self.edge-y; private property 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 presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети") : tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно"); private property 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: 4px; 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 has-hover: grid.has-hover && grid.hover-index == index; private property 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 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; GalleryButton { 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: UiRadius.control; background: root.controlling ? #e7f1fd : #f0f2f5; focus-state := Text { x: 8px; width: parent.width - 16px; height: parent.height; text: root.group-controlling ? GalleryStrings.tr("群控中", "Group control", "Групповое") : 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; } } GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; primary: root.group-controlling; enabled: root.group-controlling || (root.focused.connected && root.focused.has-frame && !root.focused.stale); text: root.group-controlling ? GalleryStrings.tr("关闭群控", "Stop group", "Откл. группу") : GalleryStrings.tr("开启群控", "Group control", "Групп. ввод"); alternate-text: root.group-controlling ? GalleryStrings.tr("开启群控", "Group control", "Групп. ввод") : GalleryStrings.tr("关闭群控", "Stop group", "Откл. группу"); accessible-role: button; accessible-label: self.text + GalleryStrings.tr(":同步键鼠到所有已连接设备", ": mirror input to all connected devices", ": ввод на всех подключённых устройствах"); accessible-action-default => { self.clicked(); } clicked => { root.action(root.group-controlling ? "group-release" : "group-control", root.focused.id); } } GalleryButton { 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 ? focus-divider.y + focus-divider.height : 36px; width: parent.width; height: parent.height - self.y; background: root.focused.online ? #eff1f4 : DeviceCardStyle.offline-background; border-radius: UiRadius.surface; 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 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 { mouse-cursor: root.controlling && !root.input-suspended ? RemoteCursor.shape(root.remote-cursor-shape) : MouseCursor.default; 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.focus-rail-visible; 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) * root.rail-slot-width); 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 : root.rail-slot-width; height: root.rail-card-height; visible: tile.id != root.focused.id; rail-card := Rectangle { private property presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети") : tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно"); private property details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence; width: root.rail-preview-width + 10px; height: root.rail-card-height; border-radius: UiRadius.surface; 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: parent.width - 14px; 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 = ""; } } changed mouse-x => { if self.has-hover { self.show-details(); } } changed mouse-y => { if self.has-hover { self.show-details(); } } clicked => { root.device-tooltip-id = ""; root.action("focus", tile.id); } } } } } } focus-divider := Rectangle { visible: root.focus-rail-visible; x: 0px; y: rail.y + rail.height; width: parent.width; height: 12px; private property 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 start-y; private property 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 { 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.group-controlling ? GalleryStrings.tr("群控中 · 所有已连接设备", "Group control · All connected devices", "Групповое управление · Все подключения") : 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: UiRadius.control; 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 := GalleryButton { 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 := GalleryButton { 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; border-radius: UiRadius.control; 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 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: UiRadius.surface; 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: UiRadius.surface; 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" || root.device-tooltip-id == "resize-focus"): 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: UiRadius.control; 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: UiRadius.surface; 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 := RoundedLineEdit { 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 := RoundedLineEdit { 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 := RoundedLineEdit { 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; } GalleryButton { x: 172px; y: 260px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { password.text = ""; root.adding = false; } } GalleryButton { 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: UiRadius.surface; 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-available > 0; checked: root.recent-devices.length > 0 && root.recent-selected == root.recent-available; } Text { x: 45px; y: 74px; height: 21px; text: GalleryStrings.tr("全选", "Select all", "Выбрать все"); color: root.recent-available > 0 ? #24272d : #92969e; font-size: 12px; vertical-alignment: center; } TouchArea { x: 18px; y: 74px; width: 144px; height: 21px; enabled: root.recent-available > 0; clicked => { root.action("recent-all", ""); } } Text { x: 224px; y: 74px; width: 198px; height: 21px; text: GalleryStrings.tr("待添加 ", "To add ", "К добавлению ") + root.recent-selected + " / " + root.recent-available; 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: UiRadius.surface; 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.added ? #f1f2f4 : device.selected ? #edf5fe : row-touch.has-hover ? #f4f5f6 : white; ImGuiCheckBox { x: 8px; y: 11px; enabled: !device.added; checked: device.added || device.selected; } Text { x: 36px; y: 3px; width: parent.width - 126px; height: 20px; text: device.name; color: device.added ? #92969e : #24272d; 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: device.added ? #92969e : #808080; } Rectangle { x: parent.width - 74px; y: 19px; width: 6px; height: 6px; border-radius: 3px; background: !device.added && device.online ? #21b567 : #92969e; } Text { x: parent.width - 61px; y: 12px; width: 57px; height: 20px; text: device.added ? GalleryStrings.tr("已添加", "Added", "Добавлено") : device.online ? GalleryStrings.tr("在线", "Online", "В сети") : GalleryStrings.tr("离线", "Offline", "Не в сети"); color: device.added ? #92969e : device.online ? #17854c : #808080; font-size: 11px; vertical-alignment: center; } row-touch := TouchArea { enabled: !device.added; 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; } Rectangle { x: -UiRadius.surface; y: -UiRadius.surface; width: parent.width + 2 * UiRadius.surface; height: parent.height + 2 * UiRadius.surface; border-width: UiRadius.surface; border-radius: 2 * UiRadius.surface; border-color: white; } Rectangle { width: parent.width; height: parent.height; border-width: 1px; border-radius: UiRadius.surface; border-color: #d8dadd; } } Text { x: 18px; y: 274px; width: 60px; height: 30px; text: GalleryStrings.tr("分组", "Group", "Группа"); font-size: 12px; vertical-alignment: center; } import-group := RoundedLineEdit { 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; } GalleryButton { x: 192px; y: 336px; width: 72px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.importing = false; } } GalleryButton { 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: UiRadius.surface; 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: UiRadius.surface; 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: UiRadius.surface; 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; } } } } GalleryButton { x: parent.width - 208px; y: parent.height - 46px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.confirming = false; } } GalleryButton { 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, ""); } } } } }