mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
917 lines
60 KiB
Plaintext
917 lines
60 KiB
Plaintext
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, ""); }
|
||
}
|
||
}
|
||
}
|
||
}
|