mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-23 04:53:06 +08:00
[feat] add in-app updates for desktop installations
This commit is contained in:
@@ -86,6 +86,9 @@
|
||||
#include "ui/release_notes_text.h"
|
||||
#include "ui/ui_localization.h"
|
||||
#include "update_checker.h"
|
||||
#if CROSSDESK_HAS_UPDATER
|
||||
#include "updater/update_controller.h"
|
||||
#endif
|
||||
#include "window_geometry.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -1134,6 +1137,9 @@ int GuiApplication::Run() {
|
||||
#endif
|
||||
InitializeModules();
|
||||
update_checker_ = std::make_unique<UpdateChecker>(CROSSDESK_VERSION);
|
||||
#if CROSSDESK_HAS_UPDATER
|
||||
updater_ = std::make_unique<updater::Controller>(path_manager_->GetCachePath());
|
||||
#endif
|
||||
InitializeUi();
|
||||
#ifndef __OHOS__
|
||||
ui_->activation_receiver = std::move(activation_receiver);
|
||||
@@ -1735,8 +1741,24 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
|
||||
? UiText(std::to_string(signal_port))
|
||||
: slint::SharedString{});
|
||||
});
|
||||
main->on_cancel_update([this] {
|
||||
#if CROSSDESK_HAS_UPDATER
|
||||
if (updater_ && updater_->busy()) updater_->Cancel();
|
||||
#endif
|
||||
});
|
||||
main->on_open_download([this] {
|
||||
const auto& latest = update_checker_->latest();
|
||||
#if CROSSDESK_HAS_UPDATER
|
||||
if (updater_ && updater_->busy()) return;
|
||||
if (updater_ && latest && updater_->Available(latest->update_envelope)) {
|
||||
const int language = localization::detail::ClampLanguageIndex(localization_language_index_);
|
||||
if (tinyfd_messageBox("CrossDesk", localization::install_update_confirm[language].c_str(),
|
||||
"okcancel", "question", 0)) {
|
||||
updater_->Start(latest->update_envelope);
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
OpenUrl(latest && !latest->download_url.empty() ? latest->download_url
|
||||
: "https://crossdesk.cn");
|
||||
});
|
||||
@@ -2326,6 +2348,36 @@ void GuiApplication::Tick() {
|
||||
}
|
||||
|
||||
void GuiApplication::PollUpdateCheck() {
|
||||
#if CROSSDESK_HAS_UPDATER
|
||||
if (updater_) {
|
||||
// The event loop is running and initialization has completed.
|
||||
static bool startup_confirmed = false;
|
||||
if (!startup_confirmed) {
|
||||
updater_->ConfirmStartup();
|
||||
startup_confirmed = true;
|
||||
}
|
||||
if (updater_->Poll()) {
|
||||
exit_ = true;
|
||||
return;
|
||||
}
|
||||
const auto& latest = update_checker_->latest();
|
||||
const bool can_install = latest && updater_->Available(latest->update_envelope);
|
||||
const int language = localization::detail::ClampLanguageIndex(localization_language_index_);
|
||||
std::string status;
|
||||
if (updater_->busy()) {
|
||||
status = localization::update_preparing[language];
|
||||
if (updater_->state() == updater::Controller::State::Downloading)
|
||||
status += " " + std::to_string(updater_->progress()) + "%";
|
||||
} else if (updater_->state() == updater::Controller::State::Failed) {
|
||||
status = localization::update_install_failed[language] + "\n" + updater_->error();
|
||||
}
|
||||
for (const auto& main : {ui_->main, ui_->wall}) {
|
||||
main->set_update_install_available(can_install);
|
||||
main->set_update_install_busy(updater_->busy());
|
||||
main->set_update_install_status(UiText(status));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// 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() ||
|
||||
@@ -6163,6 +6215,9 @@ void GuiApplication::Cleanup() {
|
||||
if (update_checker_) {
|
||||
update_checker_->Stop();
|
||||
}
|
||||
#if CROSSDESK_HAS_UPDATER
|
||||
updater_.reset();
|
||||
#endif
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
namespace crossdesk {
|
||||
|
||||
class UpdateChecker;
|
||||
namespace updater { class Controller; }
|
||||
|
||||
// Slint application shell. GuiRuntime remains the owner of transport, media,
|
||||
// device, clipboard, transfer and settings behavior.
|
||||
@@ -126,6 +127,9 @@ private:
|
||||
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
std::unique_ptr<UpdateChecker> update_checker_;
|
||||
#if CROSSDESK_HAS_UPDATER
|
||||
std::unique_ptr<updater::Controller> updater_;
|
||||
#endif
|
||||
CursorStateProvider cursor_state_provider_;
|
||||
std::unordered_map<std::string, CursorDeliveryState>
|
||||
cursor_delivery_states_;
|
||||
|
||||
@@ -317,6 +317,13 @@ struct TranslationRow {
|
||||
X(access_website, u8"访问官网: ", \
|
||||
"Access Website: ", u8"Официальный сайт: ") \
|
||||
X(update, u8"更新", "Update", u8"Обновить") \
|
||||
X(install_update, u8"安装更新", "Install update", u8"Установить обновление") \
|
||||
X(manual_download, u8"手动下载", "Download", u8"Скачать") \
|
||||
X(install_update_confirm, u8"下载完成后将关闭 CrossDesk 并安装更新,当前连接和文件传输会中断。安装需要系统授权,完成后重新启动。是否继续?", \
|
||||
"CrossDesk will close after downloading the update. Connections and file transfers will be interrupted. Installation requires system authorization, then CrossDesk will restart. Continue?", \
|
||||
u8"После загрузки CrossDesk закроется для обновления. Соединения и передача файлов будут прерваны. Потребуется разрешение системы, затем приложение перезапустится. Продолжить?") \
|
||||
X(update_preparing, u8"正在准备更新…", "Preparing update…", u8"Подготовка обновления…") \
|
||||
X(update_install_failed, u8"更新未完成,可重试或手动安装。", "Update did not complete. Retry or install manually.", u8"Обновление не завершено. Повторите или установите вручную.") \
|
||||
X(connection_alias, u8"修改名称", "Edit Alias", \
|
||||
u8"Изменить имя подключения") \
|
||||
X(delete_connection, u8"删除连接", "Delete Connection", \
|
||||
|
||||
@@ -99,6 +99,7 @@ export global UiStrings {
|
||||
in-out property <string> view-release-notes: "Release notes";
|
||||
in-out property <string> later: "Later";
|
||||
in-out property <string> download: "Download";
|
||||
in-out property <string> install-update: "Install update";
|
||||
in-out property <string> input-password: "Input password";
|
||||
in-out property <string> reinput-password: "Incorrect password, please try again";
|
||||
in-out property <string> remember-password: "Remember password";
|
||||
@@ -408,6 +409,10 @@ export component MainWindow inherits Window {
|
||||
callback read-all-announcements;
|
||||
callback load-more-announcements;
|
||||
in property <bool> update-available: false;
|
||||
in property <bool> update-install-available: false;
|
||||
in property <bool> update-install-busy: false;
|
||||
in property <string> update-install-status: "";
|
||||
private property <string> update-action-text: root.update-install-available ? UiStrings.install-update : UiStrings.download;
|
||||
in property <UpdateCheckStatus> update-check-status: idle;
|
||||
private property <string> update-status-text:
|
||||
root.update-check-status == UpdateCheckStatus.checking ? UiStrings.checking-for-updates :
|
||||
@@ -460,7 +465,7 @@ export component MainWindow inherits Window {
|
||||
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(); } }
|
||||
changed autostart-guide-open => { if root.autostart-guide-open { root.dialog-focus-pending = true; } }
|
||||
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.
|
||||
@@ -492,6 +497,7 @@ export component MainWindow inherits Window {
|
||||
callback cancel-self-hosted-settings;
|
||||
callback browse-save-path;
|
||||
callback open-download;
|
||||
callback cancel-update;
|
||||
callback open-website;
|
||||
callback open-afdian-sponsor;
|
||||
callback open-github-sponsor;
|
||||
@@ -558,6 +564,23 @@ export component MainWindow inherits Window {
|
||||
private property <bool> recent-tooltip-online: false;
|
||||
private property <length> recent-tooltip-pointer-x: 0px;
|
||||
private property <length> recent-tooltip-pointer-y: 0px;
|
||||
private property <bool> dialog-focus-pending: false;
|
||||
|
||||
// A changed handler can run lazily while winit's accessibility adapter
|
||||
// borrows its tree. Switching focus there reenters that adapter and panics.
|
||||
// Coalesce requests and move focus on the next event-loop turn instead.
|
||||
Timer {
|
||||
interval: 1ms;
|
||||
running: root.dialog-focus-pending;
|
||||
triggered => {
|
||||
root.dialog-focus-pending = false;
|
||||
if root.settings-open || root.autostart-guide-open
|
||||
|| root.wayland-notice-open || root.messages-open || root.about-open
|
||||
|| (root.sponsorship-open && root.sponsorship-unobstructed) {
|
||||
focus-sink.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changed menu-open => {
|
||||
if root.menu-open { root.copy-feedback = CopyFeedback.idle; root.gallery-action("release", ""); }
|
||||
@@ -575,26 +598,26 @@ export component MainWindow inherits Window {
|
||||
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();
|
||||
root.dialog-focus-pending = true;
|
||||
} else {
|
||||
root.autostart-guide-open = false;
|
||||
}
|
||||
}
|
||||
|
||||
changed wayland-notice-open => {
|
||||
if root.wayland-notice-open { focus-sink.focus(); }
|
||||
if root.wayland-notice-open { root.dialog-focus-pending = true; }
|
||||
}
|
||||
|
||||
changed messages-open => {
|
||||
if root.messages-open { focus-sink.focus(); }
|
||||
if root.messages-open { root.dialog-focus-pending = true; }
|
||||
}
|
||||
|
||||
changed about-open => {
|
||||
if root.about-open { focus-sink.focus(); }
|
||||
if root.about-open { root.dialog-focus-pending = true; }
|
||||
}
|
||||
|
||||
changed sponsorship-open => {
|
||||
if root.sponsorship-open { focus-sink.focus(); }
|
||||
if root.sponsorship-open { root.dialog-focus-pending = true; }
|
||||
}
|
||||
|
||||
reset-remote-id => {
|
||||
@@ -2452,9 +2475,10 @@ export component MainWindow inherits Window {
|
||||
height: 32px;
|
||||
primary: true;
|
||||
text-size: 12px;
|
||||
text: UiStrings.download;
|
||||
clicked => { root.open-website(); }
|
||||
about-update-button-metrics := Text { visible: false; text: UiStrings.download; font-size: 12px; }
|
||||
text: root.update-action-text;
|
||||
enabled: !root.update-install-busy;
|
||||
clicked => { root.open-download(); root.about-open = false; root.update-open = true; }
|
||||
about-update-button-metrics := Text { visible: false; text: root.update-action-text; font-size: 12px; }
|
||||
}
|
||||
SettingsButton {
|
||||
width: max(116px, check-button-metrics.preferred-width + 24px);
|
||||
@@ -2514,14 +2538,14 @@ export component MainWindow inherits Window {
|
||||
width: update-website-label.preferred-width;
|
||||
accessible-role: button;
|
||||
accessible-label: update-website-label.text;
|
||||
accessible-action-default => { root.open-download(); }
|
||||
accessible-action-default => { root.open-website(); }
|
||||
update-website-label := Text {
|
||||
text: UiStrings.access-website + "https://crossdesk.cn";
|
||||
color: website-touch.has-hover ? #254cc0 : WorkbenchStyle.accent;
|
||||
font-size: 13px;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
|
||||
website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-website(); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2588,6 +2612,14 @@ export component MainWindow inherits Window {
|
||||
vertical-stretch: 0;
|
||||
spacing: 16px;
|
||||
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
|
||||
if root.update-install-status != "": Text {
|
||||
text: root.update-install-status;
|
||||
font-size: 12px;
|
||||
color: WorkbenchStyle.muted;
|
||||
wrap: word-wrap;
|
||||
max-height: 64px;
|
||||
overflow: elide;
|
||||
}
|
||||
HorizontalLayout {
|
||||
alignment: end;
|
||||
spacing: 8px;
|
||||
@@ -2595,14 +2627,16 @@ export component MainWindow inherits Window {
|
||||
width: 88px;
|
||||
height: 34px;
|
||||
text: UiStrings.later;
|
||||
clicked => { root.update-open = false; }
|
||||
clicked => { root.cancel-update(); root.update-open = false; }
|
||||
}
|
||||
WorkbenchButton {
|
||||
width: 88px;
|
||||
width: max(110px, update-action-metrics.preferred-width + 24px);
|
||||
height: 34px;
|
||||
primary: true;
|
||||
text: UiStrings.download;
|
||||
clicked => { root.open-download(); root.update-open = false; }
|
||||
enabled: !root.update-install-busy;
|
||||
text: root.update-action-text;
|
||||
clicked => { root.open-download(); }
|
||||
update-action-metrics := Text { visible: false; text: root.update-action-text; font-size: 12px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,8 @@ inline int ApplyMainWindowStrings(
|
||||
Text(localization::update_check_failed[language]));
|
||||
strings.set_release_notes(Text(localization::release_notes[language]));
|
||||
strings.set_view_release_notes(Text(localization::view_release_notes[language]));
|
||||
strings.set_download(Text(localization::update[language]));
|
||||
strings.set_download(Text(localization::manual_download[language]));
|
||||
strings.set_install_update(Text(localization::install_update[language]));
|
||||
strings.set_input_password(Text(localization::input_password[language]));
|
||||
strings.set_reinput_password(Text(localization::reinput_password[language]));
|
||||
strings.set_remember_password(
|
||||
|
||||
Reference in New Issue
Block a user