[feat] add background and manual update checks

This commit is contained in:
dijunkun
2026-09-24 17:36:41 +08:00
parent e0cd4c0567
commit c661319890
18 changed files with 629 additions and 90 deletions
@@ -10,7 +10,6 @@
#include <atomic>
#include <chrono>
#include <cstdint>
#include <nlohmann/json.hpp>
#include <string>
namespace crossdesk::gui_detail {
@@ -47,7 +46,6 @@ struct InteractionState {
};
struct UpdateState {
nlohmann::json latest_version_info_ = nlohmann::json{};
bool update_available_ = false;
std::string latest_version_;
std::string release_name_;
@@ -58,7 +58,7 @@
#include "rd_log.h"
#include "server_window_state.h"
#include "ui/ui_localization.h"
#include "version_checker.h"
#include "update_checker.h"
#include "window_geometry.h"
namespace crossdesk {
@@ -989,33 +989,6 @@ int GuiApplication::Run() {
"%d", signal_port);
}
latest_version_info_ = CheckUpdate();
if (!latest_version_info_.empty()) {
std::string version;
if (latest_version_info_.contains("latest_version") &&
latest_version_info_["latest_version"].is_string()) {
version = latest_version_info_["latest_version"].get<std::string>();
} else if (latest_version_info_.contains("version") &&
latest_version_info_["version"].is_string()) {
version = latest_version_info_["version"].get<std::string>();
}
latest_version_ = version.empty() ? std::string{} : "v" + version;
if (latest_version_info_.contains("releaseNotes") &&
latest_version_info_["releaseNotes"].is_string()) {
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
}
if (latest_version_info_.contains("releaseName") &&
latest_version_info_["releaseName"].is_string()) {
release_name_ = latest_version_info_["releaseName"].get<std::string>();
}
if (latest_version_info_.contains("releaseDate") &&
latest_version_info_["releaseDate"].is_string()) {
release_date_ = latest_version_info_["releaseDate"].get<std::string>();
}
update_available_ =
!version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_);
}
InitializeSettings();
if (!InitializeSDL()) {
return -1;
@@ -1030,6 +1003,7 @@ int GuiApplication::Run() {
LOG_INFO("Slint backend: {}; {}", backend.backend, backend.diagnostic);
#endif
InitializeModules();
update_checker_ = std::make_unique<UpdateChecker>(CROSSDESK_VERSION);
InitializeUi();
ui_->timer.start(slint::TimerMode::Repeated, 16ms, [this] { Tick(); });
@@ -1155,14 +1129,6 @@ void GuiApplication::InitializeUi() {
ui_->main->set_custom_titlebar(use_x11_custom_titlebar_);
ui_->main->set_wayland_titlebar(use_xwayland_gui_);
#endif
std::vector<ui::ReleaseNoteBlock> release_note_blocks;
for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) {
ui::ReleaseNoteBlock block;
block.content = parsed.content;
block.section_gap = parsed.section_gap;
release_note_blocks.push_back(std::move(block));
}
ui_->release_note_blocks_model->set_vector(std::move(release_note_blocks));
ui_->main->set_release_note_blocks(ui_->release_note_blocks_model);
RegisterFontAwesome(ui_->main->window());
ui_->main->set_recent_connections(ui_->recent_model);
@@ -1398,6 +1364,10 @@ void GuiApplication::BindMainCallbacks() {
: slint::SharedString{});
});
main->on_open_download([this] { OpenUrl("https://crossdesk.cn"); });
main->on_check_for_updates([this] {
update_checker_->RequestCheck();
SyncMainWindow();
});
main->on_connection_cancel([this] {
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
if (!props) {
@@ -1783,6 +1753,7 @@ void GuiApplication::Tick() {
slint::quit_event_loop();
return;
}
PollUpdateCheck();
HandleSessionCleanup();
HandlePasswordChangeResult();
HandleCredentialRecovery();
@@ -1857,6 +1828,29 @@ void GuiApplication::Tick() {
SyncServerWindow();
}
void GuiApplication::PollUpdateCheck() {
// The first tick runs after show()/event-loop startup. Poll never waits for
// networking and all application/model changes remain on this UI thread.
if (!update_checker_->Poll() ||
update_checker_->status() == UpdateChecker::Status::Failed) {
return;
}
const auto& info = *update_checker_->latest();
latest_version_ = "v" + info.version;
release_name_ = info.release_name;
release_notes_ = info.release_notes;
release_date_ = info.release_date;
update_available_ = update_checker_->update_available();
std::vector<ui::ReleaseNoteBlock> blocks;
for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) {
ui::ReleaseNoteBlock block;
block.content = parsed.content;
block.section_gap = parsed.section_gap;
blocks.push_back(std::move(block));
}
ui_->release_note_blocks_model->set_vector(std::move(blocks));
}
void GuiApplication::ShareLocalCursorState() {
constexpr auto kCursorEchoSuppressionInterval = 300ms;
constexpr auto kCursorStateHeartbeatInterval = 500ms;
@@ -2253,6 +2247,23 @@ void GuiApplication::SyncMainWindow() {
ui_->main->set_signal_tls_error(signal_status_ ==
SignalStatus::SignalTlsCertError);
ui_->main->set_update_available(update_available_);
switch (update_checker_->status()) {
case UpdateChecker::Status::Idle:
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Idle);
break;
case UpdateChecker::Status::Checking:
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Checking);
break;
case UpdateChecker::Status::UpToDate:
ui_->main->set_update_check_status(ui::UpdateCheckStatus::UpToDate);
break;
case UpdateChecker::Status::Available:
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Available);
break;
case UpdateChecker::Status::Failed:
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Failed);
break;
}
ui_->main->set_current_version(CROSSDESK_VERSION);
ui_->main->set_latest_version(UiText(latest_version_));
ui_->main->set_release_name(UiText(release_name_));
@@ -3495,6 +3506,9 @@ void GuiApplication::Cleanup() {
}
#endif
ui_.reset();
if (update_checker_) {
update_checker_->Stop();
}
SDL_Quit();
}
@@ -19,6 +19,8 @@
namespace crossdesk {
class UpdateChecker;
// Slint application shell. GuiRuntime remains the owner of transport, media,
// device, clipboard, transfer and settings behavior.
class GuiApplication final : private GuiRuntime {
@@ -48,6 +50,7 @@ private:
void BindStreamCallbacks();
void BindServerCallbacks();
void Tick();
void PollUpdateCheck();
void ShareLocalCursorState();
bool RequestPasswordChange(const std::string &password);
void HandlePasswordChangeResult();
@@ -88,6 +91,7 @@ private:
bool OpenUrl(const std::string &url);
std::unique_ptr<SlintUi> ui_;
std::unique_ptr<UpdateChecker> update_checker_;
CursorStateProvider cursor_state_provider_;
std::unordered_map<std::string, CursorDeliveryState>
cursor_delivery_states_;
@@ -199,6 +199,15 @@ struct TranslationRow {
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \
X(check_for_updates, u8"检查更新", "Check for updates", \
u8"Проверить обновления") \
X(checking_for_updates, u8"正在检查更新…", "Checking for updates…", \
u8"Проверка обновлений…") \
X(up_to_date, u8"已是最新版本", "You're up to date", \
u8"Установлена последняя версия") \
X(update_check_failed, u8"检查失败,请稍后重试", \
"Check failed. Please try again later.", \
u8"Не удалось проверить обновления. Повторите позже.") \
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
@@ -9,6 +9,7 @@
#include <fstream>
#include <functional>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
#include <thread>
@@ -1,6 +1,7 @@
#include "runtime/peer_event_handler.h"
#include <remote_action.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <chrono>
@@ -1,6 +1,7 @@
#include "runtime/peer_event_handler.h"
#include <remote_action.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <chrono>
+1 -1
View File
@@ -1,5 +1,5 @@
// Single AOT entry point. Generating one header for the entire UI avoids
// duplicate imported-component definitions when C++ uses all three windows.
export { MainWindow, RecentConnection, UiStrings } from "main_window.slint";
export { MainWindow, RecentConnection, UpdateCheckStatus, UiStrings } from "main_window.slint";
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
export { ServerWindow, ControllerEntry } from "server_window.slint";
+91 -17
View File
@@ -14,6 +14,8 @@ export struct ReleaseNoteBlock {
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";
@@ -59,6 +61,10 @@ export global UiStrings {
in-out property <string> signal-disconnected: "Signal server disconnected";
in-out property <string> tls-error: "Signal server TLS certificate error";
in-out property <string> update-available: "A new version is available";
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> later: "Later";
in-out property <string> download: "Download";
@@ -257,6 +263,11 @@ export component MainWindow inherits Window {
in property <bool> signal-tls-error: false;
in property <[RecentConnection]> recent-connections;
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: "";
@@ -321,6 +332,7 @@ export component MainWindow inherits Window {
callback cancel-self-hosted-settings;
callback browse-save-path;
callback open-download;
callback check-for-updates;
callback connection-cancel;
callback connection-acknowledge;
callback connection-submit-password(string, bool);
@@ -340,7 +352,7 @@ export component MainWindow inherits Window {
private property <length> settings-scroll-y: 0px;
in-out property <bool> self-host-settings-open: false;
in-out property <bool> about-open: false;
in-out property <bool> update-open: root.update-available;
in-out property <bool> update-open: false;
in-out property <bool> reset-password-open: false;
in-out property <bool> reset-password-invalid: false;
in-out property <string> new-password-input: "";
@@ -1594,10 +1606,10 @@ export component MainWindow inherits Window {
z: 46;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: 170px;
y: 127px;
width: 300px;
height: 196px;
x: (root.width - self.width) / 2;
y: (root.height - self.height) / 2;
width: 320px;
height: 216px;
border-radius: 7px;
Rectangle {
@@ -1611,19 +1623,81 @@ export component MainWindow inherits Window {
Rectangle { y: parent.height - 7px; height: 7px; background: #d3d3d3; }
Text { x: 6px; text: UiStrings.about; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
}
Text { x: 30px; y: 40px; width: 240px; height: 20px; text: UiStrings.version + ": CrossDesk " + root.current-version; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
if root.update-available: Text { x: 30px; y: 60px; width: 240px; height: 20px; text: UiStrings.update-available + ":"; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
if root.update-available: Rectangle {
x: 30px;
y: 60px;
width: 240px;
height: 20px;
version-link := Text { text: root.latest-version; color: version-touch.has-hover ? #174ea6 : #334fd1; font-size: ImGuiFontStyle.body; horizontal-alignment: center; vertical-alignment: center; }
version-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
// Keep the dialog/footer stable while visible text rows determine
// each group's size and the layout redistributes the space between.
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-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; } }
}
}
Text { x: 30px; y: 85px; width: 240px; height: 20px; text: UiStrings.copyright; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
Text { x: 30px; y: 105px; width: 240px; height: 20px; text: UiStrings.license; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
CompactButton { x: 133px; y: 145px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
}
}
@@ -80,6 +80,12 @@ inline int ApplyMainWindowStrings(
strings.set_tls_error(Text(localization::signal_tls_cert_error[language]));
strings.set_update_available(
Text(localization::new_version_available[language]));
strings.set_check_for_updates(Text(localization::check_for_updates[language]));
strings.set_checking_for_updates(
Text(localization::checking_for_updates[language]));
strings.set_up_to_date(Text(localization::up_to_date[language]));
strings.set_update_check_failed(
Text(localization::update_check_failed[language]));
strings.set_release_notes(Text(localization::release_notes[language]));
strings.set_download(Text(localization::update[language]));
strings.set_input_password(Text(localization::input_password[language]));