From e7a49361f7bebcc7de49c6e3875d70ace9a9b1b0 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Mon, 19 Oct 2026 17:39:50 +0800 Subject: [PATCH] [feat] add a one-time sponsorship prompt --- .../src/config_center/config_center.cpp | 17 +++++ .../desktop/src/config_center/config_center.h | 5 +- .../src/gui/application/gui_application.cpp | 20 +++++ .../assets/localization/localization_data.h | 9 ++- apps/desktop/src/gui/ui/main_window.slint | 76 ++++++++++++++++++- apps/desktop/src/gui/ui/ui_localization.h | 2 + 6 files changed, 125 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/config_center/config_center.cpp b/apps/desktop/src/config_center/config_center.cpp index caa636bb..e71998ff 100644 --- a/apps/desktop/src/config_center/config_center.cpp +++ b/apps/desktop/src/config_center/config_center.cpp @@ -131,6 +131,8 @@ int ConfigCenter::Load() { portable_service_prompt_suppressed_ = ini_->GetBoolValue(section_, "portable_service_prompt_suppressed", portable_service_prompt_suppressed_); + sponsorship_prompt_dismissed_ = + ini_->GetBoolValue(section_, "sponsorship_prompt_dismissed", false); const char* file_transfer_save_path_value = ini_->GetValue(section_, "file_transfer_save_path", nullptr); @@ -221,6 +223,8 @@ int ConfigCenter::Save() { save_remote_previews_.load()); ini_->SetBoolValue(section_, "portable_service_prompt_suppressed", portable_service_prompt_suppressed_); + ini_->SetBoolValue(section_, "sponsorship_prompt_dismissed", + sponsorship_prompt_dismissed_); ini_->SetValue(section_, "file_transfer_save_path", file_transfer_save_path_.c_str()); @@ -395,6 +399,15 @@ int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) { return 0; } +int ConfigCenter::SetSponsorshipPromptDismissed(bool dismissed) { + if (StoreValues({{"sponsorship_prompt_dismissed", + dismissed ? "true" : "false"}}) != 0) { + return -1; + } + sponsorship_prompt_dismissed_ = dismissed; + return 0; +} + // getters ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; } @@ -463,6 +476,10 @@ bool ConfigCenter::IsPortableServicePromptSuppressed() const { return portable_service_prompt_suppressed_; } +bool ConfigCenter::IsSponsorshipPromptDismissed() const { + return sponsorship_prompt_dismissed_; +} + int ConfigCenter::SetFileTransferSavePath(const std::string& path) { if (path.size() >= 512 || std::any_of(path.begin(), path.end(), [](unsigned char c) { return c < 32 || c == 127; })) return -1; std::lock_guard lock(file_path_mutex_); diff --git a/apps/desktop/src/config_center/config_center.h b/apps/desktop/src/config_center/config_center.h index 3b4fc82b..055a761d 100644 --- a/apps/desktop/src/config_center/config_center.h +++ b/apps/desktop/src/config_center/config_center.h @@ -1,7 +1,7 @@ /* * @Author: DI JUNKUN * @Date: 2024-05-29 - * Copyright (c) 2024 by DI JUNKUN, All Rights Reserved. + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */ #ifndef _CONFIG_CENTER_H_ @@ -49,6 +49,7 @@ class ConfigCenter { int SetPrivacyScreen(bool enable_privacy_screen); int SetSaveRemotePreviews(bool enabled); int SetPortableServicePromptSuppressed(bool suppressed); + int SetSponsorshipPromptDismissed(bool dismissed); int SetFileTransferSavePath(const std::string& path); // read config @@ -73,6 +74,7 @@ class ConfigCenter { bool IsEnablePrivacyScreen() const; bool IsSaveRemotePreviews() const; bool IsPortableServicePromptSuppressed() const; + bool IsSponsorshipPromptDismissed() const; std::string GetFileTransferSavePath() const; int Load(); @@ -102,6 +104,7 @@ class ConfigCenter { std::atomic enable_privacy_screen_{true}; std::atomic save_remote_previews_{true}; bool portable_service_prompt_suppressed_ = false; + bool sponsorship_prompt_dismissed_ = false; std::string file_transfer_save_path_ = ""; }; } // namespace crossdesk diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index b49d4e56..4b4ad8db 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -1018,6 +1018,7 @@ struct GuiApplication::SlintUi { bool connection_password_initialized = false; bool connection_dialog_gallery = false; bool portable_service_dialog_initialized = false; + bool sponsorship_prompt_pending = false; std::string recent_model_signature; std::optional recent_group_filter; std::vector recent_filter_groups; @@ -1265,6 +1266,8 @@ void GuiApplication::InitializeUi() { slint::set_xdg_app_id("crossdesk"); #endif ui_ = std::make_unique(); + ui_->sponsorship_prompt_pending = + !config_center_->IsSponsorshipPromptDismissed(); ui_->video_presenter = std::make_unique(*video_renderer_); #if _WIN32 @@ -1746,6 +1749,13 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) { main->on_open_kofi_sponsor([this] { OpenUrl("https://ko-fi.com/kunkundi"); }); + main->on_dismiss_sponsorship([this, main] { + main->set_sponsorship_open(false); + ui_->sponsorship_prompt_pending = false; + if (config_center_->SetSponsorshipPromptDismissed(true) != 0) { + LOG_WARN("Could not save sponsorship prompt dismissal"); + } + }); main->on_check_for_updates([this] { update_checker_->RequestCheck(); SyncMainWindow(); @@ -2299,6 +2309,16 @@ void GuiApplication::Tick() { SyncGallery(); SyncConnectionDialog(); SyncPlatformDialogs(); + // Wait for startup notices and user interactions to finish. Once shown, + // never reopen it during this run, even if saving the dismissal fails. + if (ui_->sponsorship_prompt_pending && + ui_->main->window().is_visible() && + !ui_->main->window().is_minimized() && + ui_->main->get_can_show_sponsorship() && + !ui_->file_dialog_open && !HasActiveSession()) { + ui_->sponsorship_prompt_pending = false; + ui_->main->set_sponsorship_open(true); + } SyncStreamWindow(); SyncServerWindow(); SyncFileTransferNotifications(); diff --git a/apps/desktop/src/gui/assets/localization/localization_data.h b/apps/desktop/src/gui/assets/localization/localization_data.h index 0e4b89e7..0a1f0817 100644 --- a/apps/desktop/src/gui/assets/localization/localization_data.h +++ b/apps/desktop/src/gui/assets/localization/localization_data.h @@ -1,8 +1,9 @@ /* * @Author: DI JUNKUN * @Date: 2024-05-29 - * Copyright (c) 2024 by DI JUNKUN, All Rights Reserved. + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */ + #ifndef _LOCALIZATION_DATA_H_ #define _LOCALIZATION_DATA_H_ @@ -289,6 +290,12 @@ struct TranslationRow { X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \ X(about, u8"关于", "About", u8"О программе") \ X(sponsorship_title, u8"赞助支持", "Sponsorship", u8"Поддержка проекта") \ + X(sponsorship_message, u8"如果 CrossDesk 对你有帮助,欢迎赞助支持项目的持续开发、跨平台测试与日常维护。感谢你的支持!", \ + "If CrossDesk is useful to you, consider supporting its continued development, cross-platform testing, and maintenance. Thank you for your support!", \ + u8"Если CrossDesk вам полезен, поддержите его дальнейшую разработку, тестирование на разных платформах и сопровождение. Спасибо за вашу поддержку!") \ + X(sponsorship_dismiss_hint, u8"关闭后不再自动弹出,你仍可在「关于」中找到赞助入口。", \ + "This prompt will not appear again after you close it. Sponsorship links remain available in About.", \ + u8"После закрытия это окно больше не появится автоматически. Ссылки для поддержки останутся в разделе «О программе».") \ X(sponsor_via_afdian, u8"爱发电", "AFDIAN", "AFDIAN") \ X(notification, u8"通知", "Notification", u8"Уведомление") \ X(new_version, u8"新版本", "New version", u8"Новая версия") \ diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index 8694aa38..df0127cb 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -53,6 +53,8 @@ export global UiStrings { in-out property settings: "Settings"; in-out property about: "About"; in-out property sponsorship-title: "Sponsorship"; + in-out property sponsorship-message; + in-out property sponsorship-dismiss-hint; in-out property sponsor-via-afdian: "AFDIAN"; in-out property ok: "OK"; in-out property cancel: "Cancel"; @@ -127,14 +129,15 @@ export global UiStrings { in-out property release-date-label: "Release Date: "; } -// Keep sponsor labels, order, and sizing consistent across both dialogs. +// Keep sponsor labels, order, and sizing consistent across dialogs. component SponsorshipSection inherits VerticalLayout { + in property show-title: true; callback open-afdian; callback open-kofi; callback open-github; vertical-stretch: 0; spacing: 10px; - Text { + if root.show-title: Text { text: UiStrings.sponsorship-title; color: WorkbenchStyle.text; font-size: 12px; font-weight: 600; wrap: word-wrap; @@ -492,6 +495,7 @@ export component MainWindow inherits Window { callback open-afdian-sponsor; callback open-github-sponsor; callback open-kofi-sponsor; + callback dismiss-sponsorship; callback check-for-updates; callback signal-reconnect-requested; callback connection-cancel; @@ -517,6 +521,20 @@ export component MainWindow inherits Window { if !root.self-host-settings-open { root.self-host-settings-error = ""; } } in-out property about-open: false; + in-out property sponsorship-open: false; + // Automatic prompts must not interrupt another dialog or menu. + private property sponsorship-unobstructed: !root.gallery-mode + && !root.menu-open && !root.settings-open + && !root.self-host-settings-open && !root.about-open + && !(root.update-open && root.update-available) && !root.messages-open + && !root.reset-password-open && !root.alias-open && !root.delete-open + && !root.device-group-open && !root.connection-dialog-open + && !root.permission-dialog-open && !root.wayland-notice-open + && !root.portable-service-dialog-open + && !root.portable-service-suppressed-notice-open + && root.offline-warning == ""; + out property can-show-sponsorship: !root.sponsorship-open + && root.sponsorship-unobstructed; // Follow the first available update. Dismissing the dialog replaces this // binding, so background checks do not reopen it during this launch. in-out property update-open: root.update-available; @@ -574,6 +592,10 @@ export component MainWindow inherits Window { if root.about-open { focus-sink.focus(); } } + changed sponsorship-open => { + if root.sponsorship-open { focus-sink.focus(); } + } + reset-remote-id => { root.remote-id-input = ""; remote-id-editor.focus(); @@ -799,6 +821,10 @@ export component MainWindow inherits Window { if event.text == Key.Escape { root.about-open = false; } return accept; } + if root.sponsorship-open && root.sponsorship-unobstructed { + if event.text == Key.Escape { root.dismiss-sponsorship(); } + return accept; + } if root.messages-open { if event.text == Key.Escape { root.messages-open = false; } return accept; @@ -2253,6 +2279,52 @@ export component MainWindow inherits Window { } } + if root.sponsorship-open && root.sponsorship-unobstructed: Rectangle { + background: #15223850; + z: 38; + TouchArea { clicked => { focus-sink.focus(); } } + SettingsDialog { + x: (parent.width - self.width) / 2; + y: (parent.height - self.height) / 2; + width: min(480px, parent.width - 40px); + height: self.content-top + sponsorship-content.preferred-height + 24px; + title: UiStrings.sponsorship-title; + close-label: MessageStrings.close; + close => { root.dismiss-sponsorship(); } + sponsorship-content := VerticalLayout { + x: 24px; y: parent.content-top; + width: parent.width - 48px; height: self.preferred-height; + padding: 0px; spacing: 18px; + Text { + text: UiStrings.sponsorship-message; + color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap; + } + SponsorshipSection { + show-title: false; + open-afdian => { root.open-afdian-sponsor(); } + open-kofi => { root.open-kofi-sponsor(); } + open-github => { root.open-github-sponsor(); } + } + Text { + text: UiStrings.sponsorship-dismiss-hint; + color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap; + } + Rectangle { height: 1px; background: WorkbenchStyle.separator; } + HorizontalLayout { + alignment: end; + SettingsButton { + width: max(80px, sponsorship-close-metrics.preferred-width + 24px); + primary: true; text: MessageStrings.close; + clicked => { root.dismiss-sponsorship(); } + sponsorship-close-metrics := Text { + visible: false; text: MessageStrings.close; font-size: 12px; + } + } + } + } + } + } + if root.about-open: Rectangle { background: #15223850; z: 46; diff --git a/apps/desktop/src/gui/ui/ui_localization.h b/apps/desktop/src/gui/ui/ui_localization.h index f84a1703..c23a15f8 100644 --- a/apps/desktop/src/gui/ui/ui_localization.h +++ b/apps/desktop/src/gui/ui/ui_localization.h @@ -75,6 +75,8 @@ inline int ApplyMainWindowStrings( strings.set_settings(Text(localization::settings[language])); strings.set_about(Text(localization::about[language])); strings.set_sponsorship_title(Text(localization::sponsorship_title[language])); + strings.set_sponsorship_message(Text(localization::sponsorship_message[language])); + strings.set_sponsorship_dismiss_hint(Text(localization::sponsorship_dismiss_hint[language])); strings.set_sponsor_via_afdian(Text(localization::sponsor_via_afdian[language])); strings.set_ok(Text(localization::ok[language])); strings.set_cancel(Text(localization::cancel[language]));