diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp
index a51e6829..d241d03d 100644
--- a/apps/desktop/src/gui/application/gui_application.cpp
+++ b/apps/desktop/src/gui/application/gui_application.cpp
@@ -1726,6 +1726,15 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
OpenUrl(latest && !latest->download_url.empty() ? latest->download_url
: "https://crossdesk.cn");
});
+ main->on_open_afdian_sponsor([this] {
+ OpenUrl("https://afdian.com/a/kunkundi");
+ });
+ main->on_open_github_sponsor([this] {
+ OpenUrl("https://github.com/sponsors/kunkundi");
+ });
+ main->on_open_kofi_sponsor([this] {
+ OpenUrl("https://ko-fi.com/kunkundi");
+ });
main->on_check_for_updates([this] {
update_checker_->RequestCheck();
SyncMainWindow();
diff --git a/apps/desktop/src/gui/assets/localization/localization_data.h b/apps/desktop/src/gui/assets/localization/localization_data.h
index 5b222fa7..b2931721 100644
--- a/apps/desktop/src/gui/assets/localization/localization_data.h
+++ b/apps/desktop/src/gui/assets/localization/localization_data.h
@@ -288,6 +288,8 @@ struct TranslationRow {
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
X(about, u8"关于", "About", u8"О программе") \
+ X(sponsorship_title, u8"赞助支持", "Sponsorship", u8"Поддержка проекта") \
+ X(sponsor_via_afdian, u8"爱发电", "AFDIAN", "AFDIAN") \
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version, u8"新版本", "New version", u8"Новая версия") \
X(new_version_available, u8"新版本可用", "New Version Available", \
diff --git a/apps/desktop/src/gui/ui/icons/sponsors/SOURCES.md b/apps/desktop/src/gui/ui/icons/sponsors/SOURCES.md
new file mode 100644
index 00000000..113b5288
--- /dev/null
+++ b/apps/desktop/src/gui/ui/icons/sponsors/SOURCES.md
@@ -0,0 +1,16 @@
+# Sponsorship platform icons
+
+These platform marks identify external sponsorship links. They remain the
+property of their respective owners and are not covered by CrossDesk's own
+artwork license. Retrieved on 2026-10-19.
+
+- `afdian.png`: [AFDIAN](https://afdian.com/) navigation logo, downloaded from
+ [its official CDN](https://static.afdiancdn.com/static/img/logo/logo.png)
+ without modification (80 × 80).
+- `kofi.png`: cup icon from [Ko-fi's brand assets](https://more.ko-fi.com/brand-assets),
+ downloaded from [its official asset host](https://cdn.prod.website-files.com/5c14e387dab576fe667689cf/670f5a01229bf8a18f97a3c1_favion.avif),
+ converted to PNG and proportionally resized to 64 × 64.
+- `github.svg`: unmodified `GitHub_Invertocat_Black.svg` from the
+ [official logo archive](https://brand.github.com/GitHub_Logos.zip).
+ [GitHub's brand guidelines](https://brand.github.com/foundations/logo)
+ permit the logo to identify a link to GitHub.
diff --git a/apps/desktop/src/gui/ui/icons/sponsors/afdian.png b/apps/desktop/src/gui/ui/icons/sponsors/afdian.png
new file mode 100644
index 00000000..efe0f4b7
Binary files /dev/null and b/apps/desktop/src/gui/ui/icons/sponsors/afdian.png differ
diff --git a/apps/desktop/src/gui/ui/icons/sponsors/github.svg b/apps/desktop/src/gui/ui/icons/sponsors/github.svg
new file mode 100644
index 00000000..4f46bf35
--- /dev/null
+++ b/apps/desktop/src/gui/ui/icons/sponsors/github.svg
@@ -0,0 +1,10 @@
+
diff --git a/apps/desktop/src/gui/ui/icons/sponsors/kofi.png b/apps/desktop/src/gui/ui/icons/sponsors/kofi.png
new file mode 100644
index 00000000..15a8ec22
Binary files /dev/null and b/apps/desktop/src/gui/ui/icons/sponsors/kofi.png differ
diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint
index b804989b..75671b36 100644
--- a/apps/desktop/src/gui/ui/main_window.slint
+++ b/apps/desktop/src/gui/ui/main_window.slint
@@ -35,6 +35,7 @@ export struct ReleaseNoteBlock {
export enum UpdateCheckStatus { idle, checking, up-to-date, available, failed }
export enum AutostartStatus { checking, enabled, disabled, unsupported, failed }
enum CopyFeedback { idle, success, failed }
+enum SponsorPlatform { afdian, kofi, github }
export global UiStrings {
in-out property local-desktop: "Local Desktop";
@@ -51,6 +52,8 @@ export global UiStrings {
in-out property connect: "Connect";
in-out property settings: "Settings";
in-out property about: "About";
+ in-out property sponsorship-title: "Sponsorship";
+ in-out property sponsor-via-afdian: "AFDIAN";
in-out property ok: "OK";
in-out property cancel: "Cancel";
in-out property save: "Save";
@@ -124,6 +127,48 @@ export global UiStrings {
in-out property release-date-label: "Release Date: ";
}
+// Keep sponsor labels, order, and sizing consistent across both dialogs.
+component SponsorshipSection inherits VerticalLayout {
+ callback open-afdian;
+ callback open-kofi;
+ callback open-github;
+ vertical-stretch: 0;
+ spacing: 10px;
+ Text {
+ text: UiStrings.sponsorship-title;
+ color: WorkbenchStyle.text; font-size: 12px; font-weight: 600;
+ wrap: word-wrap;
+ }
+ HorizontalLayout {
+ spacing: 8px;
+ for platform in (GalleryStrings.language == 0
+ ? [SponsorPlatform.afdian, SponsorPlatform.kofi, SponsorPlatform.github]
+ : [SponsorPlatform.kofi, SponsorPlatform.github, SponsorPlatform.afdian]): Rectangle {
+ // Size to the label, icon, and padding, then share extra row space.
+ min-width: sponsor-label-metrics.preferred-width + 56px;
+ preferred-width: self.min-width;
+ horizontal-stretch: 1;
+ height: 32px;
+ SettingsButton {
+ width: parent.width; height: parent.height;
+ icon: platform == SponsorPlatform.afdian ? @image-url("icons/sponsors/afdian.png")
+ : platform == SponsorPlatform.kofi ? @image-url("icons/sponsors/kofi.png")
+ : @image-url("icons/sponsors/github.svg");
+ text: platform == SponsorPlatform.afdian ? UiStrings.sponsor-via-afdian
+ : platform == SponsorPlatform.kofi ? "Ko-fi" : "GitHub Sponsors";
+ sponsor-label-metrics := Text {
+ visible: false; text: parent.text; font-size: 12px;
+ }
+ clicked => {
+ if platform == SponsorPlatform.afdian { root.open-afdian(); }
+ else if platform == SponsorPlatform.kofi { root.open-kofi(); }
+ else { root.open-github(); }
+ }
+ }
+ }
+ }
+}
+
// Prompt copy in modal dialogs sits slightly low in the legacy ImGui layout.
// Offset only the rendered text so input fields and action buttons keep their
// established geometry.
@@ -444,6 +489,9 @@ export component MainWindow inherits Window {
callback cancel-self-hosted-settings;
callback browse-save-path;
callback open-download;
+ callback open-afdian-sponsor;
+ callback open-github-sponsor;
+ callback open-kofi-sponsor;
callback check-for-updates;
callback signal-reconnect-requested;
callback connection-cancel;
@@ -2211,102 +2259,123 @@ export component MainWindow inherits Window {
TouchArea { clicked => { focus-sink.focus(); } }
SettingsDialog {
x: (parent.width - self.width) / 2;
- y: (parent.height - self.height) / 2;
- width: min(400px, parent.width - 40px);
- height: self.content-top + about-content.preferred-height + 24px;
+ y: (root.height - self.height) / 2;
+ width: min(480px, parent.width - 40px);
+ min-height: 0px; max-height: root.height - 32px;
+ height: min(root.height - 32px, self.content-top + about-content.preferred-height
+ + 16px + about-footer.preferred-height + 24px);
title: UiStrings.about;
close-label: MessageStrings.close;
close => { root.about-open = false; }
- // Content-driven height keeps translated update states and actions
- // above the footer without reserving an empty update section.
- about-content := VerticalLayout {
- x: 24px; y: parent.content-top; width: parent.width - 48px;
- height: self.preferred-height;
- padding: 0px; spacing: 20px;
- Rectangle {
- height: max(96px, product-info.preferred-height + 32px);
- background: WorkbenchStyle.surface;
- border-radius: UiRadius.surface;
- border-width: 1px; border-color: WorkbenchStyle.border;
- Image {
- x: 16px; y: (parent.height - self.height) / 2;
- width: 56px; height: 56px;
- source: @image-url("../../../resources/linux/crossdesk_128x128.png");
- image-fit: contain;
- }
- product-info := VerticalLayout {
- x: 88px; y: 16px; width: parent.width - 104px; height: parent.height - 32px;
- alignment: center; spacing: 5px;
- Text { text: "CrossDesk"; font-size: 22px; font-weight: 600; color: WorkbenchStyle.text; }
- Text {
- text: UiStrings.version + " " + root.current-version;
- color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
+ // Preserve section spacing and keep actions visible when longer
+ // translations need scrolling inside a small window.
+ about-scroll := ScrollView {
+ x: 24px; y: parent.content-top;
+ width: parent.width - 48px + 22px;
+ height: about-footer.y - self.y - 16px;
+ viewport-width: self.visible-width;
+ viewport-height: about-content.preferred-height;
+ horizontal-scrollbar-policy: always-off;
+ vertical-scrollbar-policy: as-needed;
+ about-content := VerticalLayout {
+ min-width: 0px;
+ width: about-scroll.width; height: self.preferred-height;
+ // Keep the expanding scrollbar outside the content.
+ padding: 0px; padding-right: 22px; spacing: 16px;
+ Rectangle {
+ height: max(80px, product-info.preferred-height + 32px);
+ background: WorkbenchStyle.surface;
+ border-radius: UiRadius.surface;
+ border-width: 1px; border-color: WorkbenchStyle.border;
+ Image {
+ x: 16px; y: (parent.height - self.height) / 2;
+ width: 56px; height: 56px;
+ source: @image-url("../../../resources/linux/crossdesk_128x128.png");
+ image-fit: contain;
}
- }
- }
- if root.update-available || root.update-status-text != "": Rectangle {
- height: update-summary.preferred-height + 24px;
- background: root.update-check-status == UpdateCheckStatus.failed ? #fef2f2 : WorkbenchStyle.field-surface;
- border-radius: UiRadius.control;
- border-width: 1px;
- border-color: root.update-check-status == UpdateCheckStatus.failed ? #f3d4d1 : WorkbenchStyle.separator;
- update-summary := VerticalLayout {
- x: 12px; y: 12px; width: parent.width - 24px;
- height: self.preferred-height;
- padding: 0px; spacing: 10px;
- if root.update-available: Rectangle {
- height: version-link.preferred-height;
- accessible-role: button;
- accessible-label: UiStrings.release-notes + " " + root.latest-version;
- accessible-action-default => { version-touch.clicked(); }
- version-link := Text {
- width: parent.width;
- text: UiStrings.update-available + ": " + root.latest-version;
- color: version-touch.has-hover ? #254cc0 : WorkbenchStyle.accent;
- font-size: 13px; font-weight: 500; 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
- : root.update-check-status == UpdateCheckStatus.up-to-date ? WorkbenchStyle.success : WorkbenchStyle.muted;
- font-size: 12px; wrap: word-wrap;
- }
- if root.update-available: HorizontalLayout {
- alignment: start;
- SettingsButton {
- width: max(116px, release-notes-button-metrics.preferred-width + 24px);
- text: UiStrings.view-release-notes;
- clicked => { root.about-open = false; root.update-open = true; }
- release-notes-button-metrics := Text { visible: false; text: UiStrings.view-release-notes; font-size: 12px; }
+ product-info := VerticalLayout {
+ x: 88px; y: 16px; width: parent.width - 104px; height: parent.height - 32px;
+ alignment: center; spacing: 5px;
+ Text { text: "CrossDesk"; font-size: 22px; font-weight: 600; color: WorkbenchStyle.text; }
+ Text {
+ text: UiStrings.version + " " + root.current-version;
+ color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
}
}
}
- }
- VerticalLayout {
- spacing: 5px;
- Text { text: UiStrings.copyright; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
- Text { text: UiStrings.license; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
- }
- VerticalLayout {
- spacing: 16px;
- Rectangle { height: 1px; background: WorkbenchStyle.separator; }
- HorizontalLayout {
- alignment: end; spacing: 8px;
- SettingsButton {
- width: max(116px, check-button-metrics.preferred-width + 24px);
- text: UiStrings.check-for-updates;
- enabled: root.update-check-status != UpdateCheckStatus.checking;
- clicked => { root.check-for-updates(); }
- check-button-metrics := Text { visible: false; text: UiStrings.check-for-updates; font-size: 12px; }
+ if root.update-available || root.update-status-text != "": Rectangle {
+ height: update-summary.preferred-height + 24px;
+ background: root.update-check-status == UpdateCheckStatus.failed ? #fef2f2 : WorkbenchStyle.field-surface;
+ border-radius: UiRadius.control;
+ border-width: 1px;
+ border-color: root.update-check-status == UpdateCheckStatus.failed ? #f3d4d1 : WorkbenchStyle.separator;
+ update-summary := VerticalLayout {
+ x: 12px; y: 12px; width: parent.width - 24px;
+ height: self.preferred-height;
+ padding: 0px; spacing: 10px;
+ if root.update-available: HorizontalLayout {
+ spacing: 12px;
+ height: max(32px, version-link.preferred-height);
+ Rectangle {
+ min-width: 0px; horizontal-stretch: 1;
+ accessible-role: button;
+ accessible-label: UiStrings.release-notes + " " + root.latest-version;
+ accessible-action-default => { version-touch.clicked(); }
+ version-link := Text {
+ width: parent.width; height: parent.height;
+ text: UiStrings.update-available + ": " + root.latest-version;
+ color: version-touch.has-hover ? #254cc0 : WorkbenchStyle.accent;
+ font-size: 13px; font-weight: 500; wrap: word-wrap;
+ vertical-alignment: center;
+ }
+ version-touch := TouchArea {
+ mouse-cursor: pointer;
+ clicked => { root.about-open = false; root.update-open = true; }
+ }
+ }
+ SettingsButton {
+ width: max(116px, release-notes-button-metrics.preferred-width + 24px);
+ text: UiStrings.view-release-notes;
+ clicked => { root.about-open = false; root.update-open = true; }
+ release-notes-button-metrics := Text { visible: false; text: UiStrings.view-release-notes; font-size: 12px; }
+ }
+ }
+ if root.update-status-text != "": Text {
+ text: root.update-status-text;
+ color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e
+ : root.update-check-status == UpdateCheckStatus.up-to-date ? WorkbenchStyle.success : WorkbenchStyle.muted;
+ font-size: 12px; wrap: word-wrap;
+ }
}
- SettingsButton { width: 80px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
}
+ SponsorshipSection {
+ open-afdian => { root.open-afdian-sponsor(); }
+ open-kofi => { root.open-kofi-sponsor(); }
+ open-github => { root.open-github-sponsor(); }
+ }
+ VerticalLayout {
+ spacing: 5px;
+ Text { text: UiStrings.copyright; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
+ Text { text: UiStrings.license; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
+ }
+ }
+ }
+ about-footer := VerticalLayout {
+ x: 24px; y: parent.height - self.height - 24px;
+ width: parent.width - 48px; height: self.preferred-height;
+ spacing: 16px;
+ Rectangle { height: 1px; background: WorkbenchStyle.separator; }
+ HorizontalLayout {
+ alignment: end; spacing: 8px;
+ SettingsButton {
+ width: max(116px, check-button-metrics.preferred-width + 24px);
+ text: UiStrings.check-for-updates;
+ enabled: root.update-check-status != UpdateCheckStatus.checking;
+ clicked => { root.check-for-updates(); }
+ check-button-metrics := Text { visible: false; text: UiStrings.check-for-updates; font-size: 12px; }
+ }
+ SettingsButton { width: 80px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
}
}
}
@@ -2320,8 +2389,9 @@ export component MainWindow inherits Window {
DialogSurface {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
- width: 464px;
- height: 400px;
+ width: min(480px, parent.width - 40px);
+ min-height: 0px; max-height: root.height - 32px;
+ height: min(488px, root.height - 32px);
border-radius: UiRadius.surface;
border-color: WorkbenchStyle.border;
drop-shadow-color: #07142b24;
@@ -2360,7 +2430,7 @@ export component MainWindow inherits Window {
update-website-label := Text {
text: UiStrings.access-website + "https://crossdesk.cn";
color: website-touch.has-hover ? #254cc0 : WorkbenchStyle.accent;
- font-size: 11px;
+ font-size: 13px;
vertical-alignment: center;
}
website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
@@ -2369,6 +2439,7 @@ export component MainWindow inherits Window {
}
VerticalLayout {
+ vertical-stretch: 1;
spacing: 8px;
Text {
height: 18px;
@@ -2419,6 +2490,12 @@ export component MainWindow inherits Window {
}
}
+ SponsorshipSection {
+ open-afdian => { root.open-afdian-sponsor(); }
+ open-kofi => { root.open-kofi-sponsor(); }
+ open-github => { root.open-github-sponsor(); }
+ }
+
VerticalLayout {
vertical-stretch: 0;
spacing: 16px;
diff --git a/apps/desktop/src/gui/ui/settings_controls.slint b/apps/desktop/src/gui/ui/settings_controls.slint
index c5f7350a..1e244176 100644
--- a/apps/desktop/src/gui/ui/settings_controls.slint
+++ b/apps/desktop/src/gui/ui/settings_controls.slint
@@ -5,10 +5,13 @@ import { WorkbenchStyle } from "workbench_controls.slint";
// Shared visual treatment for the global settings pages.
export component SettingsButton inherits Rectangle {
in property text;
+ in property icon;
in property primary: false;
in property quiet: false;
in property align-left: false;
in property enabled: true;
+ private property has-icon: root.icon.width > 0;
+ private property content-padding: root.has-icon ? 8px : 12px;
callback clicked();
width: 88px; height: 32px; border-radius: UiRadius.control;
background: !root.enabled ? #f0f3f7
@@ -19,11 +22,26 @@ export component SettingsButton inherits Rectangle {
: touch.has-hover ? WorkbenchStyle.hover-border : WorkbenchStyle.control-border;
accessible-role: button; accessible-label: root.text;
accessible-action-default => { if root.enabled { root.clicked(); } }
- Text {
- x: 12px; width: parent.width - 24px;
- text: root.text; font-size: 12px; font-weight: root.primary ? 600 : 400;
- color: !root.enabled ? #98a2b3 : root.primary ? white : root.quiet ? #65758b : #3c4d63;
- horizontal-alignment: root.align-left ? left : center; vertical-alignment: center; overflow: elide;
+ Rectangle {
+ x: root.align-left ? root.content-padding : (parent.width - self.width) / 2;
+ width: root.has-icon
+ ? min(parent.width - 2 * root.content-padding, button-label.preferred-width + 24px)
+ : parent.width - 2 * root.content-padding;
+ height: parent.height;
+ if root.has-icon: Image {
+ x: 0px; y: (parent.height - self.height) / 2;
+ width: 18px; height: 18px;
+ source: root.icon; image-fit: contain;
+ opacity: root.enabled ? 1 : 0.45;
+ accessible-role: none;
+ }
+ button-label := Text {
+ x: root.has-icon ? 24px : 0px;
+ width: parent.width - self.x; height: parent.height;
+ text: root.text; font-size: 12px; font-weight: root.primary ? 600 : 400;
+ color: !root.enabled ? #98a2b3 : root.primary ? white : root.quiet ? #65758b : #3c4d63;
+ horizontal-alignment: root.align-left ? left : center; vertical-alignment: center; overflow: elide;
+ }
}
touch := TouchArea { enabled: root.enabled; mouse-cursor: pointer; clicked => { root.clicked(); } }
}
diff --git a/apps/desktop/src/gui/ui/ui_localization.h b/apps/desktop/src/gui/ui/ui_localization.h
index 59a6e29a..f84a1703 100644
--- a/apps/desktop/src/gui/ui/ui_localization.h
+++ b/apps/desktop/src/gui/ui/ui_localization.h
@@ -74,6 +74,8 @@ inline int ApplyMainWindowStrings(
strings.set_connect(Text(localization::connect[language]));
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_sponsor_via_afdian(Text(localization::sponsor_via_afdian[language]));
strings.set_ok(Text(localization::ok[language]));
strings.set_cancel(Text(localization::cancel[language]));
strings.set_new_password(Text(localization::new_password[language]));