forked from 1b-agent/opencadstudio-upstream
255 lines
7.8 KiB
Rust
255 lines
7.8 KiB
Rust
use crate::app::Message;
|
|
use iced::widget::{button, column, container, row, scrollable, text, Space};
|
|
use iced::{Background, Border, Element, Theme};
|
|
use crate::t;
|
|
use std::borrow::Cow;
|
|
|
|
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
|
|
iced::widget::text::Style {
|
|
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
|
|
}
|
|
}
|
|
|
|
fn primary_style(theme: &Theme) -> iced::widget::text::Style {
|
|
iced::widget::text::Style {
|
|
color: Some(theme.palette().primary.base.color),
|
|
}
|
|
}
|
|
|
|
/// Renders one of the two "Installed" / "Latest" cards. The `highlight`
|
|
/// flag tints the border + label with the accent colour, making the new
|
|
/// version the visual anchor of the row.
|
|
fn version_card<'a>(
|
|
label: Cow<'static, str>,
|
|
value: String,
|
|
highlight: bool,
|
|
width: iced::Length,
|
|
) -> Element<'a, Message> {
|
|
container(
|
|
column![
|
|
text(label)
|
|
.size(10)
|
|
.style(move |theme: &Theme| if highlight {
|
|
primary_style(theme)
|
|
} else {
|
|
muted_style(theme)
|
|
}),
|
|
text(value).size(20),
|
|
]
|
|
.spacing(4)
|
|
.align_x(iced::Center),
|
|
)
|
|
.width(width)
|
|
.padding(iced::Padding {
|
|
top: 14.0,
|
|
right: 12.0,
|
|
bottom: 14.0,
|
|
left: 12.0,
|
|
})
|
|
.align_x(iced::Center)
|
|
.style(move |theme: &Theme| {
|
|
let palette = theme.palette();
|
|
let pair = if highlight {
|
|
palette.primary.weak
|
|
} else {
|
|
palette.background.base
|
|
};
|
|
container::Style {
|
|
background: Some(Background::Color(pair.color)),
|
|
border: Border {
|
|
color: if highlight {
|
|
palette.primary.base.color
|
|
} else {
|
|
palette.background.neutral.color
|
|
},
|
|
width: 1.0,
|
|
radius: 6.0.into(),
|
|
},
|
|
..Default::default()
|
|
}
|
|
})
|
|
.into()
|
|
}
|
|
|
|
/// Light-weight renderer for a single line of GitHub-style release-notes
|
|
/// markdown. Recognises:
|
|
/// * `## Heading` → bold accent line
|
|
/// * `### Heading` → smaller bold line
|
|
/// * `- bullet` → indented bullet text
|
|
/// * `**bold**` runs and `` `code` `` runs (rendered tonally, not styled
|
|
/// differently — iced's text widget has no inline run styling).
|
|
/// Anything else is plain body text. Strips the markdown markers so the
|
|
/// dialog reads cleanly even if the user has a Patreon-formatted note.
|
|
fn render_notes_line<'a>(raw: &str) -> Element<'a, Message> {
|
|
let trimmed = raw.trim_end();
|
|
if trimmed.is_empty() {
|
|
return Space::new()
|
|
.height(iced::Length::Fixed(6.0))
|
|
.into();
|
|
}
|
|
if let Some(rest) = trimmed.strip_prefix("## ") {
|
|
return text(strip_inline_md(rest))
|
|
.size(13)
|
|
.style(primary_style)
|
|
.into();
|
|
}
|
|
if let Some(rest) = trimmed.strip_prefix("### ") {
|
|
return text(strip_inline_md(rest))
|
|
.size(12)
|
|
.into();
|
|
}
|
|
if let Some(rest) = trimmed.strip_prefix("- ").or_else(|| trimmed.strip_prefix("* ")) {
|
|
return row![
|
|
container(crate::ui::icons::themed_secondary(crate::ui::icons::DOT, 5.0)).width(14),
|
|
text(strip_inline_md(rest)).size(11),
|
|
]
|
|
.spacing(4)
|
|
.into();
|
|
}
|
|
text(strip_inline_md(trimmed)).size(11).into()
|
|
}
|
|
|
|
/// Drop `**…**` and `` `…` `` markers without preserving emphasis (iced 0.14
|
|
/// Text widgets style the whole string uniformly). Keeps the inner text.
|
|
fn strip_inline_md(s: &str) -> String {
|
|
let mut out = String::with_capacity(s.len());
|
|
let mut chars = s.chars().peekable();
|
|
while let Some(c) = chars.next() {
|
|
if c == '*' && chars.peek() == Some(&'*') {
|
|
chars.next();
|
|
continue;
|
|
}
|
|
if c == '`' {
|
|
continue;
|
|
}
|
|
out.push(c);
|
|
}
|
|
out
|
|
}
|
|
|
|
pub fn view_window<'a>(
|
|
latest: &'a str,
|
|
body: &'a str,
|
|
sizing: crate::ui::modal::ModalSizing,
|
|
) -> Element<'a, Message> {
|
|
let header = container(
|
|
column![
|
|
text(t!("New Release Available")).size(20).style(primary_style),
|
|
text(t!("A newer Open CAD Studio version is published on GitHub."))
|
|
.size(11)
|
|
.style(muted_style),
|
|
]
|
|
.spacing(4)
|
|
.align_x(iced::Center),
|
|
)
|
|
.width(sizing.width)
|
|
.padding(iced::Padding {
|
|
top: 14.0,
|
|
right: 0.0,
|
|
bottom: 6.0,
|
|
left: 0.0,
|
|
})
|
|
.align_x(iced::Center);
|
|
|
|
// Two side-by-side version cards (Installed → Latest) with the latest
|
|
// one accent-tinted to draw the eye. Replaces the previous label/value
|
|
// row layout. The arrow between them is purely decorative.
|
|
let installed = version_card(
|
|
t!("Installed"),
|
|
format!("v{}", env!("OCS_APP_VERSION")),
|
|
false,
|
|
sizing.width,
|
|
);
|
|
let latest_card = version_card(t!("Latest"), format!("v{}", latest), true, sizing.width);
|
|
let arrow = container(crate::ui::icons::themed_secondary(
|
|
crate::ui::icons::ARROW_LONG_RIGHT,
|
|
20.0,
|
|
))
|
|
.width(iced::Length::Fixed(32.0))
|
|
.height(sizing.height)
|
|
.align_x(iced::Center)
|
|
.align_y(iced::Center);
|
|
let info_block = row![installed, arrow, latest_card]
|
|
.spacing(0)
|
|
.align_y(iced::Center)
|
|
.width(sizing.width);
|
|
|
|
let later_btn = button(text(t!("Later")).size(11))
|
|
.on_press(Message::UpdateNoticeClose)
|
|
.style(button::secondary)
|
|
.padding([6, 16]);
|
|
|
|
let open_btn = button(text(t!("Open Release Page")).size(11))
|
|
.on_press(Message::UpdateNoticeOpenRelease)
|
|
.style(button::primary)
|
|
.padding([6, 16]);
|
|
|
|
let footer = row![Space::new().width(sizing.width), later_btn, open_btn]
|
|
.spacing(8)
|
|
.align_y(iced::Center)
|
|
.padding(iced::Padding {
|
|
top: 14.0,
|
|
right: 0.0,
|
|
bottom: 0.0,
|
|
left: 0.0,
|
|
});
|
|
|
|
// Release notes panel. Rendered as a light-markdown column inside a
|
|
// bordered scrollable so long bodies stay contained and don't
|
|
// explode the window. Empty body → "No release notes provided."
|
|
let notes_heading = container(text(t!("What's new")).size(11).style(muted_style))
|
|
.padding(iced::Padding {
|
|
top: 10.0,
|
|
right: 0.0,
|
|
bottom: 4.0,
|
|
left: 0.0,
|
|
});
|
|
|
|
let notes_body: Element<'a, Message> = if body.trim().is_empty() {
|
|
text(t!("No release notes provided."))
|
|
.size(11)
|
|
.style(muted_style)
|
|
.into()
|
|
} else {
|
|
let mut col = column![].spacing(4);
|
|
for line in body.lines() {
|
|
col = col.push(render_notes_line(line));
|
|
}
|
|
scrollable(container(col).padding([10, 14]))
|
|
.height(sizing.height)
|
|
.into()
|
|
};
|
|
|
|
let notes_block = container(notes_body)
|
|
.width(sizing.width)
|
|
.height(sizing.height)
|
|
.style(container::bordered_box);
|
|
|
|
// Keep the notes panel content-sized. The outer modal supplies the maximum
|
|
// height, so long release notes scroll instead of stretching the dialog.
|
|
let notes_fill = container(notes_block)
|
|
.width(sizing.width)
|
|
.height(sizing.height);
|
|
|
|
container(
|
|
column![header, info_block, notes_heading, notes_fill, footer]
|
|
.spacing(0)
|
|
.height(sizing.height)
|
|
.padding(iced::Padding {
|
|
top: 0.0,
|
|
right: 20.0,
|
|
bottom: 20.0,
|
|
left: 20.0,
|
|
}),
|
|
)
|
|
.style(|theme: &Theme| container::Style {
|
|
background: Some(Background::Color(
|
|
theme.palette().background.base.color,
|
|
)),
|
|
..Default::default()
|
|
})
|
|
.width(sizing.width)
|
|
.height(sizing.height)
|
|
.into()
|
|
}
|