[feat] add in-app Windows updates with automatic download and setup launch

This commit is contained in:
dijunkun
2026-09-09 18:13:21 +08:00
parent 4f200095f4
commit c2c74913e3
9 changed files with 466 additions and 4 deletions
@@ -1413,6 +1413,21 @@ void GuiApplication::BindMainCallbacks() {
: slint::SharedString{});
});
main->on_open_download([this] { OpenUrl("https://crossdesk.cn"); });
main->on_start_update([this] {
#if _WIN32
windows_updater_.Start(latest_version_info_);
SyncWindowsUpdate();
#else
OpenUrl("https://crossdesk.cn");
ui_->main->set_update_open(false);
#endif
});
main->on_cancel_update([this] {
#if _WIN32
windows_updater_.Cancel();
#endif
ui_->main->set_update_open(false);
});
main->on_connection_cancel([this] {
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
if (!props) {
@@ -2100,6 +2115,9 @@ void GuiApplication::SyncMainWindow() {
ui_->main->set_signal_tls_error(signal_status_ ==
SignalStatus::SignalTlsCertError);
ui_->main->set_update_available(update_available_);
#if _WIN32
SyncWindowsUpdate();
#endif
ui_->main->set_current_version(CROSSDESK_VERSION);
ui_->main->set_latest_version(UiText(latest_version_));
ui_->main->set_release_name(UiText(release_name_));
@@ -3257,7 +3275,51 @@ bool GuiApplication::OpenUrl(const std::string& url) {
return SDL_OpenURL(url.c_str());
}
#if _WIN32
void GuiApplication::SyncWindowsUpdate() {
using State = WindowsUpdater::State;
if (windows_updater_.GetState() == State::Ready) {
windows_updater_.Launch();
if (windows_updater_.GetState() == State::Launched) {
ui_->main->set_update_open(false);
}
}
const State state = windows_updater_.GetState();
const bool downloading = state == State::Downloading;
ui_->main->set_update_downloading(downloading || state == State::Launching);
ui_->main->set_update_failed(state == State::Failed ||
state == State::LaunchFailed);
const int language = localization_language_index_;
std::string status;
if (downloading) {
status = localization::update_downloading[language];
const uint64_t total = windows_updater_.Total();
const uint64_t received = windows_updater_.Downloaded();
if (total > 0) {
const int percent = static_cast<int>(std::min(100.0, 100.0 * received / total));
status += " " + std::to_string(percent) + "%";
ui_->main->set_update_progress(static_cast<float>(percent) / 100.0f);
} else {
status += " " + std::to_string(received / (1024 * 1024)) + " MB";
ui_->main->set_update_progress(-1.0f);
}
} else if (state == State::Failed) {
status = localization::update_download_failed[language];
} else if (state == State::LaunchFailed) {
status = localization::update_launch_failed[language];
} else if (state == State::Launching) {
status = localization::update_launching[language];
} else if (state == State::Launched) {
status = localization::update_installer_opened[language];
}
ui_->main->set_update_status(UiText(status));
}
#endif
void GuiApplication::Cleanup() {
#if _WIN32
windows_updater_.Cancel();
#endif
if (!ui_) {
SDL_Quit();
return;
@@ -16,6 +16,9 @@
#include "runtime/cursor_state_provider.h"
#include "runtime/gui_runtime.h"
#if _WIN32
#include "platform/windows/windows_updater.h"
#endif
namespace crossdesk {
@@ -52,6 +55,9 @@ private:
void HandlePasswordChangeResult();
void HandleCredentialRecovery();
void SyncMainWindow();
#if _WIN32
void SyncWindowsUpdate();
#endif
void SyncConnectionDialog();
void SyncPlatformDialogs();
void SyncStreamWindow();
@@ -87,6 +93,9 @@ private:
bool OpenUrl(const std::string &url);
std::unique_ptr<SlintUi> ui_;
#if _WIN32
WindowsUpdater windows_updater_;
#endif
CursorStateProvider cursor_state_provider_;
std::unordered_map<std::string, CursorDeliveryState>
cursor_delivery_states_;
@@ -100,4 +109,4 @@ private:
} // namespace crossdesk
#endif
#endif
@@ -184,6 +184,20 @@ struct TranslationRow {
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \
X(update_downloading, u8"正在下载安装包…", "Downloading installer…", \
u8"Загрузка установщика…") \
X(update_launching, u8"正在打开安装包,请确认管理员权限请求…", \
"Opening installer. Please confirm the administrator permission request…", \
u8"Запуск установщика. Подтвердите запрос прав администратора…") \
X(update_download_failed, u8"下载失败,请点击更新重试,或访问官网。", \
"Download failed. Click Update to retry, or visit the website.", \
u8"Ошибка загрузки. Повторите обновление или откройте сайт.") \
X(update_launch_failed, u8"安装包未能启动或授权已取消,请点击更新重试。", \
"Installer launch failed or permission was cancelled. Click Update to retry.", \
u8"Установщик не запущен или доступ отменён. Повторите обновление.") \
X(update_installer_opened, u8"安装包已打开,请在安装向导中完成更新。", \
"Installer opened. Follow the setup wizard to finish updating.", \
u8"Установщик открыт. Следуйте указаниям мастера установки.") \
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
+28 -3
View File
@@ -210,6 +210,10 @@ 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 <bool> update-downloading: false;
in property <bool> update-failed: false;
in property <float> update-progress: 0;
in property <string> update-status: "";
in property <string> current-version: "";
in property <string> latest-version: "";
in property <string> release-name: "";
@@ -268,6 +272,8 @@ export component MainWindow inherits Window {
callback cancel-self-hosted-settings;
callback browse-save-path;
callback open-download;
callback start-update;
callback cancel-update;
callback connection-cancel;
callback connection-acknowledge;
callback connection-submit-password(string, bool);
@@ -1644,7 +1650,7 @@ export component MainWindow inherits Window {
x: 30px;
y: 105px;
width: 360px;
height: 190px;
height: root.update-status != "" ? 136px : 190px;
background: white;
border-width: 1px;
border-color: #b9bdc3;
@@ -1671,8 +1677,27 @@ export component MainWindow inherits Window {
}
}
}
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; clicked => { root.open-download(); root.update-open = false; } }
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.update-open = false; } }
if root.update-status != "": Text {
x: 30px; y: 247px; width: 340px; height: 40px;
text: root.update-status;
color: root.update-failed ? #b23b3b : #3d434d;
font-size: ImGuiFontStyle.body;
wrap: word-wrap;
vertical-alignment: center;
}
if root.update-downloading: Rectangle {
x: 30px; y: 291px; width: 340px; height: 4px;
background: #e2e4e7;
clip: true;
Rectangle {
x: root.update-progress < 0 ? (parent.width + self.width) * (mod(animation-tick(), 1500ms) / 1500ms) - self.width : 0px;
width: parent.width * (root.update-progress < 0 ? 0.25 : root.update-progress);
height: parent.height;
background: #334fd1;
}
}
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; enabled: !root.update-downloading; clicked => { root.start-update(); } }
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.cancel-update(); } }
}
}