mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-03 12:32:53 +08:00
[feat] integrate IP direct mode with desktop controls and history
This commit is contained in:
@@ -7,10 +7,19 @@ export component GroupFilter inherits Rectangle {
|
||||
in property <[string]> model;
|
||||
in property <int> system-count: 2;
|
||||
in property <bool> enabled: true;
|
||||
// An embedded picker shares its enclosing field's border and focus ring.
|
||||
in property <bool> embedded: false;
|
||||
in property <bool> show-indicator: true;
|
||||
in property <length> popup-min-width: 0px;
|
||||
in property <length> text-size: 12px;
|
||||
in property <int> text-weight: 400;
|
||||
in property <color> text-color: #30343b;
|
||||
in property <color> normal-border-color: #c9cdd2;
|
||||
in property <color> hover-border-color: root.normal-border-color;
|
||||
in property <color> focus-border-color: #8db5df;
|
||||
in-out property <int> current-index: 0;
|
||||
out property <bool> has-focus: trigger-focus.has-focus || popup.is-open;
|
||||
out property <bool> has-hover: root.enabled && trigger-touch.has-hover && !popup.is-open;
|
||||
callback selected(int);
|
||||
private property <bool> has-divider: root.system-count > 0 && root.model.length > root.system-count;
|
||||
private property <length> list-height: root.model.length * 26px + (root.has-divider ? 6px : 0px);
|
||||
@@ -25,8 +34,9 @@ export component GroupFilter inherits Rectangle {
|
||||
}
|
||||
|
||||
width: 155px; height: 28px;
|
||||
background: !root.enabled ? #f3f4f5 : trigger-touch.has-hover ? #f8f9fb : white;
|
||||
border-width: 1px;
|
||||
background: root.embedded ? (root.enabled && trigger-touch.has-hover ? #eef3fb : transparent)
|
||||
: !root.enabled ? #f3f4f5 : trigger-touch.has-hover ? #f8f9fb : white;
|
||||
border-width: root.embedded ? 0px : 1px;
|
||||
border-color: popup.is-open || trigger-focus.has-focus ? root.focus-border-color
|
||||
: trigger-touch.has-hover ? root.hover-border-color : root.normal-border-color;
|
||||
border-radius: UiRadius.control;
|
||||
@@ -52,16 +62,18 @@ export component GroupFilter inherits Rectangle {
|
||||
root.reveal-selection();
|
||||
root.selected(root.current-index);
|
||||
}
|
||||
function open-menu() {
|
||||
public function open-menu() {
|
||||
if root.enabled && root.model.length > 0 {
|
||||
popup-open-timer.running = false;
|
||||
root.popup-animation-enabled = false;
|
||||
root.popup-progress = 0;
|
||||
root.reveal-selection();
|
||||
trigger-focus.focus();
|
||||
popup.show();
|
||||
popup-open-timer.running = true;
|
||||
}
|
||||
}
|
||||
public function close-menu() { popup.close(); }
|
||||
// Defer until the popup has its initial frame; animate its contents only,
|
||||
// keeping the native popup bounds and scrolling geometry stable.
|
||||
popup-open-timer := Timer {
|
||||
@@ -87,14 +99,19 @@ export component GroupFilter inherits Rectangle {
|
||||
return reject;
|
||||
}
|
||||
Text {
|
||||
x: 10px; width: parent.width - 36px; height: parent.height;
|
||||
x: root.embedded ? 8px : 10px;
|
||||
width: parent.width - (root.show-indicator
|
||||
? (root.embedded ? 26px : 36px) : (root.embedded ? 16px : 20px));
|
||||
height: parent.height;
|
||||
text: root.model[root.current-index];
|
||||
color: root.enabled ? #30343b : #92979f;
|
||||
font-size: 12px; overflow: elide; vertical-alignment: center;
|
||||
color: root.enabled ? root.text-color : #92979f;
|
||||
font-size: root.text-size; font-weight: root.text-weight;
|
||||
overflow: elide; vertical-alignment: center;
|
||||
accessible-role: none;
|
||||
}
|
||||
Text {
|
||||
x: parent.width - 25px; width: 16px; height: parent.height;
|
||||
if root.show-indicator: Text {
|
||||
x: parent.width - (root.embedded ? 16px : 25px);
|
||||
width: root.embedded ? 10px : 16px; height: parent.height;
|
||||
text: FontAwesomeIcons.angle-down; font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900; font-size: 10px; color: #858c96;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
@@ -107,12 +124,13 @@ export component GroupFilter inherits Rectangle {
|
||||
key-pressed(event) => { return root.handle-key(event); }
|
||||
trigger-touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => { trigger-focus.focus(); root.open-menu(); }
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.open-menu(); }
|
||||
}
|
||||
}
|
||||
popup := PopupWindow {
|
||||
x: 0px; y: root.height + 4px;
|
||||
width: root.width; height: root.visible-height + 8px;
|
||||
width: max(root.width, root.popup-min-width); height: root.visible-height + 8px;
|
||||
close-policy: close-on-click-outside;
|
||||
forward-focus: popup-focus;
|
||||
popup-focus := FocusScope {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { GalleryStrings } from "device_strings.slint";
|
||||
|
||||
export global LanStrings {
|
||||
// Short localized lines let the warning dialog add leading even though
|
||||
// Slint Text does not expose a line-height property.
|
||||
public pure function access-error-lines(code: string) -> [string] {
|
||||
if code == "LanPortInUse" { return [
|
||||
GalleryStrings.tr("此端口已被占用,", "This port is in use.", "Порт занят."),
|
||||
GalleryStrings.tr("请更换端口。", "Choose another port.", "Выберите другой порт."),
|
||||
GalleryStrings.tr("原设置已保留。", "Your previous settings are unchanged.", "Прежние настройки сохранены.")
|
||||
]; }
|
||||
if code == "LanPortCheckFailed" { return [
|
||||
GalleryStrings.tr("无法确认此端口是否可用,", "Could not check this port.", "Не удалось проверить порт."),
|
||||
GalleryStrings.tr("请重试或检查本机防火墙。", "Retry or check the local firewall.", "Повторите попытку или проверьте брандмауэр."),
|
||||
GalleryStrings.tr("原设置已保留。", "Your previous settings are unchanged.", "Прежние настройки сохранены.")
|
||||
]; }
|
||||
if code == "LanPortPermissionDenied" { return [
|
||||
GalleryStrings.tr("无法使用此端口,", "Cannot use this port.", "Порт недоступен."),
|
||||
GalleryStrings.tr("请选择其他端口或检查系统权限。", "Choose another port or check system permissions.", "Смените порт или проверьте разрешения."),
|
||||
GalleryStrings.tr("原设置已保留。", "Your previous settings are unchanged.", "Прежние настройки сохранены.")
|
||||
]; }
|
||||
if code == "Selected LAN interface is unavailable" { return [
|
||||
GalleryStrings.tr("未找到可用的局域网连接。", "Connect to Wi-Fi or Ethernet,", "Подключитесь к Wi-Fi или Ethernet"),
|
||||
GalleryStrings.tr("请连接 Wi-Fi 或网线,", "or select a local IP", "либо выберите IP"),
|
||||
GalleryStrings.tr("或在主界面选择本机 IP。", "on the main screen.", "в главном окне.")
|
||||
]; }
|
||||
// Keep all branches the same size for Slint's C++ model generator.
|
||||
return ["", "", ""];
|
||||
}
|
||||
public pure function error-message(code: string) -> string {
|
||||
let lines = LanStrings.access-error-lines(code);
|
||||
if lines[0] != "" {
|
||||
let separator = GalleryStrings.tr("", " ", " ");
|
||||
return lines[0] + separator + lines[1] + separator + lines[2];
|
||||
}
|
||||
if code == "InvalidCredentials" { return GalleryStrings.tr("密码不正确,或对方密码已更新。请查看对方的局域网访问密码后重试。", "Check the other desktop's current LAN password and try again.", "Проверьте текущий пароль LAN другого компьютера и повторите попытку."); }
|
||||
if code == "AuthenticationRateLimited" { return GalleryStrings.tr("尝试次数过多,请等待倒计时结束后重试。", "Too many attempts. Try again when the countdown ends.", "Слишком много попыток. Дождитесь окончания обратного отсчёта."); }
|
||||
if code == "CredentialsChanged" { return GalleryStrings.tr("对方密码已更新,请使用新密码重试。", "The password changed. Try again with the new password.", "Пароль изменился. Повторите с новым паролем."); }
|
||||
if code == "Busy" { return GalleryStrings.tr("设备正忙,请稍后重试。", "The desktop is busy. Try again shortly.", "Компьютер занят. Повторите позже."); }
|
||||
if code == "LAN password must contain exactly 6 ASCII letters or digits" { return GalleryStrings.tr("访问密码必须为 6 位英文字母或数字,区分大小写。", "Use exactly 6 letters (A–Z, a–z) or digits (0–9). Case-sensitive.", "Пароль: ровно 6 латинских букв или цифр. Регистр учитывается."); }
|
||||
if code == "ConnectionFailed" || code == "ConnectionTimeout" || code == "Timeout" || code == "ConnectionClosed" { return GalleryStrings.tr("无法连接,请检查对方 IP、程序是否运行,以及防火墙是否放行。", "Check the IP, whether the other desktop is running CrossDesk, and its firewall settings.", "Проверьте IP, работу CrossDesk на другом компьютере и брандмауэр."); }
|
||||
if code == "Enter a numeric IPv4 address or IPv4:port" { return GalleryStrings.tr("请输入有效的 IP,例如 192.168.1.20;指定端口可用 IP:端口,端口范围 1–65535。", "Enter a valid IP such as 192.168.1.20, or IP:port (1–65535).", "Введите IP, например 192.168.1.20, или IP:порт (1–65535)."); }
|
||||
if code == "Invalid LAN port" { return GalleryStrings.tr("请输入 1–65535 之间的整数端口。", "Enter a port number from 1 to 65535.", "Введите целый номер порта от 1 до 65535."); }
|
||||
if code == "LAN password is not ready" { return GalleryStrings.tr("请点击主界面密码旁的修改按钮设置密码。", "Set a password using Edit next to the password on the main screen.", "Задайте пароль кнопкой редактирования в главном окне."); }
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LanStrings } from "lan_strings.slint";
|
||||
import { AboutPanel, AboutPage, AboutComponent, AboutStrings, AboutNavigationRow, AboutNavigationGroup, AboutRowSeparator } from "about_panel.slint";
|
||||
import { UiRadius } from "ui_radius.slint";
|
||||
import { WorkbenchStyle, WorkbenchButton, WorkbenchCard, WorkbenchNotice } from "workbench_controls.slint";
|
||||
@@ -22,6 +23,8 @@ export struct RecentConnection {
|
||||
host-name: string,
|
||||
group-names: string,
|
||||
online: bool,
|
||||
lan: bool,
|
||||
endpoint: string,
|
||||
connected: bool,
|
||||
thumbnail: image,
|
||||
has-thumbnail: bool,
|
||||
@@ -372,6 +375,172 @@ export component MainWindow inherits Window {
|
||||
private property <length> wayland-corner-radius: 12px;
|
||||
in property <string> local-id: "";
|
||||
in property <string> local-password: "";
|
||||
in property <bool> ip-direct-enabled: true;
|
||||
changed ip-direct-enabled => {
|
||||
if !root.ip-direct-enabled {
|
||||
root.lan-port-dirty = false;
|
||||
root.lan-port-editing = false;
|
||||
root.lan-apply-pending = false;
|
||||
root.lan-local-port = root.lan-listener-port;
|
||||
root.lan-endpoint-error = "";
|
||||
root.lan-access-warning = "";
|
||||
root.lan-access-warning-endpoint = "";
|
||||
root.lan-listener-error = "";
|
||||
root.lan-open = false;
|
||||
root.lan-connecting = false;
|
||||
root.lan-listening = false;
|
||||
root.lan-password-pending = false;
|
||||
root.lan-retry-after = 0;
|
||||
root.lan-access-password = "";
|
||||
root.lan-new-password = "";
|
||||
root.lan-message = "";
|
||||
lan-ip-picker.close-menu();
|
||||
if root.lan-mode {
|
||||
root.reset-password-open = false;
|
||||
root.set-connection-mode(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
in-out property <bool> lan-open;
|
||||
changed lan-open => {
|
||||
if !root.lan-open { root.lan-access-password = ""; }
|
||||
if root.lan-open { root.gallery-action("release", ""); }
|
||||
}
|
||||
in-out property <bool> lan-mode;
|
||||
callback connection-mode-changed(bool);
|
||||
changed lan-mode => {
|
||||
root.recent-tooltip-visible = false;
|
||||
recent-scroll.viewport-x = 0px;
|
||||
if !root.lan-mode {
|
||||
root.lan-port-editing = false;
|
||||
if root.lan-port-dirty && root.lan-endpoint-error == "" { root.lan-apply-pending = true; }
|
||||
}
|
||||
}
|
||||
private property <string> id-connection-draft;
|
||||
private property <string> ip-connection-draft;
|
||||
private property <bool> lan-password-visible: true;
|
||||
private property <bool> active-password-visible: root.lan-mode ? root.lan-password-visible : root.password-visible;
|
||||
private property <string> active-password: root.lan-mode ? root.lan-new-password : root.local-password;
|
||||
function set-connection-mode(lan: bool) {
|
||||
recent-group-filter.close-menu();
|
||||
if root.lan-mode { root.ip-connection-draft = root.remote-id-input; }
|
||||
else { root.id-connection-draft = root.remote-id-input; }
|
||||
root.lan-mode = lan;
|
||||
root.remote-id-input = root.lan-mode ? root.ip-connection-draft : root.id-connection-draft;
|
||||
root.connection-input-error = "";
|
||||
root.copy-feedback = CopyFeedback.idle;
|
||||
root.menu-open = false;
|
||||
focus-sink.focus();
|
||||
}
|
||||
function switch-connection-mode() {
|
||||
if !root.ip-direct-enabled { return; }
|
||||
root.set-connection-mode(!root.lan-mode);
|
||||
root.connection-mode-changed(root.lan-mode);
|
||||
}
|
||||
function submit-connection() {
|
||||
if root.connection-pending || root.lan-connecting { return; }
|
||||
if root.lan-mode {
|
||||
root.request-lan-connection(root.remote-id-input);
|
||||
} else { root.connect-requested(root.remote-id-input); }
|
||||
}
|
||||
public function request-lan-connection(address: string) {
|
||||
if !root.ip-direct-enabled || root.connection-pending || root.lan-connecting || root.lan-open { return; }
|
||||
root.lan-access-password = "";
|
||||
root.lan-remember-password = false;
|
||||
let error = root.lan-prepare-connect(address);
|
||||
root.connection-input-error = error == "Enter a numeric IPv4 address or IPv4:port"
|
||||
? GalleryStrings.tr("请输入有效的 IPv4 或 IPv4:端口", "Enter a valid IPv4 or IPv4:port", "Укажите IPv4 или IPv4:порт")
|
||||
: LanStrings.error-message(error);
|
||||
if error == "" {
|
||||
root.lan-open = true;
|
||||
if root.lan-remember-password && root.lan-access-password != "" && root.lan-retry-after == 0 {
|
||||
root.submit-connection-password();
|
||||
}
|
||||
}
|
||||
}
|
||||
in-out property <bool> lan-listening;
|
||||
in-out property <string> lan-listener-ip;
|
||||
in-out property <string> lan-listener-port: "52888";
|
||||
out property <string> lan-listener-address: root.lan-listener-ip == "" ? "" : root.lan-listener-ip + ":" + root.lan-listener-port;
|
||||
in-out property <[string]> lan-interfaces;
|
||||
in-out property <int> lan-interface-index: 0;
|
||||
in-out property <int> lan-interface-draft-index: 0;
|
||||
in-out property <string> lan-local-port: "52888";
|
||||
in-out property <bool> lan-port-editing;
|
||||
in-out property <bool> lan-port-dirty;
|
||||
in-out property <string> lan-endpoint-error;
|
||||
in-out property <string> lan-access-warning;
|
||||
in-out property <string> lan-access-warning-endpoint;
|
||||
in-out property <string> lan-listener-error;
|
||||
function dismiss-lan-access-warning() {
|
||||
root.lan-access-warning = "";
|
||||
root.lan-access-warning-endpoint = "";
|
||||
// Keep the failed draft ready to edit and validate it on every new blur.
|
||||
if root.lan-mode && root.lan-port-dirty && root.sponsorship-unobstructed {
|
||||
lan-port-editor.focus();
|
||||
} else { focus-sink.focus(); }
|
||||
}
|
||||
private property <bool> lan-apply-pending;
|
||||
callback lan-configure(string, string) -> bool;
|
||||
callback lan-select-address(string) -> bool;
|
||||
function apply-lan-endpoint() -> bool {
|
||||
root.lan-apply-pending = false;
|
||||
if !root.ip-direct-enabled || root.lan-password-pending { return false; }
|
||||
if root.lan-configure(root.lan-listener-ip, root.lan-local-port) {
|
||||
root.lan-port-dirty = false;
|
||||
return true;
|
||||
}
|
||||
root.lan-access-warning-endpoint = root.lan-listener-ip
|
||||
+ (root.lan-endpoint-error == "Invalid LAN port" ? "" : ":" + root.lan-local-port);
|
||||
root.lan-access-warning = root.lan-endpoint-error;
|
||||
return false;
|
||||
}
|
||||
Timer {
|
||||
interval: 1ms;
|
||||
running: root.lan-apply-pending;
|
||||
triggered => {
|
||||
root.lan-apply-pending = false;
|
||||
if root.lan-port-dirty { root.apply-lan-endpoint(); }
|
||||
}
|
||||
}
|
||||
in-out property <string> lan-remote-address;
|
||||
in-out property <string> lan-remote-port: "52888";
|
||||
in-out property <bool> lan-password-pending;
|
||||
in-out property <bool> lan-connecting;
|
||||
in-out property <int> lan-retry-after;
|
||||
in-out property <string> lan-access-password;
|
||||
in-out property <bool> lan-remember-password;
|
||||
in-out property <string> lan-new-password;
|
||||
in-out property <string> lan-message;
|
||||
callback lan-reset-password;
|
||||
callback lan-copy-text(string) -> bool;
|
||||
callback lan-cancel-connect;
|
||||
callback lan-prepare-connect(string) -> string;
|
||||
callback lan-connect(string, string, string, bool);
|
||||
private property <bool> prompt-password-required: root.lan-open || root.connection-password-required;
|
||||
private property <bool> prompt-validating: root.lan-open ? root.lan-connecting : root.connection-validating;
|
||||
private property <bool> prompt-pending: root.lan-open ? root.lan-connecting : root.connection-pending;
|
||||
private property <string> prompt-status-text: !root.lan-open ? root.connection-status-text
|
||||
: root.lan-connecting ? GalleryStrings.tr("正在连接…", "Connecting…", "Подключение…")
|
||||
: root.lan-retry-after > 0 ? GalleryStrings.tr("请在 " + root.lan-retry-after + " 秒后重试", "Try again in " + root.lan-retry-after + "s", "Повторите через " + root.lan-retry-after + " с")
|
||||
: root.lan-message == "InvalidCredentials" ? UiStrings.reinput-password
|
||||
: root.lan-message == "" || root.lan-message == "AuthenticationRateLimited" ? UiStrings.input-password
|
||||
: LanStrings.error-message(root.lan-message);
|
||||
function submit-connection-password() {
|
||||
if root.lan-open {
|
||||
if root.lan-connecting || root.lan-retry-after > 0 || root.lan-access-password.character-count != 6 { return; }
|
||||
root.lan-connect(root.lan-remote-address, root.lan-remote-port, root.lan-access-password, root.lan-remember-password);
|
||||
root.lan-access-password = "";
|
||||
} else { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
|
||||
}
|
||||
function cancel-connection-prompt() {
|
||||
if root.lan-open {
|
||||
root.lan-cancel-connect();
|
||||
root.lan-access-password = "";
|
||||
root.lan-open = false;
|
||||
focus-sink.focus();
|
||||
} else { root.connection-cancel(); }
|
||||
}
|
||||
in-out property <string> remote-id-input: "";
|
||||
in property <bool> password-visible: true;
|
||||
in property <bool> password-change-pending: false;
|
||||
@@ -456,6 +625,8 @@ export component MainWindow inherits Window {
|
||||
in-out property <int> capture-method-index: 0;
|
||||
in-out property <int> codec-index: 0;
|
||||
in-out property <bool> turn-enabled: true;
|
||||
in-out property <bool> ip-direct-enabled-draft: true;
|
||||
in property <string> settings-save-error;
|
||||
in-out property <bool> force-relay-enabled: false;
|
||||
changed turn-enabled => {
|
||||
if !root.turn-enabled { root.force-relay-enabled = false; }
|
||||
@@ -555,11 +726,11 @@ export component MainWindow inherits Window {
|
||||
&& !root.update-confirm-open
|
||||
&& !root.reset-password-open && !root.alias-open && !root.delete-open
|
||||
&& !root.disconnect-open
|
||||
&& !root.device-group-open && !root.connection-dialog-open
|
||||
&& !root.device-group-open && !root.connection-dialog-open && !root.lan-open
|
||||
&& !root.permission-dialog-open && !root.wayland-notice-open
|
||||
&& !root.portable-service-dialog-open
|
||||
&& !root.portable-service-suppressed-notice-open
|
||||
&& root.offline-warning == "";
|
||||
&& root.offline-warning == "" && root.lan-access-warning == "";
|
||||
out property <bool> can-show-sponsorship: !root.sponsorship-open
|
||||
&& root.sponsorship-unobstructed;
|
||||
// Follow the first available update. Dismissing the dialog replaces this
|
||||
@@ -570,6 +741,10 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
in-out property <bool> update-confirm-open: false;
|
||||
in-out property <bool> reset-password-open: false;
|
||||
changed reset-password-open => {
|
||||
root.new-password-input = "";
|
||||
root.reset-password-invalid = false;
|
||||
}
|
||||
in-out property <bool> reset-password-invalid: false;
|
||||
in-out property <string> new-password-input: "";
|
||||
in-out property <bool> alias-open: false;
|
||||
@@ -926,9 +1101,21 @@ export component MainWindow inherits Window {
|
||||
|
||||
// A compact application toolbar keeps global actions outside device cards.
|
||||
// Center the 28px buttons above the cards with 6px of space on each side.
|
||||
connection-mode-button := WorkbenchButton {
|
||||
visible: !root.gallery-mode && root.ip-direct-enabled;
|
||||
x: 20px; y: 6px;
|
||||
width: 120px; height: 28px;
|
||||
text: root.lan-mode ? GalleryStrings.tr("IP 直连模式", "Direct IP mode", "Режим IP") : GalleryStrings.tr("ID 连接模式", "ID mode", "Режим ID");
|
||||
tooltip: root.lan-mode ? GalleryStrings.tr("切换到 ID 连接模式", "Switch to ID mode", "Переключить в режим ID") : GalleryStrings.tr("切换到 IP 直连模式", "Switch to direct IP mode", "Переключить в режим IP");
|
||||
accessible-label: self.text + " · " + self.tooltip;
|
||||
enabled: !root.connection-pending && !root.lan-connecting;
|
||||
z: 8;
|
||||
clicked => { root.switch-connection-mode(); }
|
||||
}
|
||||
open-gallery-button := WorkbenchButton {
|
||||
visible: !root.gallery-mode;
|
||||
x: 20px; y: 6px;
|
||||
x: root.ip-direct-enabled ? connection-mode-button.x + connection-mode-button.width + 8px : 20px;
|
||||
y: connection-mode-button.y;
|
||||
width: 80px; height: 28px;
|
||||
text: GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub");
|
||||
z: 8;
|
||||
@@ -1024,22 +1211,166 @@ export component MainWindow inherits Window {
|
||||
x: 0px; y: 0px;
|
||||
width: (parent.width - 16px) / 2; height: parent.height;
|
||||
Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.local-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; }
|
||||
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: GalleryStrings.tr("将 ID 和密码分享给连接方", "Share your ID and password to get connected", "Передайте ID и пароль для подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
|
||||
Text { x: 16px; y: 68px; height: 18px; text: UiStrings.local-id; font-size: 12px; color: WorkbenchStyle.muted; }
|
||||
Rectangle {
|
||||
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("将 IP 和密码分享给连接方", "Share your IP and password to get connected", "Передайте IP и пароль для управления") : GalleryStrings.tr("将 ID 和密码分享给连接方", "Share your ID and password to get connected", "Передайте ID и пароль для подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
|
||||
local-address-label := Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("本机 IP / 端口", "Local IP / Port", "IP / Порт") : UiStrings.local-id; font-size: 12px; color: WorkbenchStyle.muted; vertical-alignment: top; }
|
||||
lan-switch-ip-metrics := Text {
|
||||
visible: false; accessible-role: none;
|
||||
text: GalleryStrings.tr("切换 IP", "Switch IP", "Сменить IP");
|
||||
font-size: 12px; font-weight: 500;
|
||||
}
|
||||
lan-edit-port-metrics := Text {
|
||||
visible: false; accessible-role: none;
|
||||
text: GalleryStrings.tr("修改端口", "Edit port", "Изменить порт");
|
||||
font-size: 12px; font-weight: 500;
|
||||
}
|
||||
lan-switch-ip-action := WorkbenchButton {
|
||||
visible: root.lan-mode;
|
||||
x: local-address-label.x + local-address-label.preferred-width + 4px;
|
||||
y: local-address-label.y - 5px;
|
||||
width: lan-switch-ip-metrics.preferred-width + 20px; height: 22px;
|
||||
quiet: true; text: lan-switch-ip-metrics.text;
|
||||
text-size: 12px; text-color: WorkbenchStyle.accent;
|
||||
enabled: root.lan-mode && !root.lan-password-pending && root.lan-interfaces.length > 0;
|
||||
clicked => {
|
||||
if root.lan-port-dirty && !root.apply-lan-endpoint() { return; }
|
||||
lan-ip-picker.open-menu();
|
||||
}
|
||||
}
|
||||
WorkbenchButton {
|
||||
visible: root.lan-mode;
|
||||
x: lan-switch-ip-action.x + lan-switch-ip-action.width + 2px;
|
||||
y: lan-switch-ip-action.y;
|
||||
width: lan-edit-port-metrics.preferred-width + 20px; height: 22px;
|
||||
quiet: true; text: lan-edit-port-metrics.text;
|
||||
text-size: 12px; text-color: WorkbenchStyle.accent;
|
||||
enabled: root.lan-mode && !root.lan-password-pending && root.lan-interfaces.length > 0;
|
||||
clicked => {
|
||||
// Moving from the editor to this action is still editing.
|
||||
root.lan-apply-pending = false;
|
||||
lan-port-editor.focus();
|
||||
lan-port-editor.select-all();
|
||||
}
|
||||
}
|
||||
local-address-field := Rectangle {
|
||||
x: 16px; y: 90px; width: parent.width - 32px; height: 40px;
|
||||
// Size the endpoint as one line; reserve five port digits so
|
||||
// editing never shifts the IP or changes the font size.
|
||||
private property <float> lan-font-scale: min(1,
|
||||
(self.width - 80px) / (lan-ip-metrics.preferred-width + lan-port-metrics.preferred-width));
|
||||
lan-ip-metrics := Text {
|
||||
visible: false; accessible-role: none;
|
||||
text: root.lan-interfaces[root.lan-interface-draft-index];
|
||||
font-size: 18px; font-weight: 600;
|
||||
}
|
||||
lan-port-metrics := Text {
|
||||
visible: false; accessible-role: none;
|
||||
text: "65535"; font-size: 18px; font-weight: 600;
|
||||
}
|
||||
// Raise the whole field: a child's z cannot escape its
|
||||
// parent's stacking order above the following password row.
|
||||
z: 1;
|
||||
background: WorkbenchStyle.field-surface; border-radius: UiRadius.control;
|
||||
border-width: 1px; border-color: WorkbenchStyle.border;
|
||||
Text { x: 12px; width: parent.width - 64px; height: parent.height; text: root.local-id; font-size: 24px; font-weight: 600; letter-spacing: 1px; color: WorkbenchStyle.text; vertical-alignment: center; }
|
||||
border-width: 1px;
|
||||
border-color: !root.lan-mode ? WorkbenchStyle.border
|
||||
: root.lan-endpoint-error != "" ? #d92d20
|
||||
: lan-ip-picker.has-focus || root.lan-port-editing ? WorkbenchStyle.accent
|
||||
: WorkbenchStyle.border;
|
||||
animate border-color { duration: 120ms; }
|
||||
if !root.lan-mode: Text { x: 12px; width: parent.width - 64px; height: parent.height; text: root.local-id; font-size: 24px; font-weight: 600; letter-spacing: 1px; overflow: elide; color: WorkbenchStyle.text; vertical-alignment: center; }
|
||||
Rectangle { x: parent.width - 48px; y: 10px; width: 1px; height: parent.height - 20px; background: WorkbenchStyle.border; }
|
||||
lan-ip-picker := GroupFilter {
|
||||
visible: root.lan-mode;
|
||||
x: 4px; y: 2px;
|
||||
width: lan-ip-metrics.preferred-width * local-address-field.lan-font-scale + 16px;
|
||||
height: parent.height - 4px;
|
||||
embedded: true; text-size: 18px * local-address-field.lan-font-scale;
|
||||
text-weight: 600; text-color: WorkbenchStyle.text;
|
||||
show-indicator: false;
|
||||
popup-min-width: 160px;
|
||||
model: root.lan-interfaces; system-count: 0;
|
||||
current-index <=> root.lan-interface-draft-index;
|
||||
enabled: root.lan-mode && !root.lan-password-pending && root.lan-interfaces.length > 0;
|
||||
normal-border-color: WorkbenchStyle.control-border;
|
||||
hover-border-color: WorkbenchStyle.hover-border;
|
||||
focus-border-color: WorkbenchStyle.accent;
|
||||
accessible-label: GalleryStrings.tr("切换本机 IP", "Switch local IP", "Сменить локальный IP");
|
||||
selected(index) => {
|
||||
root.copy-feedback = CopyFeedback.idle;
|
||||
// Save the draft for its original IP before changing
|
||||
// the display. Selecting an IP never rebinds sockets.
|
||||
if root.lan-port-dirty && !root.apply-lan-endpoint() {
|
||||
root.lan-interface-draft-index = root.lan-interface-index;
|
||||
return;
|
||||
}
|
||||
if root.lan-select-address(root.lan-interfaces[index]) {
|
||||
root.lan-interface-index = index;
|
||||
} else {
|
||||
root.lan-access-warning-endpoint = root.lan-interfaces[index];
|
||||
root.lan-access-warning = root.lan-endpoint-error;
|
||||
root.lan-interface-draft-index = root.lan-interface-index;
|
||||
}
|
||||
}
|
||||
}
|
||||
if root.lan-mode: Text {
|
||||
x: lan-ip-picker.x + lan-ip-picker.width - 4px; width: 6px; height: parent.height;
|
||||
text: ":"; font-size: 18px * local-address-field.lan-font-scale;
|
||||
font-weight: 600; color: WorkbenchStyle.text;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
}
|
||||
lan-port-field := Rectangle {
|
||||
visible: root.lan-mode;
|
||||
x: lan-ip-picker.x + lan-ip-picker.width + 2px; y: 2px;
|
||||
width: lan-port-metrics.preferred-width * local-address-field.lan-font-scale + 4px;
|
||||
height: parent.height - 4px;
|
||||
background: lan-port-editor.has-focus ? WorkbenchStyle.surface
|
||||
: lan-port-hover.has-hover ? WorkbenchStyle.hover-surface : transparent;
|
||||
border-radius: UiRadius.control;
|
||||
border-width: 1px;
|
||||
border-color: lan-port-editor.has-focus ? WorkbenchStyle.accent : transparent;
|
||||
animate background, border-color { duration: 120ms; }
|
||||
lan-port-hover := TouchArea {
|
||||
enabled: root.lan-mode && !root.lan-password-pending;
|
||||
mouse-cursor: text;
|
||||
FocusScope {
|
||||
width: parent.width; height: parent.height;
|
||||
key-pressed(event) => {
|
||||
if event.text == Key.Escape {
|
||||
root.lan-apply-pending = false;
|
||||
root.lan-port-dirty = false;
|
||||
root.lan-local-port = root.lan-listener-port;
|
||||
root.lan-endpoint-error = "";
|
||||
focus-sink.focus();
|
||||
return accept;
|
||||
}
|
||||
return reject;
|
||||
}
|
||||
lan-port-editor := TextInput {
|
||||
x: 2px; width: parent.width - 4px; height: parent.height;
|
||||
text <=> root.lan-local-port;
|
||||
font-size: 18px * local-address-field.lan-font-scale; font-weight: 600;
|
||||
input-type: InputType.number; color: WorkbenchStyle.text;
|
||||
enabled: root.lan-mode && !root.lan-password-pending;
|
||||
accessible-label: GalleryStrings.tr("局域网端口", "LAN port", "Порт ЛВС");
|
||||
vertical-alignment: center; horizontal-alignment: left;
|
||||
selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent;
|
||||
changed has-focus => {
|
||||
root.lan-port-editing = self.has-focus;
|
||||
// Opening the warning also moves focus: do not
|
||||
// treat that as another attempt to save.
|
||||
if !self.has-focus && root.lan-port-dirty && root.lan-access-warning == "" { root.lan-apply-pending = true; }
|
||||
}
|
||||
edited => { root.lan-port-dirty = true; root.lan-endpoint-error = ""; root.copy-feedback = CopyFeedback.idle; }
|
||||
accepted => { if root.apply-lan-endpoint() { focus-sink.focus(); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
copy-button := WorkbenchButton {
|
||||
x: parent.width - 42px; y: 2px; width: 36px; quiet: true;
|
||||
icon: root.copy-feedback == CopyFeedback.success ? FontAwesomeIcons.check : FontAwesomeIcons.copy;
|
||||
icon-color: root.copy-feedback == CopyFeedback.success ? WorkbenchStyle.success : WorkbenchStyle.muted;
|
||||
tooltip: GalleryStrings.tr("复制本机 ID", "Copy local ID", "Копировать ID");
|
||||
tooltip: root.lan-mode ? GalleryStrings.tr("复制 IP 和端口", "Copy IP and port", "Копировать IP и порт") : GalleryStrings.tr("复制本机 ID", "Copy local ID", "Копировать ID");
|
||||
enabled: !root.lan-mode || root.lan-listener-address != "";
|
||||
tooltip-suppressed: root.copy-feedback != CopyFeedback.idle;
|
||||
// Let other controls receive the original click; moving
|
||||
// focus away from Copy dismisses its transient feedback.
|
||||
@@ -1047,35 +1378,61 @@ export component MainWindow inherits Window {
|
||||
if !self.has-focus { root.copy-feedback = CopyFeedback.idle; }
|
||||
}
|
||||
clicked => {
|
||||
root.copy-feedback = root.copy-local-id() ? CopyFeedback.success : CopyFeedback.failed;
|
||||
if root.lan-mode && root.lan-port-dirty && !root.apply-lan-endpoint() { return; }
|
||||
let copied = root.lan-mode ? root.lan-copy-text(root.lan-listener-address) : root.copy-local-id();
|
||||
root.copy-feedback = copied ? CopyFeedback.success : CopyFeedback.failed;
|
||||
copy-timer.restart();
|
||||
}
|
||||
}
|
||||
private property <string> edit-hint: !root.lan-mode || !root.sponsorship-unobstructed
|
||||
|| root.lan-password-pending ? ""
|
||||
: lan-ip-picker.has-hover ? GalleryStrings.tr("点击切换本机 IP", "Click to switch the local IP", "Нажмите, чтобы сменить IP")
|
||||
: lan-port-hover.has-hover && !lan-port-editor.has-focus ? GalleryStrings.tr("点击修改端口", "Click to edit the port", "Нажмите, чтобы изменить порт") : "";
|
||||
private property <bool> edit-hint-visible;
|
||||
changed edit-hint => { local-address-field.edit-hint-visible = false; }
|
||||
Timer {
|
||||
interval: 450ms;
|
||||
running: local-address-field.edit-hint != "" && !local-address-field.edit-hint-visible;
|
||||
triggered => { local-address-field.edit-hint-visible = true; }
|
||||
}
|
||||
if local-address-field.edit-hint != "" && local-address-field.edit-hint-visible: Rectangle {
|
||||
x: clamp((lan-ip-picker.has-hover ? lan-ip-picker.x + lan-ip-picker.width / 2
|
||||
: lan-port-field.x + lan-port-field.width / 2) - self.width / 2, 0px, parent.width - self.width);
|
||||
y: parent.height + 6px;
|
||||
width: min(parent.width, edit-hint-text.preferred-width + 20px);
|
||||
height: max(28px, edit-hint-text.preferred-height + 12px);
|
||||
background: #24324a; border-radius: UiRadius.control; z: 20;
|
||||
edit-hint-text := Text {
|
||||
x: 10px; y: 6px; width: parent.width - 20px; height: parent.height - 12px;
|
||||
text: local-address-field.edit-hint; font-size: 12px; color: white;
|
||||
wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
Text { x: 16px; y: 138px; height: 18px; text: UiStrings.password; font-size: 12px; color: WorkbenchStyle.muted; }
|
||||
Rectangle {
|
||||
x: 16px; y: 158px; width: parent.width - 32px; height: 36px;
|
||||
background: WorkbenchStyle.field-surface; border-radius: UiRadius.control;
|
||||
border-width: 1px; border-color: WorkbenchStyle.border;
|
||||
Text { x: 12px; width: parent.width - 132px; height: parent.height; text: root.password-visible ? root.local-password : "••••••"; font-size: 20px; font-weight: 500; letter-spacing: 2px; color: WorkbenchStyle.text; vertical-alignment: center; }
|
||||
Text { x: 12px; width: parent.width - 132px; height: parent.height; text: root.active-password == "" ? "—" : root.active-password-visible ? root.active-password : "••••••"; font-size: 20px; font-weight: 500; letter-spacing: 2px; color: WorkbenchStyle.text; vertical-alignment: center; }
|
||||
Rectangle { x: parent.width - 118px; y: 8px; width: 1px; height: parent.height - 16px; background: WorkbenchStyle.border; }
|
||||
WorkbenchButton {
|
||||
x: parent.width - 112px; y: 0px; width: 36px; quiet: true;
|
||||
icon: root.password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash;
|
||||
tooltip: root.password-visible ? GalleryStrings.tr("隐藏密码", "Hide password", "Скрыть пароль") : GalleryStrings.tr("显示密码", "Show password", "Показать пароль");
|
||||
clicked => { root.toggle-password-visibility(); }
|
||||
icon: root.active-password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash;
|
||||
tooltip: root.active-password-visible ? GalleryStrings.tr("隐藏密码", "Hide password", "Скрыть пароль") : GalleryStrings.tr("显示密码", "Show password", "Показать пароль");
|
||||
clicked => { if root.lan-mode { root.lan-password-visible = !root.lan-password-visible; } else { root.toggle-password-visibility(); } }
|
||||
}
|
||||
WorkbenchButton {
|
||||
x: parent.width - 74px; y: 0px; width: 36px; quiet: true;
|
||||
icon: FontAwesomeIcons.rotate; tooltip: UiStrings.refresh-password;
|
||||
enabled: !root.password-change-pending;
|
||||
clicked => { root.refresh-password(); }
|
||||
enabled: root.lan-mode ? !root.lan-password-pending : !root.password-change-pending;
|
||||
clicked => { if root.lan-mode { root.lan-reset-password(); } else { root.refresh-password(); } }
|
||||
}
|
||||
WorkbenchButton {
|
||||
x: parent.width - 36px; y: 0px; width: 36px; quiet: true;
|
||||
icon: FontAwesomeIcons.pen;
|
||||
tooltip: GalleryStrings.tr("修改密码", "Change password", "Изменить пароль");
|
||||
enabled: !root.password-change-pending;
|
||||
enabled: root.lan-mode ? !root.lan-password-pending : !root.password-change-pending;
|
||||
clicked => { root.reset-password-open = true; }
|
||||
}
|
||||
}
|
||||
@@ -1083,8 +1440,8 @@ export component MainWindow inherits Window {
|
||||
WorkbenchCard {
|
||||
x: local-card.width + 16px; y: 0px; width: local-card.width; height: parent.height;
|
||||
Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.remote-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; }
|
||||
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: GalleryStrings.tr("输入设备 ID,连接远程桌面", "Enter a device ID to start a remote session", "Введите ID для удалённого подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
|
||||
Text { x: 16px; y: 68px; height: 18px; text: UiStrings.remote-id; font-size: 12px; color: WorkbenchStyle.muted; }
|
||||
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("输入 IP:端口,通过局域网连接", "Enter IP:port to connect over LAN", "Введите IP:порт для подключения по LAN") : GalleryStrings.tr("输入设备 ID,连接远程桌面", "Enter a device ID to start a remote session", "Введите ID для удалённого подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
|
||||
Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("远端 IP / 端口", "Remote IP / Port", "Удалённый IP / Порт") : UiStrings.remote-id; font-size: 12px; color: WorkbenchStyle.muted; }
|
||||
Rectangle {
|
||||
x: 16px; y: 90px; width: parent.width - 32px; height: 40px;
|
||||
background: white; border-radius: UiRadius.control; border-width: 1px;
|
||||
@@ -1092,40 +1449,42 @@ export component MainWindow inherits Window {
|
||||
animate border-color { duration: 120ms; }
|
||||
if root.remote-id-input == "": Text {
|
||||
x: 12px; width: parent.width - 24px; height: parent.height;
|
||||
text: GalleryStrings.tr("请输入 9 位设备 ID", "Enter a 9-digit device ID", "Введите ID из 9 цифр");
|
||||
text: root.lan-mode ? "192.168.1.20:52888" : GalleryStrings.tr("请输入 9 位设备 ID", "Enter a 9-digit device ID", "Введите ID из 9 цифр");
|
||||
font-size: 13px; color: #8290a4; vertical-alignment: center;
|
||||
}
|
||||
remote-id-editor := TextInput {
|
||||
x: 12px; width: parent.width - 24px; height: parent.height;
|
||||
text <=> root.remote-id-input; font-size: 24px; font-weight: 600; letter-spacing: 1px;
|
||||
input-type: InputType.number; color: WorkbenchStyle.text;
|
||||
accessible-label: UiStrings.remote-id;
|
||||
text <=> root.remote-id-input; font-size: root.lan-mode ? 18px : 24px; font-weight: 600; letter-spacing: root.lan-mode ? 0px : 1px;
|
||||
input-type: root.lan-mode ? InputType.text : InputType.number; color: WorkbenchStyle.text;
|
||||
accessible-label: root.lan-mode ? GalleryStrings.tr("远端 IP / 端口", "Remote IP / Port", "Удалённый IP / Порт") : UiStrings.remote-id;
|
||||
enabled: !root.connection-pending && !root.lan-connecting;
|
||||
vertical-alignment: center;
|
||||
selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent;
|
||||
edited => {
|
||||
root.connection-input-error = "";
|
||||
// Enforce the digit limit before the next frame can
|
||||
// render the text from typing, paste, or replacement.
|
||||
let formatted = root.format-remote-id(self.text);
|
||||
let formatted = root.lan-mode ? self.text : root.format-remote-id(self.text);
|
||||
self.text = formatted;
|
||||
// Formatted IDs are ASCII, so these are UTF-8 offsets.
|
||||
self.set-selection-offsets(
|
||||
formatted.character-count, formatted.character-count);
|
||||
}
|
||||
accepted => { if !root.connection-pending { root.connect-requested(root.remote-id-input); } }
|
||||
accepted => { root.submit-connection(); }
|
||||
}
|
||||
}
|
||||
Text {
|
||||
x: 16px; y: 133px; width: parent.width - 32px; height: 24px;
|
||||
text: root.connection-input-error;
|
||||
font-size: 11px; color: #be3d42; wrap: word-wrap;
|
||||
text: root.connection-input-error != "" ? root.connection-input-error
|
||||
: root.lan-mode ? GalleryStrings.tr("不填端口时,默认使用 52888", "Port defaults to 52888 if omitted", "Без порта используется 52888") : "";
|
||||
font-size: 11px; color: root.connection-input-error != "" ? #be3d42 : WorkbenchStyle.muted; wrap: word-wrap;
|
||||
}
|
||||
WorkbenchButton {
|
||||
x: 16px; y: 158px; width: parent.width - 32px; height: 36px;
|
||||
primary: true; icon: FontAwesomeIcons.arrow-right-long;
|
||||
text: root.connection-pending ? GalleryStrings.tr("正在连接…", "Connecting…", "Подключение…") : UiStrings.connect;
|
||||
enabled: !root.connection-pending;
|
||||
clicked => { root.connect-requested(root.remote-id-input); }
|
||||
text: root.connection-pending || root.lan-connecting ? GalleryStrings.tr("正在连接…", "Connecting…", "Подключение…") : UiStrings.connect;
|
||||
enabled: !root.connection-pending && !root.lan-connecting;
|
||||
clicked => { root.submit-connection(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1137,6 +1496,7 @@ export component MainWindow inherits Window {
|
||||
|
||||
Text { x: 20px; y: 0px; width: recent-group-filter.x - self.x - 12px; height: 32px; text: UiStrings.recent-connections; font-size: 15px; font-weight: 600; color: WorkbenchStyle.text; vertical-alignment: center; }
|
||||
recent-group-filter := GroupFilter {
|
||||
visible: !root.lan-mode;
|
||||
x: parent.width - self.width - 20px; y: 0px;
|
||||
width: 155px; height: 32px;
|
||||
normal-border-color: WorkbenchStyle.control-border;
|
||||
@@ -1169,14 +1529,18 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
Text {
|
||||
x: 84px; y: 36px; width: parent.width - 104px; height: 24px;
|
||||
text: root.recent-group-index == 0
|
||||
text: root.lan-mode
|
||||
? GalleryStrings.tr("暂无 IP 直连设备", "No recent IP connections", "Нет недавних IP-подключений")
|
||||
: root.recent-group-index == 0
|
||||
? GalleryStrings.tr("暂无近期连接", "No recent connections", "Нет недавних подключений")
|
||||
: GalleryStrings.tr("该分组暂无近期连接", "No recent connections in this group", "В этой группе нет недавних подключений");
|
||||
color: WorkbenchStyle.text; font-size: 14px; font-weight: 600;
|
||||
}
|
||||
Text {
|
||||
x: 84px; y: 68px; width: parent.width - 104px; height: 20px;
|
||||
text: root.recent-group-index == 0
|
||||
text: root.lan-mode
|
||||
? GalleryStrings.tr("通过 IP 连接过的设备会显示在这里。", "Devices you connect to by IP will appear here.", "Здесь появятся устройства, подключённые по IP.")
|
||||
: root.recent-group-index == 0
|
||||
? GalleryStrings.tr("连接过的设备会显示在这里,方便下次快速访问。", "Devices you connect to will appear here for quick access.", "Устройства появятся здесь после первого подключения.")
|
||||
: GalleryStrings.tr("切换设备分组,查看其他连接记录。", "Choose another group to see your other connections.", "Выберите другую группу для просмотра подключений.");
|
||||
color: WorkbenchStyle.muted; font-size: 12px;
|
||||
@@ -1202,7 +1566,7 @@ export component MainWindow inherits Window {
|
||||
width: (recent-scroll.width - 24px) / 3;
|
||||
height: 126px;
|
||||
background: transparent;
|
||||
private property <bool> available: connection.online || connection.connected;
|
||||
private property <bool> available: connection.lan || connection.online || connection.connected;
|
||||
private property <length> content-width: self.width;
|
||||
private property <length> scroll-offset: recent-scroll.viewport-x;
|
||||
private property <string> device-id: connection.remote-id;
|
||||
@@ -1263,7 +1627,7 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
presence-label := Text {
|
||||
x: 17px; width: self.preferred-width; height: parent.height;
|
||||
text: connection-card.available ? UiStrings.online : UiStrings.offline;
|
||||
text: connection.lan ? "LAN" : connection-card.available ? UiStrings.online : UiStrings.offline;
|
||||
color: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted;
|
||||
font-size: 10px; font-weight: 500; vertical-alignment: center;
|
||||
}
|
||||
@@ -1286,7 +1650,7 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
Text {
|
||||
x: 12px; y: 24px; height: 16px; width: parent.width - 56px;
|
||||
text: connection.remote-id; font-size: 11px; color: WorkbenchStyle.muted;
|
||||
text: connection.lan ? connection.endpoint : connection.remote-id; font-size: 11px; color: WorkbenchStyle.muted;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
@@ -1367,7 +1731,7 @@ export component MainWindow inherits Window {
|
||||
menu-width: root.compact-language ? 116px : root.language-index == 2 ? 156px : 128px;
|
||||
items: [
|
||||
{label: GalleryStrings.tr("修改名称", "Rename", "Переименовать"), enabled: true},
|
||||
{label: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы"), enabled: true},
|
||||
{label: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы"), enabled: !connection.lan},
|
||||
{label: GalleryStrings.tr("删除记录", "Delete record", "Удалить запись"), enabled: true},
|
||||
];
|
||||
item-icons: [FontAwesomeIcons.pen, FontAwesomeIcons.tags, FontAwesomeIcons.trash-can];
|
||||
@@ -1465,7 +1829,7 @@ export component MainWindow inherits Window {
|
||||
root.delete-remote-id = id; root.delete-from-gallery = true; root.delete-open = true;
|
||||
}
|
||||
}
|
||||
// Server status and reconnect actions belong to the connection window.
|
||||
// Show availability for the selected connection mode.
|
||||
if !root.gallery-mode: Rectangle {
|
||||
y: parent.height - 28px;
|
||||
height: 28px;
|
||||
@@ -1477,13 +1841,16 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
Rectangle {
|
||||
x: 20px; y: 11px; width: 6px; height: 6px; border-radius: 3px;
|
||||
background: root.signal-tls-error || root.signal-reconnecting ? #d6932b : root.signal-connected ? WorkbenchStyle.success : #d65252;
|
||||
background: root.lan-mode ? (root.lan-message != "" || root.lan-listener-error != "" ? #d6932b : root.lan-listening ? WorkbenchStyle.success : WorkbenchStyle.muted) : root.signal-tls-error || root.signal-reconnecting ? #d6932b : root.signal-connected ? WorkbenchStyle.success : #d65252;
|
||||
}
|
||||
Text {
|
||||
x: 34px;
|
||||
y: 0px;
|
||||
height: parent.height;
|
||||
text: root.signal-tls-error ? UiStrings.tls-error
|
||||
width: parent.width - 160px;
|
||||
overflow: elide;
|
||||
text: root.lan-mode ? (root.lan-message != "" ? LanStrings.error-message(root.lan-message) : root.lan-listener-error != "" ? (root.lan-listener-address == "" ? "" : root.lan-listener-address + " · ") + LanStrings.error-message(root.lan-listener-error) : root.lan-listening ? GalleryStrings.tr("局域网模式已启用", "LAN mode enabled", "Режим LAN включён") : root.lan-password-pending ? GalleryStrings.tr("局域网模式准备中…", "Preparing LAN mode…", "Подготовка режима LAN…") : GalleryStrings.tr("局域网模式未就绪", "LAN mode not ready", "Режим LAN не готов"))
|
||||
: root.signal-tls-error ? UiStrings.tls-error
|
||||
: root.signal-connected ? UiStrings.signal-connected
|
||||
: root.signal-reconnecting ? UiStrings.signal-reconnecting
|
||||
: UiStrings.signal-disconnected;
|
||||
@@ -1491,12 +1858,12 @@ export component MainWindow inherits Window {
|
||||
font-size: 11px;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
if root.signal-connected && !root.signal-tls-error: Text {
|
||||
if root.lan-mode || (root.signal-connected && !root.signal-tls-error): Text {
|
||||
x: parent.width - self.preferred-width - 20px; height: parent.height;
|
||||
text: root.current-version;
|
||||
font-size: 11px; color: WorkbenchStyle.muted; vertical-alignment: center;
|
||||
}
|
||||
if !root.signal-connected && !root.signal-tls-error: WorkbenchButton {
|
||||
if !root.lan-mode && !root.signal-connected && !root.signal-tls-error: WorkbenchButton {
|
||||
x: parent.width - self.width - 20px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: max(64px, reconnect-label-metrics.preferred-width + 22px);
|
||||
@@ -1521,7 +1888,7 @@ export component MainWindow inherits Window {
|
||||
// Shared device details sit outside the clipped recent list and wall rail,
|
||||
// below the modal layer (z >= 39).
|
||||
if (root.gallery-mode ? gallery.hovered-device.id != "" : root.recent-tooltip-visible)
|
||||
&& !root.connection-dialog-open: device-tip := Rectangle {
|
||||
&& !root.connection-dialog-open && !root.lan-open: device-tip := Rectangle {
|
||||
private property <string> device-name: root.gallery-mode ? gallery.hovered-device.name : root.recent-tooltip-name;
|
||||
private property <string> host-name: root.gallery-mode ? gallery.hovered-device.host-name : root.recent-tooltip-host;
|
||||
private property <string> device-id: root.gallery-mode ? gallery.hovered-device.id : root.recent-tooltip-id;
|
||||
@@ -1824,7 +2191,7 @@ export component MainWindow inherits Window {
|
||||
icon: self.error ? FontAwesomeIcons.xmark : FontAwesomeIcons.check;
|
||||
text: self.error
|
||||
? GalleryStrings.tr("复制失败,请重试", "Couldn’t copy. Try again", "Не удалось скопировать. Повторите попытку")
|
||||
: UiStrings.copied;
|
||||
: root.lan-mode ? GalleryStrings.tr("IP 和端口已复制", "IP and port copied", "IP и порт скопированы") : UiStrings.copied;
|
||||
z: 30;
|
||||
}
|
||||
|
||||
@@ -1854,6 +2221,7 @@ export component MainWindow inherits Window {
|
||||
alignment: center; spacing: 8px;
|
||||
PrimaryButton {
|
||||
text: UiStrings.ok;
|
||||
enabled: root.lan-mode ? !root.lan-password-pending : !root.password-change-pending;
|
||||
clicked => {
|
||||
if (root.reset-password(root.new-password-input)) {
|
||||
root.new-password-input = "";
|
||||
@@ -2047,32 +2415,41 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
|
||||
// Connection feedback follows the workbench spacing and action hierarchy.
|
||||
if root.connection-dialog-open: Rectangle {
|
||||
if root.connection-dialog-open || root.lan-open: Rectangle {
|
||||
background: #15223850;
|
||||
z: 48;
|
||||
TouchArea { clicked => { focus-sink.focus(); } }
|
||||
TouchArea { clicked => { if !root.lan-open { focus-sink.focus(); } } }
|
||||
WorkbenchCard {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: 340px;
|
||||
height: root.connection-password-required && !root.connection-validating ? 236px : 168px;
|
||||
Text {
|
||||
x: 24px; y: 24px; width: parent.width - 48px; height: 46px;
|
||||
text: root.connection-status-text;
|
||||
height: (root.prompt-password-required && !root.prompt-validating ? 236px : 168px)
|
||||
+ max(0px, connection-prompt-text.preferred-height - 46px);
|
||||
connection-prompt-text := Text {
|
||||
x: 24px; y: 24px; width: parent.width - 48px; height: max(46px, self.preferred-height);
|
||||
text: root.prompt-status-text;
|
||||
color: WorkbenchStyle.text; font-size: 14px;
|
||||
wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center;
|
||||
}
|
||||
if root.connection-password-required && !root.connection-validating: PasswordInput {
|
||||
x: (parent.width - self.width) / 2; y: 86px;
|
||||
if !root.lan-open && root.connection-password-required && !root.connection-validating: PasswordInput {
|
||||
x: (parent.width - self.width) / 2; y: connection-prompt-text.y + connection-prompt-text.height + 16px;
|
||||
width: 164px; height: 36px; text-size: 16px;
|
||||
border-radius: UiRadius.control; border-color: WorkbenchStyle.control-border;
|
||||
text <=> root.connection-password;
|
||||
accepted => { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
|
||||
accepted => { root.submit-connection-password(); }
|
||||
}
|
||||
if root.connection-password-required && !root.connection-validating: Rectangle {
|
||||
x: (parent.width - self.width) / 2; y: 134px;
|
||||
if root.lan-open && !root.lan-connecting: PasswordInput {
|
||||
x: (parent.width - self.width) / 2; y: connection-prompt-text.y + connection-prompt-text.height + 16px;
|
||||
width: 164px; height: 36px; text-size: 16px; max-length: 6;
|
||||
border-radius: UiRadius.control; border-color: WorkbenchStyle.control-border;
|
||||
text <=> root.lan-access-password;
|
||||
accepted => { root.submit-connection-password(); }
|
||||
}
|
||||
if root.prompt-password-required && !root.prompt-validating: Rectangle {
|
||||
x: (parent.width - self.width) / 2; y: connection-prompt-text.y + connection-prompt-text.height + 64px;
|
||||
width: remember-label.preferred-width + 29px; height: 21px;
|
||||
ImGuiCheckBox { x: 0px; y: 0px; checked <=> root.connection-remember-password; }
|
||||
if root.lan-open: ImGuiCheckBox { x: 0px; y: 0px; checked <=> root.lan-remember-password; }
|
||||
if !root.lan-open: ImGuiCheckBox { x: 0px; y: 0px; checked <=> root.connection-remember-password; }
|
||||
remember-label := Text {
|
||||
x: 29px; height: parent.height;
|
||||
text: UiStrings.remember-password;
|
||||
@@ -2080,21 +2457,25 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
TouchArea {
|
||||
x: 29px; width: parent.width - 29px; height: parent.height;
|
||||
clicked => { root.connection-remember-password = !root.connection-remember-password; }
|
||||
clicked => {
|
||||
if root.lan-open { root.lan-remember-password = !root.lan-remember-password; }
|
||||
else { root.connection-remember-password = !root.connection-remember-password; }
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalLayout {
|
||||
x: 24px; y: parent.height - 60px; width: parent.width - 48px; height: 36px;
|
||||
spacing: 12px; alignment: center;
|
||||
if root.connection-password-required || root.connection-pending: WorkbenchButton {
|
||||
if root.prompt-password-required || root.prompt-pending: WorkbenchButton {
|
||||
text: UiStrings.cancel;
|
||||
clicked => { root.connection-cancel(); }
|
||||
clicked => { root.cancel-connection-prompt(); }
|
||||
}
|
||||
if root.connection-password-required && !root.connection-validating: WorkbenchButton {
|
||||
if root.prompt-password-required && !root.prompt-validating: WorkbenchButton {
|
||||
primary: true; text: UiStrings.ok;
|
||||
clicked => { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
|
||||
enabled: !root.lan-open || (root.lan-retry-after == 0 && root.lan-access-password.character-count == 6);
|
||||
clicked => { root.submit-connection-password(); }
|
||||
}
|
||||
if !root.connection-password-required && !root.connection-pending: WorkbenchButton {
|
||||
if !root.prompt-password-required && !root.prompt-pending: WorkbenchButton {
|
||||
primary: true; text: UiStrings.ok;
|
||||
clicked => { root.connection-acknowledge(); }
|
||||
}
|
||||
@@ -2253,7 +2634,7 @@ export component MainWindow inherits Window {
|
||||
if root.settings-tab == 0: settings-scroll := ScrollView {
|
||||
x: settings-dialog.content-inset; y: 108px;
|
||||
width: parent.width - 2 * settings-dialog.content-inset + settings-dialog.scrollbar-gutter;
|
||||
height: parent.height - 174px;
|
||||
height: parent.height - 174px - (root.settings-save-error == "" ? 0px : 28px);
|
||||
viewport-width: self.visible-width;
|
||||
viewport-height: settings-content.preferred-height;
|
||||
viewport-y <=> root.settings-scroll-y;
|
||||
@@ -2288,7 +2669,11 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
SettingsGroup {
|
||||
title: GalleryStrings.tr("网络连接", "Network", "Сеть"); rows: 3;
|
||||
title: GalleryStrings.tr("网络连接", "Network", "Сеть"); rows: 4;
|
||||
SettingsToggleRow {
|
||||
label: GalleryStrings.tr("启用 IP 直连", "Enable direct IP", "Прямое IP-подключение");
|
||||
checked <=> root.ip-direct-enabled-draft;
|
||||
}
|
||||
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 {
|
||||
@@ -2296,6 +2681,10 @@ export component MainWindow inherits Window {
|
||||
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; } }
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: GalleryStrings.tr("关闭 IP 直连将隐藏模式按钮,并断开 IP 直连会话。", "Disabling direct IP hides the mode button and disconnects direct IP sessions.", "Отключение скроет кнопку режима и завершит прямые IP-сеансы.");
|
||||
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
|
||||
}
|
||||
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; }
|
||||
@@ -2324,12 +2713,18 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
if root.settings-tab == 1: GallerySettings {
|
||||
x: settings-dialog.content-inset; y: 108px;
|
||||
width: parent.width - 2 * settings-dialog.content-inset; height: parent.height - 174px;
|
||||
width: parent.width - 2 * settings-dialog.content-inset;
|
||||
height: parent.height - 174px - (root.settings-save-error == "" ? 0px : 28px);
|
||||
resolution-index <=> root.gallery-resolution-draft;
|
||||
frame-rate-index <=> root.gallery-frame-rate-draft;
|
||||
bandwidth-index <=> root.gallery-bandwidth-draft;
|
||||
error: root.gallery-settings-error;
|
||||
}
|
||||
if root.settings-save-error != "": Text {
|
||||
x: settings-dialog.content-inset; y: parent.height - 92px;
|
||||
width: parent.width - 2 * settings-dialog.content-inset; height: 24px;
|
||||
text: root.settings-save-error; font-size: 11px; color: #c53e3e; wrap: word-wrap;
|
||||
}
|
||||
Rectangle {
|
||||
x: settings-dialog.content-inset; y: parent.height - 64px;
|
||||
width: parent.width - 2 * settings-dialog.content-inset; height: 1px; background: #e6ebf2;
|
||||
@@ -3104,4 +3499,79 @@ export component MainWindow inherits Window {
|
||||
z: 200;
|
||||
}
|
||||
}
|
||||
// Each explicit validation failure opens one modal, without a focus loop.
|
||||
if root.lan-access-warning != "": Rectangle {
|
||||
width: parent.width; height: parent.height;
|
||||
background: #00000042;
|
||||
z: 300;
|
||||
TouchArea { scroll-event(event) => { accept } }
|
||||
lan-warning-focus := FocusScope {
|
||||
width: parent.width; height: parent.height;
|
||||
key-pressed(event) => {
|
||||
if event.text == Key.Escape || event.text == Key.Return || event.text == " " {
|
||||
root.dismiss-lan-access-warning();
|
||||
} else if event.text == Key.Tab {
|
||||
self.focus();
|
||||
}
|
||||
return accept;
|
||||
}
|
||||
Timer {
|
||||
interval: 1ms;
|
||||
running: true;
|
||||
triggered => {
|
||||
self.running = false;
|
||||
// Defer focus until after Slint's accessibility update.
|
||||
root.menu-open = false;
|
||||
lan-ip-picker.close-menu();
|
||||
lan-warning-focus.focus();
|
||||
}
|
||||
}
|
||||
DialogSurface {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: min(root.compact-language ? 270px : 320px, parent.width - 40px);
|
||||
// Let short messages stay compact; longer translations grow
|
||||
// naturally without stretching the gap above the action.
|
||||
height: lan-warning-content.preferred-height;
|
||||
TouchArea { scroll-event(event) => { accept } }
|
||||
lan-warning-content := VerticalLayout {
|
||||
padding: 20px; spacing: 20px;
|
||||
padding-top: 28px; padding-bottom: 28px;
|
||||
if root.lan-access-warning-endpoint != "": Text {
|
||||
text: root.lan-access-warning-endpoint;
|
||||
font-size: 12px; font-weight: 600; color: WorkbenchStyle.text;
|
||||
horizontal-alignment: center; wrap: word-wrap;
|
||||
}
|
||||
lan-warning-message := Rectangle {
|
||||
private property <[string]> lines: LanStrings.access-error-lines(root.lan-access-warning);
|
||||
height: warning-lines.preferred-height;
|
||||
vertical-stretch: 0;
|
||||
accessible-role: text;
|
||||
accessible-label: LanStrings.error-message(root.lan-access-warning);
|
||||
warning-lines := VerticalLayout {
|
||||
width: parent.width;
|
||||
height: self.preferred-height;
|
||||
spacing: 6px;
|
||||
for line in (lan-warning-message.lines[0] != "" ? lan-warning-message.lines : [LanStrings.error-message(root.lan-access-warning)]): Text {
|
||||
text: line; font-size: ImGuiFontStyle.base; color: #30343b;
|
||||
horizontal-alignment: center; wrap: word-wrap;
|
||||
accessible-role: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalLayout {
|
||||
height: 30px; alignment: center;
|
||||
PrimaryButton {
|
||||
text: UiStrings.ok;
|
||||
accessible-role: button;
|
||||
accessible-label: UiStrings.ok;
|
||||
accessible-action-default => { root.dismiss-lan-access-warning(); }
|
||||
clicked => { root.dismiss-lan-access-warning(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user