mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 19:48:14 -06:00
* Give dialogs roomier text fields and padding Single-line fields in dialogs and side panels were only as tall as one line of text, and modals used the popup frame's tight margin. Fields are now 30 points with more inset, and every modal shares one padded card so titles and buttons sit clear of the edge. * Keep long labels inside their rows and dialogs Painted names ran past the edge of fixed rows, and a page hover tip was drawn above open dialogs. Labels now end in an ellipsis, tab rows wrap, and those tips stay hidden while a dialog is open. --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
192 lines
7.0 KiB
Rust
192 lines
7.0 KiB
Rust
//! Help ▸ Check for updates (issue #28): ask for the latest release and offer its download page.
|
|
//!
|
|
//! The desktop app supplies how to ask ([`PdfCraftApp::update_source`]), so this crate has no
|
|
//! network code; without a source (the web build, tests) the command opens the releases page.
|
|
//! PdfCraft never downloads or installs anything itself: the user downloads the new version.
|
|
//! It asks only when the user does: there is no check at start (the owner's decision).
|
|
|
|
use std::sync::Arc;
|
|
|
|
use egui::{Align, Layout};
|
|
|
|
use crate::{PdfCraftApp, theme, widgets};
|
|
|
|
/// Where every PdfCraft release is listed.
|
|
pub const RELEASES_PAGE: &str = "https://github.com/storytold/pdfcraft/releases";
|
|
|
|
/// The latest published release.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct Release {
|
|
/// Its version tag, such as `v0.2.0`.
|
|
pub version: String,
|
|
/// Its page on [`RELEASES_PAGE`], where the downloads are.
|
|
pub url: String,
|
|
}
|
|
|
|
/// Asks for the latest release (blocking; it runs on its own thread).
|
|
pub type UpdateSource = Arc<dyn Fn() -> Result<Release, String> + Send + Sync>;
|
|
|
|
/// Whether release `latest` (a tag such as `v0.2.0`) is newer than version `current` (`0.1.1`).
|
|
/// Pre-release and build suffixes are ignored; a version that doesn't parse is never newer.
|
|
pub fn is_newer(latest: &str, current: &str) -> bool {
|
|
matches!((parse(latest), parse(current)), (Some(l), Some(c)) if l > c)
|
|
}
|
|
|
|
fn parse(v: &str) -> Option<(u64, u64, u64)> {
|
|
let v = v.trim().trim_start_matches(['v', 'V']);
|
|
let core = v.split(['-', '+']).next()?;
|
|
let mut parts = core.split('.');
|
|
let mut next = |required: bool| match parts.next() {
|
|
Some(p) => p.parse::<u64>().ok(),
|
|
None if required => None,
|
|
None => Some(0),
|
|
};
|
|
let version = (next(true)?, next(false)?, next(false)?);
|
|
parts.next().is_none().then_some(version)
|
|
}
|
|
|
|
/// Where a check is.
|
|
#[derive(Default)]
|
|
pub(crate) enum Check {
|
|
#[default]
|
|
Idle,
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
Running(std::sync::mpsc::Receiver<Result<Release, String>>),
|
|
Done(Result<Release, String>),
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(crate) struct Updates {
|
|
pub(crate) check: Check,
|
|
/// The Updates dialog is showing.
|
|
pub(crate) open: bool,
|
|
}
|
|
|
|
impl PdfCraftApp {
|
|
/// Help ▸ Check for updates: ask for the latest release and show the outcome.
|
|
pub fn check_for_updates(&mut self) {
|
|
let Some(source) = self.update_source.clone() else {
|
|
self.open_url(RELEASES_PAGE);
|
|
return;
|
|
};
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
{
|
|
self.updates.open = true;
|
|
if matches!(self.updates.check, Check::Running(_)) {
|
|
return;
|
|
}
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
let ctx = self.ctx.clone();
|
|
std::thread::spawn(move || {
|
|
// The receiver may be gone (the app quit): nothing to report to then.
|
|
let _ = tx.send(source());
|
|
if let Some(ctx) = ctx {
|
|
ctx.request_repaint();
|
|
}
|
|
});
|
|
self.updates.check = Check::Running(rx);
|
|
}
|
|
#[cfg(target_arch = "wasm32")]
|
|
{
|
|
let _ = source;
|
|
self.open_url(RELEASES_PAGE);
|
|
}
|
|
}
|
|
|
|
/// Pick up a finished check (each frame).
|
|
pub(crate) fn poll_updates(&mut self) {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if let Check::Running(rx) = &self.updates.check {
|
|
let result = match rx.try_recv() {
|
|
Ok(r) => r,
|
|
Err(std::sync::mpsc::TryRecvError::Empty) => return,
|
|
Err(std::sync::mpsc::TryRecvError::Disconnected) => Err("the update check stopped unexpectedly".into()),
|
|
};
|
|
self.updates.check = Check::Done(result);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The Updates dialog.
|
|
pub(crate) fn dialog(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
|
if !app.updates.open {
|
|
return;
|
|
}
|
|
let t = theme::Tokens::get(ctx);
|
|
let current = env!("CARGO_PKG_VERSION");
|
|
let mut close = false;
|
|
let mut download: Option<String> = None;
|
|
let modal = crate::widgets::show_modal(ctx, "updates", |ui| {
|
|
ui.set_width(420.0);
|
|
ui.horizontal(|ui| {
|
|
ui.add(crate::icons::image("cloud", 22.0, t.accent));
|
|
ui.label(egui::RichText::new(tl!("Check for updates")).font(theme::semibold(16.0)));
|
|
});
|
|
ui.add_space(8.0);
|
|
match &app.updates.check {
|
|
Check::Idle => {
|
|
ui.label(tl!("No check has run yet."));
|
|
}
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
Check::Running(_) => {
|
|
ui.horizontal(|ui| {
|
|
ui.spinner();
|
|
ui.label(tl!("Checking for a newer version…"));
|
|
});
|
|
}
|
|
Check::Done(Ok(r)) if is_newer(&r.version, current) => {
|
|
let version = r.version.trim_start_matches(['v', 'V']);
|
|
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("PdfCraft {v} is available."), &[("v", version)])).strong());
|
|
ui.label(
|
|
egui::RichText::new(crate::i18n::fmt(
|
|
tl!("You have version {c}. Download the new version from its release page."),
|
|
&[("c", current)],
|
|
))
|
|
.color(t.text_muted),
|
|
);
|
|
download = Some(r.url.clone());
|
|
}
|
|
Check::Done(Ok(_)) => {
|
|
ui.label(crate::i18n::fmt(tl!("PdfCraft {c} is up to date."), &[("c", current)]));
|
|
}
|
|
Check::Done(Err(e)) => {
|
|
ui.label(crate::i18n::fmt(tl!("Couldn't check for updates: {e}"), &[("e", &e.to_string())]));
|
|
ui.label(
|
|
egui::RichText::new(crate::i18n::fmt(
|
|
tl!("You have version {c}. All releases are listed at {page}."),
|
|
&[("c", current), ("page", RELEASES_PAGE)],
|
|
))
|
|
.color(t.text_muted),
|
|
);
|
|
}
|
|
}
|
|
ui.add_space(10.0);
|
|
ui.label(
|
|
egui::RichText::new(tl!("Asks GitHub for the latest release. Nothing is downloaded or installed automatically."))
|
|
.color(t.text_muted)
|
|
.small(),
|
|
);
|
|
ui.add_space(12.0);
|
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
|
if let Some(url) = download.take() {
|
|
let get = widgets::pill_button(ui, tl!("Download"), true).clicked();
|
|
let later = widgets::pill_button(ui, tl!("Later"), false).clicked();
|
|
close = get || later;
|
|
download = get.then_some(url);
|
|
} else if widgets::pill_button(ui, tl!("Close"), true).clicked() {
|
|
close = true;
|
|
}
|
|
});
|
|
});
|
|
if modal.should_close() {
|
|
close = true;
|
|
download = None;
|
|
}
|
|
if close {
|
|
app.updates.open = false;
|
|
if let Some(url) = download {
|
|
app.open_url(&url);
|
|
}
|
|
}
|
|
}
|