mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-20 19:14:39 +08:00
[feat] add DeskHub device wall and shared group actions
This commit is contained in:
@@ -49,6 +49,7 @@ export global FontAwesomeIcons {
|
||||
out property <string> signal: "\u{f012}";
|
||||
out property <string> shield-halved: "\u{f3ed}";
|
||||
out property <string> square-full: "\u{f45c}";
|
||||
out property <string> tags: "\u{f02c}";
|
||||
out property <string> trash-can: "\u{f2ed}";
|
||||
out property <string> user-secret: "\u{f21b}";
|
||||
out property <string> volume-high: "\u{f028}";
|
||||
@@ -59,6 +60,7 @@ export global FontAwesomeIcons {
|
||||
export component IconButton inherits Rectangle {
|
||||
in property <string> icon;
|
||||
in property <string> tooltip;
|
||||
in property <bool> tooltip-above: false;
|
||||
in property <bool> enabled: true;
|
||||
in property <bool> danger: false;
|
||||
in property <length> icon-size: 16px;
|
||||
@@ -103,7 +105,7 @@ export component IconButton inherits Rectangle {
|
||||
|
||||
if touch.has-hover && root.tooltip != "": Rectangle {
|
||||
x: min(root.width - self.width, 0px);
|
||||
y: root.height + 3px;
|
||||
y: root.tooltip-above ? -self.height - 3px : root.height + 3px;
|
||||
width: tip.preferred-width + 12px;
|
||||
height: 24px;
|
||||
background: #22252be8;
|
||||
|
||||
@@ -4,3 +4,7 @@ export { AnnouncementItem, MessageStrings } from "message_panel.slint";
|
||||
export { MainWindow, RecentConnection, UpdateCheckStatus, UiStrings } from "main_window.slint";
|
||||
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
|
||||
export { ServerWindow, ServerStatusBar, ServerStrings, ControllerEntry } from "server_window.slint";
|
||||
|
||||
export { GalleryTile, GalleryRecentDevice, GalleryActionDevice } from "gallery_view.slint";
|
||||
export { GalleryStrings } from "device_strings.slint";
|
||||
export { DeviceGroupOption } from "device_group_dialog.slint";
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { IconButton, FontAwesomeIcons } from "common.slint";
|
||||
import { GalleryStrings } from "device_strings.slint";
|
||||
|
||||
export global DeviceCardStyle {
|
||||
out property <length> toolbar-width: 104px;
|
||||
}
|
||||
|
||||
component CardAction inherits IconButton {
|
||||
width: 26px; height: 23px;
|
||||
icon-size: 12px; icon-color: white;
|
||||
hover-background: #ffffff2e;
|
||||
pressed-background: #598cf273;
|
||||
tooltip-above: true;
|
||||
}
|
||||
|
||||
// Shared by recent connections and DeskHub; callers supply only behavior.
|
||||
export component DeviceCardActions inherits Rectangle {
|
||||
in property <string> remove-label;
|
||||
in property <bool> connected: false;
|
||||
callback edit-alias();
|
||||
callback edit-groups();
|
||||
callback remove-device();
|
||||
callback connect-device();
|
||||
callback disconnect-device();
|
||||
width: DeviceCardStyle.toolbar-width; height: 23px;
|
||||
background: #4a4e55dc; border-radius: 5px;
|
||||
HorizontalLayout {
|
||||
spacing: 0px;
|
||||
CardAction {
|
||||
icon: FontAwesomeIcons.pen;
|
||||
tooltip: GalleryStrings.tr("修改名称", "Rename", "Переименовать");
|
||||
clicked => { root.edit-alias(); }
|
||||
}
|
||||
CardAction {
|
||||
icon: FontAwesomeIcons.tags;
|
||||
tooltip: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы");
|
||||
clicked => { root.edit-groups(); }
|
||||
}
|
||||
CardAction {
|
||||
icon: FontAwesomeIcons.trash-can;
|
||||
tooltip: root.remove-label;
|
||||
clicked => { root.remove-device(); }
|
||||
}
|
||||
CardAction {
|
||||
icon: root.connected ? FontAwesomeIcons.xmark : FontAwesomeIcons.arrow-right-long;
|
||||
icon-size: 13px;
|
||||
tooltip: root.connected ? GalleryStrings.tr("断开", "Disconnect", "Отключить") : GalleryStrings.tr("连接", "Connect", "Подключить");
|
||||
clicked => {
|
||||
if root.connected { root.disconnect-device(); }
|
||||
else { root.connect-device(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { ScrollView, LineEdit } from "std-widgets.slint";
|
||||
import { CompactButton, ImGuiCheckBox } from "common.slint";
|
||||
import { GalleryStrings } from "device_strings.slint";
|
||||
|
||||
export struct DeviceGroupOption {
|
||||
name: string,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
export component DeviceGroupDialog inherits Rectangle {
|
||||
in property <string> device-name;
|
||||
in property <string> device-id;
|
||||
in property <[DeviceGroupOption]> options;
|
||||
in property <string> error;
|
||||
in-out property <string> new-group;
|
||||
callback toggle-group(string);
|
||||
callback save(string);
|
||||
callback cancel();
|
||||
background: #00000040;
|
||||
TouchArea {}
|
||||
dialog := Rectangle {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: min(360px, parent.width - 40px);
|
||||
height: 208px + group-list.height + (root.error == "" ? 0px : 32px);
|
||||
background: white; border-radius: 8px;
|
||||
border-width: 1px; border-color: #b2b2b2;
|
||||
TouchArea {}
|
||||
Text {
|
||||
x: 18px; y: 16px; height: 24px;
|
||||
text: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы");
|
||||
color: #24272d; font-size: 17px; font-weight: 600;
|
||||
}
|
||||
Text {
|
||||
x: 18px; y: 47px; width: parent.width - 36px; height: 18px;
|
||||
text: root.device-name; color: #30343b; font-size: 12px; overflow: elide;
|
||||
}
|
||||
Text {
|
||||
x: 18px; y: 67px; width: parent.width - 36px; height: 16px;
|
||||
text: "ID " + root.device-id; color: #858c97; font-size: 11px;
|
||||
}
|
||||
group-list := Rectangle {
|
||||
x: 18px; y: 95px; width: parent.width - 36px;
|
||||
height: min(5, max(1, root.options.length)) * 32px + 2px;
|
||||
background: #fbfcfd; border-width: 1px; border-color: #e1e5eb; border-radius: 5px;
|
||||
ScrollView {
|
||||
x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px;
|
||||
viewport-width: self.width;
|
||||
VerticalLayout {
|
||||
spacing: 0px;
|
||||
for option[index] in root.options: Rectangle {
|
||||
height: 32px;
|
||||
background: row-touch.has-hover ? #eef4fc : transparent;
|
||||
ImGuiCheckBox {
|
||||
x: 9px; y: 6px; width: 20px; height: 20px;
|
||||
checked: option.selected && (option.name != "" || root.new-group == "");
|
||||
}
|
||||
Text {
|
||||
x: 39px; width: parent.width - 48px; height: parent.height;
|
||||
text: option.name == "" ? GalleryStrings.tr("未分组", "Ungrouped", "Без группы") : option.name;
|
||||
color: #30343b; font-size: 12px; overflow: elide; vertical-alignment: center;
|
||||
}
|
||||
row-touch := TouchArea { clicked => { root.toggle-group(option.name); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LineEdit {
|
||||
x: 18px; y: group-list.y + group-list.height + 12px;
|
||||
width: parent.width - 36px; height: 30px;
|
||||
text <=> root.new-group;
|
||||
placeholder-text: GalleryStrings.tr("新建分组(可选)", "New group (optional)", "Новая группа (необязательно)");
|
||||
}
|
||||
if root.error != "": Text {
|
||||
x: 18px; y: group-list.y + group-list.height + 48px;
|
||||
width: parent.width - 36px; height: 32px;
|
||||
text: root.error; color: #c53e3e; font-size: 11px; wrap: word-wrap;
|
||||
}
|
||||
HorizontalLayout {
|
||||
x: 18px; y: parent.height - 46px; width: parent.width - 36px; height: 28px;
|
||||
spacing: 10px;
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
CompactButton { width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.cancel(); } }
|
||||
CompactButton { width: 88px; height: 28px; primary: true; text: GalleryStrings.tr("保存", "Save", "Сохранить"); clicked => { root.save(root.new-group); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export global GalleryStrings {
|
||||
in-out property <int> language: 0;
|
||||
public pure function tr(zh: string, en: string, ru: string) -> string {
|
||||
return self.language == 0 ? zh : self.language == 2 ? ru : en;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
import { GalleryStrings } from "device_strings.slint";
|
||||
import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.slint";
|
||||
import { GroupFilter } from "group_filter.slint";
|
||||
import { ScrollView, LineEdit } from "std-widgets.slint";
|
||||
import { CompactButton, FontAwesomeIcons, ImGuiCheckBox } from "common.slint";
|
||||
|
||||
export struct GalleryTile {
|
||||
id: string,
|
||||
name: 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;
|
||||
// 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;
|
||||
private property <bool> vertical-rail: focus-view.width > 840px;
|
||||
private property <int> rail-current-index: 0;
|
||||
changed rail-current-index => { root.reveal-current(); }
|
||||
changed width => { root.reveal-current(); }
|
||||
changed height => { root.reveal-current(); }
|
||||
function reveal-current() {
|
||||
if root.vertical-rail {
|
||||
rail.viewport-x = 0px;
|
||||
let top = root.rail-current-index * 100px;
|
||||
rail.viewport-y = clamp(rail.viewport-y, -top, rail.height - top - 92px);
|
||||
rail.viewport-y = clamp(rail.viewport-y, min(0px, rail.height - rail.viewport-height), 0px);
|
||||
} else {
|
||||
rail.viewport-y = 0px;
|
||||
let left = root.rail-current-index * 144px;
|
||||
rail.viewport-x = clamp(rail.viewport-x, -left, rail.width - left - 136px);
|
||||
rail.viewport-x = clamp(rail.viewport-x, min(0px, rail.width - rail.viewport-width), 0px);
|
||||
}
|
||||
}
|
||||
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;
|
||||
in property <int> columns: 3;
|
||||
in property <int> rows: 2;
|
||||
in property <int> page: 0;
|
||||
in property <int> pages: 1;
|
||||
in property <int> count: 0;
|
||||
in property <int> connected-count: 0;
|
||||
in property <int> selected-count: 0;
|
||||
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 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", ""); }
|
||||
public function reset-transient() { root.batch = false; root.adding = false; root.importing = false; root.confirming = false; }
|
||||
function confirm-action(code: string, name: string) {
|
||||
root.action-code = code; root.action-name = name; root.confirming = true;
|
||||
}
|
||||
toolbar := HorizontalLayout {
|
||||
x: 29px; y: 12px; width: parent.width - 58px; height: 24px;
|
||||
visible: root.focused.id == "";
|
||||
spacing: 8px;
|
||||
GroupFilter { width: 155px; height: 24px; system-count: 1; model: root.groups; current-index <=> root.group-index;
|
||||
selected(index) => { root.action("group", index); }
|
||||
}
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
CompactButton { width: 82px; primary: root.batch; text: root.batch ? GalleryStrings.tr("完成", "Done", "Готово") : GalleryStrings.tr("批量选择", "Select", "Выбрать"); clicked => { root.batch = !root.batch; root.action("selection", ""); } }
|
||||
CompactButton { width: 116px; text: GalleryStrings.tr("从最近连接添加", "Add from recent", "Из недавних"); clicked => { root.action("recent-open", ""); root.importing = true; } }
|
||||
CompactButton { width: 90px; primary: true; text: GalleryStrings.tr("添加设备", "Add device", "Добавить"); clicked => { root.action("add-open", ""); root.adding = true; } }
|
||||
}
|
||||
grid := Rectangle {
|
||||
x: toolbar.x; y: toolbar.y + toolbar.height + 12px; width: toolbar.width;
|
||||
height: footer.y - 12px - self.y;
|
||||
visible: root.focused.id == "";
|
||||
for tile[index] in root.tiles: Rectangle {
|
||||
x: mod(index, root.columns) * (grid.width + 12px) / root.columns;
|
||||
y: floor(index / root.columns) * (grid.height + 12px) / root.rows;
|
||||
width: (grid.width - (root.columns - 1) * 12px) / root.columns;
|
||||
height: (grid.height - (root.rows - 1) * 12px) / root.rows;
|
||||
border-radius: 6px; background: tile.selected ? #eaf3ff : #eff0f2;
|
||||
border-width: tile.selected ? 2px : 1px;
|
||||
border-color: tile.selected ? #76aeea : root.batch && tile-touch.has-hover ? #94bdeb : #c9cdd2;
|
||||
Rectangle {
|
||||
x: 6px; y: 6px; width: parent.width - 12px; height: parent.height - 36px;
|
||||
background: #24272d;
|
||||
if tile.has-frame: Image { x: 0px; y: 0px; width: parent.width; height: parent.height; source: tile.frame; image-fit: contain; opacity: tile.stale ? 0.45 : 1; }
|
||||
if !tile.has-frame || !tile.connected || tile.stale: Text {
|
||||
text: tile.status; color: #ffffff; font-size: 12px;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
Text { x: 9px; y: parent.height - 28px; width: (tile-touch.has-hover && !root.batch ? parent.width - DeviceCardStyle.toolbar-width - 6px : presence-badge.x) - 17px; height: 28px; text: tile.name; color: #24272d; font-size: 12px; overflow: elide; vertical-alignment: center; }
|
||||
presence-badge := Rectangle {
|
||||
visible: root.batch || !tile-touch.has-hover;
|
||||
x: parent.width - (root.batch ? 35px : 9px) - self.width; y: parent.height - 23px;
|
||||
width: presence-label.preferred-width + 12px; height: 18px;
|
||||
border-radius: 9px; background: tile.online ? #eaf6ee : #e2e5e9;
|
||||
presence-label := Text {
|
||||
x: 6px; height: parent.height;
|
||||
text: tile.online ? GalleryStrings.tr("在线", "Online", "В сети") : tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
|
||||
color: tile.online ? #17854c : #62666d; font-size: 10px; vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
if root.batch: Rectangle {
|
||||
x: parent.width - 27px; y: parent.height - 23px;
|
||||
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 := TouchArea {
|
||||
clicked => { root.action(root.batch ? "select" : "focus", tile.id); }
|
||||
if tile-touch.has-hover && !root.batch: DeviceCardActions {
|
||||
x: parent.width - self.width - 6px; y: parent.height - 26px;
|
||||
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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
if root.count == 0: Text {
|
||||
text: 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: 7px; width: parent.width - 58px;
|
||||
height: parent.height - 51px;
|
||||
HorizontalLayout {
|
||||
x: 0px; y: 0px; width: parent.width; height: 28px; 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: #303943; 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: 36px; width: root.vertical-rail ? parent.width - 158px : parent.width;
|
||||
height: root.vertical-rail ? parent.height - 36px : parent.height - 152px;
|
||||
background: #eff1f4; border-radius: 6px;
|
||||
border-width: root.controlling ? 2px : 1px; border-color: root.controlling ? #76aeea : #d5dbe3;
|
||||
video := Rectangle {
|
||||
x: 4px; y: 4px; width: parent.width - 8px; height: parent.height - 8px;
|
||||
background: #24272d;
|
||||
if root.focused.has-frame: 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 {
|
||||
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.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); }
|
||||
accept
|
||||
}
|
||||
TouchArea {
|
||||
pointer-event(event) => {
|
||||
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.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: white; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
}
|
||||
rail := ScrollView {
|
||||
x: root.vertical-rail ? parent.width - 148px : 0px;
|
||||
y: root.vertical-rail ? 36px : parent.height - 106px;
|
||||
width: root.vertical-rail ? 148px : parent.width;
|
||||
height: root.vertical-rail ? parent.height - 36px : 106px;
|
||||
// Reserve 14px below or 12px beside cards for the scrollbar.
|
||||
viewport-width: root.vertical-rail ? self.width : max(self.width, root.tiles.length * 144px - 8px);
|
||||
viewport-height: root.vertical-rail ? max(self.height, root.tiles.length * 100px - 8px) : self.height;
|
||||
horizontal-scrollbar-policy: root.vertical-rail ? always-off : as-needed;
|
||||
vertical-scrollbar-policy: root.vertical-rail ? as-needed : always-off;
|
||||
for tile[index] in root.tiles: Rectangle {
|
||||
private property <bool> current: tile.id == root.focused.id;
|
||||
init => { if self.current { root.rail-current-index = index; root.reveal-current(); } }
|
||||
changed current => { if self.current { root.rail-current-index = index; root.reveal-current(); } }
|
||||
x: root.vertical-rail ? 0px : index * 144px;
|
||||
y: root.vertical-rail ? index * 100px : 0px;
|
||||
width: 136px; height: 92px; border-radius: 5px;
|
||||
background: tile.id == root.focused.id ? #edf5ff : #f5f6f8;
|
||||
border-width: tile.id == root.focused.id ? 2px : 1px;
|
||||
border-color: tile.id == root.focused.id ? #76aeea : #d5dbe3;
|
||||
Rectangle {
|
||||
x: 5px; y: 5px; width: 126px; height: 52px; background: #24272d;
|
||||
if tile.has-frame: Image { width: parent.width; height: parent.height; source: tile.frame; image-fit: contain; opacity: tile.stale || !tile.connected ? 0.4 : 1; }
|
||||
if !tile.has-frame || tile.stale || !tile.connected: Text {
|
||||
text: tile.status; color: #e2e6ed; font-size: 10px; overflow: elide;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
Text { x: 7px; y: 61px; width: 122px; height: 16px; text: tile.name; color: #303943; font-size: 11px; overflow: elide; }
|
||||
Text {
|
||||
x: 7px; y: 78px; width: 86px; height: 12px; font-size: 9px;
|
||||
text: tile.online ? GalleryStrings.tr("在线", "Online", "В сети") : tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
|
||||
color: tile.online ? #17854c : #7a828d; overflow: elide;
|
||||
}
|
||||
if tile.id == root.focused.id: Text {
|
||||
x: 99px; y: 78px; width: 30px; height: 12px;
|
||||
text: GalleryStrings.tr("当前", "Current", "Выбрано"); color: #4d83bd; font-size: 9px; horizontal-alignment: right; overflow: elide;
|
||||
}
|
||||
TouchArea { clicked => { root.action("focus", tile.id); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reuse the footer for selection actions so entering batch mode never
|
||||
// changes card geometry or moves a device beneath the pointer.
|
||||
footer := HorizontalLayout {
|
||||
x: toolbar.x; y: parent.height - 36px; width: toolbar.width; height: 24px; spacing: 12px;
|
||||
if root.batch: CompactButton {
|
||||
width: 90px; enabled: root.count > 0;
|
||||
text: root.count > 0 && root.selected-count == root.count
|
||||
? GalleryStrings.tr("取消全选", "Deselect all", "Снять выбор")
|
||||
: GalleryStrings.tr("全选", "Select all", "Выбрать все");
|
||||
clicked => { root.action("all-select", ""); }
|
||||
}
|
||||
Rectangle {
|
||||
horizontal-stretch: 1; min-width: 0px;
|
||||
Text {
|
||||
y: 0px; width: parent.width; height: root.batch && root.hidden-selected > 0 ? 13px : parent.height;
|
||||
text: root.batch ? GalleryStrings.tr("已选 ", "Selected ", "Выбрано ") + root.selected-count : root.connected-count + " / " + root.count + GalleryStrings.tr(" 台已连接", " connected", " подключено");
|
||||
color: #62666d; vertical-alignment: center; font-size: 12px; overflow: elide;
|
||||
}
|
||||
if root.batch && 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;
|
||||
}
|
||||
}
|
||||
if root.batch: HorizontalLayout {
|
||||
width: 214px; spacing: 8px;
|
||||
CompactButton { width: 58px; enabled: root.selected-count > 0; text: GalleryStrings.tr("连接", "Connect", "Связь"); clicked => { root.action("connect", ""); } }
|
||||
CompactButton { width: 62px; enabled: root.selected-count > 0; text: GalleryStrings.tr("断开", "Close", "Отключить"); clicked => { root.confirm-action("disconnect", self.text); } }
|
||||
CompactButton { width: 78px; enabled: root.selected-count > 0; text: GalleryStrings.tr("分发文件", "Send file", "Файл"); clicked => { root.confirm-action("file", self.text); } }
|
||||
}
|
||||
HorizontalLayout {
|
||||
width: 112px; height: 24px; spacing: 6px;
|
||||
CompactButton { 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; }
|
||||
CompactButton { width: 24px; text: "›"; text-size: 16px; enabled: root.page + 1 < root.pages; clicked => { root.action("page", "next"); } }
|
||||
}
|
||||
}
|
||||
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, ""); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { ScrollView } from "std-widgets.slint";
|
||||
import { FontAwesomeIcons } from "common.slint";
|
||||
|
||||
// Shared by history and DeskHub; system choices precede the named groups.
|
||||
export component GroupFilter inherits Rectangle {
|
||||
in property <[string]> model;
|
||||
in property <int> system-count: 2;
|
||||
in property <bool> enabled: true;
|
||||
in-out property <int> current-index: 0;
|
||||
callback selected(int);
|
||||
private property <bool> has-divider: root.system-count > 0 && root.model.length > root.system-count;
|
||||
private property <length> list-height: root.model.length * 26px + (root.has-divider ? 6px : 0px);
|
||||
private property <length> visible-height: min(root.list-height, 162px);
|
||||
private property <length> scroll-y: 0px;
|
||||
private property <bool> popup-animation-enabled: false;
|
||||
private property <float> popup-progress: 0;
|
||||
|
||||
animate popup-progress {
|
||||
duration: 140ms; easing: ease-out;
|
||||
enabled: root.popup-animation-enabled;
|
||||
}
|
||||
|
||||
width: 155px; height: 28px;
|
||||
background: !root.enabled ? #f3f4f5 : trigger-touch.has-hover ? #f8f9fb : white;
|
||||
border-width: 1px;
|
||||
border-color: popup.is-open || trigger-focus.has-focus ? #8db5df : #c9cdd2;
|
||||
border-radius: 4px;
|
||||
animate background, border-color { duration: 100ms; easing: ease-out; }
|
||||
forward-focus: trigger-focus;
|
||||
accessible-role: combobox;
|
||||
accessible-enabled: root.enabled;
|
||||
accessible-expandable: true;
|
||||
accessible-expanded: popup.is-open;
|
||||
accessible-value: root.model[root.current-index];
|
||||
|
||||
function reveal-selection() {
|
||||
let top = root.current-index * 26px + (root.has-divider && root.current-index >= root.system-count ? 6px : 0px);
|
||||
if top < -root.scroll-y { root.scroll-y = -top; }
|
||||
if top + 26px > -root.scroll-y + root.visible-height {
|
||||
root.scroll-y = root.visible-height - top - 26px;
|
||||
}
|
||||
root.scroll-y = clamp(root.scroll-y, min(0px, root.visible-height - root.list-height), 0px);
|
||||
}
|
||||
function choose(index: int) {
|
||||
if !root.enabled || root.model.length == 0 { return; }
|
||||
root.current-index = clamp(index, 0, root.model.length - 1);
|
||||
root.reveal-selection();
|
||||
root.selected(root.current-index);
|
||||
}
|
||||
function open-menu() {
|
||||
if root.enabled && root.model.length > 0 {
|
||||
popup-open-timer.running = false;
|
||||
root.popup-animation-enabled = false;
|
||||
root.popup-progress = 0;
|
||||
root.reveal-selection();
|
||||
popup.show();
|
||||
popup-open-timer.running = true;
|
||||
}
|
||||
}
|
||||
// Defer until the popup has its initial frame; animate its contents only,
|
||||
// keeping the native popup bounds and scrolling geometry stable.
|
||||
popup-open-timer := Timer {
|
||||
interval: 16ms;
|
||||
running: false;
|
||||
triggered => {
|
||||
self.running = false;
|
||||
if popup.is-open {
|
||||
root.popup-animation-enabled = true;
|
||||
root.popup-progress = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
function handle-key(event: KeyEvent) -> EventResult {
|
||||
if !root.enabled { return reject; }
|
||||
if event.text == Key.UpArrow { root.choose(root.current-index - 1); return accept; }
|
||||
if event.text == Key.DownArrow { root.choose(root.current-index + 1); return accept; }
|
||||
if event.text == Key.Return || event.text == " " {
|
||||
if popup.is-open { popup.close(); } else { root.open-menu(); }
|
||||
return accept;
|
||||
}
|
||||
if event.text == Key.Escape && popup.is-open { popup.close(); return accept; }
|
||||
return reject;
|
||||
}
|
||||
Text {
|
||||
x: 10px; width: parent.width - 36px; height: parent.height;
|
||||
text: root.model[root.current-index];
|
||||
color: root.enabled ? #30343b : #92979f;
|
||||
font-size: 12px; overflow: elide; vertical-alignment: center;
|
||||
accessible-role: none;
|
||||
}
|
||||
Text {
|
||||
x: parent.width - 25px; width: 16px; height: parent.height;
|
||||
text: FontAwesomeIcons.angle-down; font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900; font-size: 10px; color: #858c96;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
transform-rotation: popup.is-open ? 180deg : 0deg;
|
||||
animate transform-rotation { duration: 140ms; easing: ease-out; }
|
||||
accessible-role: none;
|
||||
}
|
||||
trigger-focus := FocusScope {
|
||||
enabled: root.enabled;
|
||||
key-pressed(event) => { return root.handle-key(event); }
|
||||
trigger-touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => { trigger-focus.focus(); root.open-menu(); }
|
||||
}
|
||||
}
|
||||
popup := PopupWindow {
|
||||
x: 0px; y: root.height + 4px;
|
||||
width: root.width; height: root.visible-height + 8px;
|
||||
close-policy: close-on-click-outside;
|
||||
forward-focus: popup-focus;
|
||||
popup-focus := FocusScope {
|
||||
key-pressed(event) => { return root.handle-key(event); }
|
||||
Rectangle {
|
||||
y: (1 - root.popup-progress) * -4px;
|
||||
width: parent.width; height: parent.height;
|
||||
opacity: root.popup-progress;
|
||||
background: white; border-width: 1px;
|
||||
border-color: #c9cdd2; border-radius: 5px;
|
||||
ScrollView {
|
||||
x: 4px; y: 4px; width: parent.width - 8px; height: parent.height - 8px;
|
||||
viewport-width: self.width;
|
||||
viewport-height: root.list-height;
|
||||
viewport-y <=> root.scroll-y;
|
||||
Rectangle {
|
||||
width: parent.width; height: root.list-height;
|
||||
if root.has-divider: Rectangle {
|
||||
x: 6px; y: root.system-count * 26px + 3px;
|
||||
width: parent.width - 12px; height: 1px; background: #e9ebee;
|
||||
}
|
||||
for choice[index] in root.model: Rectangle {
|
||||
y: index * 26px + (root.has-divider && index >= root.system-count ? 6px : 0px);
|
||||
width: parent.width; height: 26px; border-radius: 3px;
|
||||
background: index == root.current-index ? #edf4fc : option-touch.has-hover ? #f1f3f5 : transparent;
|
||||
animate background { duration: 100ms; easing: ease-out; }
|
||||
accessible-role: list-item;
|
||||
accessible-label: choice;
|
||||
Text {
|
||||
x: 8px; width: parent.width - 34px; height: parent.height;
|
||||
text: choice; color: #30343b; font-size: 12px;
|
||||
overflow: elide; vertical-alignment: center;
|
||||
accessible-role: none;
|
||||
}
|
||||
if index == root.current-index: Text {
|
||||
x: parent.width - 24px; width: 16px; height: parent.height;
|
||||
text: FontAwesomeIcons.check; font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900; font-size: 10px; color: #4e87c0;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
accessible-role: none;
|
||||
}
|
||||
option-touch := TouchArea {
|
||||
clicked => { root.choose(index); popup.close(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
import { GalleryStrings } from "device_strings.slint";
|
||||
import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.slint";
|
||||
import { DeviceGroupDialog, DeviceGroupOption } from "device_group_dialog.slint";
|
||||
import { GalleryView, GalleryTile, GalleryRecentDevice, GalleryActionDevice } from "gallery_view.slint";
|
||||
import { GroupFilter } from "group_filter.slint";
|
||||
import { LineEdit, ScrollView } from "std-widgets.slint";
|
||||
import { AnnouncementItem, MessagePanel, MessageStrings } from "message_panel.slint";
|
||||
import { FontAwesomeIcons, IconButton, CompactComboBox, ImGuiCheckBox, MiniToggleSwitch, CompactButton, PrimaryButton, SecondaryButton, ToggleSwitch, SectionTitle, DialogSurface, ImGuiLineStyle, ImGuiFontStyle } from "common.slint";
|
||||
@@ -6,7 +11,9 @@ export struct RecentConnection {
|
||||
remote-id: string,
|
||||
display-name: string,
|
||||
host-name: string,
|
||||
group-names: string,
|
||||
online: bool,
|
||||
connected: bool,
|
||||
thumbnail: image,
|
||||
has-thumbnail: bool,
|
||||
platform: string,
|
||||
@@ -286,20 +293,86 @@ export component MainWindow inherits Window {
|
||||
title: "CrossDesk";
|
||||
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
|
||||
preferred-width: 640px;
|
||||
preferred-height: 450px + root.titlebar-height;
|
||||
preferred-height: 486px + root.titlebar-height;
|
||||
min-width: 640px;
|
||||
max-width: 640px;
|
||||
min-height: 450px + root.titlebar-height;
|
||||
max-height: 450px + root.titlebar-height;
|
||||
max-width: root.gallery-mode ? 16384px : 640px;
|
||||
min-height: 486px + root.titlebar-height;
|
||||
max-height: root.gallery-mode ? 16384px : 486px + root.titlebar-height;
|
||||
no-frame: root.custom-titlebar;
|
||||
background: root.wayland-titlebar ? transparent : white;
|
||||
default-font-size: ImGuiFontStyle.base;
|
||||
|
||||
in-out property <bool> gallery-mode: false;
|
||||
in property <bool> gallery-fullscreen: false;
|
||||
in property <bool> gallery-transitioning: false;
|
||||
in property <bool> gallery-content-visible: true;
|
||||
changed gallery-transitioning => {
|
||||
if root.gallery-transitioning {
|
||||
focus-sink.focus();
|
||||
root.menu-open = false;
|
||||
root.recent-tooltip-visible = false;
|
||||
}
|
||||
}
|
||||
in-out property <string> connection-input-error;
|
||||
in property <bool> gallery-controlling: false;
|
||||
out property <float> gallery-video-width: gallery.video-width;
|
||||
out property <float> gallery-video-height: gallery.video-height;
|
||||
callback gallery-pointer(PointerEventButton, PointerEventKind, length, length);
|
||||
callback gallery-scroll(length, length, length, length);
|
||||
callback gallery-key(string, bool, bool, bool, bool, bool);
|
||||
in property <[GalleryTile]> gallery-tiles;
|
||||
in property <[string]> gallery-groups;
|
||||
in-out property <int> gallery-group-index: 0;
|
||||
in property <int> gallery-columns: 3;
|
||||
in property <int> gallery-rows: 2;
|
||||
in property <int> gallery-page: 0;
|
||||
in property <int> gallery-pages: 1;
|
||||
in property <int> gallery-count: 0;
|
||||
in property <int> gallery-connected: 0;
|
||||
in property <int> gallery-selected: 0;
|
||||
in property <int> gallery-hidden-selected: 0;
|
||||
in property <string> gallery-notice;
|
||||
in property <[GalleryActionDevice]> gallery-selected-devices;
|
||||
in property <GalleryTile> gallery-focused;
|
||||
in-out property <bool> gallery-batch: false;
|
||||
in-out property <bool> gallery-adding: false;
|
||||
in-out property <bool> gallery-importing: false;
|
||||
in property <[GalleryRecentDevice]> gallery-recent-devices;
|
||||
in property <int> gallery-recent-selected: 0;
|
||||
in property <string> gallery-import-group;
|
||||
out property <float> gallery-width: root.width / 1px;
|
||||
out property <float> gallery-height: gallery.height / 1px;
|
||||
callback change-mode(bool);
|
||||
callback toggle-gallery-fullscreen();
|
||||
callback gallery-action(string, string);
|
||||
callback gallery-add-device(string, string, string, bool) -> bool;
|
||||
callback gallery-import-recent(string) -> bool;
|
||||
in-out property <bool> device-group-open: false;
|
||||
private property <string> device-group-id;
|
||||
private property <string> device-group-name;
|
||||
in property <[DeviceGroupOption]> device-group-options;
|
||||
in property <string> device-group-error;
|
||||
in-out property <string> device-group-new-name;
|
||||
callback device-group-action(string, string);
|
||||
callback device-group-save(string) -> bool;
|
||||
function open-device-groups(id: string, name: string) {
|
||||
root.gallery-action("release", "");
|
||||
focus-sink.focus();
|
||||
root.recent-tooltip-visible = false;
|
||||
root.device-group-id = id;
|
||||
root.device-group-name = name;
|
||||
root.device-group-new-name = "";
|
||||
root.device-group-action("open", id);
|
||||
root.device-group-open = true;
|
||||
}
|
||||
|
||||
public function reset-gallery-transient() { gallery.reset-transient(); root.menu-open = false; }
|
||||
in property <bool> custom-titlebar: false;
|
||||
in property <bool> wayland-titlebar: false;
|
||||
in property <bool> window-active: true;
|
||||
private property <length> titlebar-height:
|
||||
root.custom-titlebar ? (root.wayland-titlebar ? 32px : 31px) : 0px;
|
||||
root.custom-titlebar && !root.gallery-fullscreen ? (root.wayland-titlebar ? 32px : 31px) : 0px;
|
||||
private property <length> content-header-height: 38px;
|
||||
private property <length> wayland-corner-radius: 12px;
|
||||
in property <string> local-id: "";
|
||||
in property <string> local-password: "";
|
||||
@@ -310,6 +383,13 @@ export component MainWindow inherits Window {
|
||||
in property <bool> signal-reconnecting: false;
|
||||
in property <bool> signal-tls-error: false;
|
||||
in property <[RecentConnection]> recent-connections;
|
||||
in property <[string]> recent-groups: [GalleryStrings.tr("全部设备", "All devices", "Все устройства"), GalleryStrings.tr("未分组", "Ungrouped", "Без группы")];
|
||||
in-out property <int> recent-group-index: 0;
|
||||
callback recent-group-selected(int);
|
||||
changed recent-group-index => {
|
||||
root.recent-tooltip-visible = false;
|
||||
recent-scroll.viewport-x = 0px;
|
||||
}
|
||||
in property <[AnnouncementItem]> announcements;
|
||||
in property <int> announcement-unread;
|
||||
private property <string> announcement-unread-label:
|
||||
@@ -389,13 +469,14 @@ export component MainWindow inherits Window {
|
||||
// as disabled so the fixed settings layout never turns into a blank row.
|
||||
in property <bool> hardware-codec-available: true;
|
||||
|
||||
callback copy-local-id;
|
||||
callback copy-local-id() -> bool;
|
||||
callback toggle-password-visibility;
|
||||
callback refresh-password;
|
||||
callback reset-password(string) -> bool;
|
||||
callback format-remote-id(string) -> string;
|
||||
callback connect-requested(string);
|
||||
callback recent-connect(string);
|
||||
callback recent-disconnect(string);
|
||||
callback recent-edit-alias(string, string);
|
||||
callback recent-delete(string);
|
||||
callback save-settings() -> bool;
|
||||
@@ -439,29 +520,28 @@ export component MainWindow inherits Window {
|
||||
in-out property <string> alias-input: "";
|
||||
in-out property <bool> delete-open: false;
|
||||
private property <string> delete-remote-id: "";
|
||||
private property <bool> delete-from-gallery: false;
|
||||
private property <bool> copy-toast: false;
|
||||
private property <bool> compact-language: root.language-index == 0;
|
||||
private property <bool> recent-tooltip-visible: false;
|
||||
private property <string> recent-tooltip-name: "";
|
||||
private property <string> recent-tooltip-host: "";
|
||||
private property <string> recent-tooltip-id: "";
|
||||
private property <string> recent-tooltip-groups: "";
|
||||
private property <bool> recent-tooltip-online: false;
|
||||
private property <length> recent-tooltip-pointer-x: 0px;
|
||||
private property <length> recent-tooltip-pointer-y: 0px;
|
||||
private property <length> recent-tooltip-width: 260px;
|
||||
private property <length> recent-tooltip-height:
|
||||
root.recent-tooltip-host != "" && root.recent-tooltip-host != root.recent-tooltip-name
|
||||
? 80px : 64px;
|
||||
private property <length> recent-tooltip-x: min(
|
||||
root.width - root.recent-tooltip-width - 8px,
|
||||
max(8px,
|
||||
root.recent-tooltip-pointer-x > root.width * 0.7
|
||||
? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px
|
||||
: root.recent-tooltip-pointer-x + 12px));
|
||||
private property <length> recent-tooltip-y: min(
|
||||
root.height - root.titlebar-height - root.recent-tooltip-height - 8px,
|
||||
max(8px, root.recent-tooltip-pointer-y - root.recent-tooltip-height - 8px));
|
||||
|
||||
changed menu-open => { if root.menu-open { root.gallery-action("release", ""); } }
|
||||
changed connection-dialog-open => { if root.connection-dialog-open { root.gallery-action("release", ""); } }
|
||||
changed permission-dialog-open => { if root.permission-dialog-open { root.gallery-action("release", ""); } }
|
||||
changed settings-open => {
|
||||
if root.settings-open {
|
||||
root.settings-scroll-y = 0px;
|
||||
@@ -490,7 +570,7 @@ export component MainWindow inherits Window {
|
||||
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
|
||||
clip: root.wayland-titlebar;
|
||||
|
||||
if root.custom-titlebar && root.wayland-titlebar: Rectangle {
|
||||
if root.custom-titlebar && root.wayland-titlebar && !root.gallery-fullscreen: Rectangle {
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
width: parent.width;
|
||||
@@ -583,7 +663,7 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
if root.custom-titlebar && !root.wayland-titlebar: Rectangle {
|
||||
if root.custom-titlebar && !root.wayland-titlebar && !root.gallery-fullscreen: Rectangle {
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
width: root.width;
|
||||
@@ -673,6 +753,12 @@ export component MainWindow inherits Window {
|
||||
width: root.width;
|
||||
height: root.height - self.y;
|
||||
clip: true;
|
||||
opacity: root.gallery-content-visible || !root.gallery-mode ? 1 : 0;
|
||||
animate opacity {
|
||||
duration: 60ms;
|
||||
easing: ease-in-out;
|
||||
enabled: root.gallery-mode;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width;
|
||||
@@ -714,10 +800,10 @@ export component MainWindow inherits Window {
|
||||
// App actions live in the content header; window controls and dragging
|
||||
// remain in the native or Windows-specific title bar above it.
|
||||
menu-button := IconButton {
|
||||
x: parent.width - 40px;
|
||||
y: 4px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
x: parent.width - mode-switch.x - self.width;
|
||||
y: mode-switch.y;
|
||||
width: mode-switch.height;
|
||||
height: mode-switch.height;
|
||||
icon: FontAwesomeIcons.bars;
|
||||
icon-size: 14px;
|
||||
hover-background: #e1e5eb;
|
||||
@@ -726,7 +812,7 @@ export component MainWindow inherits Window {
|
||||
? MessageStrings.unread + " " + root.announcement-unread + (root.update-available ? " · " + UiStrings.update-available : "")
|
||||
: root.update-available ? UiStrings.update-available : "";
|
||||
z: 8;
|
||||
clicked => { root.menu-open = !root.menu-open; }
|
||||
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
|
||||
}
|
||||
// Unread messages take precedence so the menu has only one indicator.
|
||||
if root.update-available && root.announcement-unread <= 0: Rectangle {
|
||||
@@ -767,7 +853,7 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.menu-open = !root.menu-open; }
|
||||
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
|
||||
}
|
||||
}
|
||||
if root.announcement-unread > 0: Rectangle {
|
||||
@@ -794,10 +880,51 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.menu-open = !root.menu-open; }
|
||||
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
|
||||
}
|
||||
}
|
||||
|
||||
mode-switch := Rectangle {
|
||||
x: 29px; y: root.content-header-height - self.height; width: 164px; height: 24px;
|
||||
background: #eef1f5;
|
||||
border-width: 0.5px; border-color: #d5dbe3; border-radius: 6px;
|
||||
Rectangle {
|
||||
x: root.gallery-mode ? 82px : 2px;
|
||||
y: 2px; width: 80px; height: parent.height - 4px;
|
||||
background: @linear-gradient(180deg, #c4e0fd, #abd0f8);
|
||||
border-width: 0.5px; border-color: #97bde5; border-radius: 4px;
|
||||
drop-shadow-color: #315b8b20;
|
||||
drop-shadow-blur: 2px; drop-shadow-offset-y: 1px;
|
||||
Rectangle {
|
||||
x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px;
|
||||
border-width: 0.5px; border-color: #ffffff70; border-radius: 3px;
|
||||
}
|
||||
animate x { duration: 160ms; easing: ease-out; }
|
||||
}
|
||||
for label[index] in [GalleryStrings.tr("连接", "Connect", "Связь"), GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub")]: Rectangle {
|
||||
x: 2px + index * 80px; y: 2px; width: 80px; height: parent.height - 4px;
|
||||
Text {
|
||||
text: label;
|
||||
color: (index == 1) == root.gallery-mode ? #234b75 : #697483;
|
||||
font-weight: (index == 1) == root.gallery-mode ? 600 : 400;
|
||||
horizontal-alignment: center; vertical-alignment: center; font-size: 12px;
|
||||
animate color { duration: 120ms; easing: ease-out; }
|
||||
}
|
||||
TouchArea {
|
||||
clicked => { root.change-mode(index == 1); }
|
||||
}
|
||||
}
|
||||
}
|
||||
if root.gallery-mode: CompactButton {
|
||||
x: menu-button.x - self.width - 8px;
|
||||
y: mode-switch.y;
|
||||
width: 87px;
|
||||
height: mode-switch.height;
|
||||
text: root.gallery-fullscreen ? GalleryStrings.tr("退出全屏", "Exit full", "Выйти") : GalleryStrings.tr("全屏", "Fullscreen", "На весь экран");
|
||||
clicked => { root.toggle-gallery-fullscreen(); }
|
||||
}
|
||||
Rectangle {
|
||||
y: root.content-header-height; height: parent.height - self.y - 24px; visible: !root.gallery-mode;
|
||||
// Local and remote desktop panels.
|
||||
Rectangle {
|
||||
y: 0px;
|
||||
@@ -856,13 +983,14 @@ export component MainWindow inherits Window {
|
||||
y: 46px;
|
||||
width: 29px;
|
||||
height: 30px;
|
||||
icon: FontAwesomeIcons.copy;
|
||||
icon: root.copy-toast ? FontAwesomeIcons.check : FontAwesomeIcons.copy;
|
||||
icon-size: 16px;
|
||||
tooltip: UiStrings.copied;
|
||||
clicked => {
|
||||
root.copy-local-id();
|
||||
root.copy-toast = true;
|
||||
copy-timer.restart();
|
||||
if root.copy-local-id() {
|
||||
root.copy-toast = true;
|
||||
copy-timer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle { x: 8px; y: 88px; width: 240px; height: 1px; background: ImGuiLineStyle.separator; }
|
||||
@@ -921,7 +1049,7 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { x: parent.width / 2; y: 18px; width: 1px; height: 206px; background: ImGuiLineStyle.divider; }
|
||||
Rectangle { x: parent.width / 2; y: 18px; width: 1px; height: 206px; background: ImGuiLineStyle.border; }
|
||||
|
||||
SectionTitle { x: 356px; y: 9px; width: 256px; height: 31px; text: UiStrings.remote-desktop; }
|
||||
Rectangle {
|
||||
@@ -959,10 +1087,11 @@ export component MainWindow inherits Window {
|
||||
// Run formatting after the input event has
|
||||
// finished so the caret state isn't reset by
|
||||
// a synchronous two-way binding rewrite.
|
||||
edited => { remote-id-format-timer.restart(); }
|
||||
edited => { root.connection-input-error = ""; remote-id-format-timer.restart(); }
|
||||
accepted => { root.connect-requested(root.remote-id-input); }
|
||||
}
|
||||
}
|
||||
Text { x: 8px; y: 92px; width: 240px; height: 52px; text: root.connection-input-error; font-size: 12px; color: #a33832; wrap: word-wrap; }
|
||||
IconButton {
|
||||
x: 175px;
|
||||
y: 42px;
|
||||
@@ -982,7 +1111,7 @@ export component MainWindow inherits Window {
|
||||
// Recent connections: horizontal cards with thumbnail, state and actions.
|
||||
recent-panel := Rectangle {
|
||||
y: 234px;
|
||||
height: 192px;
|
||||
height: parent.height - self.y;
|
||||
background: white;
|
||||
border-width: 0px;
|
||||
border-color: ImGuiLineStyle.border;
|
||||
@@ -990,6 +1119,13 @@ export component MainWindow inherits Window {
|
||||
Rectangle { y: 0px; height: 1px; background: ImGuiLineStyle.border; }
|
||||
|
||||
SectionTitle { x: 29px; y: 7px; height: 28px; text: UiStrings.recent-connections; }
|
||||
GroupFilter {
|
||||
x: parent.width - self.width - 29px; y: 8px;
|
||||
width: 155px; height: 28px;
|
||||
model: root.recent-groups;
|
||||
current-index <=> root.recent-group-index;
|
||||
selected(index) => { root.recent-group-selected(index); }
|
||||
}
|
||||
|
||||
recent-list := Rectangle {
|
||||
x: 29px;
|
||||
@@ -1002,6 +1138,13 @@ export component MainWindow inherits Window {
|
||||
border-radius: 10px;
|
||||
clip: true;
|
||||
|
||||
if root.recent-connections.length == 0: Text {
|
||||
text: root.recent-group-index == 0
|
||||
? GalleryStrings.tr("暂无近期连接", "No recent connections", "Нет недавних подключений")
|
||||
: GalleryStrings.tr("该分组暂无近期连接", "No recent connections in this group", "В этой группе нет недавних подключений");
|
||||
color: #808080; font-size: ImGuiFontStyle.base;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
}
|
||||
recent-scroll := ScrollView {
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
@@ -1018,7 +1161,7 @@ export component MainWindow inherits Window {
|
||||
height: 117px;
|
||||
background: transparent;
|
||||
private property <length> content-width: self.width - 30px;
|
||||
private property <length> toolbar-width: 79px;
|
||||
private property <length> toolbar-width: DeviceCardStyle.toolbar-width;
|
||||
private property <length> toolbar-right-padding: 3px;
|
||||
|
||||
Rectangle {
|
||||
@@ -1113,12 +1256,19 @@ export component MainWindow inherits Window {
|
||||
image-touch := TouchArea {
|
||||
width: parent.width;
|
||||
height: 84px;
|
||||
private property <string> group-names: connection.group-names;
|
||||
changed group-names => {
|
||||
if self.has-hover && root.recent-tooltip-id == connection.remote-id {
|
||||
root.recent-tooltip-groups = self.group-names;
|
||||
}
|
||||
}
|
||||
clicked => { root.recent-connect(connection.remote-id); }
|
||||
changed has-hover => {
|
||||
if self.has-hover {
|
||||
root.recent-tooltip-name = connection.display-name;
|
||||
root.recent-tooltip-host = connection.host-name;
|
||||
root.recent-tooltip-id = connection.remote-id;
|
||||
root.recent-tooltip-groups = self.group-names;
|
||||
root.recent-tooltip-online = connection.online;
|
||||
root.recent-tooltip-pointer-x = recent-panel.x + recent-list.x
|
||||
+ recent-scroll.x + recent-scroll.viewport-x
|
||||
@@ -1142,41 +1292,24 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
}
|
||||
if card-touch.has-hover: Rectangle {
|
||||
if card-touch.has-hover: DeviceCardActions {
|
||||
x: parent.width - self.width - connection-card.toolbar-right-padding;
|
||||
y: 85px;
|
||||
width: connection-card.toolbar-width;
|
||||
height: 23px;
|
||||
background: #4a4e55dc;
|
||||
border-radius: 5px;
|
||||
HorizontalLayout {
|
||||
spacing: 0px;
|
||||
IconButton {
|
||||
width: 26px; height: 23px; icon: FontAwesomeIcons.pen; icon-size: 12px; icon-color: white;
|
||||
hover-background: #ffffff2e;
|
||||
pressed-background: #598cf273;
|
||||
clicked => {
|
||||
root.alias-remote-id = connection.remote-id;
|
||||
root.alias-input = connection.display-name;
|
||||
root.alias-open = true;
|
||||
}
|
||||
}
|
||||
IconButton {
|
||||
width: 26px; height: 23px; icon: FontAwesomeIcons.trash-can; icon-size: 12px; icon-color: white;
|
||||
hover-background: #ffffff2e;
|
||||
pressed-background: #598cf273;
|
||||
clicked => {
|
||||
root.delete-remote-id = connection.remote-id;
|
||||
root.delete-open = true;
|
||||
}
|
||||
}
|
||||
IconButton {
|
||||
width: 27px; height: 23px; icon: FontAwesomeIcons.arrow-right-long; icon-size: 13px; icon-color: white;
|
||||
hover-background: #ffffff2e;
|
||||
pressed-background: #598cf273;
|
||||
clicked => { root.recent-connect(connection.remote-id); }
|
||||
}
|
||||
remove-label: GalleryStrings.tr("删除记录", "Delete record", "Удалить запись");
|
||||
connected: connection.connected;
|
||||
edit-alias => {
|
||||
root.alias-remote-id = connection.remote-id;
|
||||
root.alias-input = connection.display-name;
|
||||
root.alias-open = true;
|
||||
}
|
||||
edit-groups => { root.open-device-groups(connection.remote-id, connection.display-name); }
|
||||
remove-device => {
|
||||
root.delete-remote-id = connection.remote-id;
|
||||
root.delete-from-gallery = false;
|
||||
root.delete-open = true;
|
||||
}
|
||||
connect-device => { root.recent-connect(connection.remote-id); }
|
||||
disconnect-device => { root.recent-disconnect(connection.remote-id); }
|
||||
}
|
||||
}
|
||||
if index < root.recent-connections.length - 1: Rectangle {
|
||||
@@ -1192,9 +1325,38 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
gallery := GalleryView {
|
||||
y: root.content-header-height; width: parent.width; height: parent.height - self.y - 24px;
|
||||
visible: root.gallery-mode;
|
||||
controlling: root.gallery-controlling;
|
||||
pointer-input(button, kind, x, y) => { root.gallery-pointer(button, kind, x, y); }
|
||||
scroll-input(dx, dy, x, y) => { root.gallery-scroll(dx, dy, x, y); }
|
||||
key-input(text, pressed, ctrl, alt, shift, meta) => { root.gallery-key(text, pressed, ctrl, alt, shift, meta); }
|
||||
tiles: root.gallery-tiles; groups: root.gallery-groups;
|
||||
group-index <=> root.gallery-group-index;
|
||||
columns: root.gallery-columns; rows: root.gallery-rows;
|
||||
page: root.gallery-page; pages: root.gallery-pages; count: root.gallery-count;
|
||||
connected-count: root.gallery-connected; selected-count: root.gallery-selected;
|
||||
hidden-selected: root.gallery-hidden-selected; notice: root.gallery-notice;
|
||||
selected-devices: root.gallery-selected-devices;
|
||||
focused: root.gallery-focused; batch <=> root.gallery-batch; adding <=> root.gallery-adding;
|
||||
importing <=> root.gallery-importing; recent-devices: root.gallery-recent-devices;
|
||||
recent-selected: root.gallery-recent-selected; import-group: root.gallery-import-group;
|
||||
action(code, value) => { root.gallery-action(code, value); }
|
||||
add-device(id, password, group, remember) => { root.gallery-add-device(id, password, group, remember) }
|
||||
import-recent(group) => { root.gallery-import-recent(group) }
|
||||
edit-alias(id, name) => {
|
||||
root.alias-remote-id = id; root.alias-input = name; root.alias-open = true;
|
||||
}
|
||||
edit-groups(id, name) => { root.open-device-groups(id, name); }
|
||||
remove-device(id) => {
|
||||
root.delete-remote-id = id; root.delete-from-gallery = true; root.delete-open = true;
|
||||
}
|
||||
}
|
||||
// Status bar.
|
||||
Rectangle {
|
||||
y: 426px;
|
||||
y: parent.height - 24px;
|
||||
height: 24px;
|
||||
background: #fafbfc;
|
||||
Rectangle {
|
||||
@@ -1255,9 +1417,11 @@ export component MainWindow inherits Window {
|
||||
// pointer can otherwise cover a dialog button and hide it from the user.
|
||||
if root.recent-tooltip-visible && !root.connection-dialog-open: Rectangle {
|
||||
x: root.recent-tooltip-x;
|
||||
y: root.recent-tooltip-y;
|
||||
y: min(root.height - root.titlebar-height - self.height - 8px,
|
||||
max(8px, root.recent-tooltip-pointer-y - self.height - 8px));
|
||||
width: root.recent-tooltip-width;
|
||||
height: root.recent-tooltip-height;
|
||||
height: (root.recent-tooltip-host != "" && root.recent-tooltip-host != root.recent-tooltip-name
|
||||
? 80px : 64px) + tooltip-groups.preferred-height + 2px;
|
||||
background: #22252bf2;
|
||||
border-width: 1px;
|
||||
border-color: #ffffff38;
|
||||
@@ -1285,6 +1449,14 @@ export component MainWindow inherits Window {
|
||||
font-size: ImGuiFontStyle.small;
|
||||
overflow: elide;
|
||||
}
|
||||
tooltip-groups := Text {
|
||||
text: GalleryStrings.tr("分组", "Groups", "Группы") + ": " +
|
||||
(root.recent-tooltip-groups == "" ? GalleryStrings.tr("未分组", "Ungrouped", "Без группы") : root.recent-tooltip-groups);
|
||||
color: #e1e4e8;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
wrap: word-wrap;
|
||||
vertical-stretch: 0;
|
||||
}
|
||||
Text {
|
||||
text: root.recent-tooltip-online ? UiStrings.online : UiStrings.offline;
|
||||
color: root.recent-tooltip-online ? #62d98b : #d1d5db;
|
||||
@@ -1536,14 +1708,32 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
// One editor and one callback path for recent connections and DeskHub.
|
||||
if root.device-group-open: DeviceGroupDialog {
|
||||
z: 40;
|
||||
device-id: root.device-group-id;
|
||||
device-name: root.device-group-name;
|
||||
options: root.device-group-options;
|
||||
error: root.device-group-error;
|
||||
new-group <=> root.device-group-new-name;
|
||||
toggle-group(name) => { root.device-group-action("toggle", name); }
|
||||
save(name) => {
|
||||
if root.device-group-save(name) {
|
||||
root.device-group-action("close", "");
|
||||
root.device-group-open = false;
|
||||
}
|
||||
}
|
||||
cancel => { root.device-group-action("close", ""); root.device-group-open = false; }
|
||||
}
|
||||
|
||||
// Recent connection alias modal.
|
||||
if root.alias-open: Rectangle {
|
||||
background: #00000042;
|
||||
z: 40;
|
||||
TouchArea { clicked => { focus-sink.focus(); } }
|
||||
DialogSurface {
|
||||
x: root.compact-language ? 205px : 160px;
|
||||
y: 145px;
|
||||
x: root.gallery-mode ? (parent.width - self.width) / 2 : root.compact-language ? 205px : 160px;
|
||||
y: root.gallery-mode ? (parent.height - self.height) / 2 : 145px;
|
||||
width: root.compact-language ? 230px : 320px;
|
||||
height: 160px;
|
||||
VerticalLayout {
|
||||
@@ -1588,16 +1778,16 @@ export component MainWindow inherits Window {
|
||||
z: 40;
|
||||
TouchArea { clicked => { focus-sink.focus(); } }
|
||||
DialogSurface {
|
||||
x: root.compact-language ? 205px : 160px;
|
||||
y: 155px;
|
||||
x: root.gallery-mode ? (parent.width - self.width) / 2 : root.compact-language ? 205px : 160px;
|
||||
y: root.gallery-mode ? (parent.height - self.height) / 2 : 155px;
|
||||
width: root.compact-language ? 230px : 320px;
|
||||
height: 140px;
|
||||
VerticalLayout {
|
||||
padding: 20px; spacing: 18px;
|
||||
DialogPromptText { prompt: UiStrings.confirm-delete; }
|
||||
DialogPromptText { prompt: root.delete-from-gallery ? (root.gallery-group-index > 0 ? GalleryStrings.tr("从当前分组移除此设备?", "Remove from this group?", "Убрать из этой группы?") : GalleryStrings.tr("从桌面墙移除此设备?", "Remove from DeskHub?", "Убрать из DeskHub?")) : UiStrings.confirm-delete; }
|
||||
HorizontalLayout {
|
||||
alignment: center; spacing: 8px;
|
||||
PrimaryButton { text: UiStrings.ok; clicked => { root.recent-delete(root.delete-remote-id); root.delete-open = false; } }
|
||||
PrimaryButton { text: UiStrings.ok; clicked => { if root.delete-from-gallery { root.gallery-action("remove", root.delete-remote-id); } else { root.recent-delete(root.delete-remote-id); } root.delete-open = false; } }
|
||||
SecondaryButton { text: UiStrings.cancel; clicked => { root.delete-open = false; } }
|
||||
}
|
||||
}
|
||||
@@ -1672,8 +1862,8 @@ export component MainWindow inherits Window {
|
||||
text: UiStrings.ok;
|
||||
clicked => { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
|
||||
}
|
||||
if !root.connection-validating: ConnectionDialogButton {
|
||||
x: 117px;
|
||||
ConnectionDialogButton {
|
||||
x: root.connection-validating ? (parent.width - self.width) / 2 : 117px;
|
||||
y: 118px;
|
||||
text: UiStrings.cancel;
|
||||
clicked => { root.connection-cancel(); }
|
||||
@@ -2265,6 +2455,13 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore clicks against moving cards and duplicate fullscreen requests.
|
||||
if root.gallery-transitioning: TouchArea {
|
||||
x: 0px; y: root.titlebar-height;
|
||||
width: root.width; height: root.height - self.y;
|
||||
z: 250;
|
||||
}
|
||||
|
||||
if root.custom-titlebar: Rectangle {
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
|
||||
@@ -40,6 +40,7 @@ inline int ApplyMainWindowStrings(
|
||||
int language_index) {
|
||||
const int language =
|
||||
localization::detail::ClampLanguageIndex(language_index);
|
||||
window->global<ui::GalleryStrings>().set_language(language);
|
||||
auto& strings = window->global<ui::UiStrings>();
|
||||
auto& messages = window->global<ui::MessageStrings>();
|
||||
messages.set_messages(Text(localization::messages[language]));
|
||||
|
||||
Reference in New Issue
Block a user