mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-20 19:14:39 +08:00
[feat] add HarmonyOS desktop and mobile clients,issue #117
This commit is contained in:
@@ -182,6 +182,17 @@ export component CompactComboBox inherits Rectangle {
|
||||
property <float> popup-progress: 0.0;
|
||||
callback selected(int);
|
||||
|
||||
// Session state can disable a setting while its popup is already open.
|
||||
// Remove the stale options and stop a pending reveal on every backend.
|
||||
changed enabled => {
|
||||
if !root.enabled {
|
||||
popup-open-timer.running = false;
|
||||
root.popup-animation-enabled = false;
|
||||
root.popup-progress = 0.0;
|
||||
combo-popup.close();
|
||||
}
|
||||
}
|
||||
|
||||
animate popup-progress {
|
||||
duration: 160ms;
|
||||
easing: ease-out;
|
||||
@@ -278,12 +289,15 @@ export component CompactComboBox inherits Rectangle {
|
||||
background: option-touch.has-hover ? #dbeafb : index == root.current-index ? #eef5fd : white;
|
||||
Text { x: 5px; width: parent.width - 10px; text: choice; color: #202124; font-size: root.subtle ? 12px : ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
option-touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => {
|
||||
popup-open-timer.running = false;
|
||||
root.popup-animation-enabled = false;
|
||||
root.popup-progress = 0.0;
|
||||
root.current-index = index;
|
||||
root.selected(index);
|
||||
if root.enabled && index >= 0 && index < root.model.length {
|
||||
root.current-index = index;
|
||||
root.selected(index);
|
||||
}
|
||||
combo-popup.close();
|
||||
}
|
||||
}
|
||||
@@ -439,6 +453,8 @@ export component SecondaryButton inherits Rectangle {
|
||||
export component ToggleSwitch inherits Rectangle {
|
||||
in-out property <bool> checked;
|
||||
in property <bool> enabled: true;
|
||||
in property <bool> toggle-on-click: true;
|
||||
callback clicked();
|
||||
callback toggled(bool);
|
||||
|
||||
width: 36px;
|
||||
@@ -459,8 +475,11 @@ export component ToggleSwitch inherits Rectangle {
|
||||
TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => {
|
||||
root.checked = !root.checked;
|
||||
root.toggled(root.checked);
|
||||
if root.toggle-on-click {
|
||||
root.checked = !root.checked;
|
||||
root.toggled(root.checked);
|
||||
}
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Single AOT entry point. Generating one header for the entire UI avoids
|
||||
// duplicate imported-component definitions when C++ uses all three windows.
|
||||
export { AnnouncementItem, MessageStrings } from "message_panel.slint";
|
||||
export { MainWindow, RecentConnection, UpdateCheckStatus, UiStrings } from "main_window.slint";
|
||||
export { MainWindow, RecentConnection, UpdateCheckStatus, AutostartStatus, UiStrings } from "main_window.slint";
|
||||
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
|
||||
export { ServerWindow, ServerStatusBar, ServerStrings, ControllerEntry } from "server_window.slint";
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export struct ReleaseNoteBlock {
|
||||
}
|
||||
|
||||
export enum UpdateCheckStatus { idle, checking, up-to-date, available, failed }
|
||||
export enum AutostartStatus { checking, enabled, disabled, unsupported, failed }
|
||||
enum CopyFeedback { idle, success, failed }
|
||||
|
||||
export global UiStrings {
|
||||
@@ -401,6 +402,16 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
in-out property <bool> self-hosted-enabled: false;
|
||||
in-out property <bool> autostart-enabled: false;
|
||||
in property <bool> autostart-system-managed: false;
|
||||
in property <AutostartStatus> autostart-status: AutostartStatus.checking;
|
||||
out property <string> autostart-status-text:
|
||||
root.autostart-status == AutostartStatus.checking ? GalleryStrings.tr("正在读取…", "Checking…", "Проверка…") :
|
||||
root.autostart-status == AutostartStatus.enabled ? GalleryStrings.tr("已开启", "Enabled", "Включён") :
|
||||
root.autostart-status == AutostartStatus.disabled ? GalleryStrings.tr("未开启", "Disabled", "Выключен") :
|
||||
root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.tr("设备不支持", "Unsupported", "Не поддерживается") :
|
||||
GalleryStrings.tr("读取失败", "Read failed", "Ошибка чтения");
|
||||
in-out property <bool> autostart-guide-open: false;
|
||||
changed autostart-guide-open => { if root.autostart-guide-open { focus-sink.focus(); } }
|
||||
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.
|
||||
@@ -427,6 +438,7 @@ export component MainWindow inherits Window {
|
||||
callback recent-delete(string);
|
||||
callback save-settings() -> bool;
|
||||
callback cancel-settings;
|
||||
callback refresh-autostart-status;
|
||||
callback save-self-hosted-settings() -> bool;
|
||||
callback cancel-self-hosted-settings;
|
||||
callback browse-save-path;
|
||||
@@ -494,7 +506,10 @@ export component MainWindow inherits Window {
|
||||
root.gallery-frame-rate-draft = root.gallery-frame-rate-index;
|
||||
root.gallery-bandwidth-draft = root.gallery-bandwidth-index;
|
||||
root.gallery-action("settings-open", "");
|
||||
if root.autostart-system-managed { root.refresh-autostart-status(); }
|
||||
focus-sink.focus();
|
||||
} else {
|
||||
root.autostart-guide-open = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,6 +736,10 @@ export component MainWindow inherits Window {
|
||||
focus-sink := FocusScope {
|
||||
x: 0px; y: 0px; width: 0px; height: 0px;
|
||||
key-pressed(event) => {
|
||||
if root.autostart-guide-open {
|
||||
if event.text == Key.Return || event.text == Key.Escape { root.autostart-guide-open = false; }
|
||||
return accept;
|
||||
}
|
||||
if root.wayland-notice-open {
|
||||
if event.text == Key.Return || event.text == Key.Escape {
|
||||
root.wayland-notice-open = false;
|
||||
@@ -939,7 +958,7 @@ export component MainWindow inherits Window {
|
||||
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(); }
|
||||
edited => { root.connection-input-error = ""; remote-id-format-timer.running = true; }
|
||||
accepted => { if !root.connection-pending { root.connect-requested(root.remote-id-input); } }
|
||||
}
|
||||
}
|
||||
@@ -1550,7 +1569,12 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
remote-id-format-timer := Timer {
|
||||
interval: 1ms;
|
||||
running: false;
|
||||
triggered => {
|
||||
// Use the running property for both transitions. restart() resumes
|
||||
// the native repeating timer without restoring this property, so
|
||||
// a later stop() cannot change it and the selection keeps resetting.
|
||||
self.stop();
|
||||
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
|
||||
@@ -1963,7 +1987,13 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
SettingsGroup {
|
||||
title: GalleryStrings.tr("启动与后台", "Startup & background", "Запуск и фоновая работа"); rows: 2;
|
||||
SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
|
||||
if !root.autostart-system-managed: SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
|
||||
if root.autostart-system-managed: SettingsToggleRow {
|
||||
label: UiStrings.autostart;
|
||||
checked: root.autostart-status == AutostartStatus.enabled;
|
||||
toggle-on-click: false;
|
||||
clicked => { root.autostart-guide-open = true; }
|
||||
}
|
||||
SettingsToggleRow { label: UiStrings.daemon; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; divider: false; }
|
||||
}
|
||||
}
|
||||
@@ -1998,6 +2028,59 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
if root.autostart-guide-open && root.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: min(460px, parent.width - 32px);
|
||||
height: min(380px, parent.height - 24px);
|
||||
background: #f8fafc; border-color: #e1e7ef;
|
||||
drop-shadow-color: #14294424; drop-shadow-blur: 18px; drop-shadow-offset-y: 6px;
|
||||
TouchArea { clicked => { focus-sink.focus(); } }
|
||||
Text {
|
||||
x: 24px; y: 18px; width: parent.width - 48px; height: 26px;
|
||||
text: GalleryStrings.tr("设置开机自启动", "Set up auto start", "Настройка автозапуска");
|
||||
font-size: 18px; font-weight: 600; color: WorkbenchStyle.text;
|
||||
}
|
||||
ScrollView {
|
||||
x: 24px; y: 58px; width: parent.width - 48px; height: parent.height - 120px;
|
||||
viewport-width: self.visible-width;
|
||||
horizontal-scrollbar-policy: always-off;
|
||||
viewport-height: guide-content.preferred-height;
|
||||
guide-content := VerticalLayout {
|
||||
width: parent.width; padding: 0px; spacing: 12px;
|
||||
Text { text: root.autostart-status-text; color: WorkbenchStyle.accent; font-size: 13px; wrap: word-wrap; }
|
||||
Text {
|
||||
text: root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.tr(
|
||||
"当前设备不支持开机自启动。", "This device does not support auto start.", "Это устройство не поддерживает автозапуск.") :
|
||||
root.autostart-status == AutostartStatus.failed ? GalleryStrings.tr(
|
||||
"无法读取系统状态。请刷新重试,或在系统设置中核对。", "Could not read the system status. Retry or check it in Settings.", "Не удалось прочитать состояние. Повторите попытку или проверьте настройки системы.") :
|
||||
GalleryStrings.tr("请在系统设置中添加或移除本应用,返回后会自动刷新状态。", "Add or remove this app in system Settings. The status refreshes when you return.", "Добавьте или удалите приложение в настройках системы. После возвращения статус обновится.");
|
||||
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
|
||||
}
|
||||
if root.autostart-status != AutostartStatus.unsupported: Text {
|
||||
text: GalleryStrings.tr(
|
||||
"设置 → 应用和元服务 → 应用启动管理\n\n开启:点击“添加应用”,选择 CrossDesk,然后点击“完成”。\n\n关闭:在自启动列表中找到本应用,点击“移除”。",
|
||||
"Settings → Apps & services → App startup management\n\nEnable: choose Add app, select CrossDesk, then choose Done.\n\nDisable: find this app in the startup list and choose Remove.",
|
||||
"Настройки → Приложения и сервисы → Управление запуском\n\nВключить: добавьте CrossDesk в список и подтвердите выбор.\n\nВыключить: удалите приложение из списка автозапуска.");
|
||||
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsButton {
|
||||
x: 24px; y: parent.height - 48px; width: 112px;
|
||||
text: GalleryStrings.tr("刷新状态", "Refresh status", "Обновить");
|
||||
enabled: root.autostart-status != AutostartStatus.checking;
|
||||
clicked => { root.refresh-autostart-status(); }
|
||||
}
|
||||
SettingsButton {
|
||||
x: parent.width - 116px; y: parent.height - 48px; width: 92px; primary: true;
|
||||
text: UiStrings.ok; clicked => { root.autostart-guide-open = false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if root.self-host-settings-open: Rectangle {
|
||||
background: #15223835;
|
||||
z: 49;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-15
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _MODEL_UPDATE_H_
|
||||
#define _MODEL_UPDATE_H_
|
||||
|
||||
#include <slint.h>
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk::ui {
|
||||
|
||||
// A reset invalidates row-count bindings even if the count is unchanged.
|
||||
// Repeating it from a timer can continually restart dependent size animations.
|
||||
template <typename Row>
|
||||
void UpdateModelRows(slint::VectorModel<Row>& model, std::vector<Row> rows) {
|
||||
if (model.row_count() != rows.size()) {
|
||||
model.set_vector(std::move(rows));
|
||||
return;
|
||||
}
|
||||
for (std::size_t i = 0; i < rows.size(); ++i) {
|
||||
if (model.row_data(i) != rows[i]) {
|
||||
model.set_row_data(i, rows[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk::ui
|
||||
|
||||
#endif
|
||||
@@ -130,9 +130,13 @@ export component SettingsRow inherits Rectangle {
|
||||
|
||||
export component SettingsToggleRow inherits SettingsRow {
|
||||
in-out property <bool> checked;
|
||||
in property <bool> toggle-on-click: true;
|
||||
callback clicked();
|
||||
label-reserve: 64px;
|
||||
ToggleSwitch {
|
||||
x: parent.width - self.width - 12px; y: (parent.height - self.height) / 2;
|
||||
checked <=> root.checked; enabled: root.enabled;
|
||||
toggle-on-click: root.toggle-on-click;
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +238,11 @@ export component StreamWindow inherits Window {
|
||||
callback reorder-tab(int, length, length);
|
||||
callback close-tab(string);
|
||||
callback switch-display(int);
|
||||
in property <bool> secure-attention-supported: true;
|
||||
private property <[string]> secure-shortcut-items: ["Ctrl+Alt+Del", "Win+L"];
|
||||
private property <[string]> ordinary-shortcut-items: ["Win+L"];
|
||||
private property <[string]> shortcut-items: root.secure-attention-supported ? root.secure-shortcut-items : root.ordinary-shortcut-items;
|
||||
private property <length> shortcut-menu-height: root.shortcut-items.length * 30px + 8px;
|
||||
callback send-shortcut(string);
|
||||
callback toggle-mouse-control;
|
||||
callback toggle-privacy;
|
||||
@@ -265,6 +270,10 @@ export component StreamWindow inherits Window {
|
||||
private property <length> stats-header-height: 18px;
|
||||
private property <length> stats-content-height:
|
||||
root.stats-header-height + root.stats-rows.length * root.stats-row-height + 65px;
|
||||
private property <length> file-panel-x: 18px;
|
||||
private property <length> file-panel-y: video.height - min(210px, video.height * 0.35);
|
||||
private property <length> file-panel-width: min(430px, video.width * 0.6);
|
||||
private property <length> file-panel-height: min(190px, video.height * 0.32);
|
||||
private property <bool> display-menu-open: false;
|
||||
private property <bool> display-menu-animation-enabled: false;
|
||||
private property <float> display-menu-progress: 0.0;
|
||||
@@ -392,11 +401,14 @@ export component StreamWindow inherits Window {
|
||||
&& pointer-x >= shortcut-menu-x
|
||||
&& pointer-x <= shortcut-menu-x + 180px
|
||||
&& pointer-y >= shortcut-menu-y
|
||||
&& pointer-y <= shortcut-menu-y + 68px;
|
||||
&& pointer-y <= shortcut-menu-y + root.shortcut-menu-height;
|
||||
let over-video-menu = root.video-menu-open
|
||||
&& pointer-x >= root.video-menu-x && pointer-x <= root.video-menu-x + root.video-menu-width
|
||||
&& pointer-y >= root.video-menu-y && pointer-y <= root.video-menu-y + root.video-menu-height;
|
||||
return over-control || over-display-menu || over-shortcut-menu || over-video-menu;
|
||||
let over-file-panel = root.file-transfer-visible
|
||||
&& pointer-x >= root.file-panel-x && pointer-x <= root.file-panel-x + root.file-panel-width
|
||||
&& pointer-y >= root.file-panel-y && pointer-y <= root.file-panel-y + root.file-panel-height;
|
||||
return over-control || over-display-menu || over-shortcut-menu || over-video-menu || over-file-panel;
|
||||
}
|
||||
|
||||
begin-control-drag(press-x, press-y) => {
|
||||
@@ -1246,11 +1258,11 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
|
||||
if root.shortcut-menu-open: Rectangle {
|
||||
x: control.x + shortcut-button.x; y: control.y + 40px; width: 180px; height: 68px;
|
||||
x: control.x + shortcut-button.x; y: control.y + 40px; width: 180px; height: root.shortcut-menu-height;
|
||||
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: UiRadius.menu; z: 11;
|
||||
VerticalLayout {
|
||||
padding: 4px; spacing: 1px;
|
||||
for shortcut in ["Ctrl+Alt+Del", "Win+L"]: Rectangle {
|
||||
for shortcut in root.shortcut-items: Rectangle {
|
||||
height: 29px; background: shortcut-touch.has-hover ? #e8eef9 : transparent;
|
||||
Text { x: 8px; text: shortcut; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
shortcut-touch := TouchArea {
|
||||
@@ -1365,10 +1377,21 @@ export component StreamWindow inherits Window {
|
||||
|
||||
// Transfer progress overlay at bottom-left.
|
||||
if root.file-transfer-visible: Rectangle {
|
||||
x: 18px; y: parent.height - min(210px, parent.height * 0.35); width: min(430px, parent.width * 0.6); height: min(190px, parent.height * 0.32);
|
||||
x: root.file-panel-x; y: root.file-panel-y;
|
||||
width: root.file-panel-width; height: root.file-panel-height;
|
||||
background: #fffffff0; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: UiRadius.surface; z: 7;
|
||||
// Header/padding and drags leaving the panel belong to local UI.
|
||||
TouchArea {
|
||||
pointer-event(event) => {
|
||||
if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; }
|
||||
}
|
||||
scroll-event(event) => { accept }
|
||||
}
|
||||
Text { x: 12px; y: 8px; text: StreamStrings.file-transfer; color: #2d3138; font-weight: 600; }
|
||||
IconButton { x: parent.width - 32px; y: 1px; icon: FontAwesomeIcons.xmark; icon-size: 13px; clicked => { root.close-file-transfer(); } }
|
||||
IconButton {
|
||||
x: parent.width - 32px; y: 1px; icon: FontAwesomeIcons.xmark; icon-size: 13px;
|
||||
clicked => { root.suppress-next-remote-pointer-release = true; root.close-file-transfer(); }
|
||||
}
|
||||
ScrollView {
|
||||
x: 10px; y: 32px; width: parent.width - 20px; height: parent.height - 42px;
|
||||
VerticalLayout {
|
||||
|
||||
Reference in New Issue
Block a user