feat(start): show paying Patreon supporters on the Start page

Add a right-hand rail on the Start page listing active paying patrons
(name + pledge amount, highest first) with a "Support on Patreon" button.
The list is fetched once at boot from the Patreon API in the background;
free followers, $0 tiers, declined and former patrons are excluded.

The creator access token is read at build time from OCS_PATREON_TOKEN
(option_env!), so it never lives in source or git — the release workflow
passes it from a repo secret, and build.rs re-bakes when it changes.
Without a token the rail just shows the support button.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-02 01:00:22 +03:00
1 parent 80ad27ec67
commit 608fe5ec3e
8 files changed
+208 -3

No files matched your search

+15
View File
@@ -194,6 +194,9 @@ pub(super) struct OpenCADStudio {
ribbon: Ribbon,
app_menu: AppMenu,
command_line: CommandLine,
/// Paying Patreon supporters shown on the Start page (name, pledge cents),
/// fetched once at boot, highest pledge first.
patrons: Vec<(String, i64)>,
/// Read-only editor buffer backing the command-line history dropdown, so
/// the log can be drag-selected across lines and copied (issue #232).
/// Rebuilt from the history each time the dropdown is opened.
@@ -1537,6 +1540,8 @@ pub enum Message {
PluginRepoRemove(String),
/// The curated registry was fetched.
PluginRegistryFetched(Result<Vec<crate::plugin::external::RegistryEntry>, String>),
/// Patreon supporters fetched at boot for the Start page (name, pledge cents).
PatronsFetched(Result<Vec<(String, i64)>, String>),
/// Installable release tags fetched for `owner/repo`.
PluginReleasesFetched(String, Result<Vec<String>, String>),
/// Choose a release tag for a repo (`repo`, `tag`).
@@ -1924,6 +1929,7 @@ impl OpenCADStudio {
ribbon: Ribbon::new(),
app_menu,
command_line: CommandLine::new(),
patrons: Vec::new(),
history_content: iced::widget::text_editor::Content::new(),
status_bar: StatusBar::new(),
cursor_pos: Point::ORIGIN,
@@ -2293,6 +2299,14 @@ impl OpenCADStudio {
if !s.default_assoc_prompted {
s.active_modal = Some(ModalKind::AssocPrompt);
}
// Fetch the Patreon supporters list once at boot for the Start page.
#[cfg(not(target_arch = "wasm32"))]
let patrons_fetch = Task::perform(
async { crate::patreon::fetch_patrons() },
Message::PatronsFetched,
);
#[cfg(target_arch = "wasm32")]
let patrons_fetch = Task::none();
(
s,
Task::batch([
@@ -2302,6 +2316,7 @@ impl OpenCADStudio {
cli_open,
script,
assoc_prompt,
patrons_fetch,
]),
)
}
+7
View File
@@ -2592,6 +2592,13 @@ impl OpenCADStudio {
self.marketplace_status = format!("Registry: {e}");
Task::none()
}
Message::PatronsFetched(Ok(names)) => {
self.patrons = names;
Task::none()
}
// No token / offline: leave the list empty (Start page shows a
// "Support on Patreon" prompt instead).
Message::PatronsFetched(Err(_)) => Task::none(),
Message::PluginReleasesFetched(repo, Ok(tags)) => {
if let Some(first) = tags.first() {
self.repo_selected_tag
+82 -3
View File
@@ -77,7 +77,7 @@ impl OpenCADStudio {
// viewport draws the layout's own geometry (white sheet + entities +
// borders) and the floating content viewports blit on top.
let viewport_3d: Element<'_, Message> = if tab.is_start {
start_page_view()
start_page_view(&self.patrons)
} else if is_paper {
shader(ViewportPane::model(
&tab.scene,
@@ -1871,7 +1871,7 @@ fn pane_mouse_area<'a>(idx: usize) -> Element<'a, Message> {
.into()
}
pub(super) fn start_page_view<'a>() -> Element<'a, Message> {
pub(super) fn start_page_view<'a>(patrons: &'a [(String, i64)]) -> Element<'a, Message> {
const TEXT: Color = Color {
r: 0.94,
g: 0.93,
@@ -2199,7 +2199,86 @@ pub(super) fn start_page_view<'a>() -> Element<'a, Message> {
b: 0.085,
a: 1.0,
};
container(content)
// Right rail: Patreon supporters, fetched at boot. When the list is empty
// (no token configured / offline) only the "Support on Patreon" button
// shows, so the rail always invites support.
let supporters: Element<'a, Message> = {
const NAME_COLOR: Color = Color {
r: 0.78,
g: 0.78,
b: 0.80,
a: 1.0,
};
let mut list = column![
text("Supporters").size(15).color(TEXT),
Space::new().height(iced::Length::Fixed(12.0)),
]
.spacing(6)
.width(Fill);
for (name, cents) in patrons {
// Cents → the campaign currency's main unit. Symbol assumed "$";
// adjust if the campaign bills in another currency.
let amount = format!("${:.2}", *cents as f64 / 100.0);
list = list.push(
iced::widget::row![
text(name).size(12).color(NAME_COLOR).width(Fill),
text(amount).size(12).color(NAME_COLOR),
]
.spacing(6),
);
}
let support_btn = mouse_area(
container(
text("♥ Support on Patreon")
.size(12)
.color(Color::WHITE),
)
.padding([6, 10])
.width(Fill)
.center_x(Fill)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(Color {
r: 0.90,
g: 0.28,
b: 0.30,
a: 1.0,
})),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 6.0.into(),
},
..Default::default()
}),
)
.interaction(iced::mouse::Interaction::Pointer)
.on_press(Message::OpenUrl(
"https://patreon.com/HakanSeven12".to_string(),
));
container(column![
iced::widget::scrollable(list).height(Fill),
Space::new().height(iced::Length::Fixed(12.0)),
support_btn,
])
.width(iced::Length::Fixed(240.0))
.height(Fill)
.padding(20)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(CARD_BG)),
border: Border {
color: CARD_BORDER,
width: 1.0,
radius: 8.0.into(),
},
..Default::default()
})
.into()
};
container(iced::widget::row![
container(content).width(Fill).height(Fill),
supporters,
])
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PAGE_BG)),
..Default::default()