[feat] add DeskHub stream controls and network status

This commit is contained in:
dijunkun
2026-10-13 10:45:48 +08:00
parent 73b4d0f645
commit 3bd0676672
28 changed files with 1266 additions and 259 deletions
+18 -16
View File
@@ -173,6 +173,8 @@ export component CompactComboBox inherits Rectangle {
in property <[string]> model;
in-out property <int> current-index: 0;
in property <bool> enabled: true;
in property <bool> subtle: false;
in property <bool> show-arrow: true;
property <bool> popup-animation-enabled: false;
property <float> popup-progress: 0.0;
callback selected(int);
@@ -185,29 +187,29 @@ export component CompactComboBox inherits Rectangle {
width: 73px;
height: 24px;
background: root.enabled ? white : #f1f1f1;
background: root.enabled ? (root.subtle ? #f8fafc : white) : root.subtle ? #f1f3f6 : #f1f1f1;
border-width: 1px;
border-color: #bfc2c7;
border-radius: 3px;
border-color: root.subtle ? #dfe6ef : #bfc2c7;
border-radius: root.subtle ? 6px : 3px;
Text {
x: 5px;
width: parent.width - 24px;
x: root.subtle ? 10px : 5px;
width: parent.width - (root.show-arrow ? (root.subtle ? 36px : 24px) : (root.subtle ? 20px : 10px));
text: root.model[root.current-index];
color: root.enabled ? #202124 : #8b8f95;
font-size: ImGuiFontStyle.body;
font-size: root.subtle ? 12px : ImGuiFontStyle.body;
overflow: elide;
vertical-alignment: center;
}
Rectangle {
x: parent.width - 21px;
if root.show-arrow: Rectangle {
x: parent.width - (root.subtle ? 27px : 21px);
width: 20px;
height: parent.height - 2px;
y: 1px;
background: root.enabled ? #9bc9f8 : #d7d9dc;
background: root.subtle ? transparent : root.enabled ? #9bc9f8 : #d7d9dc;
Text {
text: FontAwesomeIcons.angle-down;
color: #202124;
color: root.subtle ? #79879a : #202124;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 10px;
@@ -222,7 +224,7 @@ export component CompactComboBox inherits Rectangle {
}
}
TouchArea {
enabled: root.enabled;
enabled: root.enabled; mouse-cursor: pointer;
clicked => {
root.popup-animation-enabled = false;
root.popup-progress = 0.0;
@@ -261,15 +263,15 @@ export component CompactComboBox inherits Rectangle {
opacity: root.popup-progress;
background: white;
border-width: 1px;
border-color: #bfc2c7;
border-radius: 3px;
border-color: root.subtle ? #dfe6ef : #bfc2c7;
border-radius: root.subtle ? 6px : 3px;
clip: true;
for choice[index] in root.model: Rectangle {
y: index * 23px;
width: parent.width;
height: 23px;
background: option-touch.has-hover ? #dbeafb : index == root.current-index ? #eef5fd : white;
Text { x: 5px; width: parent.width - 10px; text: choice; color: #202124; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
Text { x: 5px; width: parent.width - 10px; text: choice; color: #202124; font-size: root.subtle ? 12px : ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
option-touch := TouchArea {
clicked => {
popup-open-timer.running = false;
@@ -286,8 +288,8 @@ export component CompactComboBox inherits Rectangle {
height: parent.height;
background: transparent;
border-width: 1px;
border-color: #bfc2c7;
border-radius: 3px;
border-color: root.subtle ? #dfe6ef : #bfc2c7;
border-radius: root.subtle ? 6px : 3px;
z: 2;
}
}
@@ -7,4 +7,5 @@ export { ServerWindow, ServerStatusBar, ServerStrings, ControllerEntry } from "s
export { GalleryTile, GalleryRecentDevice, GalleryActionDevice } from "gallery_view.slint";
export { GalleryStrings } from "device_strings.slint";
export { GalleryNetworkDevice } from "gallery_network_dialog.slint";
export { DeviceGroupOption } from "device_group_dialog.slint";
@@ -0,0 +1,225 @@
import { GalleryStrings } from "device_strings.slint";
import { CompactButton } from "common.slint";
import { ScrollView } from "std-widgets.slint";
export struct GalleryNetworkDevice {
name: string,
id: string,
mode: string,
encryption-known: bool,
encryption-enabled: bool,
latency: string,
inbound: string,
outbound: string,
loss: string,
resolution: string,
fps: string,
}
component NetworkRow inherits Rectangle {
in property <GalleryNetworkDevice> device;
in property <bool> heading: false;
// Keep the same column geometry in the header and every row. Grouping
// related values leaves room for device names even in the 640px window.
HorizontalLayout {
x: 0px; y: 0px; width: parent.width; height: parent.height;
padding-left: 12px; padding-right: 12px; spacing: 10px;
identity := Rectangle {
horizontal-stretch: 1;
device-name := Text {
y: root.heading ? 0px : 11px; height: root.heading ? parent.height : 20px;
width: parent.width; text: root.device.name;
font-size: root.heading ? 11px : 12px; font-weight: 600;
color: root.heading ? #6b7785 : #293746;
vertical-alignment: center; overflow: elide;
}
name-hover := TouchArea {
y: 11px; height: 20px; width: parent.width;
enabled: !root.heading;
}
if !root.heading: HorizontalLayout {
x: 0px; y: 34px; width: parent.width; height: 17px; spacing: 4px;
id-text := Text {
text: root.device.id; font-size: 10px; color: #7a8490;
vertical-alignment: center; overflow: elide;
}
Rectangle {
width: max(0px, min(mode-text.preferred-width + 12px,
parent.width - id-text.preferred-width - encryption-badge.width - 12px));
background: #edf2f7; border-radius: 3px;
mode-text := Text {
x: 6px; width: parent.width - 12px; height: parent.height;
text: root.device.mode; font-size: 10px; color: #586f87;
horizontal-alignment: center; vertical-alignment: center; overflow: elide;
}
}
encryption-badge := Rectangle {
width: encryption-text.preferred-width + 12px;
background: !root.device.encryption-known ? #f0f2f5
: root.device.encryption-enabled ? #e8f5ec : #fff3df;
border-radius: 3px;
encryption-text := Text {
x: 6px; width: parent.width - 12px; height: parent.height;
text: !root.device.encryption-known
? GalleryStrings.tr("待确认", "Unknown", "Неизвестно")
: root.device.encryption-enabled
? GalleryStrings.tr("已加密", "Encrypted", "Защищено")
: GalleryStrings.tr("未加密", "Unencrypted", "Без шифр.");
color: !root.device.encryption-known ? #7a8490
: root.device.encryption-enabled ? #18794e : #8a6428;
font-size: 10px; horizontal-alignment: center;
vertical-alignment: center; overflow: elide;
}
}
Rectangle { horizontal-stretch: 1; }
}
}
Rectangle {
width: max(108px, root.width * 0.19);
Text {
y: root.heading ? 0px : 11px; height: root.heading ? parent.height : 20px;
width: parent.width;
text: root.heading ? GalleryStrings.tr("画面", "Video", "Видео") : root.device.resolution;
font-size: root.heading ? 11px : 12px; font-weight: root.heading ? 600 : 400;
color: root.heading ? #6b7785 : #293746;
vertical-alignment: center; overflow: elide;
}
if !root.heading: Text {
y: 34px; height: 17px; width: parent.width;
text: root.device.fps; font-size: 11px; color: #7a8490;
vertical-alignment: center; overflow: elide;
}
}
Rectangle {
width: max(100px, root.width * 0.18);
if root.heading: Text {
x: 0px; width: parent.width;
text: GalleryStrings.tr("收发速率", "Transfer rate", "Скорость");
font-size: 11px; font-weight: 600; color: #6b7785;
vertical-alignment: center; overflow: elide;
}
if !root.heading: Text {
x: 0px; y: 11px; height: 20px; width: 14px;
text: "↓"; color: #2676b9; font-size: 12px; vertical-alignment: center;
}
if !root.heading: Text {
x: 17px; y: 11px; height: 20px; width: parent.width - 17px;
text: root.device.inbound; color: #293746; font-size: 12px;
vertical-alignment: center; overflow: elide;
}
if !root.heading: Text {
x: 0px; y: 34px; height: 17px; width: 14px;
text: "↑"; color: #7a8490; font-size: 11px; vertical-alignment: center;
}
if !root.heading: Text {
x: 17px; y: 34px; height: 17px; width: parent.width - 17px;
text: root.device.outbound; color: #7a8490; font-size: 11px;
vertical-alignment: center; overflow: elide;
}
}
Rectangle {
width: max(94px, root.width * 0.16);
Text {
y: root.heading ? 0px : 11px; height: root.heading ? parent.height : 20px;
width: parent.width;
text: root.heading ? GalleryStrings.tr("网络质量", "Network quality", "Качество сети") : "RTT " + root.device.latency;
font-size: 11px; font-weight: root.heading ? 600 : 400;
color: root.heading ? #6b7785 : #293746;
vertical-alignment: center; overflow: elide;
}
if !root.heading: Text {
y: 34px; height: 17px; width: parent.width;
text: GalleryStrings.tr("丢包 ", "Loss ", "Потери ") + root.device.loss;
font-size: 11px; color: #7a8490;
vertical-alignment: center; overflow: elide;
}
}
}
if !root.heading && name-hover.has-hover && device-name.preferred-width > identity.width: Rectangle {
x: 12px; y: 32px; width: min(name-tip.preferred-width + 20px, root.width - 24px);
height: 26px; background: #293746; border-radius: 4px; z: 10;
name-tip := Text {
x: 10px; width: parent.width - 20px; height: parent.height;
text: root.device.name; font-size: 11px; color: white;
vertical-alignment: center; overflow: elide;
}
}
}
component NetworkEmptyState inherits Rectangle {
VerticalLayout {
alignment: center; spacing: 8px;
Text {
text: GalleryStrings.tr("暂无已连接设备", "No connected devices", "Нет подключённых устройств");
font-size: 13px; color: #596878; horizontal-alignment: center;
}
Text {
text: GalleryStrings.tr("建立连接后,网络和画面信息将在此显示", "Network and video details appear here once connected", "После подключения здесь появятся данные сети и видео");
font-size: 11px; color: #8a95a1; horizontal-alignment: center;
}
}
}
export component GalleryNetworkDialog inherits Rectangle {
in property <[GalleryNetworkDevice]> devices;
callback close();
background: #00000042;
TouchArea { clicked => { root.close(); } }
Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(960px, parent.width - 32px);
height: min(560px, parent.height - 24px);
background: white; border-radius: 8px;
border-width: 1px; border-color: #c6cdd5;
TouchArea {}
FocusScope {
init => { self.focus(); }
key-pressed(event) => {
if event.text == Key.Escape { root.close(); }
accept
}
}
Text {
x: 20px; y: 17px; width: parent.width - 120px; height: 24px;
text: GalleryStrings.tr("设备网络状态", "Device network status", "Сеть устройств");
font-size: 17px; font-weight: 600; color: #293746;
}
CompactButton {
x: parent.width - 80px; y: 17px; width: 60px; height: 26px;
text: GalleryStrings.tr("关闭", "Close", "Закрыть"); clicked => { root.close(); }
}
Rectangle {
x: 20px; y: 54px; width: 6px; height: 6px; border-radius: 3px;
background: root.devices.length > 0 ? #27a168 : #a7b0ba;
}
Text {
x: 32px; y: 48px; width: parent.width - 52px; height: 18px;
text: GalleryStrings.tr("已连接设备 · ", "Connected devices · ", "Подключённые устройства · ") + root.devices.length
+ GalleryStrings.tr(" 台 · 每秒更新", " · Updates every second", " · Обновление раз в секунду");
font-size: 11px; color: #7a8490; overflow: elide;
}
NetworkRow {
x: 12px; y: 78px; width: parent.width - 38px; height: 30px;
heading: true; background: #f3f5f8; border-radius: 4px;
device: {
name: GalleryStrings.tr("设备 / ID", "Device / ID", "Устройство / ID"),
};
}
list := ScrollView {
x: 12px; y: 108px; width: parent.width - 24px;
height: parent.height - self.y - 12px;
viewport-width: self.width - 14px;
viewport-height: max(self.height, root.devices.length * 62px);
horizontal-scrollbar-policy: always-off;
for device[index] in root.devices: NetworkRow {
y: index * 62px; width: list.viewport-width; height: 62px;
device: device; background: mod(index, 2) == 0 ? white : #f8fafc;
Rectangle { y: parent.height - 1px; height: 1px; background: #edf0f4; }
}
}
if root.devices.length == 0: NetworkEmptyState {
x: list.x; y: list.y; width: list.width; height: list.height;
}
}
}
@@ -0,0 +1,36 @@
import { GalleryStrings } from "device_strings.slint";
import { SettingsGroup, SettingsRow, SettingsSelect } from "settings_controls.slint";
export component GallerySettings inherits Rectangle {
in-out property <int> resolution-index: 0;
in-out property <int> frame-rate-index: 0;
in property <string> error;
SettingsGroup {
y: 0px; width: parent.width;
title: GalleryStrings.tr("画面传输", "Video transmission", "Передача видео"); rows: 2;
SettingsRow {
label: GalleryStrings.tr("分辨率上限", "Resolution limit", "Разрешение");
label-reserve: self.width - 112px;
SettingsSelect {
x: 136px; y: 4px; width: parent.width - 150px;
current-index <=> root.resolution-index;
model: [GalleryStrings.tr("自动", "Auto", "Авто"),
"320 × 180", "640 × 360", "1280 × 720", "1920 × 1080", "2560 × 1440", "3840 × 2160",
GalleryStrings.tr("原始分辨率", "Original resolution", "Исходное разрешение")];
}
}
SettingsRow {
label: GalleryStrings.tr("帧率", "Frame rate", "Частота кадров");
label-reserve: self.width - 112px; divider: false;
SettingsSelect {
x: 136px; y: 4px; width: parent.width - 150px;
current-index <=> root.frame-rate-index;
model: [GalleryStrings.tr("跟随设备设置", "Follow device settings", "Настройки устройства"), "15 FPS", "30 FPS", "60 FPS"];
}
}
}
if root.error != "": Text {
x: 2px; y: 108px; width: parent.width - 4px; height: 32px;
text: root.error; color: #c53e3e; font-size: 11px; wrap: word-wrap;
}
}
+49 -28
View File
@@ -29,6 +29,8 @@ export struct GalleryActionDevice {
}
export component GalleryView inherits Rectangle {
in property <bool> controlling: false;
in property <bool> simplified: false;
private property <length> grid-gap: root.simplified ? 6px : 12px;
// 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;
@@ -63,6 +65,8 @@ export component GalleryView inherits Rectangle {
in property <int> count: 0;
in property <int> connected-count: 0;
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;
@@ -91,7 +95,7 @@ export component GalleryView inherits Rectangle {
}
toolbar := HorizontalLayout {
x: 29px; y: 12px; width: parent.width - 58px; height: 24px;
visible: root.focused.id == "";
visible: root.focused.id == "" && !root.simplified;
spacing: 8px;
GroupFilter { width: 155px; height: 24px; system-count: 1; model: root.groups; current-index <=> root.group-index;
selected(index) => { root.action("group", index); }
@@ -102,30 +106,35 @@ export component GalleryView inherits Rectangle {
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;
x: root.simplified ? 12px : toolbar.x;
y: root.simplified ? 12px : toolbar.y + toolbar.height + 12px;
width: parent.width - self.x * 2;
height: (root.simplified && root.pages <= 1 ? parent.height - 12px : 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;
x: mod(index, root.columns) * (grid.width + root.grid-gap) / root.columns;
y: floor(index / root.columns) * (grid.height + root.grid-gap) / root.rows;
width: (grid.width - (root.columns - 1) * root.grid-gap) / root.columns;
height: (grid.height - (root.rows - 1) * root.grid-gap) / root.rows;
border-radius: root.simplified ? 0px : 6px;
background: root.simplified ? #24272d : tile.selected ? #eaf3ff : #eff0f2;
border-width: root.simplified ? 0px : 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;
x: root.simplified ? 0px : 6px; y: self.x;
width: parent.width - self.x * 2;
height: root.simplified ? parent.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 {
if !root.simplified && (!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; }
Text { visible: !root.simplified; 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;
visible: !root.simplified && (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;
@@ -135,7 +144,7 @@ export component GalleryView inherits Rectangle {
color: tile.online ? #17854c : #62666d; font-size: 10px; vertical-alignment: center;
}
}
if root.batch: Rectangle {
if root.batch && !root.simplified: 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;
@@ -148,8 +157,8 @@ export component GalleryView inherits Rectangle {
}
}
tile-touch := TouchArea {
clicked => { root.action(root.batch ? "select" : "focus", tile.id); }
if tile-touch.has-hover && !root.batch: DeviceCardActions {
clicked => { root.action(root.batch && !root.simplified ? "select" : "focus", tile.id); }
if tile-touch.has-hover && !root.batch && !root.simplified: DeviceCardActions {
x: parent.width - self.width - 6px; y: parent.height - 26px;
remove-label: GalleryStrings.tr("从桌面墙移除", "Remove from DeskHub", "Убрать из DeskHub");
connected: tile.connected;
@@ -162,14 +171,16 @@ export component GalleryView inherits Rectangle {
}
}
if root.count == 0: Text {
text: GalleryStrings.tr("添加设备,建立你的桌面墙", "Add devices to build your DeskHub", "Добавьте устройства в DeskHub");
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: 7px; width: parent.width - 58px;
height: parent.height - 51px;
height: parent.height - (root.simplified && root.pages <= 1 ? 19px : 51px);
HorizontalLayout {
x: 0px; y: 0px; width: parent.width; height: 28px; spacing: 8px;
CompactButton { width: 52px; text: GalleryStrings.tr("返回", "Back", "Назад"); clicked => { root.action("focus", ""); } }
@@ -256,20 +267,21 @@ export component GalleryView inherits Rectangle {
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;
x: 5px; y: 5px; width: 126px; height: root.simplified ? 82px : 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 {
if !root.simplified && (!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 { visible: !root.simplified; x: 7px; y: 61px; width: 122px; height: 16px; text: tile.name; color: #303943; font-size: 11px; overflow: elide; }
Text {
visible: !root.simplified;
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 {
if !root.simplified && 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;
}
@@ -280,8 +292,9 @@ export component GalleryView inherits Rectangle {
// 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 {
x: toolbar.x; y: parent.height - 36px; width: toolbar.width; height: 24px; spacing: root.batch ? 8px : 12px;
visible: !root.simplified || root.pages > 1;
if root.batch && !root.simplified: CompactButton {
width: 90px; enabled: root.count > 0;
text: root.count > 0 && root.selected-count == root.count
? GalleryStrings.tr("取消全选", "Deselect all", "Снять выбор")
@@ -291,20 +304,28 @@ export component GalleryView inherits Rectangle {
Rectangle {
horizontal-stretch: 1; min-width: 0px;
Text {
visible: !root.simplified;
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 {
if root.batch && !root.simplified && root.hidden-selected > 0: Text {
y: 14px; width: parent.width; height: 10px;
text: GalleryStrings.tr("跨页 ", "Other pages ", "Другие страницы ") + root.hidden-selected;
text: GalleryStrings.tr("跨页 ", "Other pages ", "Вне страницы ") + root.hidden-selected;
color: #80858d; font-size: 10px; overflow: elide;
}
}
if root.batch: HorizontalLayout {
width: 214px; spacing: 8px;
if root.batch && !root.simplified: HorizontalLayout {
width: 280px; 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: 58px; enabled: root.selected-audio-available;
text: root.selected-audio-muted
? GalleryStrings.tr("取消静音", "Unmute", "Со звуком")
: GalleryStrings.tr("静音", "Mute", "Без звука");
clicked => { root.action(root.selected-audio-muted ? "unmute" : "mute", ""); }
}
CompactButton { width: 78px; enabled: root.selected-count > 0; text: GalleryStrings.tr("分发文件", "Send file", "Файл"); clicked => { root.confirm-action("file", self.text); } }
}
HorizontalLayout {
+203 -140
View File
@@ -2,6 +2,9 @@ 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 { GalleryNetworkDialog, GalleryNetworkDevice } from "gallery_network_dialog.slint";
import { SettingsButton, SettingsSelect, SettingsGroup, SettingsRow, SettingsToggleRow } from "settings_controls.slint";
import { GallerySettings } from "gallery_settings.slint";
import { GroupFilter } from "group_filter.slint";
import { LineEdit, ScrollView } from "std-widgets.slint";
import { AnnouncementItem, MessagePanel, MessageStrings } from "message_panel.slint";
@@ -303,6 +306,22 @@ export component MainWindow inherits Window {
default-font-size: ImGuiFontStyle.base;
in-out property <bool> gallery-mode: false;
in-out property <bool> gallery-simplified: false;
changed gallery-simplified => {
root.reset-gallery-transient();
root.gallery-action("view-mode", "");
focus-sink.focus();
}
in-out property <bool> gallery-network-open: false;
in property <int> gallery-resolution-index: 0;
in property <int> gallery-frame-rate-index: 0;
in-out property <int> gallery-resolution-draft: 0;
in-out property <int> gallery-frame-rate-draft: 0;
in property <string> gallery-settings-error;
in property <[GalleryNetworkDevice]> gallery-network-devices;
changed gallery-network-open => {
if root.gallery-network-open { root.menu-open = false; root.gallery-action("network-open", ""); }
}
in property <bool> gallery-fullscreen: false;
in property <bool> gallery-transitioning: false;
in property <bool> gallery-content-visible: true;
@@ -330,6 +349,8 @@ export component MainWindow inherits Window {
in property <int> gallery-count: 0;
in property <int> gallery-connected: 0;
in property <int> gallery-selected: 0;
in property <bool> gallery-selected-audio-available: false;
in property <bool> gallery-selected-audio-muted: false;
in property <int> gallery-hidden-selected: 0;
in property <string> gallery-notice;
in property <[GalleryActionDevice]> gallery-selected-devices;
@@ -366,7 +387,7 @@ export component MainWindow inherits Window {
root.device-group-open = true;
}
public function reset-gallery-transient() { gallery.reset-transient(); root.menu-open = false; }
public function reset-gallery-transient() { gallery.reset-transient(); root.menu-open = false; root.gallery-network-open = false; }
in property <bool> custom-titlebar: false;
in property <bool> wayland-titlebar: false;
in property <bool> window-active: true;
@@ -503,6 +524,7 @@ export component MainWindow inherits Window {
private property <bool> menu-open: false;
in-out property <bool> settings-open: false;
in-out property <int> settings-tab: 0;
private property <length> settings-scroll-y: 0px;
in-out property <bool> self-host-settings-open: false;
changed self-host-settings-open => {
@@ -544,7 +566,13 @@ export component MainWindow inherits Window {
changed permission-dialog-open => { if root.permission-dialog-open { root.gallery-action("release", ""); } }
changed settings-open => {
if root.settings-open {
root.settings-tab = 0;
root.settings-scroll-y = 0px;
root.menu-open = false;
root.gallery-network-open = false;
root.gallery-resolution-draft = root.gallery-resolution-index;
root.gallery-frame-rate-draft = root.gallery-frame-rate-index;
root.gallery-action("settings-open", "");
focus-sink.focus();
}
}
@@ -780,6 +808,10 @@ export component MainWindow inherits Window {
if event.text == Key.Escape { root.messages-open = false; }
return accept;
}
if root.settings-open && !root.self-host-settings-open {
if event.text == Key.Escape { root.cancel-settings(); root.settings-open = false; }
return accept;
}
return reject;
}
}
@@ -915,6 +947,20 @@ export component MainWindow inherits Window {
}
}
}
if root.gallery-mode: CompactButton {
x: menu-button.x - 87px - 16px - 94px - 8px - self.width;
y: mode-switch.y; width: 94px; height: mode-switch.height;
primary: root.gallery-simplified;
text: root.gallery-simplified ? GalleryStrings.tr("退出精简", "Exit compact", "Обычный вид")
: GalleryStrings.tr("精简模式", "Compact", "Компактно");
clicked => { root.gallery-simplified = !root.gallery-simplified; }
}
if root.gallery-mode: CompactButton {
x: menu-button.x - 87px - 16px - self.width;
y: mode-switch.y; width: 94px; height: mode-switch.height;
text: GalleryStrings.tr("网络状态", "Network", "Сеть");
clicked => { root.gallery-network-open = true; }
}
if root.gallery-mode: CompactButton {
x: menu-button.x - self.width - 8px;
y: mode-switch.y;
@@ -1327,8 +1373,10 @@ export component MainWindow inherits Window {
}
gallery := GalleryView {
y: root.content-header-height; width: parent.width; height: parent.height - self.y - 24px;
y: root.content-header-height; width: parent.width;
height: parent.height - self.y - (root.gallery-simplified ? 0px : 24px);
visible: root.gallery-mode;
simplified: root.gallery-simplified;
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); }
@@ -1338,6 +1386,8 @@ export component MainWindow inherits Window {
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;
selected-audio-available: root.gallery-selected-audio-available;
selected-audio-muted: root.gallery-selected-audio-muted;
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;
@@ -1356,6 +1406,7 @@ export component MainWindow inherits Window {
}
// Status bar.
Rectangle {
visible: !root.gallery-mode || !root.gallery-simplified;
y: parent.height - 24px;
height: 24px;
background: #fafbfc;
@@ -1930,172 +1981,176 @@ export component MainWindow inherits Window {
}
}
// Settings modal.
// Both tabs share one draft and the same save/cancel actions.
if root.settings-open: Rectangle {
background: transparent;
background: #15223850;
z: 45;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: root.compact-language ? 219px : 190px;
y: 60px;
width: root.compact-language ? 202px : 269px;
height: 330px;
border-radius: 7px;
Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: 21px;
background: #d3d3d3;
border-radius: 7px;
z: 5;
Rectangle { y: parent.height - 7px; height: 7px; background: #d3d3d3; }
Text { x: 6px; text: UiStrings.settings; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(400px, parent.width - 40px); height: 446px;
background: #f8fafc; border-radius: 14px; border-color: #e1e7ef;
drop-shadow-color: #14294424; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px;
TouchArea { clicked => { focus-sink.focus(); } }
Text {
x: 18px; y: 18px; width: parent.width - 80px; height: 27px;
text: UiStrings.settings; font-size: 20px; font-weight: 600; color: #22324a;
}
settings-scroll := ScrollView {
x: 0px;
y: 21px;
width: parent.width;
height: 265px;
viewport-y <=> root.settings-scroll-y;
settings-content := VerticalLayout {
// Reserve space for the overlaid vertical scrollbar.
width: settings-scroll.width;
padding-top: 8px;
padding-bottom: 8px;
spacing: 0px;
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
SettingsButton {
x: parent.width - 90px; y: 18px; width: 76px; height: 28px; quiet: true;
text: GalleryStrings.tr("关闭", "Close", "Закрыть");
clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; }
}
Rectangle {
x: 18px; y: 56px; width: parent.width - 36px; height: 40px;
background: #eaf0f6; border-radius: 9px;
Rectangle {
x: 4px + root.settings-tab * (parent.width - 8px) / 2;
y: 4px; width: (parent.width - 8px) / 2; height: parent.height - 8px;
background: white; border-radius: 6px;
drop-shadow-color: #1e3e6520; drop-shadow-blur: 3px; drop-shadow-offset-y: 1px;
animate x { duration: 140ms; easing: ease-out; }
}
for label[index] in [GalleryStrings.tr("常规", "General", "Общие"), GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub")]: Rectangle {
x: 4px + index * (parent.width - 8px) / 2;
y: 4px; width: (parent.width - 8px) / 2; height: parent.height - 8px;
border-radius: 6px;
background: tab-touch.has-hover && root.settings-tab != index ? #e1e8f1 : transparent;
accessible-role: button; accessible-label: label;
accessible-action-default => { tab-touch.clicked(); }
Text {
text: label; font-size: 12px; font-weight: root.settings-tab == index ? 600 : 400;
color: root.settings-tab == index ? #2e70cf : #718096;
horizontal-alignment: center; vertical-alignment: center;
}
if root.capture-method-visible: Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.capture-method; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.capture-method-auto, "DXGI", "WGC", "GDI"]; current-index <=> root.capture-method-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.hardware-codec; color: root.hardware-codec-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.force-relay; color: root.turn-enabled ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.force-relay-enabled; enabled: root.turn-enabled && !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.save-remote-previews; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.save-remote-previews; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.privacy-on-connect; color: root.privacy-setting-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; }
}
Rectangle {
height: 30px;
CompactButton { x: 8px; y: 3px; width: root.compact-language ? 70px : 206px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.autostart; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 154px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactButton { x: root.compact-language ? 106px : 166px; y: 3px; width: root.compact-language ? 73px : 80px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
tab-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.settings-tab = index; focus-sink.focus(); }
}
}
}
Rectangle { x: 7px; y: 286px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactButton { x: root.compact-language ? 63px : 89px; y: 297px; primary: true; text: UiStrings.ok; clicked => { if root.save-settings() { root.settings-open = false; root.self-host-settings-open = false; } } }
CompactButton { x: root.compact-language ? 104px : 130px; y: 297px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
if root.settings-tab == 0: settings-scroll := ScrollView {
x: 18px; y: 108px; width: parent.width - 28px; height: 272px;
viewport-y <=> root.settings-scroll-y;
VerticalLayout {
width: settings-scroll.width - 12px;
spacing: 14px; padding-bottom: 4px;
SettingsGroup {
title: GalleryStrings.tr("界面", "Interface", "Интерфейс"); rows: 1;
SettingsRow {
label: UiStrings.language; divider: false;
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
}
}
SettingsGroup {
title: GalleryStrings.tr("视频", "Video", "Видео");
rows: root.capture-method-visible ? 3 : 2;
if root.capture-method-visible: SettingsRow {
label: UiStrings.capture-method; enabled: !root.settings-session-active;
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; enabled: !root.settings-session-active; model: [UiStrings.capture-method-auto, "DXGI", "WGC", "GDI"]; current-index <=> root.capture-method-index; }
}
SettingsRow {
label: UiStrings.codec; enabled: !root.settings-session-active;
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
}
SettingsToggleRow { label: UiStrings.hardware-codec; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; divider: false; }
}
SettingsGroup {
title: GalleryStrings.tr("网络连接", "Network", "Сеть"); rows: 3;
SettingsToggleRow { label: UiStrings.turn-relay; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
SettingsToggleRow { label: UiStrings.force-relay; checked <=> root.force-relay-enabled; enabled: root.turn-enabled && !root.settings-session-active; }
SettingsToggleRow {
checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; divider: false;
SettingsButton { x: 0px; y: 2px; width: parent.width - 60px; height: 32px; quiet: true; align-left: true; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
}
}
SettingsGroup {
title: GalleryStrings.tr("隐私保护", "Privacy", "Приватность"); rows: 1;
SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; divider: false; }
}
SettingsGroup {
title: GalleryStrings.tr("本地存储", "Local storage", "Локальное хранение"); rows: 2;
SettingsToggleRow { label: UiStrings.save-remote-previews; checked <=> root.save-remote-previews; }
SettingsRow {
label: UiStrings.file-save-path; enabled: !root.settings-session-active; divider: false;
SettingsButton { x: parent.width - 116px; y: 4px; width: 104px; height: 28px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
}
}
SettingsGroup {
title: GalleryStrings.tr("启动与后台", "Startup & background", "Запуск и фоновая работа"); rows: 2;
SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
SettingsToggleRow { label: UiStrings.daemon; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; divider: false; }
}
}
}
if root.settings-tab == 1: GallerySettings {
x: 18px; y: 108px; width: parent.width - 36px; height: 272px;
resolution-index <=> root.gallery-resolution-draft;
frame-rate-index <=> root.gallery-frame-rate-draft;
error: root.gallery-settings-error;
}
Rectangle { x: 18px; y: 382px; width: parent.width - 36px; height: 1px; background: #e6ebf2; }
HorizontalLayout {
x: 18px; y: 396px; width: parent.width - 36px; height: 32px; spacing: 10px;
if root.settings-tab == 1: SettingsButton {
width: 110px; quiet: true;
text: GalleryStrings.tr("恢复默认", "Reset defaults", "По умолчанию");
clicked => { root.gallery-resolution-draft = 0; root.gallery-frame-rate-draft = 0; }
}
Rectangle { horizontal-stretch: 1; }
SettingsButton {
width: 80px; text: UiStrings.cancel;
clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; }
}
SettingsButton {
width: 92px; primary: true;
text: GalleryStrings.tr("保存", "Save", "Сохранить");
clicked => { if root.save-settings() { root.settings-open = false; root.self-host-settings-open = false; } }
}
}
}
}
if root.self-host-settings-open: Rectangle {
background: transparent;
background: #15223835;
z: 49;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: root.compact-language ? 190px : 140px;
y: 105px;
width: root.compact-language ? 260px : 360px;
height: root.self-host-settings-error == "" ? 138px : 202px;
border-radius: 7px;
Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: 21px;
background: #d3d3d3;
border-radius: 7px;
z: 5;
Rectangle { y: parent.height - 7px; height: 7px; background: #d3d3d3; }
Text { x: 6px; text: UiStrings.self-hosted-settings; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 360px; height: root.self-host-settings-error == "" ? 266px : 322px;
border-radius: 12px; border-color: #e1e7ef; background: #f8fafc;
drop-shadow-color: #14294424; drop-shadow-blur: 18px; drop-shadow-offset-y: 6px;
TouchArea { clicked => { focus-sink.focus(); } }
Text {
x: 18px; y: 18px; width: parent.width - 36px; height: 24px;
text: UiStrings.self-hosted-settings; font-size: 16px; font-weight: 600; color: #22324a;
}
Text { x: 8px; y: 31px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.server-host; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
Text { x: 18px; y: 56px; width: parent.width - 36px; height: 20px; text: UiStrings.server-host; color: #34445a; font-size: 12px; overflow: elide; vertical-alignment: center; }
Rectangle {
x: root.compact-language ? 100px : 173px;
y: 30px;
width: root.compact-language ? 152px : 179px;
height: 24px;
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: 4px;
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-host; input-type: InputType.text; font-size: ImGuiFontStyle.body; }
x: 18px; y: 80px; width: parent.width - 36px; height: 30px;
background: white; border-width: 1px; border-color: #dfe6ef; border-radius: 6px;
LineEdit { x: 1px; y: 2px; width: parent.width - 2px; height: parent.height - 4px; text <=> root.server-host; input-type: InputType.text; font-size: 12px; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
}
Text { x: 8px; y: 61px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.server-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
Text { x: 18px; y: 120px; width: parent.width - 36px; height: 20px; text: UiStrings.server-port; color: #34445a; font-size: 12px; overflow: elide; vertical-alignment: center; }
Rectangle {
x: root.compact-language ? 100px : 173px;
y: 60px;
width: root.compact-language ? 152px : 179px;
height: 24px;
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: 4px;
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
x: 18px; y: 144px; width: parent.width - 36px; height: 30px;
background: white; border-width: 1px; border-color: #dfe6ef; border-radius: 6px;
LineEdit { x: 1px; y: 2px; width: parent.width - 2px; height: parent.height - 4px; text <=> root.server-port; input-type: InputType.number; font-size: 12px; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
}
if root.self-host-settings-error != "": Text {
x: 8px; y: 89px; width: parent.width - 16px; height: 64px;
text: root.self-host-settings-error; color: #b3261e;
font-size: ImGuiFontStyle.body; wrap: word-wrap;
x: 18px; y: 186px; width: parent.width - 36px; height: 48px;
text: root.self-host-settings-error; color: #c53e3e; font-size: 11px; wrap: word-wrap;
}
CompactButton { x: root.compact-language ? 93px : 135px; y: parent.height - 38px; primary: true; text: UiStrings.ok; clicked => { if root.save-self-hosted-settings() { root.self-host-settings-open = false; } } }
CompactButton { x: root.compact-language ? 134px : 176px; y: parent.height - 38px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
Rectangle { x: 18px; y: parent.height - 66px; width: parent.width - 36px; height: 1px; background: #e6ebf2; }
SettingsButton { x: parent.width - 114px; y: parent.height - 50px; width: 92px; primary: true; text: UiStrings.ok; clicked => { if root.save-self-hosted-settings() { root.self-host-settings-open = false; } } }
SettingsButton { x: parent.width - 204px; y: parent.height - 50px; width: 80px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
}
}
// About modal.
if root.about-open: Rectangle {
background: transparent;
z: 46;
@@ -2455,6 +2510,14 @@ export component MainWindow inherits Window {
}
}
if root.gallery-mode && root.gallery-network-open: GalleryNetworkDialog {
x: 0px; y: root.titlebar-height;
width: root.width; height: root.height - self.y;
z: 100;
devices: root.gallery-network-devices;
close => { root.gallery-network-open = false; focus-sink.focus(); }
}
// Ignore clicks against moving cards and duplicate fullscreen requests.
if root.gallery-transitioning: TouchArea {
x: 0px; y: root.titlebar-height;
@@ -0,0 +1,73 @@
import { CompactComboBox, ToggleSwitch } from "common.slint";
// Shared visual treatment for the global settings pages.
export component SettingsButton inherits Rectangle {
in property <string> text;
in property <bool> primary: false;
in property <bool> quiet: false;
in property <bool> align-left: false;
in property <bool> enabled: true;
callback clicked();
width: 88px; height: 32px; border-radius: 7px;
background: !root.enabled ? #f0f3f7
: root.primary ? (touch.pressed ? #1e4fa3 : touch.has-hover ? #2866c9 : #3275dc)
: touch.pressed ? #e9eef5 : touch.has-hover ? #f0f4fa : root.quiet ? transparent : white;
border-width: root.quiet || root.primary ? 0px : 1px;
border-color: #dce3ec;
accessible-role: button; accessible-label: root.text;
accessible-action-default => { if root.enabled { root.clicked(); } }
Text {
x: 12px; width: parent.width - 24px;
text: root.text; font-size: 12px; font-weight: root.primary ? 600 : 400;
color: !root.enabled ? #98a2b3 : root.primary ? white : root.quiet ? #65758b : #3c4d63;
horizontal-alignment: root.align-left ? left : center; vertical-alignment: center; overflow: elide;
}
touch := TouchArea { enabled: root.enabled; mouse-cursor: pointer; clicked => { root.clicked(); } }
}
export component SettingsSelect inherits CompactComboBox {
subtle: true;
show-arrow: false;
height: 28px;
}
export component SettingsGroup inherits Rectangle {
in property <string> title;
in property <int> rows: 1;
height: 24px + root.rows * 36px;
Text {
x: 2px; y: 0px; width: parent.width - 4px; height: 18px; text: root.title;
font-size: 11px; font-weight: 600; color: #718096; vertical-alignment: center;
}
Rectangle {
y: 24px; width: parent.width; height: parent.height - self.y;
background: white; border-radius: 9px; border-color: #e5eaf1; border-width: 1px;
VerticalLayout { padding: 0px; spacing: 0px; @children }
}
}
export component SettingsRow inherits Rectangle {
in property <string> label;
in property <bool> enabled: true;
in property <bool> divider: true;
in property <length> label-reserve: 128px;
height: 36px;
Text {
x: 12px; width: parent.width - root.label-reserve; height: parent.height;
text: root.label; color: root.enabled ? #34445a : #9ba5b3;
font-size: 12px; overflow: elide; vertical-alignment: center;
}
if root.divider: Rectangle {
x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; background: #eff2f6;
}
@children
}
export component SettingsToggleRow inherits SettingsRow {
in-out property <bool> checked;
label-reserve: 64px;
ToggleSwitch {
x: parent.width - self.width - 12px; y: (parent.height - self.height) / 2;
checked <=> root.checked; enabled: root.enabled;
}
}