[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
@@ -366,6 +366,19 @@ slint::SharedString UiText(std::string_view value) {
return slint::SharedString(value);
}
slint::SharedString TrustFingerprint(std::string fingerprint) {
for (size_t i = 8; i < fingerprint.size(); i += 9) fingerprint.insert(i, " ");
return UiText(fingerprint);
}
ui::TrustedDeviceEntry TrustRow(const TrustedDevices::Device& device,
const std::string& remote = {}) {
auto id = device.device_id;
if (id.rfind("C-", 0) == 0) id.erase(0, 2);
return {UiText(remote), UiText(device.public_key),
UiText(device.name.empty() ? id : device.name), UiText(id), {}};
}
void RegisterFontAwesome(slint::Window& window) {
if (const auto error = window.window_handle().register_font_from_data(
fa_solid_900_ttf, fa_solid_900_ttf_len)) {
@@ -1065,6 +1078,11 @@ struct GuiApplication::SlintUi {
std::string tab_model_signature;
std::vector<std::string> controller_ids;
std::string controller_model_signature;
std::string trusted_device_signature;
std::shared_ptr<slint::VectorModel<ui::TrustedDeviceEntry>> trusted_device_model =
std::make_shared<slint::VectorModel<ui::TrustedDeviceEntry>>();
std::shared_ptr<slint::VectorModel<ui::TrustedDeviceEntry>> trust_candidate_model =
std::make_shared<slint::VectorModel<ui::TrustedDeviceEntry>>();
std::string connection_dialog_remote_id;
bool connection_password_initialized = false;
bool connection_dialog_gallery = false;
@@ -1560,6 +1578,7 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) {
config_center_->IsEnablePrivacyScreen());
main->set_privacy_local_unlock_allowed(config_center_->IsPrivacyLocalUnlockAllowed());
main->set_lock_on_disconnect_enabled(config_center_->IsLockOnDisconnect());
main->set_trusted_devices_only(config_center_->IsTrustedDevicesOnly());
#ifdef _WIN32
main->set_capture_method_visible(true);
main->set_capture_method_index(
@@ -1933,6 +1952,7 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
main->set_file_save_path(folder);
}
});
BindTrustedDeviceCallbacks(gallery_window);
main->on_save_settings([this, main] {
return SaveSettingsFromUi(main->get_gallery_mode());
});
@@ -2704,6 +2724,7 @@ void GuiApplication::Tick() {
}
SyncStreamWindow();
SyncServerWindow();
SyncTrustedDevices();
SyncFileTransferNotifications();
}
@@ -4260,6 +4281,113 @@ void GuiApplication::PositionServerWindows() {
}
}
void GuiApplication::BindTrustedDeviceCallbacks(bool gallery_window) {
const auto main = ui_->Window(gallery_window);
main->on_trust_action([this, main](
slint::SharedString action, slint::SharedString id, slint::SharedString key) {
const auto message = [this](const char* text) {
return UiText(localization::Literal(text, localization_language_index_));
};
const bool copying = action == "copy";
const auto set_error = [&](const char* text) {
if (copying) main->set_trust_copy_error(message(text));
else main->set_trusted_devices_error(message(text));
};
if (copying || action == "open" || action == "close") {
main->set_trust_copy_error("");
main->set_trust_copy_notice("");
}
if (!copying) {
main->set_trusted_devices_error("");
main->set_trusted_devices_notice("");
}
if (action == "open" || action == "close") {
main->set_trust_pairing_input("");
SyncTrustedDevices();
return;
}
if (!trusted_devices_) {
set_error("无法读取设备身份或可信列表,请修复后再启用。");
return;
}
if (action == "edit") return;
try {
if (copying) {
const auto text = trusted_devices_->PairingInfo(client_id_, GetHostName());
if (SDL_SetClipboardText(text.c_str()))
main->set_trust_copy_notice(message("配对信息已复制。"));
else set_error("无法访问剪贴板,请重试。");
} else if (action == "import") {
const auto input = std::string(main->get_trust_pairing_input());
if (input.empty()) return;
const auto device = TrustedDevices::ParsePairingInfo(input);
if (!device) {
set_error("配对信息无效,请从控制端重新复制。");
return;
}
// The Add action approves the current input. Import only updates the
// list; a live connection must still complete its own authentication.
if (trusted_devices_->Contains(device->public_key)) {
main->set_trusted_devices_notice(message("已在可信列表中"));
} else {
trusted_devices_->Add(*device);
main->set_trusted_devices_notice(message("已加入可信设备。"));
}
main->set_trust_pairing_input("");
} else if ((action == "add" && !TrustController(std::string(id), std::string(key))) ||
(action == "remove" && !RemoveTrustedDevice(std::string(key)))) {
set_error("无法更新可信设备,请检查列表容量和存储权限后重试。");
}
} catch (const std::exception&) {
set_error(copying ? "无法复制配对信息,请重试。"
: "无法更新可信设备,请检查列表容量和存储权限后重试。");
}
SyncTrustedDevices();
});
}
void GuiApplication::SyncTrustedDevices() {
if (!ui_->main->get_trusted_devices_open() && !ui_->wall->get_trusted_devices_open()) return;
std::vector<ui::TrustedDeviceEntry> saved, candidates;
if (trusted_devices_) {
for (const auto& device : trusted_devices_->List()) saved.push_back(TrustRow(device));
for (const auto& id : ConnectedControllers())
if (const auto device = VerifiedController(id);
device && !trusted_devices_->Contains(device->public_key))
candidates.push_back(TrustRow(*device, id));
}
std::sort(candidates.begin(), candidates.end(), [](const auto& a, const auto& b) {
return std::string(a.remote_id) < std::string(b.remote_id);
});
std::string signature = trusted_devices_ ? "ready" : "unavailable";
for (const auto* rows : {&saved, &candidates}) {
signature += "|";
for (const auto& device : *rows)
signature += std::string(device.remote_id) + '\0' + std::string(device.public_key) + '\0' +
std::string(device.display_name) + '\0' + std::string(device.device_id) + '\0';
}
if (signature != ui_->trusted_device_signature) {
// Tick runs frequently. Hash keys and update Slint only when the rows change.
for (auto* rows : {&saved, &candidates})
for (auto& device : *rows)
device.fingerprint = TrustFingerprint(TrustedDevices::Fingerprint(std::string(device.public_key)));
ui_->trusted_device_model->set_vector(std::move(saved));
ui_->trust_candidate_model->set_vector(std::move(candidates));
ui_->trusted_device_signature = std::move(signature);
const auto fingerprint = TrustFingerprint(trusted_devices_ ? trusted_devices_->Fingerprint() : "");
for (auto& window : {ui_->main, ui_->wall}) {
window->set_trusted_devices(ui_->trusted_device_model);
window->set_trust_candidates(ui_->trust_candidate_model);
window->set_device_fingerprint(fingerprint);
}
}
if (!trusted_devices_) {
for (auto& window : {ui_->main, ui_->wall})
window->set_trusted_devices_error(UiText(localization::Literal(
"无法读取设备身份或可信列表,请修复后再启用。", localization_language_index_)));
}
}
void GuiApplication::SyncServerWindow() {
// The connection map is authoritative. Lifecycle flags record callback
// intent, but callbacks for different controllers can cross each other, so
@@ -4612,6 +4740,11 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
const auto previous_connection_mode = config_center_->GetConnectionMode();
auto& main = ui_->Window(gallery_window);
main->set_settings_save_error("");
if (main->get_trusted_devices_only() && !trusted_devices_) {
main->set_settings_save_error(UiText(localization::Literal(
"无法读取设备身份或可信列表,请修复后再启用。", localization_language_index_)));
return false;
}
const bool has_endpoint = main->get_self_hosted_enabled() ||
!std::string(main->get_server_host()).empty() ||
!std::string(main->get_server_port()).empty();
@@ -4771,6 +4904,12 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
props->preview_needs_sync_ = true;
ui_->gallery_synced = {};
}
if (config_center_->SetTrustedDevicesOnly(main->get_trusted_devices_only()) != 0) {
main->set_settings_save_error(UiText(localization::Literal(
"无法保存可信设备设置,请重试。", localization_language_index_)));
return false;
}
TickControllerTrust();
ApplySettingsFromConfig(previous_server !=
GetSignalServerSettings(*config_center_));
ResetSettingsUi(gallery_window);
@@ -54,6 +54,7 @@ private:
void ShowMainWindow(const std::string& activation_token = {});
bool MinimizeMainWindowToTray();
void BindMainCallbacks(bool gallery_window = false);
void BindTrustedDeviceCallbacks(bool gallery_window = false);
void BindStreamCallbacks();
void BindServerCallbacks();
void Tick();
@@ -92,6 +93,7 @@ private:
void SyncStreamKeyboardFocus();
void SetStreamKeyboardFocus(bool focused);
void SyncServerWindow();
void SyncTrustedDevices();
void SyncFileTransferNotifications();
void CancelFileTransfer(uint32_t file_id);
void SetServerPanelExpanded(bool expanded);
@@ -494,6 +494,31 @@ inline constexpr TranslationRow kTranslationRows[] = {
{":同步键鼠到所有已连接设备", {":同步键鼠到所有已连接设备", ": mirror input to all connected devices", ": ввод на всех подключённых устройствах", ":同步鍵鼠到所有已連線裝置", ": 接続されているすべてのデバイスへのミラー入力", ": 연결된 모든 장치에 대한 미러 입력", ": Eingang auf alle angeschlossenen Geräte spiegeln", ": entrée miroir sur tous les appareils connectés", ": entrada espejo para todos los dispositivos conectados", ": entrada espelhada para todos os dispositivos conectados"}},
{"监听所有 IP 接口", {"监听所有 IP 接口", "Listen on all IP interfaces", "Все IP-интерфейсы", "監聽所有 IP 介面", "すべての IP で待ち受け", "모든 IP 인터페이스 수신", "Alle IP-Schnittstellen", "Toutes les interfaces IP", "Todas las interfaces IP", "Todas as interfaces IP"}},
{"所有接口共用一个端口,包括 VPN。分享时请选择实际 IP;关闭后恢复各 IP 的独立端口。", {"所有接口共用一个端口,包括 VPN。分享时请选择实际 IP;关闭后恢复各 IP 的独立端口。", "Use one port on all interfaces, including VPNs. Select an actual IP to share. Turn off to restore separate ports per IP.", "Один порт для всех интерфейсов, включая VPN. Для подключения выберите реальный IP. Отключение вернёт отдельные порты для каждого IP.", "所有介面共用一個連接埠,包括 VPN。分享時請選擇實際 IP;關閉後恢復各 IP 的獨立連接埠。", "VPN を含む全インターフェースで同じポートを使います。共有には実際の IP を選択してください。オフにすると IP ごとのポートに戻ります。", "VPN을 포함한 모든 인터페이스가 하나의 포트를 사용합니다. 공유할 실제 IP를 선택하세요. 끄면 IP별 포트가 복원됩니다.", "Ein Port für alle Schnittstellen, einschließlich VPN. Zum Teilen eine tatsächliche IP wählen. Ausschalten stellt die Ports pro IP wieder her.", "Un port commun à toutes les interfaces, VPN inclus. Choisissez une IP réelle à partager. Désactivez pour rétablir les ports propres à chaque IP.", "Un puerto para todas las interfaces, incluidas las VPN. Seleccione una IP real para compartir. Desactive para restaurar los puertos de cada IP.", "Uma porta para todas as interfaces, incluindo VPNs. Selecione um IP real para compartilhar. Desative para restaurar as portas de cada IP."}},
{"可信设备", {"可信设备", "Trusted devices", "Доверенные устройства", "可信裝置", "信頼済みデバイス", "신뢰하는 기기", "Vertrauenswürdige Geräte", "Appareils de confiance", "Dispositivos de confianza", "Dispositivos confiáveis"}},
{"仅允许可信设备控制", {"仅允许可信设备控制", "Only allow trusted devices", "Разрешать только доверенные устройства", "僅允許可信裝置控制", "信頼済みデバイスのみ許可", "신뢰하는 기기만 제어 허용", "Nur vertrauenswürdige Geräte zulassen", "Autoriser uniquement les appareils de confiance", "Permitir solo dispositivos de confianza", "Permitir apenas dispositivos confiáveis"}},
{"管理可信设备", {"管理可信设备", "Manage trusted devices", "Управление доверенными устройствами", "管理可信裝置", "信頼済みデバイスを管理", "신뢰하는 기기 관리", "Vertrauenswürdige Geräte verwalten", "Gérer les appareils de confiance", "Administrar dispositivos de confianza", "Gerenciar dispositivos confiáveis"}},
{"可添加的已连接设备", {"可添加的已连接设备", "Connected devices available to trust", "Подключённые устройства для добавления", "可新增的已連線裝置", "追加可能な接続中のデバイス", "추가 가능한 연결된 기기", "Verbundene Geräte zum Hinzufügen", "Appareils connectés à ajouter", "Dispositivos conectados que puedes añadir", "Dispositivos conectados disponíveis"}},
{"信任此设备", {"信任此设备", "Trust this device", "Доверять устройству", "信任此裝置", "このデバイスを信頼", "이 기기 신뢰", "Diesem Gerät vertrauen", "Faire confiance à cet appareil", "Confiar en este dispositivo", "Confiar neste dispositivo"}},
{"移除信任", {"移除信任", "Remove trust", "Отозвать доверие", "移除信任", "信頼を解除", "신뢰 해제", "Vertrauen entfernen", "Retirer la confiance", "Retirar confianza", "Remover confiança"}},
{"无法读取设备身份或可信列表,请修复后再启用。", {"无法读取设备身份或可信列表,请修复后再启用。", "Could not load the device identity or trusted list. Repair the local data before enabling this restriction.", "Не удалось загрузить идентификатор устройства или список доверия. Восстановите локальные данные перед включением ограничения.", "無法讀取裝置身分或可信清單,請修復後再啟用。", "デバイス情報または信頼リストを読み込めません。ローカルデータを修復してから制限を有効にしてください。", "기기 ID 또는 신뢰 목록을 불러올 수 없습니다. 로컬 데이터를 복구한 후 제한을 켜세요.", "Geräteidentität oder Vertrauensliste konnte nicht geladen werden. Lokale Daten vor dem Aktivieren reparieren.", "Impossible de charger l’identité ou la liste de confiance. Réparez les données locales avant d’activer la restriction.", "No se pudo cargar la identidad o la lista de confianza. Repara los datos locales antes de activar la restricción.", "Não foi possível carregar a identidade ou a lista confiável. Repare os dados locais antes de ativar a restrição."}},
{"无法保存可信设备设置,请重试。", {"无法保存可信设备设置,请重试。", "Could not save trusted device settings. Try again.", "Не удалось сохранить настройки доверенных устройств. Повторите попытку.", "無法儲存可信裝置設定,請重試。", "信頼済みデバイスの設定を保存できません。再試行してください。", "신뢰하는 기기 설정을 저장할 수 없습니다. 다시 시도하세요.", "Einstellungen für vertrauenswürdige Geräte konnten nicht gespeichert werden. Erneut versuchen.", "Impossible d’enregistrer les paramètres de confiance. Réessayez.", "No se pudo guardar la configuración de confianza. Inténtalo de nuevo.", "Não foi possível salvar as configurações de confiança. Tente novamente."}},
{"粘贴", {"粘贴", "Paste", "Вставить", "貼上", "貼り付け", "붙여넣기", "Einfügen", "Coller", "Pegar", "Colar"}},
{"粘贴控制端的配对信息", {"粘贴控制端的配对信息", "Paste the controller’s pairing information", "Вставьте данные сопряжения управляющего устройства", "貼上控制端的配對資訊", "操作側のペアリング情報を貼り付け", "제어 장치의 페어링 정보 붙여넣기", "Kopplungsdaten des steuernden Geräts einfügen", "Collez les informations d’association de l’appareil de contrôle", "Pegue la información de emparejamiento del dispositivo de control", "Cole as informações de pareamento do dispositivo controlador"}},
{"已在可信列表中", {"已在可信列表中", "Already trusted", "Уже доверенное", "已在可信清單中", "信頼済み", "이미 신뢰하는 장치", "Bereits vertrauenswürdig", "Déjà approuvé", "Ya es de confianza", "Já é confiável"}},
{"无法访问剪贴板,请重试。", {"无法访问剪贴板,请重试。", "Cannot access the clipboard. Please try again.", "Не удалось получить доступ к буферу обмена. Повторите попытку.", "無法存取剪貼簿,請重試。", "クリップボードにアクセスできません。もう一度お試しください。", "클립보드에 접근할 수 없습니다. 다시 시도하세요.", "Kein Zugriff auf die Zwischenablage. Bitte erneut versuchen.", "Impossible d’accéder au presse-papiers. Réessayez.", "No se puede acceder al portapapeles. Inténtelo de nuevo.", "Não foi possível acessar a área de transferência. Tente novamente."}},
{"配对信息无效,请从控制端重新复制。", {"配对信息无效,请从控制端重新复制。", "Invalid pairing information. Copy it again from the controller.", "Недопустимые данные сопряжения. Скопируйте их заново с управляющего устройства.", "配對資訊無效,請從控制端重新複製。", "ペアリング情報が無効です。操作側からコピーし直してください。", "유효하지 않은 페어링 정보입니다. 제어 장치에서 다시 복사하세요.", "Ungültige Kopplungsdaten. Kopieren Sie sie erneut vom steuernden Gerät.", "Informations d’association invalides. Copiez-les à nouveau depuis l’appareil de contrôle.", "Información de emparejamiento no válida. Cópiela de nuevo del dispositivo de control.", "Informações de pareamento inválidas. Copie-as novamente do dispositivo controlador."}},
{"已加入可信设备。", {"已加入可信设备。", "Device added to the trusted list.", "Устройство добавлено в список доверенных.", "已加入可信裝置。", "信頼済みデバイスに追加しました。", "신뢰 장치 목록에 추가했습니다.", "Gerät zur Vertrauensliste hinzugefügt.", "Appareil ajouté à la liste de confiance.", "Dispositivo añadido a la lista de confianza.", "Dispositivo adicionado à lista de confiança."}},
{"无法更新可信设备,请检查列表容量和存储权限后重试。", {"无法更新可信设备,请检查列表容量和存储权限后重试。", "Cannot update trusted devices. Check the list capacity and storage permissions, then try again.", "Не удалось обновить список доверенных устройств. Проверьте его размер и права доступа к хранилищу.", "無法更新可信裝置,請檢查清單容量和儲存權限後重試。", "信頼済みデバイスを更新できません。リストの上限と保存先の権限を確認してください。", "신뢰 장치를 업데이트할 수 없습니다. 목록 용량과 저장 권한을 확인한 후 다시 시도하세요.", "Vertrauensliste konnte nicht aktualisiert werden. Prüfen Sie die Listenbegrenzung und Speicherberechtigungen.", "Impossible de mettre à jour les appareils de confiance. Vérifiez la capacité de la liste et les autorisations de stockage.", "No se pueden actualizar los dispositivos de confianza. Compruebe la capacidad de la lista y los permisos de almacenamiento.", "Não foi possível atualizar os dispositivos confiáveis. Verifique a capacidade da lista e as permissões de armazenamento."}},
{"复制配对信息", {"复制配对信息", "Copy pairing info", "Копировать данные", "複製配對資訊", "ペアリング情報をコピー", "페어링 정보 복사", "Kopplungsdaten kopieren", "Copier les informations", "Copiar información", "Copiar informações"}},
{"添加可信设备", {"添加可信设备", "Add a trusted device", "Добавить доверенное устройство", "新增可信裝置", "信頼するデバイスを追加", "신뢰 장치 추가", "Vertrauenswürdiges Gerät hinzufügen", "Ajouter un appareil de confiance", "Añadir dispositivo de confianza", "Adicionar dispositivo confiável"}},
{"已信任的设备", {"已信任的设备", "Trusted devices", "Доверенные устройства", "已信任的裝置", "信頼済みデバイス", "신뢰하는 장치", "Vertrauenswürdige Geräte", "Appareils de confiance", "Dispositivos de confianza", "Dispositivos confiáveis"}},
{"本机配对信息", {"本机配对信息", "This device’s pairing info", "Данные этого устройства", "本機配對資訊", "このデバイスのペアリング情報", "이 장치의 페어링 정보", "Kopplungsdaten dieses Geräts", "Informations de cet appareil", "Información de este dispositivo", "Informações deste dispositivo"}},
{"查看指纹", {"查看指纹", "View fingerprint", "Показать отпечаток", "查看指紋", "フィンガープリントを表示", "지문 보기", "Fingerabdruck anzeigen", "Afficher l’empreinte", "Ver huella", "Ver impressão digital"}},
{"收起指纹", {"收起指纹", "Hide fingerprint", "Скрыть отпечаток", "收起指紋", "フィンガープリントを隠す", "지문 숨기기", "Fingerabdruck ausblenden", "Masquer l’empreinte", "Ocultar huella", "Ocultar impressão digital"}},
{"暂无可信设备", {"暂无可信设备", "No trusted devices", "Нет доверенных устройств", "暫無可信裝置", "信頼済みデバイスはありません", "신뢰하는 장치 없음", "Keine vertrauenswürdigen Geräte", "Aucun appareil de confiance", "No hay dispositivos de confianza", "Nenhum dispositivo confiável"}},
{"请核对控制端指纹。", {"请核对控制端指纹。", "Check the fingerprint on the controller.", "Сверьте отпечаток с управляющим устройством.", "請核對控制端指紋。", "操作側のフィンガープリントと照合してください。", "제어 장치의 지문과 대조하세요.", "Mit dem Fingerabdruck des steuernden Geräts vergleichen.", "Vérifiez l’empreinte sur l’appareil de contrôle.", "Verifique la huella en el dispositivo de control.", "Confira a impressão digital no dispositivo controlador."}},
{"配对信息已复制。", {"配对信息已复制。", "Pairing information copied.", "Данные сопряжения скопированы.", "配對資訊已複製。", "ペアリング情報をコピーしました。", "페어링 정보를 복사했습니다.", "Kopplungsdaten kopiert.", "Informations d’association copiées.", "Información de emparejamiento copiada.", "Informações de pareamento copiadas."}},
{"无法复制配对信息,请重试。", {"无法复制配对信息,请重试。", "Unable to copy pairing information. Please try again.", "Не удалось скопировать данные сопряжения. Повторите попытку.", "無法複製配對資訊,請重試。", "ペアリング情報をコピーできません。もう一度お試しください。", "페어링 정보를 복사할 수 없습니다. 다시 시도하세요.", "Kopplungsdaten konnten nicht kopiert werden. Bitte erneut versuchen.", "Impossible de copier les informations d’association. Réessayez.", "No se pudo copiar la información de emparejamiento. Inténtelo de nuevo.", "Não foi possível copiar as informações de pareamento. Tente novamente."}},
};
struct NamedTranslation { const char* name; const char* literal; };
@@ -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]));