mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-18 09:53:05 +08:00
[feat] add remote host version checks and update prompts
This commit is contained in:
@@ -1481,6 +1481,12 @@ void GuiApplication::BindStreamCallbacks() {
|
||||
return;
|
||||
}
|
||||
auto& stream = *ui_->stream;
|
||||
stream->on_dismiss_remote_update([this] {
|
||||
if (auto props = SelectedSession()) {
|
||||
std::lock_guard lock(props->remote_version_mutex_);
|
||||
props->remote_update_dismissed_ = true;
|
||||
}
|
||||
});
|
||||
auto close_stream_sessions = [this] {
|
||||
std::vector<std::string> ids;
|
||||
{
|
||||
@@ -2525,6 +2531,11 @@ void GuiApplication::SyncXWaylandWindowActivation() {
|
||||
#endif
|
||||
|
||||
void GuiApplication::SetStreamKeyboardFocus(bool focused) {
|
||||
// The local upgrade dialog owns input even while the native window is active.
|
||||
if (focused && ui_ && ui_->stream &&
|
||||
(*ui_->stream)->get_remote_update_visible()) {
|
||||
focused = false;
|
||||
}
|
||||
if (focus_on_stream_window_ == focused) {
|
||||
return;
|
||||
}
|
||||
@@ -2702,6 +2713,21 @@ void GuiApplication::SyncStreamWindow() {
|
||||
return;
|
||||
}
|
||||
const ConnectionStatus status = props->connection_status_.load();
|
||||
bool remote_update_available = false;
|
||||
if (status == ConnectionStatus::Connected) {
|
||||
std::lock_guard lock(props->remote_version_mutex_);
|
||||
// Wait for host_info: a version still in transit is not a legacy peer.
|
||||
if (props->remote_host_info_received_ && !props->remote_update_dismissed_) {
|
||||
remote_update_available = props->remote_app_version_.empty() ||
|
||||
(update_checker_->latest() &&
|
||||
AvailableAppUpdate(props->remote_app_version_,
|
||||
*update_checker_->latest()).has_value());
|
||||
}
|
||||
}
|
||||
(*ui_->stream)->set_remote_update_visible(remote_update_available);
|
||||
if (remote_update_available) {
|
||||
SetStreamKeyboardFocus(false);
|
||||
}
|
||||
const bool waiting_for_frame =
|
||||
status == ConnectionStatus::Connected &&
|
||||
!(*ui_->stream)->get_has_frame();
|
||||
|
||||
@@ -191,6 +191,9 @@ struct TranslationRow {
|
||||
u8"Способ захвата:") \
|
||||
X(screen_capture_method_auto, u8"自动", "Auto", u8"Авто") \
|
||||
X(video_settings, u8"画面设置", "Video settings", u8"Настройки видео") \
|
||||
X(remote_update_title, u8"被控端版本过低", "Remote device needs an update", u8"Версия на удалённом устройстве устарела") \
|
||||
X(remote_update, u8"请升级到最新版本。", "Please update to the latest version.", u8"Обновите её до последней версии.") \
|
||||
X(remote_update_dismiss, u8"知道了", "Got it", u8"Понятно") \
|
||||
X(video_settings_pending, u8"正在应用…", "Applying…", u8"Применение…") \
|
||||
X(video_settings_failed, u8"调整失败,请重试。", "Could not apply settings. Try again.", u8"Не удалось применить настройки. Повторите попытку.") \
|
||||
X(video_settings_unavailable, u8"连接建立后可调整,需被控端支持。", "Available when connected to a supported remote device.", u8"Доступно при подключении к поддерживаемому устройству.") \
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
#include <app_version.h>
|
||||
#include <remote_action.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
@@ -318,6 +319,12 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
}
|
||||
|
||||
if (is_client_mode) {
|
||||
if (props && source_id == props->control_data_label_) {
|
||||
std::lock_guard lock(props->remote_version_mutex_);
|
||||
props->remote_host_info_received_ = true;
|
||||
props->remote_app_version_ = IsValidAppVersion(remote_action.i.app_version)
|
||||
? remote_action.i.app_version : "";
|
||||
}
|
||||
// Negotiate live settings only on the reliable control stream. Old
|
||||
// hosts omit this capability and retain their existing video behavior.
|
||||
if (props && source_id == props->control_data_label_) {
|
||||
|
||||
@@ -270,7 +270,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
{
|
||||
RemoteAction remote_action = MakeHostInformation(
|
||||
GetHostName(), runtime->devices_.host_display_list(),
|
||||
/*supports_privacy_screen=*/true);
|
||||
/*supports_privacy_screen=*/true, CROSSDESK_VERSION);
|
||||
remote_action.i.supports_video_settings = true;
|
||||
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
@@ -300,6 +300,12 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
|
||||
"connection_closed");
|
||||
props->connection_established_ = false;
|
||||
{
|
||||
std::lock_guard lock(props->remote_version_mutex_);
|
||||
props->remote_host_info_received_ = false;
|
||||
props->remote_app_version_.clear();
|
||||
props->remote_update_dismissed_ = false;
|
||||
}
|
||||
props->net_traffic_stats_.Reset();
|
||||
props->enable_mouse_control_ = false;
|
||||
runtime->ResetRemoteServiceStatus(*props);
|
||||
@@ -395,7 +401,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
{
|
||||
RemoteAction remote_action = MakeHostInformation(
|
||||
GetHostName(), runtime->devices_.host_display_list(),
|
||||
/*supports_privacy_screen=*/true);
|
||||
/*supports_privacy_screen=*/true, CROSSDESK_VERSION);
|
||||
remote_action.i.supports_video_settings = true;
|
||||
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
|
||||
@@ -124,6 +124,11 @@ struct RemoteSession {
|
||||
bool tab_selected_ = false;
|
||||
bool tab_opened_ = true;
|
||||
std::string remote_host_name_;
|
||||
// Received version and dismissal state belong to this connection.
|
||||
std::mutex remote_version_mutex_;
|
||||
bool remote_host_info_received_ = false;
|
||||
std::string remote_app_version_;
|
||||
bool remote_update_dismissed_ = false;
|
||||
bool remote_service_status_received_ = false;
|
||||
std::mutex video_settings_mutex_;
|
||||
// Requested and confirmed settings for this remote session only.
|
||||
|
||||
@@ -365,6 +365,7 @@ export component CompactButton inherits Rectangle {
|
||||
|
||||
export component PrimaryButton inherits Rectangle {
|
||||
in property <string> text;
|
||||
in property <length> text-size: ImGuiFontStyle.body;
|
||||
in property <bool> enabled: true;
|
||||
callback clicked;
|
||||
|
||||
@@ -379,7 +380,7 @@ export component PrimaryButton inherits Rectangle {
|
||||
Text {
|
||||
text: root.text;
|
||||
color: white;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
font-size: root.text-size;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ComboBox, ScrollView } from "std-widgets.slint";
|
||||
import { FontAwesomeIcons, IconButton, PrimaryButton, ImGuiLineStyle, ImGuiFontStyle } from "common.slint";
|
||||
import { FontAwesomeIcons, IconButton, PrimaryButton, DialogSurface, ImGuiLineStyle, ImGuiFontStyle } from "common.slint";
|
||||
|
||||
export struct StreamTab {
|
||||
remote-id: string,
|
||||
@@ -23,6 +23,9 @@ export struct NetworkStatsRow {
|
||||
}
|
||||
|
||||
export global StreamStrings {
|
||||
in-out property <string> remote-update-title: "Remote device needs an update";
|
||||
in-out property <string> remote-update: "Please update to the latest version.";
|
||||
in-out property <string> remote-update-dismiss: "Got it";
|
||||
in-out property <string> video-settings: "Video settings";
|
||||
in-out property <string> video-settings-unavailable: "The remote device does not support live video settings";
|
||||
in-out property <string> video-quality: "Video quality";
|
||||
@@ -196,6 +199,8 @@ export component StreamWindow inherits Window {
|
||||
in property <bool> has-frame: false;
|
||||
in property <string> status-text: "";
|
||||
in property <string> receiving-text: "";
|
||||
in property <bool> remote-update-visible: false;
|
||||
callback dismiss-remote-update;
|
||||
in property <[string]> displays;
|
||||
in-out property <int> selected-display: 0;
|
||||
in property <bool> mouse-control-enabled: true;
|
||||
@@ -271,6 +276,15 @@ export component StreamWindow inherits Window {
|
||||
changed selected-tab => { root.video-menu-open = false; }
|
||||
changed control-expanded => { if !root.control-expanded { root.video-menu-open = false; } }
|
||||
changed window-active => { if !root.window-active { root.video-menu-open = false; } }
|
||||
changed remote-update-visible => {
|
||||
if root.remote-update-visible {
|
||||
root.video-menu-open = false;
|
||||
root.display-menu-open = false;
|
||||
root.shortcut-menu-open = false;
|
||||
}
|
||||
input-focus.focus();
|
||||
root.keyboard-focus-changed(!root.remote-update-visible);
|
||||
}
|
||||
private property <bool> control-tooltip-visible: false;
|
||||
private property <ControlBarTooltip> hovered-control: {
|
||||
if root.window-active && !root.control-dragging
|
||||
@@ -345,6 +359,7 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
|
||||
is-local-control-area(pointer-x, pointer-y) => {
|
||||
if root.remote-update-visible { return true; }
|
||||
let over-control = pointer-x >= control.x
|
||||
&& pointer-x <= control.x + control.width
|
||||
&& pointer-y >= control.y
|
||||
@@ -655,9 +670,11 @@ export component StreamWindow inherits Window {
|
||||
|
||||
drop-zone := DropArea {
|
||||
can-drop(event) => {
|
||||
return root.can-drop-file(event.data) ? DragAction.copy : DragAction.none;
|
||||
return !root.remote-update-visible && root.can-drop-file(event.data)
|
||||
? DragAction.copy : DragAction.none;
|
||||
}
|
||||
dropped(event) => {
|
||||
if root.remote-update-visible { return DragAction.none; }
|
||||
root.file-dropped(event.data);
|
||||
return DragAction.copy;
|
||||
}
|
||||
@@ -668,11 +685,20 @@ export component StreamWindow inherits Window {
|
||||
focus-gained => { root.keyboard-focus-changed(true); }
|
||||
focus-lost => { root.keyboard-focus-changed(false); }
|
||||
key-pressed(event) => {
|
||||
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
|
||||
if !root.remote-update-visible {
|
||||
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
|
||||
}
|
||||
accept
|
||||
}
|
||||
key-released(event) => {
|
||||
root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
|
||||
if root.remote-update-visible {
|
||||
// Dismiss on release so the same key cannot reach the peer.
|
||||
if event.text == Key.Escape || event.text == Key.Return {
|
||||
root.dismiss-remote-update();
|
||||
}
|
||||
} else {
|
||||
root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
|
||||
}
|
||||
accept
|
||||
}
|
||||
pointer := TouchArea {
|
||||
@@ -723,7 +749,12 @@ export component StreamWindow inherits Window {
|
||||
root.suppress-next-remote-pointer-release = false;
|
||||
input-focus.focus();
|
||||
}
|
||||
if event.kind == PointerEventKind.down
|
||||
// A gesture started before the modal appeared keeps
|
||||
// pointer capture. Let its release reach the peer.
|
||||
if root.remote-update-visible && !root.control-dragging
|
||||
&& event.kind == PointerEventKind.up {
|
||||
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
|
||||
} else if event.kind == PointerEventKind.down
|
||||
&& event.button == PointerEventButton.left
|
||||
&& over-control {
|
||||
root.begin-control-drag(self.mouse-x, self.mouse-y);
|
||||
@@ -1272,6 +1303,80 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
if root.remote-update-visible: Rectangle {
|
||||
width: parent.width; height: parent.height;
|
||||
background: #0c162c52;
|
||||
z: 13;
|
||||
TouchArea {
|
||||
mouse-cursor: default;
|
||||
scroll-event(event) => { accept }
|
||||
}
|
||||
}
|
||||
remote-update-banner := DialogSurface {
|
||||
visible: root.remote-update-visible;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: min(max(64px, parent.height * 0.18), max(16px, parent.height - self.height - 16px));
|
||||
width: min(520px, parent.width - 48px);
|
||||
height: max(208px, remote-update-message.y + remote-update-message.height + 92px);
|
||||
border-color: #e5e9f0;
|
||||
border-radius: 14px;
|
||||
drop-shadow-color: #07142b30;
|
||||
drop-shadow-blur: 32px;
|
||||
drop-shadow-offset-y: 12px;
|
||||
z: 14;
|
||||
TouchArea {
|
||||
mouse-cursor: default;
|
||||
pointer-event(event) => {
|
||||
if event.kind == PointerEventKind.down {
|
||||
root.suppress-next-remote-pointer-release = true;
|
||||
}
|
||||
}
|
||||
scroll-event(event) => { accept }
|
||||
}
|
||||
Rectangle {
|
||||
x: 32px; y: 32px; width: 48px; height: 48px;
|
||||
background: #edf4ff; border-radius: 12px;
|
||||
Path {
|
||||
x: 12px; y: 12px; width: 24px; height: 24px;
|
||||
viewbox-width: 24; viewbox-height: 24;
|
||||
commands: "M 12 16 L 12 3 M 6.5 8.5 L 12 3 L 17.5 8.5 M 4 15 L 4 20 Q 4 21 5 21 L 19 21 Q 20 21 20 20 L 20 15";
|
||||
fill: transparent;
|
||||
stroke: #2463c7; stroke-width: 1.8px;
|
||||
}
|
||||
}
|
||||
remote-update-title := Text {
|
||||
x: 100px; y: 34px; width: parent.width - 144px;
|
||||
height: max(28px, self.preferred-height);
|
||||
text: StreamStrings.remote-update-title;
|
||||
color: #1b2433; font-size: 20px; font-weight: 600;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
remote-update-message := Text {
|
||||
x: remote-update-title.x;
|
||||
y: remote-update-title.y + remote-update-title.height + 8px;
|
||||
width: remote-update-title.width;
|
||||
height: max(22px, self.preferred-height);
|
||||
text: StreamStrings.remote-update;
|
||||
color: #667085; font-size: 15px;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
IconButton {
|
||||
x: parent.width - 44px; y: 12px; width: 32px; height: 32px;
|
||||
icon: FontAwesomeIcons.xmark; icon-size: 14px;
|
||||
icon-color: #8a93a3;
|
||||
hover-background: #f2f4f7;
|
||||
clicked => { root.dismiss-remote-update(); }
|
||||
}
|
||||
PrimaryButton {
|
||||
x: parent.width - 136px; y: parent.height - 68px;
|
||||
width: 104px; height: 36px;
|
||||
border-radius: 7px;
|
||||
text: StreamStrings.remote-update-dismiss;
|
||||
text-size: 14px;
|
||||
clicked => { root.dismiss-remote-update(); }
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -133,6 +133,9 @@ inline int ApplyStreamWindowStrings(
|
||||
const int language =
|
||||
localization::detail::ClampLanguageIndex(language_index);
|
||||
auto& strings = window->template global<ui::StreamStrings>();
|
||||
strings.set_remote_update_title(Text(localization::remote_update_title[language]));
|
||||
strings.set_remote_update(Text(localization::remote_update[language]));
|
||||
strings.set_remote_update_dismiss(Text(localization::remote_update_dismiss[language]));
|
||||
strings.set_select_display(Text(localization::select_display[language]));
|
||||
strings.set_send_shortcut(Text(localization::send_shortcut[language]));
|
||||
strings.set_privacy_enable(Text(localization::privacy_enable[language]));
|
||||
|
||||
Reference in New Issue
Block a user