mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-18 18:03:08 +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]));
|
||||
|
||||
@@ -11,224 +11,15 @@
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
std::vector<int> SplitVersion(const std::string& ver);
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxInlinePatchDigits = 4;
|
||||
|
||||
struct ParsedVersion {
|
||||
std::vector<int> numbers;
|
||||
std::string date;
|
||||
bool has_patch = false;
|
||||
int patch = 0;
|
||||
};
|
||||
|
||||
bool IsDigit(char c) {
|
||||
return std::isdigit(static_cast<unsigned char>(c)) != 0;
|
||||
}
|
||||
|
||||
bool IsAlphaNumeric(char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0;
|
||||
}
|
||||
|
||||
bool IsAllDigits(const std::string& value) {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (char c : value) {
|
||||
if (!IsDigit(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseNonNegativeInt(const std::string& value, int* result) {
|
||||
if (!IsAllDigits(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const long long parsed = std::stoll(value);
|
||||
if (parsed > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
*result = static_cast<int>(parsed);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool TryParseInlinePatch(const std::string& value, int* result) {
|
||||
if (value.size() > kMaxInlinePatchDigits) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParseNonNegativeInt(value, result);
|
||||
}
|
||||
|
||||
size_t FindNumericStart(const std::string& version) {
|
||||
size_t start = 0;
|
||||
while (start < version.size() && !IsDigit(version[start])) {
|
||||
start++;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
size_t FindNumericEnd(const std::string& version, size_t start) {
|
||||
size_t end = start;
|
||||
while (end < version.size() &&
|
||||
(IsDigit(version[end]) || version[end] == '.')) {
|
||||
end++;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
bool HasDigitBoundary(const std::string& value, size_t pos, size_t len) {
|
||||
const bool before_ok = pos == 0 || !IsDigit(value[pos - 1]);
|
||||
const size_t end = pos + len;
|
||||
const bool after_ok = end >= value.size() || !IsDigit(value[end]);
|
||||
return before_ok && after_ok;
|
||||
}
|
||||
|
||||
bool IsCompactDateAt(const std::string& value, size_t pos) {
|
||||
if (pos + 8 > value.size() || !HasDigitBoundary(value, pos, 8)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 8; ++i) {
|
||||
if (!IsDigit(value[pos + i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CompactDateToIso(const std::string& compact_date) {
|
||||
return compact_date.substr(0, 4) + "-" + compact_date.substr(4, 2) + "-" +
|
||||
compact_date.substr(6, 2);
|
||||
}
|
||||
|
||||
bool ExtractDateFromText(const std::string& value,
|
||||
std::string* date,
|
||||
size_t* date_end) {
|
||||
for (size_t i = 0; i < value.size(); ++i) {
|
||||
if (IsCompactDateAt(value, i)) {
|
||||
*date = CompactDateToIso(value.substr(i, 8));
|
||||
*date_end = i + 8;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ParsedVersion ParseVersion(const std::string& version) {
|
||||
const size_t numeric_start = FindNumericStart(version);
|
||||
const size_t numeric_end = FindNumericEnd(version, numeric_start);
|
||||
|
||||
ParsedVersion parsed;
|
||||
parsed.numbers = SplitVersion(version.substr(numeric_start,
|
||||
numeric_end - numeric_start));
|
||||
|
||||
const std::string suffix = version.substr(numeric_end);
|
||||
size_t pos = 0;
|
||||
while (pos < suffix.size()) {
|
||||
while (pos < suffix.size() && !IsAlphaNumeric(suffix[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
const size_t token_start = pos;
|
||||
while (pos < suffix.size() && IsAlphaNumeric(suffix[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (token_start == pos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string token = suffix.substr(token_start, pos - token_start);
|
||||
if (parsed.date.empty() && IsCompactDateAt(token, 0)) {
|
||||
parsed.date = CompactDateToIso(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
int patch = 0;
|
||||
if (!parsed.has_patch && TryParseInlinePatch(token, &patch)) {
|
||||
parsed.has_patch = true;
|
||||
parsed.patch = patch;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
int CompareNumericVersion(const std::vector<int>& current,
|
||||
const std::vector<int>& latest) {
|
||||
std::vector<int> current_parts = current;
|
||||
std::vector<int> latest_parts = latest;
|
||||
const size_t len = std::max(current_parts.size(), latest_parts.size());
|
||||
current_parts.resize(len, 0);
|
||||
latest_parts.resize(len, 0);
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (latest_parts[i] > current_parts[i]) {
|
||||
return 1;
|
||||
}
|
||||
if (latest_parts[i] < current_parts[i]) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ReadPatchField(const nlohmann::json& json, int* patch) {
|
||||
if (!json.contains("patch")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& patch_value = json["patch"];
|
||||
if (patch_value.is_number_unsigned()) {
|
||||
const auto parsed = patch_value.get<unsigned long long>();
|
||||
if (parsed > static_cast<unsigned long long>(std::numeric_limits<int>::max())) {
|
||||
return false;
|
||||
}
|
||||
*patch = static_cast<int>(parsed);
|
||||
return true;
|
||||
}
|
||||
if (patch_value.is_number_integer()) {
|
||||
const long long parsed = patch_value.get<long long>();
|
||||
if (parsed < 0 || parsed > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
*patch = static_cast<int>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (patch_value.is_string()) {
|
||||
return TryParseNonNegativeInt(patch_value.get<std::string>(), patch);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void LogHttpError(const httplib::Result& result) {
|
||||
LOG_WARN("Failed to fetch version.json: error={}, message={}",
|
||||
static_cast<int>(result.error()), httplib::to_string(result.error()));
|
||||
@@ -310,125 +101,6 @@ void ConfigureLinuxCaCerts(httplib::Client* cli) {
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string ExtractNumericPart(const std::string& ver) {
|
||||
const size_t start = FindNumericStart(ver);
|
||||
const size_t end = FindNumericEnd(ver, start);
|
||||
return ver.substr(start, end - start);
|
||||
}
|
||||
|
||||
std::vector<int> SplitVersion(const std::string& ver) {
|
||||
std::vector<int> nums;
|
||||
std::istringstream ss(ver);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, '.')) {
|
||||
try {
|
||||
nums.push_back(std::stoi(token));
|
||||
} catch (...) {
|
||||
nums.push_back(0);
|
||||
}
|
||||
}
|
||||
return nums;
|
||||
}
|
||||
|
||||
// extract date from version string (format: v1.2.3-20251113-abc
|
||||
// or 1.2.3-20251113-abc)
|
||||
std::string ExtractDateFromVersion(const std::string& version) {
|
||||
const size_t numeric_start = FindNumericStart(version);
|
||||
const size_t numeric_end = FindNumericEnd(version, numeric_start);
|
||||
const std::string suffix = version.substr(numeric_end);
|
||||
std::string date;
|
||||
size_t date_end = 0;
|
||||
if (ExtractDateFromText(suffix, &date, &date_end)) {
|
||||
return date;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// compare two dates in YYYY-MM-DD format
|
||||
bool IsNewerDate(const std::string& date1, const std::string& date2) {
|
||||
if (date1.empty() || date2.empty()) return false;
|
||||
// simple string comparison works for ISO date format (YYYY-MM-DD)
|
||||
return date2 > date1;
|
||||
}
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest) {
|
||||
return IsNewerVersionWithMetadata(current, latest, "", -1);
|
||||
}
|
||||
|
||||
bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
const std::string& latest,
|
||||
const std::string& latest_date,
|
||||
int latest_patch) {
|
||||
(void)latest_date;
|
||||
|
||||
const ParsedVersion current_version = ParseVersion(current);
|
||||
const ParsedVersion latest_version = ParseVersion(latest);
|
||||
|
||||
const int numeric_compare =
|
||||
CompareNumericVersion(current_version.numbers, latest_version.numbers);
|
||||
if (numeric_compare > 0) {
|
||||
return true;
|
||||
}
|
||||
if (numeric_compare < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool metadata_has_patch = latest_patch >= 0;
|
||||
const bool latest_has_patch = metadata_has_patch || latest_version.has_patch;
|
||||
if (latest_has_patch || current_version.has_patch) {
|
||||
const int resolved_latest_patch =
|
||||
metadata_has_patch ? latest_patch
|
||||
: (latest_version.has_patch ? latest_version.patch
|
||||
: 0);
|
||||
const int resolved_current_patch =
|
||||
current_version.has_patch ? current_version.patch : 0;
|
||||
return resolved_latest_patch > resolved_current_patch;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<VersionInfo> ParseVersionInfo(const nlohmann::json& json) {
|
||||
if (!json.is_object()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto read_string = [&](const char* key) -> std::string {
|
||||
const auto it = json.find(key);
|
||||
return it != json.end() && it->is_string() ? it->get<std::string>()
|
||||
: std::string{};
|
||||
};
|
||||
VersionInfo info;
|
||||
info.version = read_string("latest_version");
|
||||
if (info.version.empty()) {
|
||||
info.version = read_string("version");
|
||||
}
|
||||
if (!info.version.empty() &&
|
||||
(info.version.front() == 'v' || info.version.front() == 'V')) {
|
||||
info.version.erase(0, 1);
|
||||
}
|
||||
// Invalid/missing version data is a failed check, never "up to date".
|
||||
const auto numeric_end = FindNumericEnd(info.version, 0);
|
||||
if (numeric_end == 0 || info.version[numeric_end - 1] == '.' ||
|
||||
(numeric_end < info.version.size() &&
|
||||
(numeric_end + 1 == info.version.size() ||
|
||||
(info.version[numeric_end] != '-' && info.version[numeric_end] != '+')))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::istringstream parts(info.version.substr(0, numeric_end));
|
||||
std::string part;
|
||||
int number = 0;
|
||||
while (std::getline(parts, part, '.')) {
|
||||
if (!TryParseNonNegativeInt(part, &number)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
info.release_name = read_string("releaseName");
|
||||
info.release_notes = read_string("releaseNotes");
|
||||
info.release_date = read_string("releaseDate");
|
||||
ReadPatchField(json, &info.patch);
|
||||
return info;
|
||||
}
|
||||
|
||||
std::optional<VersionInfo> CheckUpdate() {
|
||||
httplib::Client cli("https://version.crossdesk.cn");
|
||||
|
||||
|
||||
@@ -8,30 +8,13 @@
|
||||
#define _VERSION_CHECKER_H_
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <app_version.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct VersionInfo {
|
||||
std::string version;
|
||||
std::string release_name;
|
||||
std::string release_notes;
|
||||
std::string release_date;
|
||||
int patch = -1;
|
||||
};
|
||||
|
||||
std::optional<VersionInfo> ParseVersionInfo(const nlohmann::json& json);
|
||||
std::optional<VersionInfo> CheckUpdate();
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest);
|
||||
|
||||
// Pass latest_patch < 0 when patch metadata is unavailable.
|
||||
bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
const std::string& latest,
|
||||
const std::string& latest_date,
|
||||
int latest_patch);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "fa_solid_900.h"
|
||||
#include "ui/ui_localization.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
@@ -381,7 +382,8 @@ int RunCaptureMode(
|
||||
|
||||
slint::Window *capture_window = &window->window();
|
||||
const bool capture_stream =
|
||||
options.page == "stream" || options.page == "stream-video-settings";
|
||||
options.page == "stream" || options.page == "stream-video-settings" ||
|
||||
options.page == "stream-update" || options.page == "stream-update-legacy";
|
||||
if (capture_stream) {
|
||||
ConfigureStreamCapturePage(stream, options.language);
|
||||
if (!options.demo_assets_path.empty()) {
|
||||
@@ -408,6 +410,11 @@ int RunCaptureMode(
|
||||
stream->set_video_settings_enabled(true);
|
||||
stream->set_video_menu_open(true);
|
||||
}
|
||||
if (options.page == "stream-update" || options.page == "stream-update-legacy") {
|
||||
stream->set_remote_update_visible(true);
|
||||
stream->on_dismiss_remote_update(
|
||||
[stream] { stream->set_remote_update_visible(false); });
|
||||
}
|
||||
window->hide();
|
||||
stream->show();
|
||||
capture_window = &stream->window();
|
||||
@@ -638,6 +645,92 @@ int main() {
|
||||
assert(keyboard_focus_changed);
|
||||
assert(keyboard_input_received);
|
||||
|
||||
stream->window().dispatch_key_release_event("a");
|
||||
|
||||
// The modal owns keyboard and pointer input. Complete a gesture that was
|
||||
// already sent to the peer before showing it, then block new gestures.
|
||||
int remote_key_events = 0;
|
||||
int remote_pointer_presses = 0;
|
||||
int remote_pointer_releases = 0;
|
||||
int notice_dismissals = 0;
|
||||
stream->on_key_input(
|
||||
[&](slint::SharedString, bool, bool, bool, bool, bool) {
|
||||
++remote_key_events;
|
||||
});
|
||||
stream->on_pointer_input(
|
||||
[&](slint::PointerEventButton, slint::PointerEventKind kind, float, float) {
|
||||
if (kind == slint::PointerEventKind::Down) ++remote_pointer_presses;
|
||||
if (kind == slint::PointerEventKind::Up) ++remote_pointer_releases;
|
||||
});
|
||||
stream->on_dismiss_remote_update([&] {
|
||||
++notice_dismissals;
|
||||
stream->set_remote_update_visible(false);
|
||||
});
|
||||
const auto outside_notice =
|
||||
slint::LogicalPosition(slint::Point<float>{640.0f, 500.0f});
|
||||
stream->window().dispatch_pointer_press_event(
|
||||
outside_notice, slint::PointerEventButton::Left);
|
||||
stream->set_remote_update_visible(true);
|
||||
stream->window().dispatch_pointer_release_event(
|
||||
outside_notice, slint::PointerEventButton::Left);
|
||||
if (remote_pointer_presses != 1 || remote_pointer_releases != 1) {
|
||||
std::cerr << "Showing the upgrade dialog must finish an active remote gesture\n";
|
||||
return 9;
|
||||
}
|
||||
const float notice_y =
|
||||
std::max(64.0f, stream->get_video_area_height() * 0.18f) + 24.0f;
|
||||
if (!stream->invoke_is_local_control_area(640.0f, notice_y) ||
|
||||
!stream->invoke_is_local_control_area(640.0f, 500.0f)) {
|
||||
std::cerr << "Remote upgrade dialog and backdrop must consume local input\n";
|
||||
return 9;
|
||||
}
|
||||
stream->window().dispatch_pointer_press_event(
|
||||
outside_notice, slint::PointerEventButton::Left);
|
||||
stream->window().dispatch_pointer_release_event(
|
||||
outside_notice, slint::PointerEventButton::Left);
|
||||
stream->window().dispatch_key_press_event("a");
|
||||
stream->window().dispatch_key_release_event("a");
|
||||
if (remote_pointer_presses != 1 || remote_pointer_releases != 1 ||
|
||||
remote_key_events != 0 || notice_dismissals != 0) {
|
||||
std::cerr << "Upgrade dialog input must stay local\n";
|
||||
return 9;
|
||||
}
|
||||
stream->window().dispatch_key_press_event("\x1b");
|
||||
stream->window().dispatch_key_release_event("\x1b");
|
||||
if (stream->get_remote_update_visible() || notice_dismissals != 1 ||
|
||||
remote_key_events != 0) {
|
||||
std::cerr << "Escape must dismiss the dialog without forwarding the key\n";
|
||||
return 9;
|
||||
}
|
||||
if (stream->invoke_is_local_control_area(640.0f, notice_y)) {
|
||||
std::cerr << "Dismissed upgrade notice must release its input area\n";
|
||||
return 9;
|
||||
}
|
||||
stream->window().dispatch_key_press_event("a");
|
||||
stream->window().dispatch_key_release_event("a");
|
||||
stream->window().dispatch_pointer_press_event(
|
||||
outside_notice, slint::PointerEventButton::Left);
|
||||
stream->window().dispatch_pointer_release_event(
|
||||
outside_notice, slint::PointerEventButton::Left);
|
||||
if (remote_key_events != 2 || remote_pointer_presses != 2 ||
|
||||
remote_pointer_releases != 2) {
|
||||
std::cerr << "Dismissing the upgrade dialog must restore remote input\n";
|
||||
return 9;
|
||||
}
|
||||
|
||||
stream->set_remote_update_visible(true);
|
||||
const auto notice_button = slint::LogicalPosition(slint::Point<float>{
|
||||
816.0f, stream->get_video_area_top() + notice_y - 24.0f + 158.0f});
|
||||
stream->window().dispatch_pointer_press_event(
|
||||
notice_button, slint::PointerEventButton::Left);
|
||||
stream->window().dispatch_pointer_release_event(
|
||||
notice_button, slint::PointerEventButton::Left);
|
||||
if (stream->get_remote_update_visible() || notice_dismissals != 2 ||
|
||||
remote_pointer_presses != 2 || remote_pointer_releases != 2) {
|
||||
std::cerr << "Upgrade confirmation click must not reach the peer\n";
|
||||
return 9;
|
||||
}
|
||||
|
||||
crossdesk::ui::FileTransferEntry transfer;
|
||||
transfer.name = "archive.zip";
|
||||
transfer.status = "Sending";
|
||||
|
||||
@@ -97,5 +97,37 @@ int main() {
|
||||
ok &= ExpectEqual("plain comparison has no hidden metadata from prior checks",
|
||||
crossdesk::IsNewerVersion("1.3.5", "1.3.5"), false);
|
||||
|
||||
const auto release = crossdesk::ParseVersionInfoJSON(
|
||||
R"({"latest_version":"v1.5.3-20260928","patch":2})");
|
||||
ok &= ExpectEqual("shared release JSON parser",
|
||||
release.has_value(), true);
|
||||
if (release) {
|
||||
for (const auto& version : {"1.5.3-2-20260927", "1.5.3-3-20260928", "1.6.0"}) {
|
||||
ok &= ExpectEqual(std::string("no remote update for ") + version,
|
||||
crossdesk::AvailableAppUpdate(version, *release).has_value(),
|
||||
false);
|
||||
}
|
||||
for (const auto& version : {"", " ", "unknown", "ios-native", "1..5", "1.5-"}) {
|
||||
ok &= ExpectEqual(std::string("unavailable peer version is legacy: ") + version,
|
||||
!crossdesk::IsValidAppVersion(version) &&
|
||||
crossdesk::AvailableAppUpdate(version, *release) == "1.5.3-2",
|
||||
true);
|
||||
}
|
||||
for (const auto& version : {"1.5.2", "v1.5.3-20260928", "1.5.3-1-20260929",
|
||||
"1.5.3-20260928-1"}) {
|
||||
ok &= ExpectEqual(std::string("remote update includes metadata patch for ") + version,
|
||||
crossdesk::AvailableAppUpdate(version, *release) == "1.5.3-2",
|
||||
true);
|
||||
}
|
||||
}
|
||||
ok &= ExpectEqual("legacy peer cannot supply a missing latest release label",
|
||||
crossdesk::AvailableAppUpdate("", crossdesk::VersionInfo{}).has_value(),
|
||||
false);
|
||||
for (const auto& response : {"", "not JSON", "{}", "[]",
|
||||
R"({"version":null})", R"({"version":"unknown"})"}) {
|
||||
ok &= ExpectEqual("invalid release response gives no update",
|
||||
crossdesk::ParseVersionInfoJSON(response).has_value(), false);
|
||||
}
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ function setup_targets()
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_packages("cpp-httplib", "nlohmann_json")
|
||||
add_deps("rd_log")
|
||||
add_deps("rd_log", "crossdesk_wire")
|
||||
add_includedirs("apps/desktop/src/version_checker")
|
||||
add_files("apps/desktop/tests/version_checker_test.cpp",
|
||||
"apps/desktop/src/version_checker/version_checker.cpp")
|
||||
@@ -421,7 +421,7 @@ function setup_targets()
|
||||
set_kind("object")
|
||||
add_packages("cpp-httplib")
|
||||
add_packages("nlohmann_json", {public = true})
|
||||
add_deps("rd_log")
|
||||
add_deps("rd_log", "crossdesk_wire")
|
||||
add_files("apps/desktop/src/version_checker/*.cpp")
|
||||
add_includedirs("apps/desktop/src/version_checker", {public = true})
|
||||
if is_os("macosx") then
|
||||
|
||||
@@ -61,6 +61,7 @@ typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
|
||||
height:(NSInteger)height;
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
didReceiveHostName:(NSString *)hostName
|
||||
appVersion:(NSString *)appVersion
|
||||
displayNames:(NSArray<NSString *> *)displayNames
|
||||
displaySizes:(NSArray<NSValue *> *)displaySizes;
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
@@ -101,6 +102,11 @@ typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
|
||||
|
||||
@property(nonatomic, weak, nullable) id<CrossDeskRTCBridgeDelegate> delegate;
|
||||
|
||||
// Uses the same release/patch ordering as the desktop controller.
|
||||
+ (nullable NSString *)availableUpdateForAppVersion:(NSString *)appVersion
|
||||
releaseJSON:(NSData *)releaseJSON
|
||||
NS_SWIFT_NAME(availableUpdate(appVersion:releaseJSON:));
|
||||
|
||||
- (void)configureWithSignalHost:(NSString *)host
|
||||
signalPort:(NSInteger)signalPort;
|
||||
- (void)setVideoAdaptationPolicy:(CrossDeskVideoAdaptationPolicy)policy;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include <app_version.h>
|
||||
#include <file_transfer_format.h>
|
||||
#include <remote_action.h>
|
||||
#include <stream_names.h>
|
||||
@@ -330,7 +331,8 @@ void DispatchMain(dispatch_block_t block) {
|
||||
- (void)handleData:(const char *)data
|
||||
size:(size_t)size
|
||||
sourceID:(const char *)sourceID
|
||||
sourceIDSize:(size_t)sourceIDSize;
|
||||
sourceIDSize:(size_t)sourceIDSize
|
||||
generation:(uint64_t)generation;
|
||||
- (void)handleStats:(const MiniRtcNetTrafficStats *)stats mode:(TraversalMode)mode;
|
||||
- (void)sendMessage:(const std::string &)message
|
||||
reliable:(BOOL)reliable
|
||||
@@ -377,7 +379,8 @@ void OnDataBuffer(const char *data, size_t size, const char *, size_t,
|
||||
[owner handleData:data
|
||||
size:size
|
||||
sourceID:source_id
|
||||
sourceIDSize:source_id_size];
|
||||
sourceIDSize:source_id_size
|
||||
generation:context->generation];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,6 +491,16 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
std::atomic<uint64_t> _presenceGeneration;
|
||||
}
|
||||
|
||||
+ (nullable NSString *)availableUpdateForAppVersion:(NSString *)appVersion
|
||||
releaseJSON:(NSData *)releaseJSON {
|
||||
const auto latest = crossdesk::ParseVersionInfoJSON(std::string_view(
|
||||
static_cast<const char *>(releaseJSON.bytes), releaseJSON.length));
|
||||
if (!latest) return nil;
|
||||
const auto update = crossdesk::AvailableAppUpdate(appVersion.UTF8String ?: "",
|
||||
*latest);
|
||||
return update ? [NSString stringWithUTF8String:update->c_str()] : nil;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
@@ -1264,6 +1277,10 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
CopyCString(action.i.host_name, sizeof(action.i.host_name),
|
||||
controller_name);
|
||||
action.i.host_name_size = std::strlen(action.i.host_name);
|
||||
NSString *appVersion = [[NSBundle mainBundle]
|
||||
objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
||||
CopyCString(action.i.app_version, sizeof(action.i.app_version),
|
||||
appVersion.UTF8String ?: "");
|
||||
action.i.display_list = nullptr;
|
||||
action.i.display_num = 0;
|
||||
action.i.left = nullptr;
|
||||
@@ -1515,7 +1532,8 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
- (void)handleData:(const char *)data
|
||||
size:(size_t)size
|
||||
sourceID:(const char *)sourceID
|
||||
sourceIDSize:(size_t)sourceIDSize {
|
||||
sourceIDSize:(size_t)sourceIDSize
|
||||
generation:(uint64_t)generation {
|
||||
if (!data || size == 0 || !sourceID) return;
|
||||
const std::string source(sourceID, sourceIDSize);
|
||||
if (source == kControlStream) {
|
||||
@@ -1528,6 +1546,8 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
initWithBytes:action.i.host_name
|
||||
length:action.i.host_name_size
|
||||
encoding:NSUTF8StringEncoding];
|
||||
NSString *app_version = crossdesk::IsValidAppVersion(action.i.app_version)
|
||||
? [NSString stringWithUTF8String:action.i.app_version] : nil;
|
||||
NSMutableArray<NSString *> *display_names =
|
||||
[NSMutableArray arrayWithCapacity:action.i.display_num];
|
||||
NSMutableArray<NSValue *> *display_sizes =
|
||||
@@ -1548,12 +1568,14 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
}
|
||||
crossdesk::FreeRemoteAction(action);
|
||||
DispatchMain(^{
|
||||
if (![self isControllerGenerationActive:generation]) return;
|
||||
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:
|
||||
@selector(rtcBridge:didReceiveHostName:displayNames:
|
||||
@selector(rtcBridge:didReceiveHostName:appVersion:displayNames:
|
||||
displaySizes:)]) {
|
||||
[delegate rtcBridge:self
|
||||
didReceiveHostName:host_name ?: @""
|
||||
appVersion:app_version ?: @""
|
||||
displayNames:display_names
|
||||
displaySizes:display_sizes];
|
||||
}
|
||||
|
||||
@@ -359,10 +359,17 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
@Published private(set) var recentConnectionPresence: [String: Bool] = [:]
|
||||
@Published private(set) var deviceOfflineAlertVisible = false
|
||||
@Published private(set) var connectionFailureMessage: String?
|
||||
@Published private(set) var remoteUpdateMessage: String?
|
||||
|
||||
let bridge = CrossDeskRTCBridge()
|
||||
private let audioPlayer = RemoteAudioPlayer()
|
||||
private var activeRemoteID = ""
|
||||
private var remoteAppVersion = ""
|
||||
private var remoteHostInfoReceived = false
|
||||
private var remoteVersionCheckAttempted = false
|
||||
private var remoteVersionCheckGeneration = UUID()
|
||||
private var remoteVersionCheckTask: URLSessionDataTask?
|
||||
private static let remoteUpdateNotice = "被控端版本过低,请升级到最新版本。"
|
||||
private var pendingRememberPassword = false
|
||||
private var connectionRecorded = false
|
||||
private var shouldCaptureThumbnail = false
|
||||
@@ -406,6 +413,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
|
||||
deinit {
|
||||
presenceTimer?.invalidate()
|
||||
remoteVersionCheckTask?.cancel()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -460,6 +468,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
}
|
||||
|
||||
private func beginRemoteConnection(_ identifier: String) {
|
||||
resetRemoteVersionCheck()
|
||||
cancelVideoRecovery()
|
||||
cancelPresenceProbe()
|
||||
activeRemoteID = identifier
|
||||
@@ -552,6 +561,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
}
|
||||
|
||||
private func resetConnection() {
|
||||
resetRemoteVersionCheck()
|
||||
connectionFailureMessage = nil
|
||||
cancelVideoRecovery()
|
||||
let wasCheckingPresence = pendingPresenceRemoteID != nil
|
||||
@@ -576,11 +586,62 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
}
|
||||
|
||||
func retry() {
|
||||
resetRemoteVersionCheck()
|
||||
cancelVideoRecovery()
|
||||
bridge.disconnect()
|
||||
connect()
|
||||
}
|
||||
|
||||
func dismissRemoteUpdate() {
|
||||
remoteUpdateMessage = nil
|
||||
}
|
||||
|
||||
private func resetRemoteVersionCheck() {
|
||||
remoteVersionCheckTask?.cancel()
|
||||
remoteVersionCheckTask = nil
|
||||
remoteVersionCheckGeneration = UUID()
|
||||
remoteVersionCheckAttempted = false
|
||||
remoteAppVersion = ""
|
||||
remoteHostInfoReceived = false
|
||||
remoteUpdateMessage = nil
|
||||
}
|
||||
|
||||
private func checkRemoteVersionIfNeeded() {
|
||||
guard isConnected, remoteHostInfoReceived,
|
||||
!remoteVersionCheckAttempted else { return }
|
||||
remoteVersionCheckAttempted = true
|
||||
if remoteAppVersion.isEmpty {
|
||||
remoteUpdateMessage = Self.remoteUpdateNotice
|
||||
return
|
||||
}
|
||||
let generation = remoteVersionCheckGeneration
|
||||
let appVersion = remoteAppVersion
|
||||
// Only public release metadata is fetched; no peer ID or version is sent.
|
||||
let url = URL(string: "https://version.crossdesk.cn/version.json")!
|
||||
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData,
|
||||
timeoutInterval: 15)
|
||||
remoteVersionCheckTask = URLSession.shared.dataTask(with: request) {
|
||||
[weak self] data, response, error in
|
||||
let latest: String?
|
||||
if error == nil, let data,
|
||||
(response as? HTTPURLResponse)?.statusCode == 200 {
|
||||
latest = CrossDeskRTCBridge.availableUpdate(appVersion: appVersion,
|
||||
releaseJSON: data)
|
||||
} else {
|
||||
latest = nil
|
||||
}
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self,
|
||||
self.remoteVersionCheckGeneration == generation,
|
||||
self.isConnected else { return }
|
||||
self.remoteVersionCheckTask = nil
|
||||
guard latest != nil else { return }
|
||||
self.remoteUpdateMessage = Self.remoteUpdateNotice
|
||||
}
|
||||
}
|
||||
remoteVersionCheckTask?.resume()
|
||||
}
|
||||
|
||||
func savedPassword(for remoteID: String) -> String {
|
||||
ConnectionCredentialStore.password(for: remoteID) ?? ""
|
||||
}
|
||||
@@ -879,6 +940,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
case 1:
|
||||
connectionStatus = "已连接"
|
||||
sessionVisible = true
|
||||
checkRemoteVersionIfNeeded()
|
||||
recordSuccessfulConnectionIfNeeded()
|
||||
audioPlayer.setEnabled(audioEnabled)
|
||||
bridge.setAudioEnabled(audioEnabled)
|
||||
@@ -966,8 +1028,12 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
|
||||
func rtcBridge(_ bridge: CrossDeskRTCBridge,
|
||||
didReceiveHostName hostName: String,
|
||||
appVersion: String,
|
||||
displayNames remoteDisplayNames: [String],
|
||||
displaySizes remoteDisplaySizes: [NSValue]) {
|
||||
remoteHostInfoReceived = true
|
||||
remoteAppVersion = appVersion
|
||||
checkRemoteVersionIfNeeded()
|
||||
updateActiveConnectionName(hostName)
|
||||
let names = remoteDisplayNames.enumerated().map { index, displayName in
|
||||
let name = displayName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
@@ -147,6 +147,14 @@ struct RemoteSessionView: View {
|
||||
.zIndex(50)
|
||||
}
|
||||
}
|
||||
.alert("升级提示", isPresented: Binding(
|
||||
get: { session.remoteUpdateMessage != nil },
|
||||
set: { if !$0 { session.dismissRemoteUpdate() } }
|
||||
)) {
|
||||
Button("知道了", role: .cancel) { session.dismissRemoteUpdate() }
|
||||
} message: {
|
||||
Text(session.remoteUpdateMessage ?? "")
|
||||
}
|
||||
.onDisappear {
|
||||
keyboardInputVisible = false
|
||||
viewport = RemoteViewportState()
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -241,12 +241,12 @@
|
||||
{
|
||||
"id": "inih",
|
||||
"name": "inih / INIReader",
|
||||
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
|
||||
"version": "24d9d9deefb95ede2cdb18e77cb01350101f70f8",
|
||||
"license": "BSD-3-Clause",
|
||||
"source": {
|
||||
"kind": "repository",
|
||||
"url": "https://github.com/kunkundi/minirtc",
|
||||
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
|
||||
"revision": "24d9d9deefb95ede2cdb18e77cb01350101f70f8"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
@@ -527,12 +527,12 @@
|
||||
{
|
||||
"id": "minirtc",
|
||||
"name": "MiniRTC",
|
||||
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
|
||||
"version": "24d9d9deefb95ede2cdb18e77cb01350101f70f8",
|
||||
"license": "LGPL-3.0-only",
|
||||
"source": {
|
||||
"kind": "repository",
|
||||
"url": "https://github.com/kunkundi/minirtc",
|
||||
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
|
||||
"revision": "24d9d9deefb95ede2cdb18e77cb01350101f70f8"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
@@ -833,12 +833,12 @@
|
||||
{
|
||||
"id": "webrtc",
|
||||
"name": "WebRTC-derived code in MiniRTC",
|
||||
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
|
||||
"version": "24d9d9deefb95ede2cdb18e77cb01350101f70f8",
|
||||
"license": "BSD-3-Clause and patent grant",
|
||||
"source": {
|
||||
"kind": "repository",
|
||||
"url": "https://github.com/kunkundi/minirtc",
|
||||
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
|
||||
"revision": "24d9d9deefb95ede2cdb18e77cb01350101f70f8"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
@@ -898,7 +898,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"bundle_sha256": "09376251bbda3f5a1f7112f15dcbb1259fef7898f85b45aacafa767348aafdc2",
|
||||
"bundle_sha256": "58702163ac79d8821007b1ae44ef054cf1d9a3cec9d6334b852f0bbe99b5af01",
|
||||
"toolchain": {
|
||||
"xmake_version": "3.1.1",
|
||||
"xmake_repository_revision": "121109352da272cd1a3d4ed52f2dcda34f839955"
|
||||
|
||||
Reference in New Issue
Block a user