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]));