mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-16 17:10:20 +08:00
2387 lines
121 KiB
Plaintext
2387 lines
121 KiB
Plaintext
import { UiRadius } from "ui_radius.slint";
|
||
import { WorkbenchStyle, WorkbenchButton, WorkbenchCard } from "workbench_controls.slint";
|
||
import { GalleryStrings } from "device_strings.slint";
|
||
import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.slint";
|
||
import { DeviceGroupDialog, DeviceGroupOption } from "device_group_dialog.slint";
|
||
import { GalleryView, GalleryTile, GalleryRecentDevice, GalleryActionDevice } from "gallery_view.slint";
|
||
import { GalleryNetworkDialog, GalleryNetworkDevice } from "gallery_network_dialog.slint";
|
||
import { SettingsButton, SettingsSelect, SettingsGroup, SettingsRow, SettingsToggleRow } from "settings_controls.slint";
|
||
import { GallerySettings } from "gallery_settings.slint";
|
||
import { GroupFilter } from "group_filter.slint";
|
||
import { ScrollView } from "std-widgets.slint";
|
||
import { RoundedLineEdit } from "rounded_line_edit.slint";
|
||
import { AnnouncementItem, MessagePanel, MessageStrings } from "message_panel.slint";
|
||
import { FontAwesomeIcons, IconButton, CompactComboBox, ImGuiCheckBox, MiniToggleSwitch, CompactButton, PrimaryButton, SecondaryButton, ToggleSwitch, SectionTitle, DialogSurface, ImGuiLineStyle, ImGuiFontStyle } from "common.slint";
|
||
|
||
export struct RecentConnection {
|
||
remote-id: string,
|
||
display-name: string,
|
||
host-name: string,
|
||
group-names: string,
|
||
online: bool,
|
||
connected: bool,
|
||
thumbnail: image,
|
||
has-thumbnail: bool,
|
||
platform: string,
|
||
}
|
||
|
||
export struct ReleaseNoteBlock {
|
||
content: styled-text,
|
||
section-gap: bool,
|
||
}
|
||
|
||
export enum UpdateCheckStatus { idle, checking, up-to-date, available, failed }
|
||
|
||
export global UiStrings {
|
||
in-out property <string> local-desktop: "Local Desktop";
|
||
in-out property <string> remote-desktop: "Remote Desktop";
|
||
in-out property <string> recent-connections: "Recent Connections";
|
||
in-out property <string> local-id: "Local ID";
|
||
in-out property <string> device-name: "Device Name";
|
||
in-out property <string> remote-id: "Remote ID";
|
||
in-out property <string> online: "Online";
|
||
in-out property <string> offline: "Offline";
|
||
in-out property <string> password: "Password";
|
||
in-out property <string> refresh-password: "Refresh random password";
|
||
in-out property <string> copied: "Local ID copied to clipboard";
|
||
in-out property <string> connect: "Connect";
|
||
in-out property <string> settings: "Settings";
|
||
in-out property <string> about: "About";
|
||
in-out property <string> ok: "OK";
|
||
in-out property <string> cancel: "Cancel";
|
||
in-out property <string> save: "Save";
|
||
in-out property <string> new-password: "New password (6 characters)";
|
||
in-out property <string> invalid-password: "Password must contain exactly 6 characters";
|
||
in-out property <string> edit-alias: "Connection alias";
|
||
in-out property <string> delete-connection: "Delete connection";
|
||
in-out property <string> confirm-delete: "Delete this recent connection?";
|
||
in-out property <string> language: "Language";
|
||
in-out property <string> capture-method: "Capture method";
|
||
in-out property <string> capture-method-auto: "Auto";
|
||
in-out property <string> codec: "Video codec";
|
||
in-out property <string> hardware-codec: "Hardware video codec";
|
||
in-out property <string> turn-relay: "TURN relay";
|
||
in-out property <string> force-relay: "Force relay";
|
||
in-out property <string> self-hosted: "Self-hosted server";
|
||
in-out property <string> autostart: "Start with system";
|
||
in-out property <string> daemon: "Run in background";
|
||
in-out property <string> save-remote-previews: "Save remote screen previews:";
|
||
in-out property <string> unknown-platform: "Unknown platform";
|
||
in-out property <string> privacy-on-connect: "Privacy screen on connect";
|
||
in-out property <string> file-save-path: "File save path";
|
||
in-out property <string> default-desktop: "Desktop";
|
||
in-out property <string> server-host: "Server host";
|
||
in-out property <string> server-port: "Signal port";
|
||
in-out property <string> version: "Version";
|
||
in-out property <string> copyright: "© 2026 by JUNKUN DI. All right reserved.";
|
||
in-out property <string> license: "Licensed under GNU GPL v3.";
|
||
in-out property <string> signal-connected: "Signal server connected";
|
||
in-out property <string> signal-disconnected: "Signal server disconnected";
|
||
in-out property <string> tls-error: "Signal server TLS certificate error";
|
||
in-out property <string> signal-reconnect: "Reconnect";
|
||
in-out property <string> signal-reconnecting: "Reconnecting...";
|
||
in-out property <string> update-available: "A new version is available";
|
||
in-out property <string> new-version: "New version";
|
||
in-out property <string> check-for-updates: "Check for updates";
|
||
in-out property <string> checking-for-updates: "Checking for updates…";
|
||
in-out property <string> up-to-date: "You’re up to date";
|
||
in-out property <string> update-check-failed: "Check failed. Please try again later.";
|
||
in-out property <string> release-notes: "Release notes";
|
||
in-out property <string> view-release-notes: "Release notes";
|
||
in-out property <string> later: "Later";
|
||
in-out property <string> download: "Download";
|
||
in-out property <string> input-password: "Input password";
|
||
in-out property <string> reinput-password: "Incorrect password, please try again";
|
||
in-out property <string> remember-password: "Remember password";
|
||
in-out property <string> validate-password: "Validating password";
|
||
in-out property <string> request-permissions: "Request permissions";
|
||
in-out property <string> permission-required: "The application requires the following permissions:";
|
||
in-out property <string> screen-recording-permission: "Screen recording permission";
|
||
in-out property <string> accessibility-permission: "Accessibility permission";
|
||
in-out property <string> wayland-notice-title: "Wayland Session Notice";
|
||
in-out property <string> wayland-notice-subtitle;
|
||
in-out property <string> wayland-permission-title;
|
||
in-out property <string> wayland-notice-message;
|
||
in-out property <string> wayland-recommendation-title;
|
||
in-out property <string> wayland-recommendation-message;
|
||
in-out property <string> wayland-notice-acknowledge: "Got it";
|
||
in-out property <string> service-setup-title: "Install CrossDesk Service";
|
||
in-out property <string> service-setup-message: "Administrator permission is required to control this device while it is locked.";
|
||
in-out property <string> service-settings-label: "Lock Screen Service:";
|
||
in-out property <string> install-service: "Install";
|
||
in-out property <string> service-installed: "Installed";
|
||
in-out property <string> do-not-remind: "Do not remind again";
|
||
in-out property <string> notification: "Notification";
|
||
in-out property <string> service-suppressed-message: "You will not be reminded again. You can install the service later in Settings.";
|
||
in-out property <string> codec-h264: "H.264";
|
||
in-out property <string> codec-av1: "AV1";
|
||
in-out property <string> self-hosted-settings: "Self-hosted server settings";
|
||
in-out property <string> access-website: "Website: ";
|
||
in-out property <string> release-date-label: "Release Date: ";
|
||
}
|
||
|
||
// Prompt copy in modal dialogs sits slightly low in the legacy ImGui layout.
|
||
// Offset only the rendered text so input fields and action buttons keep their
|
||
// established geometry.
|
||
component DialogPromptText inherits Rectangle {
|
||
in property <string> prompt;
|
||
in property <color> text-color: #30343b;
|
||
in property <length> text-size: ImGuiFontStyle.base;
|
||
in property <int> text-weight: 400;
|
||
|
||
// Mirror Text's intrinsic layout constraints exactly. This makes the
|
||
// wrapper invisible to VerticalLayout: siblings retain the same geometry
|
||
// they had when the Text item was a direct child.
|
||
min-height: prompt-label.min-height;
|
||
preferred-height: prompt-label.preferred-height;
|
||
max-height: prompt-label.max-height;
|
||
vertical-stretch: prompt-label.vertical-stretch;
|
||
|
||
prompt-label := Text {
|
||
y: 10px;
|
||
width: parent.width;
|
||
text: root.prompt;
|
||
color: root.text-color;
|
||
font-size: root.text-size;
|
||
font-weight: root.text-weight;
|
||
wrap: word-wrap;
|
||
horizontal-alignment: center;
|
||
}
|
||
}
|
||
|
||
// Use the same compact, plain-text field for connection and password changes.
|
||
component PasswordInput inherits Rectangle {
|
||
in-out property <string> text;
|
||
in property <int> max-length: 0;
|
||
in property <length> text-size: ImGuiFontStyle.base;
|
||
private property <string> previous-text;
|
||
callback accepted;
|
||
|
||
width: 70px;
|
||
height: 23px;
|
||
background: white;
|
||
border-width: 1px;
|
||
border-color: ImGuiLineStyle.border;
|
||
border-radius: UiRadius.control;
|
||
forward-focus: editor;
|
||
|
||
init => { root.previous-text = root.text; }
|
||
|
||
editor := TextInput {
|
||
x: 5px; y: 1px; width: parent.width - 10px; height: parent.height - 2px;
|
||
text <=> root.text;
|
||
input-type: InputType.text;
|
||
single-line: true;
|
||
color: #202124;
|
||
font-size: root.text-size;
|
||
vertical-alignment: center;
|
||
selection-foreground-color: white;
|
||
selection-background-color: #2463c7;
|
||
edited => {
|
||
// Validate the resulting text so typing, paste, and replacement
|
||
// all obey the limit while deletion and selection still work.
|
||
if (root.max-length > 0 && self.text.character-count > root.max-length) {
|
||
self.text = root.previous-text;
|
||
} else {
|
||
root.previous-text = self.text;
|
||
}
|
||
}
|
||
accepted => { root.accepted(); }
|
||
}
|
||
Timer {
|
||
interval: 1ms;
|
||
running: true;
|
||
triggered => {
|
||
self.running = false;
|
||
editor.focus();
|
||
// Place the caret after any restored password, including UTF-8.
|
||
editor.select-all();
|
||
editor.clear-selection();
|
||
}
|
||
}
|
||
}
|
||
|
||
export component MainWindow inherits Window {
|
||
title: root.gallery-mode ? "CrossDesk " + GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub") : "CrossDesk";
|
||
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
|
||
preferred-width: root.minimum-window-width;
|
||
preferred-height: root.minimum-window-height;
|
||
min-width: root.minimum-window-width;
|
||
max-width: root.gallery-mode ? 16384px : root.minimum-window-width;
|
||
min-height: root.minimum-window-height;
|
||
max-height: root.gallery-mode ? 16384px : root.minimum-window-height;
|
||
no-frame: root.custom-titlebar;
|
||
background: root.wayland-titlebar ? transparent : white;
|
||
default-font-size: ImGuiFontStyle.base;
|
||
|
||
// Fixed role for this window instance; opening DeskHub creates another window.
|
||
in property <bool> gallery-mode: false;
|
||
in-out property <bool> gallery-simplified: false;
|
||
changed gallery-simplified => {
|
||
root.reset-gallery-transient();
|
||
root.gallery-action("view-mode", "");
|
||
focus-sink.focus();
|
||
}
|
||
in-out property <bool> gallery-network-open: false;
|
||
in property <int> gallery-resolution-index: 0;
|
||
in property <int> gallery-frame-rate-index: 0;
|
||
in property <int> gallery-bandwidth-index: 3;
|
||
in-out property <int> gallery-resolution-draft: 0;
|
||
in-out property <int> gallery-frame-rate-draft: 0;
|
||
in-out property <int> gallery-bandwidth-draft: 3;
|
||
in property <string> gallery-settings-error;
|
||
in property <[GalleryNetworkDevice]> gallery-network-devices;
|
||
changed gallery-network-open => {
|
||
if root.gallery-network-open { root.menu-open = false; root.gallery-action("network-open", ""); }
|
||
}
|
||
in property <bool> gallery-fullscreen: false;
|
||
in property <bool> gallery-transitioning: false;
|
||
changed gallery-transitioning => {
|
||
if root.gallery-transitioning {
|
||
// Preserve the preview's keyboard focus when only changing size.
|
||
if !root.gallery-controlling { focus-sink.focus(); }
|
||
root.menu-open = false;
|
||
root.recent-tooltip-visible = false;
|
||
}
|
||
}
|
||
in-out property <string> connection-input-error;
|
||
in property <bool> gallery-controlling: false;
|
||
in property <int> gallery-remote-cursor-shape: 0;
|
||
in property <bool> gallery-group-controlling: false;
|
||
out property <float> gallery-video-width: gallery.video-width;
|
||
out property <float> gallery-video-height: gallery.video-height;
|
||
out property <float> gallery-preview-width: gallery.preview-width;
|
||
out property <float> gallery-preview-height: gallery.preview-height;
|
||
callback gallery-pointer(PointerEventButton, PointerEventKind, length, length);
|
||
callback gallery-scroll(length, length, length, length);
|
||
callback gallery-key(string, bool, bool, bool, bool, bool);
|
||
in property <[GalleryTile]> gallery-tiles;
|
||
in property <[string]> gallery-groups;
|
||
in-out property <int> gallery-group-index: 0;
|
||
out property <int> gallery-columns: gallery.columns;
|
||
out property <int> gallery-rows: gallery.rows;
|
||
in-out property <float> gallery-tile-width: 216;
|
||
in-out property <float> gallery-focus-rail-height: 70.875;
|
||
out property <bool> gallery-resizing: gallery.resizing || gallery.focus-resizing;
|
||
in property <int> gallery-page: 0;
|
||
in property <int> gallery-pages: 1;
|
||
in property <int> gallery-count: 0;
|
||
in property <int> gallery-connected: 0;
|
||
in property <int> gallery-selected: 0;
|
||
in property <bool> gallery-selected-audio-available: false;
|
||
in property <bool> gallery-selected-audio-muted: false;
|
||
in property <int> gallery-hidden-selected: 0;
|
||
in property <string> gallery-notice;
|
||
in property <[GalleryActionDevice]> gallery-selected-devices;
|
||
in property <GalleryTile> gallery-focused;
|
||
in-out property <bool> gallery-batch: false;
|
||
in-out property <bool> gallery-adding: false;
|
||
in-out property <bool> gallery-importing: false;
|
||
in property <[GalleryRecentDevice]> gallery-recent-devices;
|
||
in property <int> gallery-recent-selected: 0;
|
||
in property <int> gallery-recent-available: 0;
|
||
in property <string> gallery-import-group;
|
||
callback open-gallery();
|
||
callback toggle-gallery-fullscreen();
|
||
callback gallery-action(string, string);
|
||
callback gallery-swap(string, string) -> bool;
|
||
callback gallery-add-device(string, string, string, bool) -> bool;
|
||
callback gallery-import-recent(string) -> bool;
|
||
in-out property <bool> device-group-open: false;
|
||
private property <string> device-group-id;
|
||
private property <string> device-group-name;
|
||
in property <[DeviceGroupOption]> device-group-options;
|
||
in property <string> device-group-error;
|
||
in-out property <string> device-group-new-name;
|
||
callback device-group-action(string, string);
|
||
callback device-group-save(string) -> bool;
|
||
function open-device-groups(id: string, name: string) {
|
||
root.gallery-action("release", "");
|
||
focus-sink.focus();
|
||
root.recent-tooltip-visible = false;
|
||
root.device-group-id = id;
|
||
root.device-group-name = name;
|
||
root.device-group-new-name = "";
|
||
root.device-group-action("open", id);
|
||
root.device-group-open = true;
|
||
}
|
||
|
||
public function reset-gallery-transient() { gallery.reset-transient(); root.menu-open = false; root.gallery-network-open = false; }
|
||
in property <bool> custom-titlebar: false;
|
||
in property <bool> wayland-titlebar: false;
|
||
in property <bool> window-active: true;
|
||
private property <length> titlebar-height:
|
||
root.custom-titlebar && !root.gallery-fullscreen ? (root.wayland-titlebar ? 32px : 31px) : 0px;
|
||
out property <length> minimum-window-width: 640px;
|
||
out property <length> minimum-window-height:
|
||
(root.gallery-mode ? 448px : 472px) + root.titlebar-height;
|
||
private property <length> wayland-corner-radius: 12px;
|
||
in property <string> local-id: "";
|
||
in property <string> local-password: "";
|
||
in-out property <string> remote-id-input: "";
|
||
in property <bool> password-visible: true;
|
||
in property <bool> password-change-pending: false;
|
||
in property <bool> signal-connected: false;
|
||
in property <bool> signal-reconnecting: false;
|
||
in property <bool> signal-tls-error: false;
|
||
in property <[RecentConnection]> recent-connections;
|
||
in property <[string]> recent-groups: [GalleryStrings.tr("全部设备", "All devices", "Все устройства"), GalleryStrings.tr("未分组", "Ungrouped", "Без группы")];
|
||
in-out property <int> recent-group-index: 0;
|
||
callback recent-group-selected(int);
|
||
changed recent-group-index => {
|
||
root.recent-tooltip-visible = false;
|
||
recent-scroll.viewport-x = 0px;
|
||
}
|
||
in property <[AnnouncementItem]> announcements;
|
||
in property <int> announcement-unread;
|
||
private property <string> announcement-unread-label:
|
||
root.announcement-unread > 99 ? "99+" : "" + root.announcement-unread;
|
||
in property <int> announcement-total;
|
||
in property <bool> announcement-loading;
|
||
in property <bool> announcement-failed;
|
||
in property <bool> announcement-read-save-failed;
|
||
in property <bool> announcement-delete-failed;
|
||
in property <bool> announcement-loaded;
|
||
in-out property <bool> messages-open: false;
|
||
in property <string> announcement-selected-id;
|
||
in property <string> announcement-selected-title;
|
||
in property <styled-text> announcement-selected-body;
|
||
in property <string> announcement-selected-date;
|
||
in property <bool> announcement-selected-read;
|
||
callback open-announcement(string, string);
|
||
callback open-announcement-link(string);
|
||
callback delete-announcement(string, string);
|
||
callback refresh-announcements;
|
||
callback load-more-announcements;
|
||
in property <bool> update-available: false;
|
||
in property <UpdateCheckStatus> update-check-status: idle;
|
||
private property <string> update-status-text:
|
||
root.update-check-status == UpdateCheckStatus.checking ? UiStrings.checking-for-updates :
|
||
root.update-check-status == UpdateCheckStatus.up-to-date ? UiStrings.up-to-date :
|
||
root.update-check-status == UpdateCheckStatus.failed ? UiStrings.update-check-failed : "";
|
||
in property <string> current-version: "";
|
||
in property <string> latest-version: "";
|
||
in property <string> release-name: "";
|
||
in property <[ReleaseNoteBlock]> release-note-blocks;
|
||
in property <string> release-date: "";
|
||
in-out property <string> offline-warning: "";
|
||
in property <bool> connection-dialog-open: false;
|
||
in property <string> connection-status-text: "";
|
||
in property <bool> connection-pending: false;
|
||
in property <bool> connection-password-required: false;
|
||
in property <bool> connection-validating: false;
|
||
in-out property <string> connection-password: "";
|
||
in-out property <bool> connection-remember-password: false;
|
||
in property <bool> permission-dialog-open: false;
|
||
in property <bool> screen-recording-granted: true;
|
||
in property <bool> accessibility-granted: true;
|
||
in-out property <bool> wayland-notice-open: false;
|
||
in property <bool> portable-service-settings-visible: false;
|
||
in property <bool> portable-service-dialog-open: false;
|
||
in property <bool> portable-service-suppressed-notice-open: false;
|
||
in property <bool> portable-service-installed: false;
|
||
in property <bool> portable-service-installing: false;
|
||
in property <bool> portable-service-succeeded: false;
|
||
in property <string> portable-service-status: "";
|
||
in-out property <bool> portable-service-do-not-remind: false;
|
||
|
||
in-out property <int> language-index: 0;
|
||
in property <bool> capture-method-visible: false;
|
||
in-out property <int> capture-method-index: 0;
|
||
in-out property <int> codec-index: 0;
|
||
in-out property <bool> hardware-codec-enabled: true;
|
||
in-out property <bool> turn-enabled: true;
|
||
in-out property <bool> force-relay-enabled: false;
|
||
changed turn-enabled => {
|
||
if !root.turn-enabled { root.force-relay-enabled = false; }
|
||
}
|
||
in-out property <bool> self-hosted-enabled: false;
|
||
in-out property <bool> autostart-enabled: false;
|
||
in-out property <bool> daemon-enabled: false;
|
||
in-out property <bool> save-remote-previews: true;
|
||
// Show the preference on every platform; only adapted hosts can change it.
|
||
in property <bool> privacy-setting-available: false;
|
||
in-out property <bool> privacy-on-connect-enabled: true;
|
||
in-out property <string> file-save-path: "";
|
||
in-out property <string> server-host: "";
|
||
in-out property <string> server-port: "";
|
||
in-out property <string> self-host-settings-error: "";
|
||
in property <bool> settings-session-active: false;
|
||
// Keep the setting visible on every platform. Unsupported builds expose it
|
||
// as disabled so the fixed settings layout never turns into a blank row.
|
||
in property <bool> hardware-codec-available: true;
|
||
|
||
callback copy-local-id() -> bool;
|
||
callback toggle-password-visibility;
|
||
callback refresh-password;
|
||
callback reset-password(string) -> bool;
|
||
callback format-remote-id(string) -> string;
|
||
callback connect-requested(string);
|
||
callback recent-connect(string);
|
||
callback recent-disconnect(string);
|
||
callback recent-edit-alias(string, string);
|
||
callback recent-delete(string);
|
||
callback save-settings() -> bool;
|
||
callback cancel-settings;
|
||
callback save-self-hosted-settings() -> bool;
|
||
callback cancel-self-hosted-settings;
|
||
callback browse-save-path;
|
||
callback open-download;
|
||
callback check-for-updates;
|
||
callback signal-reconnect-requested;
|
||
callback connection-cancel;
|
||
callback connection-acknowledge;
|
||
callback connection-submit-password(string, bool);
|
||
callback reset-remote-id;
|
||
callback request-screen-recording-permission;
|
||
callback request-accessibility-permission;
|
||
callback install-portable-service;
|
||
callback cancel-portable-service;
|
||
callback acknowledge-portable-service;
|
||
callback acknowledge-portable-service-suppressed;
|
||
callback main-title-drag(int, length, length);
|
||
callback minimize-main-window;
|
||
callback close-main-window;
|
||
|
||
private property <bool> menu-open: false;
|
||
in-out property <bool> settings-open: false;
|
||
in-out property <int> settings-tab: 0;
|
||
private property <length> settings-scroll-y: 0px;
|
||
in-out property <bool> self-host-settings-open: false;
|
||
changed self-host-settings-open => {
|
||
if !root.self-host-settings-open { root.self-host-settings-error = ""; }
|
||
}
|
||
in-out property <bool> about-open: false;
|
||
// Follow the first available update. Dismissing the dialog replaces this
|
||
// binding, so background checks do not reopen it during this launch.
|
||
in-out property <bool> update-open: root.update-available;
|
||
in-out property <bool> reset-password-open: false;
|
||
in-out property <bool> reset-password-invalid: false;
|
||
in-out property <string> new-password-input: "";
|
||
in-out property <bool> alias-open: false;
|
||
private property <string> alias-remote-id: "";
|
||
in-out property <string> alias-input: "";
|
||
in-out property <bool> delete-open: false;
|
||
private property <string> delete-remote-id: "";
|
||
private property <bool> delete-from-gallery: false;
|
||
private property <bool> copy-toast: false;
|
||
private property <bool> compact-language: root.language-index == 0;
|
||
private property <bool> recent-tooltip-visible: false;
|
||
private property <string> recent-tooltip-name: "";
|
||
private property <string> recent-tooltip-host: "";
|
||
private property <string> recent-tooltip-id: "";
|
||
private property <string> recent-tooltip-groups: "";
|
||
private property <bool> recent-tooltip-online: false;
|
||
private property <length> recent-tooltip-pointer-x: 0px;
|
||
private property <length> recent-tooltip-pointer-y: 0px;
|
||
|
||
changed menu-open => { if root.menu-open { root.gallery-action("release", ""); } }
|
||
changed connection-dialog-open => { if root.connection-dialog-open { root.gallery-action("release", ""); } }
|
||
changed permission-dialog-open => { if root.permission-dialog-open { root.gallery-action("release", ""); } }
|
||
changed settings-open => {
|
||
if root.settings-open {
|
||
root.settings-tab = 0;
|
||
root.settings-scroll-y = 0px;
|
||
root.menu-open = false;
|
||
root.gallery-network-open = false;
|
||
root.gallery-resolution-draft = root.gallery-resolution-index;
|
||
root.gallery-frame-rate-draft = root.gallery-frame-rate-index;
|
||
root.gallery-bandwidth-draft = root.gallery-bandwidth-index;
|
||
root.gallery-action("settings-open", "");
|
||
focus-sink.focus();
|
||
}
|
||
}
|
||
|
||
changed wayland-notice-open => {
|
||
if root.wayland-notice-open { focus-sink.focus(); }
|
||
}
|
||
|
||
changed messages-open => {
|
||
if root.messages-open { focus-sink.focus(); }
|
||
}
|
||
|
||
reset-remote-id => {
|
||
root.remote-id-input = "";
|
||
remote-id-editor.focus();
|
||
}
|
||
|
||
window-surface := Rectangle {
|
||
width: root.width;
|
||
height: root.height;
|
||
background: root.wayland-titlebar ? #f6f5f4 : transparent;
|
||
border-radius:
|
||
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
|
||
clip: root.wayland-titlebar;
|
||
|
||
if root.custom-titlebar && root.wayland-titlebar && !root.gallery-fullscreen: Rectangle {
|
||
x: 0px;
|
||
y: 0px;
|
||
width: parent.width;
|
||
height: root.titlebar-height;
|
||
background: #f6f5f4;
|
||
z: 100;
|
||
|
||
Text {
|
||
x: 80px;
|
||
width: parent.width - 160px;
|
||
height: parent.height;
|
||
text: root.title;
|
||
color: root.window-active ? #3d3d3d : #747474;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
overflow: elide;
|
||
}
|
||
wayland-title-drag := TouchArea {
|
||
width: parent.width - 80px;
|
||
property <int> phase: self.pressed ? 1 : 0;
|
||
changed phase => {
|
||
root.main-title-drag(phase, self.mouse-x, self.mouse-y);
|
||
}
|
||
moved => {
|
||
if (self.pressed) {
|
||
root.main-title-drag(2, self.mouse-x, self.mouse-y);
|
||
}
|
||
}
|
||
}
|
||
wayland-minimize-button := Rectangle {
|
||
x: parent.width - 74px;
|
||
y: 4px;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 12px;
|
||
background: wayland-minimize-touch.pressed ? #c0bfbc
|
||
: wayland-minimize-touch.has-hover ? #deddda
|
||
: root.window-active ? #e6e6e6
|
||
: transparent;
|
||
accessible-role: button;
|
||
accessible-label: "Minimize";
|
||
accessible-action-default => { wayland-minimize-touch.clicked(); }
|
||
Rectangle {
|
||
x: 8px;
|
||
y: 13px;
|
||
width: 8px;
|
||
height: 0.75px;
|
||
background: root.window-active
|
||
|| wayland-minimize-touch.has-hover
|
||
|| wayland-minimize-touch.pressed ? #454545 : #898989;
|
||
}
|
||
wayland-minimize-touch := TouchArea {
|
||
clicked => { root.minimize-main-window(); }
|
||
}
|
||
}
|
||
wayland-close-button := Rectangle {
|
||
x: parent.width - 34px;
|
||
y: 4px;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 12px;
|
||
background: wayland-close-touch.pressed ? #b2161d
|
||
: wayland-close-touch.has-hover ? #e01b24
|
||
: root.window-active ? #e6e6e6
|
||
: transparent;
|
||
accessible-role: button;
|
||
accessible-label: "Close";
|
||
accessible-action-default => { wayland-close-touch.clicked(); }
|
||
Path {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
viewbox-width: 24;
|
||
viewbox-height: 24;
|
||
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
|
||
stroke: wayland-close-touch.has-hover
|
||
|| wayland-close-touch.pressed ? white
|
||
: root.window-active ? #454545 : #898989;
|
||
stroke-width: 1px;
|
||
}
|
||
wayland-close-touch := TouchArea {
|
||
clicked => { root.close-main-window(); }
|
||
}
|
||
}
|
||
Rectangle {
|
||
y: parent.height - 1px;
|
||
height: 1px;
|
||
background: #d5d3d0;
|
||
}
|
||
}
|
||
|
||
if root.custom-titlebar && !root.wayland-titlebar && !root.gallery-fullscreen: Rectangle {
|
||
x: 0px;
|
||
y: 0px;
|
||
width: root.width;
|
||
height: 31px;
|
||
background: #f9f9f9;
|
||
z: 100;
|
||
|
||
Image {
|
||
x: 8px;
|
||
y: 7px;
|
||
width: 16px;
|
||
height: 16px;
|
||
source: @image-url("../../../resources/linux/crossdesk_16x16.png");
|
||
image-fit: contain;
|
||
}
|
||
Text {
|
||
x: 30px;
|
||
width: parent.width - 122px;
|
||
height: parent.height;
|
||
text: root.title;
|
||
color: #202124;
|
||
font-size: 12px;
|
||
vertical-alignment: center;
|
||
overflow: elide;
|
||
}
|
||
title-drag := TouchArea {
|
||
x: 0px;
|
||
width: parent.width - 92px;
|
||
property <int> phase: self.pressed ? 1 : 0;
|
||
changed phase => { root.main-title-drag(phase, self.mouse-x, self.mouse-y); }
|
||
moved => { if (self.pressed) { root.main-title-drag(2, self.mouse-x, self.mouse-y); } }
|
||
}
|
||
minimize-button := Rectangle {
|
||
x: parent.width - 92px;
|
||
width: 46px;
|
||
height: parent.height;
|
||
background: minimize-touch.pressed ? #c7c7c7
|
||
: minimize-touch.has-hover ? #e5e5e5
|
||
: transparent;
|
||
accessible-role: button;
|
||
accessible-label: "Minimize";
|
||
accessible-action-default => { minimize-touch.clicked(); }
|
||
Text {
|
||
text: FontAwesomeIcons.minus;
|
||
color: #202124;
|
||
font-family: "Font Awesome 6 Free";
|
||
font-weight: 900;
|
||
font-size: 10px;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
accessible-role: none;
|
||
}
|
||
minimize-touch := TouchArea { clicked => { root.minimize-main-window(); } }
|
||
}
|
||
close-button := Rectangle {
|
||
x: parent.width - 46px;
|
||
width: 46px;
|
||
height: parent.height;
|
||
background: close-touch.pressed ? #b3251b
|
||
: close-touch.has-hover ? #c42b1c
|
||
: transparent;
|
||
accessible-role: button;
|
||
accessible-label: "Close";
|
||
accessible-action-default => { close-touch.clicked(); }
|
||
Text {
|
||
text: FontAwesomeIcons.xmark;
|
||
color: close-touch.has-hover || close-touch.pressed ? white : #202124;
|
||
font-family: "Font Awesome 6 Free";
|
||
font-weight: 900;
|
||
font-size: 12px;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
accessible-role: none;
|
||
}
|
||
close-touch := TouchArea { clicked => { root.close-main-window(); } }
|
||
}
|
||
Rectangle {
|
||
y: parent.height - 1px;
|
||
height: 1px;
|
||
background: #d4d4d4;
|
||
}
|
||
}
|
||
|
||
content-layer := Rectangle {
|
||
x: 0px;
|
||
y: root.titlebar-height;
|
||
width: root.width;
|
||
height: root.height - self.y;
|
||
clip: true;
|
||
|
||
Rectangle {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
background: root.gallery-mode ? white : WorkbenchStyle.canvas;
|
||
border-width: 0px;
|
||
border-radius: 0px;
|
||
|
||
focus-sink := FocusScope {
|
||
x: 0px; y: 0px; width: 0px; height: 0px;
|
||
key-pressed(event) => {
|
||
if root.wayland-notice-open {
|
||
if event.text == Key.Return || event.text == Key.Escape {
|
||
root.wayland-notice-open = false;
|
||
}
|
||
return accept;
|
||
}
|
||
if root.messages-open {
|
||
if event.text == Key.Escape { root.messages-open = false; }
|
||
return accept;
|
||
}
|
||
if root.settings-open && !root.self-host-settings-open {
|
||
if event.text == Key.Escape { root.cancel-settings(); root.settings-open = false; }
|
||
return accept;
|
||
}
|
||
return reject;
|
||
}
|
||
}
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
|
||
// Slint owns the client area below the native macOS title bar. Draw
|
||
// the boundary here because AppKit's titlebar separator is not shown
|
||
// reliably when winit creates a window without an NSToolbar.
|
||
Rectangle {
|
||
x: 0px;
|
||
y: 0px;
|
||
width: parent.width;
|
||
height: 1px;
|
||
background: ImGuiLineStyle.control-separator;
|
||
z: 7;
|
||
}
|
||
|
||
// A compact application toolbar keeps global actions outside device cards.
|
||
// Center the 28px buttons above the cards with 6px of space on each side.
|
||
open-gallery-button := WorkbenchButton {
|
||
visible: !root.gallery-mode;
|
||
x: 20px; y: 6px;
|
||
width: 80px; height: 28px;
|
||
text: GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub");
|
||
z: 8;
|
||
clicked => { root.open-gallery(); }
|
||
}
|
||
menu-button := WorkbenchButton {
|
||
visible: !root.gallery-mode;
|
||
x: parent.width - 56px; y: open-gallery-button.y;
|
||
width: 36px; height: 28px;
|
||
icon: FontAwesomeIcons.bars;
|
||
tooltip: GalleryStrings.tr("更多", "More", "Ещё");
|
||
z: 8;
|
||
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
|
||
}
|
||
// Unread messages take precedence so the menu has only one indicator.
|
||
if !root.gallery-mode && root.update-available && root.announcement-unread <= 0: Rectangle {
|
||
x: menu-button.x + menu-button.width - self.width + 4px;
|
||
y: menu-button.y - 1px;
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 8px;
|
||
border-width: 1px;
|
||
border-color: white;
|
||
background: WorkbenchStyle.accent;
|
||
opacity: root.menu-open || mod(animation-tick(), 2s) < 1s ? 1 : 0;
|
||
z: 9;
|
||
accessible-role: button;
|
||
accessible-label: UiStrings.update-available;
|
||
accessible-action-default => { root.menu-open = !root.menu-open; }
|
||
// Center the visible mark independently of font baseline metrics.
|
||
Rectangle {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: 2px;
|
||
height: 10px;
|
||
accessible-role: none;
|
||
Rectangle {
|
||
y: 0px;
|
||
width: parent.width;
|
||
height: 6px;
|
||
border-radius: 1px;
|
||
background: white;
|
||
}
|
||
Rectangle {
|
||
y: 8px;
|
||
width: parent.width;
|
||
height: 2px;
|
||
border-radius: 1px;
|
||
background: white;
|
||
}
|
||
}
|
||
TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
|
||
}
|
||
}
|
||
if !root.gallery-mode && root.announcement-unread > 0: Rectangle {
|
||
x: menu-button.x + menu-button.width - self.width + 4px;
|
||
y: menu-button.y - 1px;
|
||
width: max(16px, unread-badge-label.preferred-width + 8px);
|
||
height: 16px;
|
||
border-radius: 8px;
|
||
border-width: 1px;
|
||
border-color: white;
|
||
background: #d92d20;
|
||
z: 9;
|
||
accessible-role: button;
|
||
accessible-label: MessageStrings.messages + " · " + MessageStrings.unread + " " + root.announcement-unread;
|
||
accessible-action-default => { root.menu-open = !root.menu-open; }
|
||
unread-badge-label := Text {
|
||
text: root.announcement-unread-label;
|
||
color: white;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
accessible-role: none;
|
||
}
|
||
TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
y: 0px; height: parent.height - 28px; visible: !root.gallery-mode;
|
||
// Two balanced cards keep identity and connection actions distinct.
|
||
Rectangle {
|
||
x: 20px; y: 40px; width: parent.width - 40px; height: 208px;
|
||
// Button tooltips extend below these cards into the recent section.
|
||
z: 1;
|
||
local-card := WorkbenchCard {
|
||
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 {
|
||
x: 16px; y: 90px; width: parent.width - 32px; height: 40px;
|
||
// 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; }
|
||
Rectangle { x: parent.width - 48px; y: 10px; width: 1px; height: parent.height - 20px; background: WorkbenchStyle.border; }
|
||
WorkbenchButton {
|
||
x: parent.width - 42px; y: 2px; width: 36px; quiet: true;
|
||
icon: root.copy-toast ? FontAwesomeIcons.check : FontAwesomeIcons.copy;
|
||
tooltip: root.copy-toast ? UiStrings.copied : GalleryStrings.tr("复制本机 ID", "Copy local ID", "Копировать ID");
|
||
clicked => { if root.copy-local-id() { root.copy-toast = true; copy-timer.restart(); } }
|
||
}
|
||
}
|
||
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; }
|
||
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(); }
|
||
}
|
||
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(); }
|
||
}
|
||
WorkbenchButton {
|
||
x: parent.width - 36px; y: 0px; width: 36px; quiet: true;
|
||
icon: FontAwesomeIcons.pen;
|
||
tooltip: GalleryStrings.tr("修改密码", "Change password", "Изменить пароль");
|
||
enabled: !root.password-change-pending;
|
||
clicked => { root.reset-password-open = true; }
|
||
}
|
||
}
|
||
}
|
||
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; }
|
||
Rectangle {
|
||
x: 16px; y: 90px; width: parent.width - 32px; height: 40px;
|
||
background: white; border-radius: UiRadius.control; border-width: 1px;
|
||
border-color: root.connection-input-error != "" ? #d65252 : remote-id-editor.has-focus ? WorkbenchStyle.accent : WorkbenchStyle.control-border;
|
||
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 цифр");
|
||
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;
|
||
vertical-alignment: center;
|
||
selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent;
|
||
edited => { root.connection-input-error = ""; remote-id-format-timer.restart(); }
|
||
accepted => { if !root.connection-pending { root.connect-requested(root.remote-id-input); } }
|
||
}
|
||
}
|
||
Text {
|
||
x: 16px; y: 133px; width: parent.width - 32px; height: 24px;
|
||
text: root.connection-input-error;
|
||
font-size: 11px; color: #be3d42; 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); }
|
||
}
|
||
}
|
||
}
|
||
|
||
// Recent connections: horizontal cards with thumbnail and hover actions.
|
||
recent-panel := Rectangle {
|
||
y: 260px;
|
||
height: parent.height - self.y;
|
||
|
||
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 {
|
||
x: parent.width - self.width - 20px; y: 0px;
|
||
width: 155px; height: 32px;
|
||
normal-border-color: WorkbenchStyle.control-border;
|
||
hover-border-color: WorkbenchStyle.hover-border;
|
||
focus-border-color: WorkbenchStyle.accent;
|
||
model: root.recent-groups;
|
||
current-index <=> root.recent-group-index;
|
||
selected(index) => { root.recent-group-selected(index); }
|
||
}
|
||
|
||
recent-list := Rectangle {
|
||
x: 20px;
|
||
y: 36px;
|
||
width: parent.width - 40px;
|
||
height: 142px;
|
||
// A continuous inset surface groups the cards into one
|
||
// horizontal scrolling lane, including its scrollbar.
|
||
background: root.recent-connections.length > 0 ? #e6edf6 : transparent;
|
||
border-width: root.recent-connections.length > 0 ? 1px : 0px;
|
||
border-color: #c9d4e3;
|
||
border-radius: UiRadius.surface;
|
||
clip: true;
|
||
|
||
if root.recent-connections.length == 0: WorkbenchCard {
|
||
x: 0px; y: 6px; width: parent.width; height: 124px;
|
||
Rectangle {
|
||
x: 20px; y: 38px; width: 48px; height: 48px;
|
||
background: #f0f4fb; border-radius: UiRadius.surface;
|
||
Text { text: FontAwesomeIcons.display; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 20px; color: #8297b9; horizontal-alignment: center; vertical-alignment: center; }
|
||
}
|
||
Text {
|
||
x: 84px; y: 36px; width: parent.width - 104px; height: 24px;
|
||
text: 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
|
||
? 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;
|
||
}
|
||
}
|
||
recent-scroll := ScrollView {
|
||
visible: root.recent-connections.length > 0;
|
||
x: 6px; y: 5px;
|
||
width: parent.width - 12px;
|
||
height: parent.height - 6px;
|
||
horizontal-stretch: 1;
|
||
changed viewport-x => { root.recent-tooltip-visible = false; }
|
||
|
||
HorizontalLayout {
|
||
padding: 0px;
|
||
padding-bottom: 10px;
|
||
spacing: 12px;
|
||
alignment: start;
|
||
for connection[index] in root.recent-connections: connection-card := Rectangle {
|
||
width: (recent-scroll.width - 24px) / 3;
|
||
height: 126px;
|
||
background: transparent;
|
||
private property <bool> available: connection.online || connection.connected;
|
||
private property <length> content-width: self.width;
|
||
private property <length> toolbar-width: DeviceCardStyle.toolbar-width;
|
||
private property <length> toolbar-right-padding: 3px;
|
||
|
||
Rectangle {
|
||
width: connection-card.content-width;
|
||
height: 124px;
|
||
border-radius: UiRadius.surface;
|
||
background: white;
|
||
clip: true;
|
||
|
||
Rectangle {
|
||
x: 0px;
|
||
y: 0px;
|
||
width: parent.width;
|
||
height: 80px;
|
||
background: !connection-card.available ? DeviceCardStyle.offline-preview :
|
||
connection.platform == "windows" ? @linear-gradient(135deg, #1f61b3, #337ac4) :
|
||
connection.platform == "macos" ? @linear-gradient(135deg, #6e4aad, #8c63c4) :
|
||
connection.platform == "linux" ? @linear-gradient(135deg, #a14f14, #ba631f) :
|
||
connection.platform == "ios" ? @linear-gradient(135deg, #146e66, #1f8278) :
|
||
@linear-gradient(135deg, #596675, #737d8c);
|
||
if connection.has-thumbnail: Image {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
source: connection.thumbnail;
|
||
image-fit: cover;
|
||
// Keep the saved preview recognizable under the offline gray.
|
||
opacity: connection-card.available ? 1 : 0.16;
|
||
}
|
||
if !connection.has-thumbnail: Text {
|
||
x: 8px;
|
||
width: parent.width - 16px;
|
||
height: parent.height;
|
||
text: connection.platform == "windows" ? "Windows" :
|
||
connection.platform == "macos" ? "macOS" :
|
||
connection.platform == "linux" ? "Linux" :
|
||
connection.platform == "ios" ? "iOS" : UiStrings.unknown-platform;
|
||
color: connection-card.available ? white : DeviceCardStyle.offline-placeholder;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
overflow: elide;
|
||
}
|
||
}
|
||
Rectangle {
|
||
x: 0px;
|
||
y: 80px;
|
||
width: parent.width;
|
||
height: 44px;
|
||
background: white;
|
||
Rectangle { x: 0px; y: 0px; width: parent.width; height: 1px; background: WorkbenchStyle.border; }
|
||
Text {
|
||
x: 12px; y: 4px; height: 19px;
|
||
width: parent.width - 24px;
|
||
text: connection.display-name;
|
||
color: WorkbenchStyle.text;
|
||
font-size: 13px; font-weight: 600;
|
||
overflow: elide; vertical-alignment: center;
|
||
}
|
||
Text {
|
||
x: 12px; y: 24px; height: 16px; width: parent.width - 92px;
|
||
text: connection.remote-id; font-size: 11px; color: WorkbenchStyle.muted;
|
||
overflow: elide;
|
||
}
|
||
presence-label := Text {
|
||
x: parent.width - self.preferred-width - 12px; y: 24px; height: 16px;
|
||
text: connection-card.available ? UiStrings.online : UiStrings.offline;
|
||
font-size: 10px; color: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted;
|
||
}
|
||
}
|
||
// The software renderer only clips rectangular bounds.
|
||
// This outer border masks the four corners there too.
|
||
Rectangle {
|
||
x: -parent.border-radius;
|
||
y: -parent.border-radius;
|
||
width: parent.width + 2 * parent.border-radius;
|
||
height: parent.height + 2 * parent.border-radius;
|
||
border-width: parent.border-radius;
|
||
border-radius: 2 * parent.border-radius;
|
||
border-color: recent-list.background;
|
||
}
|
||
}
|
||
card-touch := TouchArea {
|
||
width: connection-card.content-width;
|
||
height: 124px;
|
||
mouse-cursor: pointer;
|
||
clicked => { root.recent-connect(connection.remote-id); }
|
||
image-touch := TouchArea {
|
||
x: 0px; y: 0px;
|
||
width: parent.width;
|
||
height: 80px;
|
||
private property <string> group-names: connection.group-names;
|
||
private property <bool> available: connection-card.available;
|
||
changed available => {
|
||
if self.has-hover && root.recent-tooltip-id == connection.remote-id {
|
||
root.recent-tooltip-online = self.available;
|
||
}
|
||
}
|
||
changed group-names => {
|
||
if self.has-hover && root.recent-tooltip-id == connection.remote-id {
|
||
root.recent-tooltip-groups = self.group-names;
|
||
}
|
||
}
|
||
clicked => { root.recent-connect(connection.remote-id); }
|
||
function show-details() {
|
||
if !self.has-hover { return; }
|
||
root.recent-tooltip-name = connection.display-name;
|
||
root.recent-tooltip-host = connection.host-name;
|
||
root.recent-tooltip-id = connection.remote-id;
|
||
root.recent-tooltip-groups = self.group-names;
|
||
root.recent-tooltip-online = self.available;
|
||
root.recent-tooltip-pointer-x = recent-panel.x + recent-list.x
|
||
+ recent-scroll.x + recent-scroll.viewport-x
|
||
+ connection-card.x + card-touch.x + self.x + self.mouse-x;
|
||
root.recent-tooltip-pointer-y = recent-panel.y + recent-list.y
|
||
+ recent-scroll.y + recent-scroll.viewport-y
|
||
+ connection-card.y + card-touch.y + self.y + self.mouse-y;
|
||
root.recent-tooltip-visible = true;
|
||
}
|
||
changed has-hover => {
|
||
if self.has-hover {
|
||
self.show-details();
|
||
} else if root.recent-tooltip-id == connection.remote-id {
|
||
root.recent-tooltip-visible = false;
|
||
}
|
||
}
|
||
// moved only reports pressed pointer movement;
|
||
// track both coordinates during ordinary hover.
|
||
changed mouse-x => { self.show-details(); }
|
||
changed mouse-y => { self.show-details(); }
|
||
}
|
||
if card-touch.has-hover: DeviceCardActions {
|
||
x: parent.width - self.width - connection-card.toolbar-right-padding;
|
||
y: 52px;
|
||
remove-label: GalleryStrings.tr("删除记录", "Delete record", "Удалить запись");
|
||
connected: connection.connected;
|
||
edit-alias => {
|
||
root.alias-remote-id = connection.remote-id;
|
||
root.alias-input = connection.display-name;
|
||
root.alias-open = true;
|
||
}
|
||
edit-groups => { root.open-device-groups(connection.remote-id, connection.display-name); }
|
||
remove-device => {
|
||
root.delete-remote-id = connection.remote-id;
|
||
root.delete-from-gallery = false;
|
||
root.delete-open = true;
|
||
}
|
||
connect-device => { root.recent-connect(connection.remote-id); }
|
||
disconnect-device => { root.recent-disconnect(connection.remote-id); }
|
||
}
|
||
}
|
||
Rectangle {
|
||
width: parent.width; height: 124px;
|
||
border-width: 1px; border-radius: UiRadius.surface;
|
||
border-color: card-touch.has-hover ? WorkbenchStyle.hover-border : WorkbenchStyle.border;
|
||
animate border-color { duration: 120ms; }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
gallery := GalleryView {
|
||
y: 0px; width: parent.width;
|
||
height: parent.height - self.y;
|
||
visible: root.gallery-mode;
|
||
simplified: root.gallery-simplified;
|
||
fullscreen: root.gallery-fullscreen;
|
||
toggle-simplified => { root.gallery-simplified = !root.gallery-simplified; }
|
||
toggle-fullscreen => { root.toggle-gallery-fullscreen(); }
|
||
window-active: root.window-active;
|
||
tile-width <=> root.gallery-tile-width;
|
||
focus-rail-height <=> root.gallery-focus-rail-height;
|
||
network-open => { root.gallery-network-open = true; }
|
||
controlling: root.gallery-controlling;
|
||
remote-cursor-shape: root.gallery-remote-cursor-shape;
|
||
group-controlling: root.gallery-group-controlling;
|
||
input-suspended: root.gallery-transitioning;
|
||
pointer-input(button, kind, x, y) => { root.gallery-pointer(button, kind, x, y); }
|
||
scroll-input(dx, dy, x, y) => { root.gallery-scroll(dx, dy, x, y); }
|
||
key-input(text, pressed, ctrl, alt, shift, meta) => { root.gallery-key(text, pressed, ctrl, alt, shift, meta); }
|
||
tiles: root.gallery-tiles; groups: root.gallery-groups;
|
||
group-index <=> root.gallery-group-index;
|
||
page: root.gallery-page; pages: root.gallery-pages; count: root.gallery-count;
|
||
connected-count: root.gallery-connected; selected-count: root.gallery-selected;
|
||
selected-audio-available: root.gallery-selected-audio-available;
|
||
selected-audio-muted: root.gallery-selected-audio-muted;
|
||
hidden-selected: root.gallery-hidden-selected; notice: root.gallery-notice;
|
||
selected-devices: root.gallery-selected-devices;
|
||
focused: root.gallery-focused; batch <=> root.gallery-batch; adding <=> root.gallery-adding;
|
||
importing <=> root.gallery-importing; recent-devices: root.gallery-recent-devices;
|
||
recent-selected: root.gallery-recent-selected;
|
||
recent-available: root.gallery-recent-available; import-group: root.gallery-import-group;
|
||
action(code, value) => { root.gallery-action(code, value); }
|
||
swap-devices(source, target) => { root.gallery-swap(source, target) }
|
||
add-device(id, password, group, remember) => { root.gallery-add-device(id, password, group, remember) }
|
||
import-recent(group) => { root.gallery-import-recent(group) }
|
||
edit-alias(id, name) => {
|
||
root.alias-remote-id = id; root.alias-input = name; root.alias-open = true;
|
||
}
|
||
edit-groups(id, name) => { root.open-device-groups(id, name); }
|
||
remove-device(id) => {
|
||
root.delete-remote-id = id; root.delete-from-gallery = true; root.delete-open = true;
|
||
}
|
||
}
|
||
// Server status and reconnect actions belong to the connection window.
|
||
if !root.gallery-mode: Rectangle {
|
||
y: parent.height - 28px;
|
||
height: 28px;
|
||
background: white;
|
||
Rectangle {
|
||
y: 0px;
|
||
height: 1px;
|
||
background: WorkbenchStyle.border;
|
||
}
|
||
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;
|
||
}
|
||
Text {
|
||
x: 34px;
|
||
y: 0px;
|
||
height: parent.height;
|
||
text: root.signal-tls-error ? UiStrings.tls-error
|
||
: root.signal-connected ? UiStrings.signal-connected
|
||
: root.signal-reconnecting ? UiStrings.signal-reconnecting
|
||
: UiStrings.signal-disconnected;
|
||
color: WorkbenchStyle.muted;
|
||
font-size: 11px;
|
||
vertical-alignment: center;
|
||
}
|
||
if 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: Rectangle {
|
||
x: parent.width - self.width - 20px;
|
||
y: 0px;
|
||
height: parent.height;
|
||
width: reconnect-label.preferred-width;
|
||
reconnect-label := Text {
|
||
text: UiStrings.signal-reconnect;
|
||
color: WorkbenchStyle.accent;
|
||
font-size: 11px;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
root.signal-reconnect-requested();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 {
|
||
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;
|
||
private property <string> group-names: root.gallery-mode ? gallery.hovered-device.group-names : root.recent-tooltip-groups;
|
||
private property <bool> online: root.gallery-mode ? gallery.hovered-device.online : root.recent-tooltip-online;
|
||
private property <bool> presence-known: !root.gallery-mode || gallery.hovered-device.presence-known;
|
||
private property <length> pointer-x: root.gallery-mode ? gallery.x + gallery.device-tooltip-x : root.recent-tooltip-pointer-x;
|
||
private property <length> pointer-y: root.gallery-mode ? gallery.y + gallery.device-tooltip-y : root.recent-tooltip-pointer-y;
|
||
// Follow the pointer with a gap; change sides only when space runs out.
|
||
x: clamp(self.pointer-x + self.width + 12px <= parent.width - 8px
|
||
? self.pointer-x + 12px : self.pointer-x - self.width - 12px,
|
||
8px, max(8px, parent.width - self.width - 8px));
|
||
y: clamp(self.pointer-y - self.height - 12px >= 8px
|
||
? self.pointer-y - self.height - 12px : self.pointer-y + 16px,
|
||
8px, max(8px, parent.height - self.height - 8px));
|
||
width: min(260px, parent.width - 16px);
|
||
height: (self.host-name != "" && self.host-name != self.device-name
|
||
? 80px : 64px) + tooltip-groups.preferred-height + 2px;
|
||
background: #22252bf2;
|
||
border-width: 1px;
|
||
border-color: #ffffff38;
|
||
border-radius: UiRadius.control;
|
||
z: 20;
|
||
|
||
VerticalLayout {
|
||
padding: 8px;
|
||
spacing: 2px;
|
||
Text {
|
||
text: UiStrings.device-name + ": " + device-tip.device-name;
|
||
color: white;
|
||
font-size: ImGuiFontStyle.small;
|
||
overflow: elide;
|
||
}
|
||
if device-tip.host-name != "" && device-tip.host-name != device-tip.device-name: Text {
|
||
text: device-tip.host-name;
|
||
color: #e1e4e8;
|
||
font-size: ImGuiFontStyle.small;
|
||
overflow: elide;
|
||
}
|
||
Text {
|
||
text: UiStrings.remote-id + ": " + device-tip.device-id;
|
||
color: #e1e4e8;
|
||
font-size: ImGuiFontStyle.small;
|
||
overflow: elide;
|
||
}
|
||
tooltip-groups := Text {
|
||
text: GalleryStrings.tr("分组", "Groups", "Группы") + ": " +
|
||
(device-tip.group-names == "" ? GalleryStrings.tr("未分组", "Ungrouped", "Без группы") : device-tip.group-names);
|
||
color: #e1e4e8;
|
||
font-size: ImGuiFontStyle.small;
|
||
wrap: word-wrap;
|
||
vertical-stretch: 0;
|
||
}
|
||
Text {
|
||
text: device-tip.online ? UiStrings.online : device-tip.presence-known ? UiStrings.offline
|
||
: GalleryStrings.tr("未知", "Unknown", "Неизвестно");
|
||
color: device-tip.online ? #62d98b : #d1d5db;
|
||
font-size: ImGuiFontStyle.small;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Dismiss the application menu when clicking anywhere outside of it.
|
||
if !root.gallery-mode && root.menu-open: TouchArea {
|
||
x: 0px;
|
||
y: 0px;
|
||
width: parent.width;
|
||
height: parent.height;
|
||
z: 9;
|
||
clicked => {
|
||
root.menu-open = false;
|
||
focus-sink.focus();
|
||
}
|
||
}
|
||
|
||
// Expand downward from the application toolbar.
|
||
if !root.gallery-mode && root.menu-open: Rectangle {
|
||
x: menu-button.x + menu-button.width - self.width;
|
||
y: menu-button.y + menu-button.height + 6px;
|
||
width: max(128px, max(menu-settings-label.preferred-width,
|
||
max(menu-messages-label.preferred-width + (root.announcement-unread > 0 ? menu-message-badge.width + 8px : 0px),
|
||
menu-about-label.preferred-width + (root.update-available ? menu-update-badge.width + 8px : 0px))) + 28px);
|
||
height: application-menu-content.preferred-height;
|
||
background: WorkbenchStyle.surface;
|
||
border-width: 1px;
|
||
border-color: WorkbenchStyle.control-border;
|
||
border-radius: UiRadius.menu;
|
||
z: 10;
|
||
TouchArea { }
|
||
application-menu-content := VerticalLayout {
|
||
padding: 6px;
|
||
spacing: 0px;
|
||
menu-settings := Rectangle {
|
||
height: 30px;
|
||
background: settings-touch.pressed ? WorkbenchStyle.pressed-surface
|
||
: settings-touch.has-hover ? WorkbenchStyle.hover-surface
|
||
: transparent;
|
||
border-width: 0px;
|
||
border-radius: UiRadius.control;
|
||
animate background { duration: 120ms; }
|
||
accessible-role: button;
|
||
accessible-label: UiStrings.settings;
|
||
accessible-action-default => { settings-touch.clicked(); }
|
||
menu-settings-label := Text {
|
||
x: 8px;
|
||
height: parent.height;
|
||
text: UiStrings.settings;
|
||
color: WorkbenchStyle.text; font-size: 12px;
|
||
vertical-alignment: center;
|
||
accessible-role: none;
|
||
}
|
||
settings-touch := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.menu-open = false; root.settings-open = true; }
|
||
}
|
||
}
|
||
Rectangle {
|
||
height: 7px;
|
||
Rectangle {
|
||
x: 8px; y: 3px; width: parent.width - 16px; height: 1px;
|
||
background: WorkbenchStyle.border;
|
||
}
|
||
}
|
||
menu-messages := Rectangle {
|
||
height: 30px;
|
||
background: messages-touch.pressed ? WorkbenchStyle.pressed-surface
|
||
: messages-touch.has-hover ? WorkbenchStyle.hover-surface : transparent;
|
||
border-radius: UiRadius.control;
|
||
animate background { duration: 120ms; }
|
||
accessible-role: button;
|
||
accessible-label: MessageStrings.messages + (root.announcement-unread > 0 ? " · " + MessageStrings.unread + " " + root.announcement-unread : "");
|
||
accessible-action-default => { messages-touch.clicked(); }
|
||
menu-messages-label := Text {
|
||
x: 8px;
|
||
height: parent.height;
|
||
text: MessageStrings.messages;
|
||
color: WorkbenchStyle.text; font-size: 12px;
|
||
vertical-alignment: center;
|
||
accessible-role: none;
|
||
}
|
||
menu-message-badge := Rectangle {
|
||
visible: root.announcement-unread > 0;
|
||
x: parent.width - self.width - 7px;
|
||
y: (parent.height - self.height) / 2;
|
||
width: max(18px, menu-message-count.preferred-width + 10px);
|
||
height: 18px;
|
||
border-radius: 9px;
|
||
background: #fee4e2;
|
||
accessible-role: none;
|
||
menu-message-count := Text {
|
||
text: root.announcement-unread-label;
|
||
font-size: 10px; font-weight: 600; color: #b42318;
|
||
horizontal-alignment: center; vertical-alignment: center;
|
||
accessible-role: none;
|
||
}
|
||
}
|
||
messages-touch := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
root.menu-open = false;
|
||
root.messages-open = true;
|
||
root.refresh-announcements();
|
||
}
|
||
}
|
||
}
|
||
Rectangle {
|
||
height: 7px;
|
||
Rectangle {
|
||
x: 8px; y: 3px; width: parent.width - 16px; height: 1px;
|
||
background: WorkbenchStyle.border;
|
||
}
|
||
}
|
||
menu-about := Rectangle {
|
||
height: 30px;
|
||
background: about-touch.pressed ? WorkbenchStyle.pressed-surface
|
||
: about-touch.has-hover ? WorkbenchStyle.hover-surface
|
||
: transparent;
|
||
border-width: 0px;
|
||
border-radius: UiRadius.control;
|
||
animate background { duration: 120ms; }
|
||
accessible-role: button;
|
||
accessible-label: UiStrings.about + (root.update-available ? " · " + UiStrings.update-available : "");
|
||
accessible-action-default => { about-touch.clicked(); }
|
||
menu-about-label := Text {
|
||
x: 8px;
|
||
height: parent.height;
|
||
text: UiStrings.about;
|
||
color: WorkbenchStyle.text; font-size: 12px;
|
||
vertical-alignment: center;
|
||
accessible-role: none;
|
||
}
|
||
menu-update-badge := Rectangle {
|
||
visible: root.update-available;
|
||
x: parent.width - self.width - 7px;
|
||
y: (parent.height - self.height) / 2;
|
||
width: menu-update-label.preferred-width + 12px;
|
||
height: 18px;
|
||
border-radius: 9px;
|
||
background: #dbeafe;
|
||
accessible-role: none;
|
||
menu-update-label := Text {
|
||
text: UiStrings.new-version;
|
||
font-size: 11px; font-weight: 400; color: #1d4ed8;
|
||
horizontal-alignment: center; vertical-alignment: center;
|
||
accessible-role: none;
|
||
}
|
||
}
|
||
about-touch := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.menu-open = false; root.about-open = true; }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if root.messages-open: Rectangle {
|
||
background: #00000042;
|
||
z: 40;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
MessagePanel {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: 520px;
|
||
height: 330px;
|
||
items: root.announcements;
|
||
unread-count: root.announcement-unread; total: root.announcement-total;
|
||
loading: root.announcement-loading; failed: root.announcement-failed; loaded: root.announcement-loaded;
|
||
read-save-failed: root.announcement-read-save-failed;
|
||
delete-failed: root.announcement-delete-failed;
|
||
connected: root.signal-connected;
|
||
selected-id: root.announcement-selected-id; selected-title: root.announcement-selected-title;
|
||
selected-body: root.announcement-selected-body; selected-date: root.announcement-selected-date;
|
||
selected-read: root.announcement-selected-read;
|
||
open-message(id, revision) => { root.open-announcement(id, revision); }
|
||
open-link(url) => { root.open-announcement-link(url); }
|
||
delete-message(id, revision) => { root.delete-announcement(id, revision); }
|
||
load-more => { root.load-more-announcements(); }
|
||
close => { root.messages-open = false; }
|
||
}
|
||
}
|
||
|
||
copy-timer := Timer {
|
||
interval: 1000ms;
|
||
triggered => { root.copy-toast = false; }
|
||
}
|
||
remote-id-format-timer := Timer {
|
||
interval: 1ms;
|
||
triggered => {
|
||
let formatted = root.format-remote-id(root.remote-id-input);
|
||
root.remote-id-input = formatted;
|
||
// IDs are ASCII-only, so character-count is also the UTF-8 offset
|
||
// expected by set-selection-offsets().
|
||
remote-id-editor.set-selection-offsets(
|
||
formatted.character-count, formatted.character-count);
|
||
}
|
||
}
|
||
if root.copy-toast: Rectangle {
|
||
x: (parent.width - self.width) / 2; y: parent.height - 92px; width: 280px; height: 40px;
|
||
background: #fffffff2;
|
||
border-radius: UiRadius.surface;
|
||
border-width: 1px;
|
||
border-color: ImGuiLineStyle.border;
|
||
z: 30;
|
||
Text { text: UiStrings.copied; color: #333840; horizontal-alignment: center; vertical-alignment: center; }
|
||
}
|
||
|
||
// Reset password modal.
|
||
if root.reset-password-open: Rectangle {
|
||
background: #00000042;
|
||
z: 40;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
DialogSurface {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: root.compact-language ? 230px : 320px;
|
||
height: 170px;
|
||
VerticalLayout {
|
||
padding: 20px;
|
||
spacing: 12px;
|
||
DialogPromptText { prompt: UiStrings.new-password; }
|
||
HorizontalLayout {
|
||
alignment: center;
|
||
PasswordInput {
|
||
text <=> root.new-password-input;
|
||
max-length: 6;
|
||
}
|
||
}
|
||
Text { height: root.reset-password-invalid ? 18px : 0px; visible: root.reset-password-invalid; text: UiStrings.invalid-password; color: #d92d20; font-size: ImGuiFontStyle.small; horizontal-alignment: center; }
|
||
HorizontalLayout {
|
||
alignment: center; spacing: 8px;
|
||
PrimaryButton {
|
||
text: UiStrings.ok;
|
||
clicked => {
|
||
if (root.reset-password(root.new-password-input)) {
|
||
root.new-password-input = "";
|
||
root.reset-password-invalid = false;
|
||
root.reset-password-open = false;
|
||
} else { root.reset-password-invalid = true; }
|
||
}
|
||
}
|
||
SecondaryButton { text: UiStrings.cancel; clicked => { root.new-password-input = ""; root.reset-password-invalid = false; root.reset-password-open = false; } }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// One editor and one callback path for recent connections and DeskHub.
|
||
if root.device-group-open: DeviceGroupDialog {
|
||
z: 40;
|
||
device-id: root.device-group-id;
|
||
device-name: root.device-group-name;
|
||
options: root.device-group-options;
|
||
error: root.device-group-error;
|
||
new-group <=> root.device-group-new-name;
|
||
toggle-group(name) => { root.device-group-action("toggle", name); }
|
||
save(name) => {
|
||
if root.device-group-save(name) {
|
||
root.device-group-action("close", "");
|
||
root.device-group-open = false;
|
||
}
|
||
}
|
||
cancel => { root.device-group-action("close", ""); root.device-group-open = false; }
|
||
}
|
||
|
||
// Recent connection alias modal.
|
||
if root.alias-open: Rectangle {
|
||
background: #00000042;
|
||
z: 40;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
DialogSurface {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: root.compact-language ? 230px : 320px;
|
||
height: 160px;
|
||
VerticalLayout {
|
||
padding: 20px; spacing: 14px;
|
||
DialogPromptText { prompt: UiStrings.edit-alias; }
|
||
RoundedLineEdit {
|
||
height: 28px;
|
||
normal-border-color: ImGuiLineStyle.border;
|
||
text <=> root.alias-input;
|
||
accepted => {
|
||
root.recent-edit-alias(root.alias-remote-id, root.alias-input);
|
||
root.alias-open = false;
|
||
}
|
||
}
|
||
HorizontalLayout {
|
||
alignment: center; spacing: 8px;
|
||
PrimaryButton { text: UiStrings.ok; clicked => { root.recent-edit-alias(root.alias-remote-id, root.alias-input); root.alias-open = false; } }
|
||
SecondaryButton { text: UiStrings.cancel; clicked => { root.alias-open = false; } }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Delete confirmation modal.
|
||
if root.delete-open: Rectangle {
|
||
background: #00000042;
|
||
z: 40;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
DialogSurface {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: root.compact-language ? 230px : 320px;
|
||
height: 140px;
|
||
VerticalLayout {
|
||
padding: 20px; spacing: 18px;
|
||
DialogPromptText { prompt: root.delete-from-gallery ? (root.gallery-group-index > 0 ? GalleryStrings.tr("从当前分组移除此设备?", "Remove from this group?", "Убрать из этой группы?") : GalleryStrings.tr("从桌面墙移除此设备?", "Remove from DeskHub?", "Убрать из DeskHub?")) : UiStrings.confirm-delete; }
|
||
HorizontalLayout {
|
||
alignment: center; spacing: 8px;
|
||
PrimaryButton { text: UiStrings.ok; clicked => { if root.delete-from-gallery { root.gallery-action("remove", root.delete-remote-id); } else { root.recent-delete(root.delete-remote-id); } root.delete-open = false; } }
|
||
SecondaryButton { text: UiStrings.cancel; clicked => { root.delete-open = false; } }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Offline/error warning modal.
|
||
if root.offline-warning != "": Rectangle {
|
||
background: #00000042;
|
||
z: 39;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
DialogSurface {
|
||
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 270px; height: 150px;
|
||
VerticalLayout {
|
||
padding: 20px; spacing: 18px;
|
||
DialogPromptText { prompt: root.offline-warning; }
|
||
PrimaryButton { text: UiStrings.ok; clicked => { root.offline-warning = ""; } }
|
||
}
|
||
}
|
||
}
|
||
|
||
// Connection feedback follows the workbench spacing and action hierarchy.
|
||
if root.connection-dialog-open: Rectangle {
|
||
background: #15223850;
|
||
z: 48;
|
||
TouchArea { clicked => { 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;
|
||
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;
|
||
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); }
|
||
}
|
||
if root.connection-password-required && !root.connection-validating: Rectangle {
|
||
x: (parent.width - self.width) / 2; y: 134px;
|
||
width: remember-label.preferred-width + 29px; height: 21px;
|
||
ImGuiCheckBox { x: 0px; y: 0px; checked <=> root.connection-remember-password; }
|
||
remember-label := Text {
|
||
x: 29px; height: parent.height;
|
||
text: UiStrings.remember-password;
|
||
color: WorkbenchStyle.muted; font-size: 12px; vertical-alignment: center;
|
||
}
|
||
TouchArea {
|
||
x: 29px; width: parent.width - 29px; height: parent.height;
|
||
clicked => { 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 {
|
||
text: UiStrings.cancel;
|
||
clicked => { root.connection-cancel(); }
|
||
}
|
||
if root.connection-password-required && !root.connection-validating: WorkbenchButton {
|
||
primary: true; text: UiStrings.ok;
|
||
clicked => { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
|
||
}
|
||
if !root.connection-password-required && !root.connection-pending: WorkbenchButton {
|
||
primary: true; text: UiStrings.ok;
|
||
clicked => { root.connection-acknowledge(); }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// macOS permission gate. On other platforms this remains hidden.
|
||
if root.permission-dialog-open: Rectangle {
|
||
background: transparent;
|
||
z: 50;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
DialogSurface {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: root.compact-language ? 130px : 260px;
|
||
height: 125px;
|
||
border-radius: UiRadius.surface;
|
||
Rectangle {
|
||
x: 0px;
|
||
y: 0px;
|
||
width: parent.width;
|
||
height: 21px;
|
||
background: #d3d3d3;
|
||
border-top-left-radius: UiRadius.surface;
|
||
border-top-right-radius: UiRadius.surface;
|
||
z: 5;
|
||
Text { x: 6px; text: UiStrings.request-permissions; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
|
||
}
|
||
Text { x: 10px; y: 32px; width: root.compact-language ? 110px : 240px; height: 24px; text: UiStrings.permission-required; color: #202124; font-size: 8px; wrap: word-wrap; }
|
||
Text { x: 10px; y: 61px; width: root.compact-language ? 78px : 198px; height: 18px; text: "1. " + UiStrings.accessibility-permission + ":"; color: #202124; font-size: 8px; overflow: elide; vertical-alignment: center; }
|
||
MiniToggleSwitch {
|
||
x: root.compact-language ? 90px : 210px;
|
||
y: 61px;
|
||
checked: root.accessibility-granted;
|
||
enabled: !root.accessibility-granted;
|
||
toggled(value) => { if (value) { root.request-accessibility-permission(); } }
|
||
}
|
||
Text { x: 10px; y: 85px; width: root.compact-language ? 78px : 198px; height: 18px; text: "2. " + UiStrings.screen-recording-permission + ":"; color: #202124; font-size: 8px; overflow: elide; vertical-alignment: center; }
|
||
MiniToggleSwitch {
|
||
x: root.compact-language ? 90px : 210px;
|
||
y: 85px;
|
||
checked: root.screen-recording-granted;
|
||
enabled: !root.screen-recording-granted;
|
||
toggled(value) => { if (value) { root.request-screen-recording-permission(); } }
|
||
}
|
||
}
|
||
}
|
||
|
||
// Both tabs share one draft and the same save/cancel actions.
|
||
if root.settings-open: Rectangle {
|
||
background: #15223850;
|
||
z: 45;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
settings-dialog := DialogSurface {
|
||
// Center every content section; the scrollbar sits in the right margin.
|
||
private property <length> content-inset: 26px;
|
||
private property <length> scrollbar-gutter: 22px;
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: min(400px, parent.width - 40px);
|
||
height: min(446px, parent.height - 16px);
|
||
background: #f8fafc; border-radius: UiRadius.surface; border-color: #e1e7ef;
|
||
drop-shadow-color: #14294424; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
Text {
|
||
x: settings-dialog.content-inset; y: 18px;
|
||
width: parent.width - 2 * settings-dialog.content-inset; height: 27px;
|
||
text: UiStrings.settings; font-size: 20px; font-weight: 600; color: #22324a;
|
||
}
|
||
Rectangle {
|
||
x: settings-dialog.content-inset; y: 56px;
|
||
width: parent.width - 2 * settings-dialog.content-inset; height: 40px;
|
||
background: WorkbenchStyle.canvas; border-radius: UiRadius.menu;
|
||
border-width: 1px; border-color: WorkbenchStyle.border;
|
||
Rectangle {
|
||
x: 4px + root.settings-tab * (parent.width - 4px) / 2;
|
||
y: 4px; width: (parent.width - 12px) / 2; height: parent.height - 8px;
|
||
background: WorkbenchStyle.surface; border-radius: UiRadius.control;
|
||
border-width: 1px; border-color: WorkbenchStyle.control-border;
|
||
animate x { duration: 140ms; easing: ease-out; }
|
||
}
|
||
for label[index] in [GalleryStrings.tr("常规", "General", "Общие"), GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub")]: Rectangle {
|
||
x: 4px + index * (parent.width - 4px) / 2;
|
||
y: 4px; width: (parent.width - 12px) / 2; height: parent.height - 8px;
|
||
border-radius: UiRadius.control;
|
||
background: root.settings-tab == index ? transparent
|
||
: tab-touch.pressed ? WorkbenchStyle.pressed-surface
|
||
: tab-touch.has-hover ? WorkbenchStyle.hover-surface : transparent;
|
||
animate background { duration: 100ms; }
|
||
accessible-role: button; accessible-label: label;
|
||
accessible-action-default => { tab-touch.clicked(); }
|
||
Text {
|
||
text: label; font-size: 12px; font-weight: root.settings-tab == index ? 600 : 500;
|
||
color: root.settings-tab == index ? WorkbenchStyle.accent : WorkbenchStyle.muted;
|
||
horizontal-alignment: center; vertical-alignment: center;
|
||
}
|
||
tab-touch := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.settings-tab = index; focus-sink.focus(); }
|
||
}
|
||
}
|
||
}
|
||
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;
|
||
viewport-width: self.visible-width;
|
||
viewport-height: settings-content.preferred-height;
|
||
viewport-y <=> root.settings-scroll-y;
|
||
horizontal-scrollbar-policy: always-off;
|
||
vertical-scrollbar-policy: as-needed;
|
||
// Fluent's scrollbar expands over the viewport on hover.
|
||
// Keep its full 14px track plus an 8px gap outside the groups.
|
||
Rectangle {
|
||
x: 0px; y: 0px;
|
||
width: settings-scroll.viewport-width;
|
||
height: settings-scroll.viewport-height;
|
||
settings-content := VerticalLayout {
|
||
padding: 0px; padding-right: settings-dialog.scrollbar-gutter; padding-bottom: 4px;
|
||
spacing: 14px;
|
||
SettingsGroup {
|
||
title: GalleryStrings.tr("界面", "Interface", "Интерфейс"); rows: 1;
|
||
SettingsRow {
|
||
label: UiStrings.language; divider: false;
|
||
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
|
||
}
|
||
}
|
||
SettingsGroup {
|
||
title: GalleryStrings.tr("视频", "Video", "Видео");
|
||
rows: root.capture-method-visible ? 3 : 2;
|
||
if root.capture-method-visible: SettingsRow {
|
||
label: UiStrings.capture-method; enabled: !root.settings-session-active;
|
||
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; enabled: !root.settings-session-active; model: [UiStrings.capture-method-auto, "DXGI", "WGC", "GDI"]; current-index <=> root.capture-method-index; }
|
||
}
|
||
SettingsRow {
|
||
label: UiStrings.codec; enabled: !root.settings-session-active;
|
||
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
|
||
}
|
||
SettingsToggleRow { label: UiStrings.hardware-codec; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; divider: false; }
|
||
}
|
||
SettingsGroup {
|
||
title: GalleryStrings.tr("网络连接", "Network", "Сеть"); rows: 3;
|
||
SettingsToggleRow { label: UiStrings.turn-relay; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
|
||
SettingsToggleRow { label: UiStrings.force-relay; checked <=> root.force-relay-enabled; enabled: root.turn-enabled && !root.settings-session-active; }
|
||
SettingsToggleRow {
|
||
checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; divider: false;
|
||
SettingsButton { x: 0px; y: 2px; width: parent.width - 60px; height: 32px; quiet: true; align-left: true; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
|
||
}
|
||
}
|
||
SettingsGroup {
|
||
title: GalleryStrings.tr("隐私保护", "Privacy", "Приватность"); rows: 1;
|
||
SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; divider: false; }
|
||
}
|
||
SettingsGroup {
|
||
title: GalleryStrings.tr("本地存储", "Local storage", "Локальное хранение"); rows: 2;
|
||
SettingsToggleRow { label: UiStrings.save-remote-previews; checked <=> root.save-remote-previews; }
|
||
SettingsRow {
|
||
label: UiStrings.file-save-path; enabled: !root.settings-session-active; divider: false;
|
||
SettingsButton { x: parent.width - 116px; y: 4px; width: 104px; height: 28px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
|
||
}
|
||
}
|
||
SettingsGroup {
|
||
title: GalleryStrings.tr("启动与后台", "Startup & background", "Запуск и фоновая работа"); rows: 2;
|
||
SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
|
||
SettingsToggleRow { label: UiStrings.daemon; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; divider: false; }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if root.settings-tab == 1: GallerySettings {
|
||
x: settings-dialog.content-inset; y: 108px;
|
||
width: parent.width - 2 * settings-dialog.content-inset; height: parent.height - 174px;
|
||
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;
|
||
}
|
||
Rectangle {
|
||
x: settings-dialog.content-inset; y: parent.height - 64px;
|
||
width: parent.width - 2 * settings-dialog.content-inset; height: 1px; background: #e6ebf2;
|
||
}
|
||
HorizontalLayout {
|
||
x: settings-dialog.content-inset; y: parent.height - 50px;
|
||
width: parent.width - 2 * settings-dialog.content-inset; height: 32px; spacing: 10px;
|
||
Rectangle { horizontal-stretch: 1; }
|
||
SettingsButton {
|
||
width: 80px; text: UiStrings.cancel;
|
||
clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; }
|
||
}
|
||
SettingsButton {
|
||
width: 92px; primary: true;
|
||
text: GalleryStrings.tr("保存", "Save", "Сохранить");
|
||
clicked => { if root.save-settings() { root.settings-open = false; root.self-host-settings-open = false; } }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if root.self-host-settings-open: Rectangle {
|
||
background: #15223835;
|
||
z: 49;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
DialogSurface {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: 360px; height: root.self-host-settings-error == "" ? 266px : 322px;
|
||
border-radius: UiRadius.surface; border-color: #e1e7ef; background: #f8fafc;
|
||
drop-shadow-color: #14294424; drop-shadow-blur: 18px; drop-shadow-offset-y: 6px;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
Text {
|
||
x: 18px; y: 18px; width: parent.width - 36px; height: 24px;
|
||
text: UiStrings.self-hosted-settings; font-size: 16px; font-weight: 600; color: #22324a;
|
||
}
|
||
Text { x: 18px; y: 56px; width: parent.width - 36px; height: 20px; text: UiStrings.server-host; color: #34445a; font-size: 12px; overflow: elide; vertical-alignment: center; }
|
||
RoundedLineEdit {
|
||
x: 18px; y: 80px; width: parent.width - 36px; height: 30px;
|
||
surrounding-background: #f8fafc;
|
||
normal-border-color: WorkbenchStyle.control-border;
|
||
text <=> root.server-host;
|
||
}
|
||
Text { x: 18px; y: 120px; width: parent.width - 36px; height: 20px; text: UiStrings.server-port; color: #34445a; font-size: 12px; overflow: elide; vertical-alignment: center; }
|
||
RoundedLineEdit {
|
||
x: 18px; y: 144px; width: parent.width - 36px; height: 30px;
|
||
surrounding-background: #f8fafc;
|
||
normal-border-color: WorkbenchStyle.control-border;
|
||
text <=> root.server-port; input-type: InputType.number;
|
||
}
|
||
if root.self-host-settings-error != "": Text {
|
||
x: 18px; y: 186px; width: parent.width - 36px; height: 48px;
|
||
text: root.self-host-settings-error; color: #c53e3e; font-size: 11px; wrap: word-wrap;
|
||
}
|
||
Rectangle { x: 18px; y: parent.height - 66px; width: parent.width - 36px; height: 1px; background: #e6ebf2; }
|
||
SettingsButton { x: parent.width - 114px; y: parent.height - 50px; width: 92px; primary: true; text: UiStrings.ok; clicked => { if root.save-self-hosted-settings() { root.self-host-settings-open = false; } } }
|
||
SettingsButton { x: parent.width - 204px; y: parent.height - 50px; width: 80px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
|
||
}
|
||
}
|
||
|
||
if root.about-open: Rectangle {
|
||
background: transparent;
|
||
z: 46;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
DialogSurface {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: 320px;
|
||
height: max(216px, about-content.preferred-height);
|
||
border-radius: UiRadius.surface;
|
||
|
||
Rectangle {
|
||
x: 0px;
|
||
y: 0px;
|
||
width: parent.width;
|
||
height: 21px;
|
||
background: #d3d3d3;
|
||
border-top-left-radius: UiRadius.surface;
|
||
border-top-right-radius: UiRadius.surface;
|
||
z: 5;
|
||
Text { x: 6px; text: UiStrings.about; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
|
||
}
|
||
// Grow for update actions and wrapped translations while keeping
|
||
// the footer below the version and check result.
|
||
about-content := VerticalLayout {
|
||
padding: 16px;
|
||
padding-top: 35px;
|
||
spacing: 12px;
|
||
alignment: space-between;
|
||
|
||
VerticalLayout {
|
||
vertical-stretch: 0;
|
||
spacing: 4px;
|
||
Text {
|
||
text: "CrossDesk " + root.current-version;
|
||
color: #202124;
|
||
font-size: ImGuiFontStyle.base;
|
||
font-weight: 600;
|
||
min-height: 18px;
|
||
overflow: elide;
|
||
}
|
||
if root.update-available: Rectangle {
|
||
height: max(18px, version-link.preferred-height);
|
||
accessible-role: button;
|
||
accessible-label: UiStrings.release-notes + " " + root.latest-version;
|
||
accessible-action-default => { version-touch.clicked(); }
|
||
version-link := Text {
|
||
x: 0px;
|
||
y: 0px;
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: UiStrings.update-available + ": " + root.latest-version;
|
||
color: version-touch.has-hover ? #174ea6 : #334fd1;
|
||
font-size: ImGuiFontStyle.body;
|
||
vertical-alignment: center;
|
||
horizontal-alignment: left;
|
||
wrap: word-wrap;
|
||
}
|
||
version-touch := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.about-open = false; root.update-open = true; }
|
||
}
|
||
}
|
||
if root.update-available: HorizontalLayout {
|
||
alignment: start;
|
||
padding-top: 4px;
|
||
CompactButton {
|
||
width: max(76px, release-notes-button-metrics.preferred-width + 20px);
|
||
text: UiStrings.view-release-notes;
|
||
accessible-role: button;
|
||
accessible-label: self.text;
|
||
accessible-action-default => { self.clicked(); }
|
||
clicked => { root.about-open = false; root.update-open = true; }
|
||
release-notes-button-metrics := Text { visible: false; text: UiStrings.view-release-notes; font-size: ImGuiFontStyle.body; }
|
||
}
|
||
}
|
||
if root.update-status-text != "": Text {
|
||
text: root.update-status-text;
|
||
color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e : #61656c;
|
||
font-size: ImGuiFontStyle.body;
|
||
min-height: 18px;
|
||
wrap: word-wrap;
|
||
}
|
||
}
|
||
|
||
VerticalLayout {
|
||
vertical-stretch: 0;
|
||
spacing: 3px;
|
||
Rectangle { height: 1px; background: #e6e8eb; }
|
||
Text { height: 16px; text: UiStrings.copyright; color: #61656c; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||
Text { height: 16px; text: UiStrings.license; color: #61656c; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||
}
|
||
|
||
HorizontalLayout {
|
||
vertical-stretch: 0;
|
||
alignment: end;
|
||
spacing: 8px;
|
||
CompactButton {
|
||
width: max(76px, check-button-metrics.preferred-width + 20px);
|
||
text: UiStrings.check-for-updates;
|
||
enabled: root.update-check-status != UpdateCheckStatus.checking;
|
||
accessible-role: button;
|
||
accessible-label: self.text;
|
||
accessible-action-default => { if self.enabled { root.check-for-updates(); } }
|
||
clicked => { root.check-for-updates(); }
|
||
check-button-metrics := Text { visible: false; text: UiStrings.check-for-updates; font-size: ImGuiFontStyle.body; }
|
||
}
|
||
CompactButton { width: 48px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update notification modal.
|
||
if root.update-open && root.update-available: Rectangle {
|
||
background: #14233b45;
|
||
z: 47;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
DialogSurface {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: 464px;
|
||
height: 400px;
|
||
border-radius: UiRadius.surface;
|
||
border-color: WorkbenchStyle.border;
|
||
drop-shadow-color: #07142b24;
|
||
drop-shadow-blur: 24px;
|
||
drop-shadow-offset-y: 8px;
|
||
|
||
VerticalLayout {
|
||
padding: 24px;
|
||
spacing: 16px;
|
||
VerticalLayout {
|
||
vertical-stretch: 0;
|
||
spacing: 6px;
|
||
Text {
|
||
height: 24px;
|
||
text: UiStrings.update-available;
|
||
color: WorkbenchStyle.text;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
vertical-alignment: center;
|
||
}
|
||
HorizontalLayout {
|
||
height: 20px;
|
||
spacing: 12px;
|
||
Text {
|
||
text: root.latest-version;
|
||
color: WorkbenchStyle.muted;
|
||
font-size: 13px;
|
||
vertical-alignment: center;
|
||
}
|
||
Rectangle { horizontal-stretch: 1; }
|
||
Rectangle {
|
||
width: update-website-label.preferred-width;
|
||
accessible-role: button;
|
||
accessible-label: update-website-label.text;
|
||
accessible-action-default => { root.open-download(); }
|
||
update-website-label := Text {
|
||
text: UiStrings.access-website + "https://crossdesk.cn";
|
||
color: website-touch.has-hover ? #254cc0 : WorkbenchStyle.accent;
|
||
font-size: 11px;
|
||
vertical-alignment: center;
|
||
}
|
||
website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
|
||
}
|
||
}
|
||
}
|
||
|
||
VerticalLayout {
|
||
spacing: 8px;
|
||
Text {
|
||
height: 18px;
|
||
text: UiStrings.release-notes;
|
||
color: WorkbenchStyle.text;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
vertical-alignment: center;
|
||
}
|
||
Rectangle {
|
||
background: WorkbenchStyle.field-surface;
|
||
border-width: 1px;
|
||
border-color: WorkbenchStyle.separator;
|
||
border-radius: UiRadius.control;
|
||
// Inset the viewport so text and the scrollbar stay clear
|
||
// of the rounded frame, including at either scroll limit.
|
||
update-notes-scroll := ScrollView {
|
||
x: 12px;
|
||
y: 12px;
|
||
width: parent.width - 24px;
|
||
height: parent.height - 24px;
|
||
viewport-width: self.width - 12px;
|
||
viewport-height: update-notes-content.preferred-height;
|
||
horizontal-scrollbar-policy: always-off;
|
||
vertical-scrollbar-policy: as-needed;
|
||
update-notes-content := VerticalLayout {
|
||
width: update-notes-scroll.viewport-width;
|
||
alignment: start;
|
||
spacing: 8px;
|
||
padding-bottom: 6px;
|
||
for block in root.release-note-blocks: Rectangle {
|
||
height: update-note.preferred-height + (block.section-gap ? 6px : 0px);
|
||
update-note := StyledText {
|
||
y: block.section-gap ? 6px : 0px;
|
||
// Measure wrapped paragraphs at the viewport
|
||
// width before stacking the following block.
|
||
width: update-notes-scroll.viewport-width;
|
||
height: self.preferred-height;
|
||
text: block.content;
|
||
default-color: WorkbenchStyle.text;
|
||
default-font-size: 13px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
VerticalLayout {
|
||
vertical-stretch: 0;
|
||
spacing: 16px;
|
||
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
|
||
HorizontalLayout {
|
||
alignment: end;
|
||
spacing: 8px;
|
||
WorkbenchButton {
|
||
width: 88px;
|
||
height: 34px;
|
||
text: UiStrings.later;
|
||
clicked => { root.update-open = false; }
|
||
}
|
||
WorkbenchButton {
|
||
width: 88px;
|
||
height: 34px;
|
||
primary: true;
|
||
text: UiStrings.download;
|
||
clicked => { root.open-download(); root.update-open = false; }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Linux Wayland sessions show this advisory once per application launch.
|
||
if root.wayland-notice-open: Rectangle {
|
||
background: #14233b66;
|
||
z: 50;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
DialogSurface {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: 512px;
|
||
height: wayland-notice-content.preferred-height;
|
||
border-radius: UiRadius.surface;
|
||
border-color: #dce3ee;
|
||
drop-shadow-color: #07142b30;
|
||
drop-shadow-blur: 28px;
|
||
drop-shadow-offset-y: 10px;
|
||
|
||
wayland-notice-content := VerticalLayout {
|
||
padding: 24px; spacing: 18px;
|
||
VerticalLayout {
|
||
spacing: 7px;
|
||
Text {
|
||
text: UiStrings.wayland-notice-title;
|
||
color: #1c2b43;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
wrap: word-wrap;
|
||
}
|
||
Text {
|
||
text: UiStrings.wayland-notice-subtitle;
|
||
color: #68758a;
|
||
font-size: ImGuiFontStyle.base;
|
||
wrap: word-wrap;
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
background: #fff8eb;
|
||
border-color: #f0dfba;
|
||
border-width: 1px;
|
||
border-radius: UiRadius.surface;
|
||
VerticalLayout {
|
||
padding: 14px; spacing: 8px;
|
||
Text {
|
||
text: UiStrings.wayland-permission-title;
|
||
color: #805719;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
wrap: word-wrap;
|
||
}
|
||
Text {
|
||
text: UiStrings.wayland-notice-message;
|
||
color: #6c5a3d;
|
||
font-size: ImGuiFontStyle.base;
|
||
wrap: word-wrap;
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
background: #f3f7fd;
|
||
border-color: #dfe8f5;
|
||
border-width: 1px;
|
||
border-radius: UiRadius.surface;
|
||
VerticalLayout {
|
||
padding: 14px; spacing: 8px;
|
||
Text {
|
||
text: UiStrings.wayland-recommendation-title;
|
||
color: #244c87;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
wrap: word-wrap;
|
||
}
|
||
Text {
|
||
text: UiStrings.wayland-recommendation-message;
|
||
color: #59677c;
|
||
font-size: ImGuiFontStyle.base;
|
||
wrap: word-wrap;
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle { height: 1px; background: #e9edf3; }
|
||
HorizontalLayout {
|
||
height: 34px; alignment: end;
|
||
PrimaryButton {
|
||
width: 104px; height: 34px;
|
||
border-radius: UiRadius.control;
|
||
text-size: ImGuiFontStyle.base;
|
||
text: UiStrings.wayland-notice-acknowledge;
|
||
accessible-role: button;
|
||
accessible-label: UiStrings.wayland-notice-acknowledge;
|
||
accessible-action-default => { root.wayland-notice-open = false; }
|
||
clicked => { root.wayland-notice-open = false; }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Portable Windows builds prompt for the optional lock-screen service.
|
||
if root.portable-service-dialog-open: Rectangle {
|
||
background: #00000052;
|
||
z: 50;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
DialogSurface {
|
||
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 480px; height: 300px;
|
||
VerticalLayout {
|
||
padding: 24px; spacing: 14px;
|
||
DialogPromptText {
|
||
prompt: UiStrings.service-setup-title;
|
||
text-color: #242832;
|
||
text-size: ImGuiFontStyle.prominent;
|
||
text-weight: 600;
|
||
}
|
||
Rectangle {
|
||
background: #f7f8fa; border-color: #d9dde4; border-width: 1px; border-radius: UiRadius.surface;
|
||
Text { x: 14px; y: 22px; width: parent.width - 28px; height: parent.height - 24px; text: UiStrings.service-setup-message; color: #3d434d; wrap: word-wrap; vertical-alignment: center; }
|
||
}
|
||
if root.portable-service-status != "": Text {
|
||
text: root.portable-service-status;
|
||
color: root.portable-service-succeeded ? #25874b : root.portable-service-installing ? #68707d : #b23b3b;
|
||
horizontal-alignment: center;
|
||
wrap: word-wrap;
|
||
}
|
||
if !root.portable-service-succeeded: HorizontalLayout {
|
||
height: 26px; alignment: center; spacing: 8px;
|
||
Text { text: UiStrings.do-not-remind; vertical-alignment: center; color: #4a505a; }
|
||
ToggleSwitch { checked <=> root.portable-service-do-not-remind; enabled: !root.portable-service-installing; }
|
||
}
|
||
HorizontalLayout {
|
||
height: 34px; alignment: center; spacing: 8px;
|
||
if root.portable-service-succeeded: PrimaryButton { text: UiStrings.ok; clicked => { root.acknowledge-portable-service(); } }
|
||
if !root.portable-service-succeeded: PrimaryButton {
|
||
text: UiStrings.install-service;
|
||
enabled: !root.portable-service-installing;
|
||
clicked => { root.install-portable-service(); }
|
||
}
|
||
if !root.portable-service-succeeded: SecondaryButton {
|
||
text: UiStrings.cancel;
|
||
enabled: !root.portable-service-installing;
|
||
clicked => { root.cancel-portable-service(); }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if root.portable-service-suppressed-notice-open: Rectangle {
|
||
background: #00000052;
|
||
z: 51;
|
||
TouchArea { clicked => { focus-sink.focus(); } }
|
||
DialogSurface {
|
||
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 440px; height: 190px;
|
||
VerticalLayout {
|
||
padding: 24px; spacing: 16px;
|
||
DialogPromptText {
|
||
prompt: UiStrings.notification;
|
||
text-color: #242832;
|
||
text-size: ImGuiFontStyle.prominent;
|
||
text-weight: 600;
|
||
}
|
||
DialogPromptText {
|
||
prompt: UiStrings.service-suppressed-message;
|
||
text-color: #3d434d;
|
||
}
|
||
PrimaryButton { text: UiStrings.ok; clicked => { root.acknowledge-portable-service-suppressed(); } }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if root.gallery-mode && root.gallery-network-open: GalleryNetworkDialog {
|
||
x: 0px; y: root.titlebar-height;
|
||
width: root.width; height: root.height - self.y;
|
||
z: 100;
|
||
devices: root.gallery-network-devices;
|
||
close => { root.gallery-network-open = false; focus-sink.focus(); }
|
||
}
|
||
|
||
// Ignore clicks against moving cards and duplicate fullscreen requests.
|
||
if root.gallery-transitioning: TouchArea {
|
||
x: 0px; y: root.titlebar-height;
|
||
width: root.width; height: root.height - self.y;
|
||
z: 250;
|
||
}
|
||
|
||
if root.custom-titlebar: Rectangle {
|
||
x: 0px;
|
||
y: 0px;
|
||
width: root.width;
|
||
height: root.height;
|
||
background: transparent;
|
||
border-width: 1px;
|
||
border-color: root.wayland-titlebar ? #aaa8a6 : #8b8b8b;
|
||
border-radius:
|
||
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
|
||
z: 200;
|
||
}
|
||
}
|
||
}
|