[feat] add trusted device pairing and management UI

This commit is contained in:
dijunkun
2026-11-01 23:27:17 +08:00
parent e15e2c33bd
commit 489491008a
10 changed files with 823 additions and 7 deletions
@@ -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";
+39 -3
View File
@@ -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;
}
}
}
}
}
}
}
+18
View File
@@ -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]));