Check for updates at every start; show "Update to X" as a title-bar pill (#1538)
Thanks @bherbruck.
This commit is contained in:
1 parent
4d862cd76c
commit
6c1a898e3d
11 files changed
+268
-50
No files matched your search
@@ -1139,6 +1139,7 @@
|
||||
"update.available": "SolidCraft {version} ist verfügbar",
|
||||
"update.button": "Update",
|
||||
"update.button_tip": "SolidCraft {version} ist verfügbar",
|
||||
"update.button_version": "Auf {version} aktualisieren",
|
||||
"update.check_automatically": "Automatisch nach Updates suchen",
|
||||
"update.check_now": "Jetzt suchen",
|
||||
"update.checking": "Suche läuft…",
|
||||
|
||||
@@ -1409,6 +1409,7 @@
|
||||
"update.available": "SolidCraft {version} is available",
|
||||
"update.button": "Update",
|
||||
"update.button_tip": "SolidCraft {version} is available",
|
||||
"update.button_version": "Update to {version}",
|
||||
"update.check_automatically": "Check for updates automatically",
|
||||
"update.check_now": "Check now",
|
||||
"update.checking": "Checking…",
|
||||
|
||||
@@ -1235,6 +1235,7 @@
|
||||
"update.available": "SolidCraft {version} est disponible",
|
||||
"update.button": "Mettre à jour",
|
||||
"update.button_tip": "SolidCraft {version} est disponible",
|
||||
"update.button_version": "Mettre à jour vers {version}",
|
||||
"update.check_automatically": "Rechercher automatiquement les mises à jour",
|
||||
"update.check_now": "Vérifier maintenant",
|
||||
"update.checking": "Vérification…",
|
||||
|
||||
@@ -1409,6 +1409,7 @@
|
||||
"update.available": "SolidCraft {version} が利用できます",
|
||||
"update.button": "アップデート",
|
||||
"update.button_tip": "SolidCraft {version} が利用できます",
|
||||
"update.button_version": "{version} にアップデート",
|
||||
"update.check_automatically": "アップデートを自動的に確認",
|
||||
"update.check_now": "今すぐ確認",
|
||||
"update.checking": "確認中…",
|
||||
|
||||
@@ -1139,6 +1139,7 @@
|
||||
"update.available": "SolidCraft {version} 사용 가능",
|
||||
"update.button": "업데이트",
|
||||
"update.button_tip": "SolidCraft {version} 사용 가능",
|
||||
"update.button_version": "{version}(으)로 업데이트",
|
||||
"update.check_automatically": "업데이트 자동 확인",
|
||||
"update.check_now": "지금 확인",
|
||||
"update.checking": "확인 중…",
|
||||
|
||||
@@ -1066,6 +1066,7 @@
|
||||
"update.available": "Доступна версія SolidCraft {version}",
|
||||
"update.button": "Оновити",
|
||||
"update.button_tip": "Доступна версія SolidCraft {version}",
|
||||
"update.button_version": "Оновити до {version}",
|
||||
"update.check_automatically": "Автоматично перевіряти оновлення",
|
||||
"update.check_now": "Перевірити зараз",
|
||||
"update.checking": "Перевірка…",
|
||||
|
||||
@@ -1409,6 +1409,7 @@
|
||||
"update.available": "SolidCraft {version} 已发布",
|
||||
"update.button": "更新",
|
||||
"update.button_tip": "SolidCraft {version} 已发布",
|
||||
"update.button_version": "更新到 {version}",
|
||||
"update.check_automatically": "自动检查更新",
|
||||
"update.check_now": "立即检查",
|
||||
"update.checking": "正在检查…",
|
||||
|
||||
@@ -166,30 +166,55 @@ fn report_rect(bar: Rect, captions: f32, tabs_start: f32, label_width: f32) -> (
|
||||
(Rect::from_min_size(pos2(right - width, bar.top() + 4.0), vec2(width, 26.0)), labeled)
|
||||
}
|
||||
|
||||
/// An application-bar button for something on offer (Update, Install) at the right of `span` (the
|
||||
/// bar's free space): an accent-framed `icon`, labeled with `text` when `labeled` and it fits.
|
||||
/// None when even the icon alone doesn't fit: it never covers the bar's other buttons. `handle`
|
||||
/// names it for scenarios.
|
||||
pub(crate) fn offer_button(ui: &mut egui::Ui, span: Rect, labeled: bool, text: &str, icon: &str, handle: &str) -> Option<(Rect, egui::Response)> {
|
||||
/// The height of an application-bar pill ([`offer_pill`]), a little under the bar's 26 px buttons.
|
||||
pub(crate) const PILL_HEIGHT: f32 = 22.0;
|
||||
|
||||
/// An application-bar pill for something on offer (Update) at the right of `span` (the bar's free
|
||||
/// space): accent-filled, with `icon` and the first of `labels` that fits (the icon alone when
|
||||
/// none does or `labeled` is false). Every label but the last also leaves `keep` points of the
|
||||
/// span free (for the document tabs). None when even the icon doesn't fit: it never covers the
|
||||
/// bar's other buttons. `handle` names it for scenarios.
|
||||
pub(crate) fn offer_pill(
|
||||
ui: &mut egui::Ui,
|
||||
span: Rect,
|
||||
labeled: bool,
|
||||
labels: &[&str],
|
||||
keep: f32,
|
||||
icon: &str,
|
||||
handle: &str,
|
||||
) -> Option<(Rect, egui::Response)> {
|
||||
let t = Tokens::get();
|
||||
let full = 30.0 + ui.fonts_mut(|f| f.layout_no_wrap(text.to_string(), FontId::proportional(13.0), t.app_bar_text).size().x) + 10.0;
|
||||
let labeled = labeled && full <= span.width();
|
||||
let width = if labeled { full } else { 28.0 };
|
||||
let font = FontId::proportional(12.5);
|
||||
let (icon_x, text_x, right_pad) = (16.0, 29.0, 11.0);
|
||||
let last = labels.len().saturating_sub(1);
|
||||
let label = labels.iter().enumerate().filter(|_| labeled).find_map(|(i, text)| {
|
||||
let galley = ui.fonts_mut(|f| f.layout_no_wrap(text.to_string(), font.clone(), t.app_bar_text));
|
||||
let width = text_x + galley.size().x + right_pad;
|
||||
let room = if i < last { span.width() - keep } else { span.width() };
|
||||
(width <= room).then_some((*text, width))
|
||||
});
|
||||
let width = label.map_or(PILL_HEIGHT + 4.0, |(_, w)| w);
|
||||
if width > span.width() {
|
||||
return None;
|
||||
}
|
||||
let rect = Rect::from_min_size(pos2(span.right() - width, span.top()), vec2(width, 26.0));
|
||||
let response = ui.interact(rect, ui.id().with(("sc_offer_button", handle)), Sense::click());
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), text));
|
||||
let rect = Rect::from_min_size(pos2(span.right() - width, span.center().y - PILL_HEIGHT / 2.0), vec2(width, PILL_HEIGHT));
|
||||
let response = ui.interact(rect, ui.id().with(("sc_offer_pill", handle)), Sense::click());
|
||||
let name = label.map(|(text, _)| text).or(labels.first().copied()).unwrap_or_default().to_string();
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &name));
|
||||
crate::scenario::publish_handle(handle, rect.center());
|
||||
crate::scenario::publish_rect(handle, rect);
|
||||
let fill = if response.hovered() || response.has_focus() { t.hover } else { t.app_bar };
|
||||
ui.painter().rect_filled(rect, 4.0, fill);
|
||||
ui.painter().rect_stroke(rect, 4.0, Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
|
||||
let glyph = Rect::from_center_size(pos2(rect.left() + 14.0, rect.center().y), vec2(18.0, 18.0));
|
||||
icons::paint(ui.painter(), glyph, icon, t.accent, t.accent, t.app_bar);
|
||||
if labeled {
|
||||
ui.painter().text(pos2(rect.left() + 29.0, rect.center().y), Align2::LEFT_CENTER, text, FontId::proportional(13.0), t.app_bar_text);
|
||||
let hot = response.hovered() || response.has_focus();
|
||||
let fill = if hot { t.accent.lerp_to_gamma(t.app_bar_text, 0.18) } else { t.accent };
|
||||
let radius = PILL_HEIGHT / 2.0;
|
||||
ui.painter().rect_filled(rect, radius, fill);
|
||||
if response.has_focus() {
|
||||
ui.painter().rect_stroke(rect.expand(2.0), radius + 2.0, Stroke::new(1.0, t.app_bar_text), egui::StrokeKind::Outside);
|
||||
}
|
||||
let glyph_x = if label.is_some() { rect.left() + icon_x } else { rect.center().x };
|
||||
let glyph = Rect::from_center_size(pos2(glyph_x, rect.center().y), vec2(16.0, 16.0));
|
||||
icons::paint(ui.painter(), glyph, icon, t.app_bar_text, fill, t.app_bar_text);
|
||||
if let Some((text, _)) = label {
|
||||
ui.painter().text(pos2(rect.left() + text_x, rect.center().y), Align2::LEFT_CENTER, text, font, t.app_bar_text);
|
||||
}
|
||||
Some((rect, response))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Update notifications (docs/updates.md). At start, and at most once a day after that, a
|
||||
//! background thread asks GitHub for the latest stable release (`releases/latest`; nightlies and
|
||||
//! Update notifications (docs/book/src/guide/updates.md). At every start, and once a day after
|
||||
//! that while the app stays open, a background thread asks GitHub for the latest stable release (`releases/latest`; nightlies and
|
||||
//! release candidates are never "latest"). When it is newer than this build, an Update button
|
||||
//! appears in the application bar beside Report a bug; it opens a small window with the version,
|
||||
//! the release notes and Download, Skip this version and Remind me later.
|
||||
@@ -32,17 +32,19 @@ pub const RELEASES_PAGE: &str = "https://github.com/bherbruck/solidcraft/release
|
||||
/// Release pages and assets must be on GitHub, under this repository.
|
||||
const RELEASE_PREFIX: &str = "https://github.com/bherbruck/solidcraft/releases/";
|
||||
|
||||
/// Automatic checks: at most one a day.
|
||||
/// While the app stays open, automatic checks run once a day (every start checks too).
|
||||
pub const CHECK_INTERVAL_S: f64 = 24.0 * 3600.0;
|
||||
/// Remind me later hides the button for three days.
|
||||
pub const REMIND_DELAY_S: f64 = 3.0 * 24.0 * 3600.0;
|
||||
/// Check now can't be pressed more often than this (GitHub allows 60 anonymous requests an hour).
|
||||
pub const MANUAL_COOLDOWN_S: f64 = 30.0;
|
||||
/// After a failed automatic check, try again after this long (an hour), not every start.
|
||||
/// After a failed automatic check, the open app tries again after this long (an hour).
|
||||
pub const RETRY_AFTER_FAILURE_S: f64 = 3600.0;
|
||||
/// Caps on what a release response may hold (it comes from the network).
|
||||
const MAX_NOTES: usize = 64 * 1024;
|
||||
const MAX_ASSETS: usize = 400;
|
||||
/// The room the Update pill's long label leaves for the document tabs.
|
||||
const TAB_ROOM: f32 = 300.0;
|
||||
/// The Update window's width, and its release notes' wrap width.
|
||||
const WINDOW_W: f32 = 460.0;
|
||||
const NOTES_W: f32 = WINDOW_W - 30.0;
|
||||
@@ -407,7 +409,8 @@ impl Release {
|
||||
pub struct Memo {
|
||||
/// When the last check finished (successfully or not).
|
||||
pub last_check: f64,
|
||||
/// The next automatic check: a day after a successful one, an hour after a failed one.
|
||||
/// The next automatic check while the app stays open: a day after a successful one, an hour
|
||||
/// after a failed one. A new start doesn't wait for it.
|
||||
pub next_check: f64,
|
||||
/// GitHub's rate limit was reached: no request at all before this, not even Check now.
|
||||
pub rate_limited_until: f64,
|
||||
@@ -418,9 +421,13 @@ pub struct Memo {
|
||||
}
|
||||
|
||||
impl Memo {
|
||||
/// An automatic check is due (a clock set back by more than a day counts as due).
|
||||
pub fn due(&self, now: f64) -> bool {
|
||||
now >= self.rate_limited_until && (now >= self.next_check || self.next_check - now > CHECK_INTERVAL_S)
|
||||
/// An automatic check is due. `launch`: no automatic check has run since SolidCraft started,
|
||||
/// so one is due whatever the daily schedule says (someone who started yesterday's version
|
||||
/// this morning hears about today's release at the next start). GitHub's rate limit always
|
||||
/// holds. A clock set back by more than a day counts as due.
|
||||
pub fn due(&self, now: f64, launch: bool) -> bool {
|
||||
let limited = now < self.rate_limited_until && self.rate_limited_until - now <= CHECK_INTERVAL_S;
|
||||
!limited && (launch || now >= self.next_check || self.next_check - now > CHECK_INTERVAL_S)
|
||||
}
|
||||
|
||||
/// The Update button shows for `release`.
|
||||
@@ -484,6 +491,10 @@ pub struct Updater {
|
||||
/// The check in flight, and whether Check now started it.
|
||||
rx: Option<(Receiver<Result<Release, FetchError>>, bool)>,
|
||||
started: bool,
|
||||
/// An automatic check has started since launch (until then one is due at once).
|
||||
launch_checked: bool,
|
||||
/// `SOLIDCRAFT_UPDATE_CHECK=0`: no checks at all.
|
||||
off: bool,
|
||||
/// When Check now was last pressed (Unix seconds).
|
||||
last_manual: f64,
|
||||
/// A release given by `SOLIDCRAFT_UPDATE_FEED` (a file), for tests and screenshots.
|
||||
@@ -511,10 +522,11 @@ pub fn offered(app: &SolveApp) -> Option<&Release> {
|
||||
|
||||
/// Checks may run here: a release build with the host's HTTP client, or a test feed.
|
||||
pub fn available(app: &mut SolveApp) -> bool {
|
||||
app.updater.feed.is_some() || (app.services.http_get.is_some() && app.updater.kind().checks())
|
||||
!app.updater.off && (app.updater.feed.is_some() || (app.services.http_get.is_some() && app.updater.kind().checks()))
|
||||
}
|
||||
|
||||
/// Per frame: the first frame starts a due automatic check; a finished check is collected.
|
||||
/// Per frame: the first frame of each launch starts an automatic check, the open app checks
|
||||
/// again once a day (an hour after a failure), and a finished check is collected.
|
||||
pub fn tick(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
poll_install(app);
|
||||
// Quitting to restart or install was cancelled (a "save changes?" answered Cancel).
|
||||
@@ -527,6 +539,7 @@ pub fn tick(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
{
|
||||
install::clean_up_after_update(std::env::current_exe().ok().as_deref());
|
||||
if std::env::var("SOLIDCRAFT_UPDATE_CHECK").is_ok_and(|v| v == "0") {
|
||||
app.updater.off = true;
|
||||
return;
|
||||
}
|
||||
app.updater.feed = std::env::var_os("SOLIDCRAFT_UPDATE_FEED").map(PathBuf::from);
|
||||
@@ -534,7 +547,9 @@ pub fn tick(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
poll(app);
|
||||
let now = unix_now();
|
||||
if app.updater.rx.is_none() && app.preferences.check_updates && app.preferences.update.due(now) && available(app) {
|
||||
let launch = !app.updater.launch_checked;
|
||||
if app.updater.rx.is_none() && app.preferences.check_updates && app.preferences.update.due(now, launch) && available(app) {
|
||||
app.updater.launch_checked = true;
|
||||
start(app, ctx, false);
|
||||
}
|
||||
}
|
||||
@@ -714,11 +729,15 @@ pub fn take_launch(app: &mut SolveApp) -> Option<(PathBuf, Vec<String>)> {
|
||||
|
||||
// ---- user interface ---------------------------------------------------------------------------
|
||||
|
||||
/// The application bar's Update button, at the right of `span`; returns its rect (None: not shown).
|
||||
/// The application bar's Update pill ("Update to 0.14.0"), at the right of `span`: it opens and
|
||||
/// closes the Update window. Returns its rect (None: not shown).
|
||||
pub fn button(app: &mut SolveApp, ui: &mut egui::Ui, span: egui::Rect, labeled: bool) -> Option<egui::Rect> {
|
||||
offered(app)?;
|
||||
let (rect, response) = crate::toolbar::offer_button(ui, span, labeled, crate::i18n::tr("update.button"), "update", "titlebar:update")?;
|
||||
let version = app.updater.release.as_ref().map(|r| r.version().to_string()).unwrap_or_default();
|
||||
let version = offered(app)?.version().to_string();
|
||||
// "Update to 0.14.0" while the document tabs keep room; a short "Update" when they would be
|
||||
// squeezed; the icon alone in a narrow bar.
|
||||
let full = crate::i18n::format("update.button_version", &[("version", &version)]);
|
||||
let labels = [full.as_str(), crate::i18n::tr("update.button")];
|
||||
let (rect, response) = crate::toolbar::offer_pill(ui, span, labeled, &labels, TAB_ROOM, "update", "titlebar:update")?;
|
||||
if response.on_hover_text(crate::i18n::format("update.button_tip", &[("version", &version)])).clicked() {
|
||||
app.updater.open = !app.updater.open;
|
||||
}
|
||||
|
||||
@@ -217,24 +217,186 @@ fn skip_and_remind_later() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_run_daily_and_back_off() {
|
||||
fn every_launch_checks_and_the_open_app_checks_daily() {
|
||||
let mut m = Memo::default();
|
||||
let now = 1_800_000_000.0;
|
||||
assert!(m.due(now), "the first start checks");
|
||||
assert!(m.due(now, true), "the first start checks");
|
||||
m.checked(now, &Ok(release()));
|
||||
assert!(!m.due(now + 3600.0));
|
||||
assert!(m.due(now + CHECK_INTERVAL_S));
|
||||
assert!(m.due(now - 2.0 * CHECK_INTERVAL_S), "a clock set back doesn't block checks for days");
|
||||
// Started this morning, checked then: the next start checks again at once.
|
||||
assert!(m.due(now + 3600.0, true), "every start checks, whatever the daily schedule says");
|
||||
assert!(m.due(now + 5.0, true));
|
||||
// While it stays open: once a day.
|
||||
assert!(!m.due(now + 3600.0, false));
|
||||
assert!(m.due(now + CHECK_INTERVAL_S, false));
|
||||
assert!(m.due(now - 2.0 * CHECK_INTERVAL_S, false), "a clock set back doesn't block checks for days");
|
||||
// A failed check: the open app tries again an hour later; a new start checks anyway.
|
||||
m.checked(now, &Err(FetchError::Failed("offline".into())));
|
||||
assert!(!m.due(now + 60.0));
|
||||
assert!(m.due(now + RETRY_AFTER_FAILURE_S));
|
||||
assert!(!m.due(now + 60.0, false));
|
||||
assert!(m.due(now + RETRY_AFTER_FAILURE_S, false));
|
||||
assert!(m.due(now + 60.0, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_rate_limit_holds_at_launch_too() {
|
||||
let mut m = Memo::default();
|
||||
let now = 1_800_000_000.0;
|
||||
m.checked(now, &Err(FetchError::RateLimited { retry_after_s: 1800 }));
|
||||
assert!(!m.due(now + 1700.0));
|
||||
assert!(m.due(now + 1800.0));
|
||||
assert_eq!(m.rate_limited_until, now + 1800.0);
|
||||
for launch in [false, true] {
|
||||
assert!(!m.due(now + 1700.0, launch), "GitHub said to wait (launch: {launch})");
|
||||
assert!(m.due(now + 1800.0, launch));
|
||||
}
|
||||
// Absurd waits are capped at a day.
|
||||
m.checked(now, &Err(FetchError::RateLimited { retry_after_s: u64::MAX }));
|
||||
assert!(m.due(now + CHECK_INTERVAL_S));
|
||||
assert!(!m.due(now + 3600.0, true));
|
||||
assert!(m.due(now + CHECK_INTERVAL_S, false));
|
||||
// A clock set back more than a day doesn't keep the limit for days.
|
||||
assert!(m.due(now - 2.0 * CHECK_INTERVAL_S, true));
|
||||
}
|
||||
|
||||
/// A fresh folder for a test's feed file, removed with it.
|
||||
struct Scratch(PathBuf);
|
||||
|
||||
impl Drop for Scratch {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn scratch() -> Scratch {
|
||||
static N: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-update-test-{}-{n}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
Scratch(dir)
|
||||
}
|
||||
|
||||
/// A release one minor version past this build, as the `SOLIDCRAFT_UPDATE_FEED` file gives it.
|
||||
fn next_release_feed(dir: &Path) -> (PathBuf, String) {
|
||||
let mut v = parse_version(current_version()).unwrap();
|
||||
v = semver::Version::new(v.major, v.minor + 1, 0);
|
||||
let path = dir.join("latest.json");
|
||||
let body = json!({
|
||||
"tag_name": format!("v{v}"),
|
||||
"name": format!("SolidCraft {v}"),
|
||||
"body": "## What's new\n\n- Faster rebuilds.",
|
||||
"html_url": format!("https://github.com/bherbruck/solidcraft/releases/tag/v{v}"),
|
||||
"assets": []
|
||||
});
|
||||
std::fs::write(&path, body.to_string()).unwrap();
|
||||
(path, v.to_string())
|
||||
}
|
||||
|
||||
/// Frames until the check in flight has been collected.
|
||||
fn wait_for_check(h: &mut crate::scenario::Harness) {
|
||||
for _ in 0..500 {
|
||||
h.frame();
|
||||
if h.app.updater.rx.is_none() && h.app.updater.status != Status::Checking {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
}
|
||||
panic!("the update check didn't finish");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_launch_checks_even_when_the_last_check_was_today() {
|
||||
let dir = scratch();
|
||||
let (feed, version) = next_release_feed(&dir.0);
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
// Yesterday's version was started and checked this morning: no check due until tomorrow.
|
||||
let now = unix_now();
|
||||
h.app.preferences.update.last_check = now - 3600.0;
|
||||
h.app.preferences.update.next_check = now + CHECK_INTERVAL_S - 3600.0;
|
||||
h.app.updater.feed = Some(feed);
|
||||
h.frame();
|
||||
assert_eq!(h.app.updater.status, Status::Checking, "the launch checks at once");
|
||||
wait_for_check(&mut h);
|
||||
assert_eq!(h.app.updater.release.as_ref().map(|r| r.version().to_string()), Some(version.clone()));
|
||||
assert!(h.app.preferences.update.next_check > now + CHECK_INTERVAL_S - 60.0, "the next check is a day later");
|
||||
// While the app stays open it doesn't ask again before then.
|
||||
h.frames(5);
|
||||
assert!(h.app.updater.rx.is_none() && h.app.updater.status == Status::Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turning_off_automatic_checks_stops_the_launch_check() {
|
||||
let dir = scratch();
|
||||
let (feed, _) = next_release_feed(&dir.0);
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.preferences.check_updates = false;
|
||||
h.app.updater.feed = Some(feed);
|
||||
h.frames(3);
|
||||
assert_eq!(h.app.updater.status, Status::Idle);
|
||||
assert!(h.app.updater.release.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_title_bar_offers_the_new_version_and_opens_the_update_window() {
|
||||
let dir = scratch();
|
||||
let (feed, version) = next_release_feed(&dir.0);
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
let report = crate::scenario::rect_of("titlebar:report").unwrap();
|
||||
assert!(crate::scenario::rect_of("titlebar:update").is_none(), "nothing offered before a check");
|
||||
h.app.updater.feed = Some(feed);
|
||||
h.frame();
|
||||
wait_for_check(&mut h);
|
||||
h.frames(2);
|
||||
// An accent pill reading "Update to <version>", just left of Report a bug, on the bar.
|
||||
let label = format!("Update to {version}");
|
||||
let at = h.widget(&label).unwrap_or_else(|| panic!("no `{label}` in the title bar"));
|
||||
let pill = crate::scenario::rect_of("titlebar:update").unwrap();
|
||||
assert!(pill.contains(egui::pos2(at[0] as f32, at[1] as f32)));
|
||||
assert!(pill.right() <= report.left() && report.left() - pill.right() < 12.0, "beside Report a bug: {pill:?} {report:?}");
|
||||
assert!((pill.center().y - report.center().y).abs() < 1.0 && pill.height() <= report.height());
|
||||
assert_eq!(crate::scenario::rect_of("titlebar:report"), Some(report), "Report a bug stays where it was");
|
||||
assert!(!h.app.updater.open, "no window pops up by itself");
|
||||
assert!(crate::scenario::rect_of("update:window").is_none());
|
||||
// Clicking it opens the Update window (not a modal: the bar stays usable).
|
||||
h.call("ui.click", json!({"x": pill.center().x, "y": pill.center().y}));
|
||||
h.frames(2);
|
||||
assert!(h.app.updater.open);
|
||||
assert!(h.widget("Skip this version").is_some() && h.widget("Remind me later").is_some());
|
||||
// Remind me later hides the pill.
|
||||
let later = h.widget("Remind me later").unwrap();
|
||||
h.call("ui.click", json!({"x": later[0], "y": later[1]}));
|
||||
h.frames(2);
|
||||
assert!(!h.app.updater.open);
|
||||
assert!(h.widget(&label).is_none(), "Remind me later hides it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pill_shortens_in_a_narrow_title_bar() {
|
||||
let dir = scratch();
|
||||
let (feed, version) = next_release_feed(&dir.0);
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.updater.feed = Some(feed);
|
||||
h.frame();
|
||||
wait_for_check(&mut h);
|
||||
let wide = crate::scenario::rect_of("titlebar:update").unwrap();
|
||||
let label = format!("Update to {version}");
|
||||
assert!(h.widget(&label).is_some());
|
||||
// Narrower and narrower: the pill never grows, never reaches Report a bug, and its label gives
|
||||
// way (to "Update", then the icon alone) before the document tabs run out of room.
|
||||
let mut last = wide.width();
|
||||
let mut shortened = false;
|
||||
for width in [1000.0, 800.0, 700.0, 600.0, 520.0, 440.0] {
|
||||
h.call("ui.resize", json!({"width": width, "height": 600}));
|
||||
h.frames(3);
|
||||
let report = crate::scenario::rect_of("titlebar:report").unwrap();
|
||||
// Icon alone, it keeps the full text for screen readers.
|
||||
let pill = crate::scenario::rect_of("titlebar:update").filter(|_| h.widget(&label).is_some() || h.widget("Update").is_some());
|
||||
if let Some(p) = pill {
|
||||
assert!(p.width() <= last + 0.5, "at {width}: {p:?} grew from {last}");
|
||||
assert!(p.right() <= report.left(), "at {width}: {p:?} overlaps Report a bug {report:?}");
|
||||
last = p.width();
|
||||
}
|
||||
shortened |= pill.is_none_or(|p| p.width() < wide.width());
|
||||
if width == 800.0 {
|
||||
assert!(h.widget("Update").is_some() && h.widget(&label).is_none(), "the short label keeps room for the tabs");
|
||||
}
|
||||
}
|
||||
assert!(shortened, "the long label gives way in a narrow bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,15 +5,20 @@ the check sends nothing about you or your designs.
|
||||
|
||||
## What you see
|
||||
|
||||
When a newer stable release exists, an **Update** button appears in the title bar next to
|
||||
**Report a bug**. It opens a small window with the new version, its release notes and three
|
||||
choices:
|
||||
When a newer stable release exists, a small accent-coloured pill reading **Update to 0.14.0**
|
||||
(the new version) appears in the title bar next to **Report a bug**. Nothing pops up by itself:
|
||||
clicking the pill opens a small window with the new version, its release notes and three
|
||||
choices. In a narrower window the pill shortens to **Update** so the document tabs keep their
|
||||
room, then to its icon alone.
|
||||
|
||||
- **Download** opens the file that fits your installation (see the table below) in your
|
||||
browser, or the release page when there isn't one.
|
||||
- **Skip this version** hides the button until an even newer release comes out.
|
||||
- **Skip this version** hides the pill until an even newer release comes out.
|
||||
- **Remind me later** hides it for three days.
|
||||
|
||||
SolidCraft checks every time it starts, so a release that came out since you last opened it shows
|
||||
up straight away, and again once a day while it stays open.
|
||||
|
||||
**Preferences ▸ General ▸ Updates** has **Check for updates automatically** (on by default) and
|
||||
**Check now**, which checks right away and shows the window even for a version you skipped.
|
||||
|
||||
@@ -43,7 +48,7 @@ fails with a message, and **Download** is the way to update it.
|
||||
|
||||
## What is sent
|
||||
|
||||
One HTTPS request, at start and at most once a day while SolidCraft runs:
|
||||
One HTTPS request each time SolidCraft starts, and once a day while it stays open:
|
||||
|
||||
```text
|
||||
GET https://api.github.com/repos/bherbruck/solidcraft/releases/latest
|
||||
@@ -60,9 +65,9 @@ sees the request like any other visit to the API (your IP address, the time); se
|
||||
SolidCraft compares the answer with its own version on your computer.
|
||||
|
||||
- Offline, or GitHub doesn't answer within 15 seconds: nothing is shown, and the check is tried
|
||||
again an hour later.
|
||||
again an hour later (or at the next start, whichever comes first).
|
||||
- GitHub's rate limit for anonymous requests (60 an hour per address) is respected: when GitHub
|
||||
says to wait, SolidCraft waits that long, Check now included.
|
||||
says to wait, SolidCraft waits that long, Check now and the check at start included.
|
||||
- Turning off **Check for updates automatically** stops the checks; only **Check now** asks.
|
||||
- Setting the environment variable `SOLIDCRAFT_UPDATE_CHECK=0` turns all checks off, for
|
||||
example on managed machines.
|
||||
|
||||
Reference in new issue
Block a user