mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-04 04:52:39 +08:00
[feat] add trusted device pairing and management UI
This commit is contained in:
@@ -12,3 +12,5 @@ export { GalleryStrings } from "device_strings.slint";
|
||||
export { LanStrings } from "lan_strings.slint";
|
||||
export { GalleryNetworkDevice } from "gallery_network_dialog.slint";
|
||||
export { DeviceGroupOption } from "device_group_dialog.slint";
|
||||
|
||||
export { TrustedDeviceEntry, TrustStrings } from "trusted_devices.slint";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SurfaceSeparator, FlatSurface, UiSurface } from "ui_surface.slint";
|
||||
import { TrustedDevicesDialog, TrustedDeviceEntry, TrustStrings } from "trusted_devices.slint";
|
||||
import { LanStrings } from "lan_strings.slint";
|
||||
import { AboutPanel, AboutPage, AboutComponent, AboutStrings, AboutNavigationRow, AboutNavigationGroup, AboutRowSeparator } from "about_panel.slint";
|
||||
import { UiRadius } from "ui_radius.slint";
|
||||
@@ -660,6 +661,17 @@ export component MainWindow inherits Window {
|
||||
in-out property <bool> privacy-local-unlock-allowed: true;
|
||||
in property <bool> lock-on-disconnect-available: false;
|
||||
in-out property <bool> lock-on-disconnect-enabled: true;
|
||||
in-out property <bool> trusted-devices-only: false;
|
||||
in-out property <bool> trusted-devices-open: false;
|
||||
in property <[TrustedDeviceEntry]> trusted-devices;
|
||||
in property <[TrustedDeviceEntry]> trust-candidates;
|
||||
in property <string> device-fingerprint;
|
||||
in property <string> trusted-devices-error;
|
||||
in property <string> trusted-devices-notice;
|
||||
in property <string> trust-copy-error;
|
||||
in property <string> trust-copy-notice;
|
||||
in-out property <string> trust-pairing-input;
|
||||
callback trust-action(string, string, string);
|
||||
in-out property <string> file-save-path: "";
|
||||
in-out property <string> server-host: "";
|
||||
in-out property <string> server-port: "";
|
||||
@@ -1087,7 +1099,7 @@ 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 root.settings-open && !root.self-host-settings-open && !root.trusted-devices-open {
|
||||
if event.text == Key.Escape { root.cancel-settings(); root.settings-open = false; }
|
||||
return accept;
|
||||
}
|
||||
@@ -2758,10 +2770,20 @@ export component MainWindow inherits Window {
|
||||
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
|
||||
}
|
||||
SettingsGroup {
|
||||
title: GalleryStrings.texts[120]; rows: 3;
|
||||
title: GalleryStrings.texts[120]; rows: 5;
|
||||
SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; }
|
||||
SettingsToggleRow { label: UiStrings.privacy-local-unlock; checked <=> root.privacy-local-unlock-allowed; enabled: root.privacy-setting-available; }
|
||||
SettingsToggleRow { label: UiStrings.lock-on-disconnect; checked <=> root.lock-on-disconnect-enabled; enabled: root.lock-on-disconnect-available; divider: false; }
|
||||
SettingsToggleRow { label: UiStrings.lock-on-disconnect; checked <=> root.lock-on-disconnect-enabled; enabled: root.lock-on-disconnect-available; }
|
||||
SettingsToggleRow { label: TrustStrings.only; checked <=> root.trusted-devices-only; }
|
||||
SettingsRow {
|
||||
label: TrustStrings.title; divider: false;
|
||||
SettingsButton {
|
||||
x: parent.width - self.width - 12px; y: 4px;
|
||||
width: max(104px, self.label-width); height: 28px;
|
||||
text: TrustStrings.manage;
|
||||
clicked => { root.trusted-devices-open = true; root.trust-action("open", "", ""); }
|
||||
}
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: GalleryStrings.texts[121];
|
||||
@@ -2871,6 +2893,20 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
if root.trusted-devices-open: TrustedDevicesDialog {
|
||||
z: 51;
|
||||
devices: root.trusted-devices;
|
||||
candidates: root.trust-candidates;
|
||||
local-fingerprint: root.device-fingerprint;
|
||||
error: root.trusted-devices-error;
|
||||
notice: root.trusted-devices-notice;
|
||||
copy-error: root.trust-copy-error;
|
||||
copy-notice: root.trust-copy-notice;
|
||||
pairing-input <=> root.trust-pairing-input;
|
||||
close => { root.trust-action("close", "", ""); root.trusted-devices-open = false; }
|
||||
action(action, id, key) => { root.trust-action(action, id, key); }
|
||||
}
|
||||
|
||||
if root.self-host-settings-open: Rectangle {
|
||||
background: #15223835;
|
||||
z: 49;
|
||||
|
||||
@@ -6,11 +6,13 @@ import { WorkbenchStyle } from "workbench_controls.slint";
|
||||
// Shared visual treatment for the global settings pages.
|
||||
export component SettingsButton inherits FlatSurface {
|
||||
in property <string> text;
|
||||
in property <length> text-size: 12px;
|
||||
in property <image> icon;
|
||||
in property <bool> primary: false;
|
||||
in property <bool> quiet: false;
|
||||
in property <bool> align-left: false;
|
||||
in property <bool> enabled: true;
|
||||
in property <bool> destructive: false;
|
||||
out property <length> label-width: button-label.preferred-width + 2 * root.content-padding + (root.has-icon ? 24px : 0px);
|
||||
private property <bool> has-icon: root.icon.width > 0;
|
||||
private property <length> content-padding: root.has-icon ? 8px : 12px;
|
||||
@@ -18,7 +20,8 @@ export component SettingsButton inherits FlatSurface {
|
||||
width: 88px; height: 32px; border-radius: UiRadius.control;
|
||||
background: !root.enabled ? ControlStyle.disabled
|
||||
: root.primary ? (touch.pressed ? #1e4fa3 : touch.has-hover ? #2866c9 : #3275dc)
|
||||
: touch.pressed ? ButtonStyle.pressed : touch.has-hover ? ButtonStyle.hover
|
||||
: touch.pressed ? (root.destructive ? #f6babb : ButtonStyle.pressed)
|
||||
: touch.has-hover ? (root.destructive ? #fbe0e0 : ButtonStyle.hover)
|
||||
: root.quiet ? transparent : ButtonStyle.surface;
|
||||
outline-enabled: !root.quiet && !root.primary;
|
||||
outline-color: !root.enabled ? ButtonStyle.disabled-border
|
||||
@@ -44,8 +47,9 @@ export component SettingsButton inherits FlatSurface {
|
||||
button-label := Text {
|
||||
x: root.has-icon ? 24px : 0px;
|
||||
width: parent.width - self.x; height: parent.height;
|
||||
text: root.text; font-size: 12px; font-weight: root.primary ? 600 : 400;
|
||||
color: !root.enabled ? #98a2b3 : root.primary ? white : root.quiet ? #65758b : #3c4d63;
|
||||
text: root.text; font-size: root.text-size; font-weight: root.primary ? 600 : 400;
|
||||
color: !root.enabled ? #98a2b3 : root.primary ? white
|
||||
: root.destructive ? #c43c43 : root.quiet ? #65758b : #3c4d63;
|
||||
horizontal-alignment: root.align-left ? left : center; vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import { SettingsDialog, SettingsButton } from "settings_controls.slint";
|
||||
import { WorkbenchStyle } from "workbench_controls.slint";
|
||||
import { ScrollView } from "std-widgets.slint";
|
||||
import { FlatSurface, UiSurface } from "ui_surface.slint";
|
||||
import { UiRadius } from "ui_radius.slint";
|
||||
import { RoundedLineEdit } from "rounded_line_edit.slint";
|
||||
|
||||
export struct TrustedDeviceEntry {
|
||||
remote-id: string,
|
||||
public-key: string,
|
||||
display-name: string,
|
||||
device-id: string,
|
||||
fingerprint: string,
|
||||
}
|
||||
|
||||
export global TrustStrings {
|
||||
in property <string> title: "Trusted devices";
|
||||
in property <string> only: "Only allow trusted devices";
|
||||
in property <string> manage: "Manage";
|
||||
in property <string> copy: "Copy pairing info";
|
||||
in property <string> import-title: "Add a trusted device";
|
||||
in property <string> add: "Add";
|
||||
in property <string> placeholder: "Paste the controller's pairing information";
|
||||
in property <string> confirm: "Check the fingerprint on the controller.";
|
||||
in property <string> empty: "No trusted devices";
|
||||
in property <string> candidates: "Connected devices available to trust";
|
||||
in property <string> trust: "Trust";
|
||||
in property <string> remove: "Remove";
|
||||
in property <string> saved: "Trusted devices";
|
||||
in property <string> sharing: "This device’s pairing info";
|
||||
in property <string> show-fingerprint: "View fingerprint";
|
||||
in property <string> hide-fingerprint: "Hide fingerprint";
|
||||
in property <string> close: "Close";
|
||||
}
|
||||
|
||||
component FingerprintBlock inherits Rectangle {
|
||||
in property <string> fingerprint;
|
||||
background: #f0f4fa; border-radius: UiRadius.control;
|
||||
height: content.preferred-height;
|
||||
content := VerticalLayout {
|
||||
width: parent.width; height: self.preferred-height;
|
||||
padding: 10px;
|
||||
Text { text: root.fingerprint; color: #465a76; font-size: 11px; wrap: word-wrap; }
|
||||
}
|
||||
}
|
||||
|
||||
component DeviceRow inherits FlatSurface {
|
||||
in property <TrustedDeviceEntry> device;
|
||||
in property <string> action-text;
|
||||
in property <bool> confirming: false;
|
||||
private property <bool> fingerprint-open: false;
|
||||
callback action;
|
||||
height: content.preferred-height;
|
||||
background: white; outline-color: UiSurface.edge;
|
||||
border-radius: UiRadius.control;
|
||||
content := VerticalLayout {
|
||||
width: parent.width; height: self.preferred-height;
|
||||
padding: 12px; spacing: 8px;
|
||||
padding-top: root.confirming ? 12px : 8px;
|
||||
padding-bottom: root.confirming ? 12px : 8px;
|
||||
HorizontalLayout {
|
||||
spacing: 6px;
|
||||
VerticalLayout {
|
||||
spacing: 2px; horizontal-stretch: 1; min-width: 0px;
|
||||
Text {
|
||||
text: root.device.display-name;
|
||||
font-size: root.confirming ? 13px : 12px; font-weight: root.confirming ? 600 : 500;
|
||||
color: WorkbenchStyle.text; overflow: elide;
|
||||
}
|
||||
Text {
|
||||
text: "ID: " + (root.device.device-id == "" ? "—" : root.device.device-id);
|
||||
font-size: 11px; color: WorkbenchStyle.muted; overflow: elide;
|
||||
}
|
||||
}
|
||||
if !root.confirming: SettingsButton {
|
||||
width: min(144px, self.label-width); height: 28px; quiet: true;
|
||||
text-size: 11px;
|
||||
text: root.fingerprint-open ? TrustStrings.hide-fingerprint : TrustStrings.show-fingerprint;
|
||||
clicked => { root.fingerprint-open = !root.fingerprint-open; }
|
||||
}
|
||||
if !root.confirming: SettingsButton {
|
||||
width: min(128px, max(64px, self.label-width)); height: 28px;
|
||||
text-size: 11px;
|
||||
quiet: true; text: root.action-text;
|
||||
destructive: true;
|
||||
clicked => { root.action(); }
|
||||
}
|
||||
}
|
||||
if root.confirming || root.fingerprint-open: FingerprintBlock {
|
||||
fingerprint: root.device.fingerprint;
|
||||
}
|
||||
if root.confirming: HorizontalLayout {
|
||||
spacing: 12px;
|
||||
Text {
|
||||
text: TrustStrings.confirm; font-size: 11px;
|
||||
color: WorkbenchStyle.muted; wrap: word-wrap;
|
||||
horizontal-stretch: 1; min-width: 0px; vertical-alignment: center;
|
||||
}
|
||||
SettingsButton {
|
||||
width: min(parent.width * 0.6, max(112px, self.label-width)); height: 30px;
|
||||
primary: true; text: root.action-text;
|
||||
clicked => { root.action(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component TrustedDevicesDialog inherits Rectangle {
|
||||
in property <[TrustedDeviceEntry]> devices;
|
||||
in property <[TrustedDeviceEntry]> candidates;
|
||||
in property <string> local-fingerprint;
|
||||
in property <string> error;
|
||||
in property <string> notice;
|
||||
in property <string> copy-error;
|
||||
in property <string> copy-notice;
|
||||
in-out property <string> pairing-input;
|
||||
private property <bool> local-fingerprint-open: false;
|
||||
callback close;
|
||||
callback action(string, string, string);
|
||||
background: #15223835;
|
||||
TouchArea { }
|
||||
forward-focus: focus;
|
||||
focus := FocusScope {
|
||||
key-pressed(event) => {
|
||||
if event.text == Key.Escape { root.close(); return accept; }
|
||||
return reject;
|
||||
}
|
||||
}
|
||||
dialog := SettingsDialog {
|
||||
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2;
|
||||
width: min(540px, parent.width - 32px);
|
||||
height: min(400px, parent.height - 32px);
|
||||
min-width: 0px; max-width: parent.width - 32px;
|
||||
min-height: 0px; max-height: parent.height - 32px;
|
||||
title: TrustStrings.title; close-label: TrustStrings.close;
|
||||
close => { root.close(); }
|
||||
ScrollView {
|
||||
x: 24px; y: 64px; width: parent.width - 48px; height: parent.height - 84px;
|
||||
min-width: 0px; min-height: 0px;
|
||||
horizontal-scrollbar-policy: always-off;
|
||||
viewport-width: self.visible-width;
|
||||
viewport-height: content.preferred-height;
|
||||
content := VerticalLayout {
|
||||
width: parent.visible-width - 8px; height: self.preferred-height;
|
||||
spacing: 16px; padding: 0px; alignment: start;
|
||||
FlatSurface {
|
||||
background: #f1f6ff; outline-color: #d7e3f7; border-radius: UiRadius.surface;
|
||||
height: add-content.preferred-height;
|
||||
add-content := VerticalLayout {
|
||||
width: parent.width; height: self.preferred-height; padding: 14px; spacing: 10px;
|
||||
Text {
|
||||
text: TrustStrings.import-title; font-size: 14px; font-weight: 600;
|
||||
color: WorkbenchStyle.text; min-width: 0px; wrap: word-wrap;
|
||||
}
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
RoundedLineEdit {
|
||||
horizontal-stretch: 1; height: 32px; text <=> root.pairing-input;
|
||||
placeholder-text: TrustStrings.placeholder; surrounding-background: #f1f6ff;
|
||||
edited => { root.action("edit", "", ""); }
|
||||
accepted => { root.action("import", "", ""); }
|
||||
}
|
||||
SettingsButton {
|
||||
width: max(64px, self.label-width); height: 32px; primary: true;
|
||||
text: TrustStrings.add; enabled: root.pairing-input != "" && root.local-fingerprint != "";
|
||||
clicked => { root.action("import", "", ""); }
|
||||
}
|
||||
}
|
||||
if root.error != "": Text {
|
||||
text: root.error; color: #b33c39; font-size: 11px; wrap: word-wrap;
|
||||
}
|
||||
if root.notice != "": Text {
|
||||
text: root.notice; color: #28734a; font-size: 11px; wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
spacing: 6px;
|
||||
Rectangle {
|
||||
// Align mixed font sizes by their baselines rather than their top edges.
|
||||
private property <length> baseline: max(saved-title.font-metrics.ascent, saved-count.font-metrics.ascent);
|
||||
height: max(saved-title.y + saved-title.preferred-height, saved-count.y + saved-count.preferred-height);
|
||||
saved-title := Text {
|
||||
x: 0px; width: self.preferred-width; height: self.preferred-height;
|
||||
y: parent.baseline - self.font-metrics.ascent;
|
||||
text: TrustStrings.saved; font-size: 13px; font-weight: 600; color: WorkbenchStyle.text;
|
||||
}
|
||||
saved-count := Text {
|
||||
x: saved-title.preferred-width + 6px;
|
||||
width: self.preferred-width; height: self.preferred-height;
|
||||
y: parent.baseline - self.font-metrics.ascent;
|
||||
text: "" + root.devices.length; font-size: 12px; color: WorkbenchStyle.muted;
|
||||
}
|
||||
}
|
||||
if root.devices.length == 0: FlatSurface {
|
||||
height: 48px; background: white;
|
||||
outline-color: UiSurface.edge; border-radius: UiRadius.control;
|
||||
Text {
|
||||
x: 14px; width: parent.width - 28px; height: parent.height;
|
||||
text: TrustStrings.empty; font-size: 12px; color: WorkbenchStyle.muted;
|
||||
horizontal-alignment: center; vertical-alignment: center; wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
for device in root.devices: DeviceRow {
|
||||
device: device; action-text: TrustStrings.remove;
|
||||
action => { root.action("remove", device.remote-id, device.public-key); }
|
||||
}
|
||||
}
|
||||
if root.candidates.length > 0: VerticalLayout {
|
||||
spacing: 8px;
|
||||
Text { text: TrustStrings.candidates; font-size: 13px; font-weight: 600; color: WorkbenchStyle.text; wrap: word-wrap; }
|
||||
for device in root.candidates: DeviceRow {
|
||||
device: device; confirming: true; action-text: TrustStrings.trust;
|
||||
action => { root.action("add", device.remote-id, device.public-key); }
|
||||
}
|
||||
}
|
||||
FlatSurface {
|
||||
height: sharing-content.preferred-height; background: white;
|
||||
outline-color: UiSurface.edge; border-radius: UiRadius.control;
|
||||
sharing-content := VerticalLayout {
|
||||
width: parent.width; height: self.preferred-height; padding: 14px; spacing: 8px;
|
||||
HorizontalLayout {
|
||||
spacing: 12px;
|
||||
Text {
|
||||
horizontal-stretch: 1; min-width: 0px;
|
||||
text: TrustStrings.sharing; font-size: 13px; font-weight: 600;
|
||||
color: WorkbenchStyle.text; wrap: word-wrap; vertical-alignment: center;
|
||||
}
|
||||
SettingsButton {
|
||||
width: min(160px, self.label-width); height: 30px; text: TrustStrings.copy;
|
||||
enabled: root.local-fingerprint != ""; clicked => { root.action("copy", "", ""); }
|
||||
}
|
||||
}
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
SettingsButton {
|
||||
width: self.label-width; height: 22px; quiet: true; align-left: true;
|
||||
text: root.local-fingerprint-open ? TrustStrings.hide-fingerprint : TrustStrings.show-fingerprint;
|
||||
enabled: root.local-fingerprint != "";
|
||||
clicked => { root.local-fingerprint-open = !root.local-fingerprint-open; }
|
||||
}
|
||||
Text {
|
||||
text: root.copy-error != "" ? root.copy-error : root.copy-notice;
|
||||
color: root.copy-error != "" ? #b33c39 : #28734a; font-size: 11px;
|
||||
horizontal-stretch: 1; min-width: 0px; wrap: word-wrap;
|
||||
horizontal-alignment: right; vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
if root.local-fingerprint-open: FingerprintBlock {
|
||||
fingerprint: root.local-fingerprint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,24 @@ inline int ApplyMainWindowStrings(
|
||||
languages.push_back(Text(option.display_name));
|
||||
languages.push_back(Text(localization::Literal("跟随系统", language)));
|
||||
window->set_language_options(std::make_shared<slint::VectorModel<slint::SharedString>>(languages));
|
||||
auto& trust = window->global<ui::TrustStrings>();
|
||||
trust.set_title(Text(localization::Literal("可信设备", language)));
|
||||
trust.set_only(Text(localization::Literal("仅允许可信设备控制", language)));
|
||||
trust.set_manage(Text(localization::Literal("管理可信设备", language)));
|
||||
trust.set_copy(Text(localization::Literal("复制配对信息", language)));
|
||||
trust.set_import_title(Text(localization::Literal("添加可信设备", language)));
|
||||
trust.set_add(Text(localization::Literal("添加", language)));
|
||||
trust.set_placeholder(Text(localization::Literal("粘贴控制端的配对信息", language)));
|
||||
trust.set_confirm(Text(localization::Literal("请核对控制端指纹。", language)));
|
||||
trust.set_saved(Text(localization::Literal("已信任的设备", language)));
|
||||
trust.set_sharing(Text(localization::Literal("本机配对信息", language)));
|
||||
trust.set_show_fingerprint(Text(localization::Literal("查看指纹", language)));
|
||||
trust.set_hide_fingerprint(Text(localization::Literal("收起指纹", language)));
|
||||
trust.set_empty(Text(localization::Literal("暂无可信设备", language)));
|
||||
trust.set_candidates(Text(localization::Literal("可添加的已连接设备", language)));
|
||||
trust.set_trust(Text(localization::Literal("信任此设备", language)));
|
||||
trust.set_remove(Text(localization::Literal("移除信任", language)));
|
||||
trust.set_close(Text(localization::message_close[language]));
|
||||
auto& strings = window->global<ui::UiStrings>();
|
||||
strings.set_window_minimize(Text(localization::window_minimize[language]));
|
||||
strings.set_window_close(Text(localization::window_close[language]));
|
||||
|
||||
Reference in New Issue
Block a user