1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! First launch: look for a newer PinReady before anything else is set up.
//!
//! Someone who installs PinReady gets the version they downloaded, which may
//! already be behind a fix. On the very first launch (no `VPinballX.ini`, the
//! wizard never finished) the window starts with a short check, and offers the
//! update before the wizard opens. No network, a slow network or no newer
//! version: the wizard opens as it always did, after a few seconds at most.
use super::*;
use std::time::Duration;
/// How long the first launch waits for GitHub before opening the wizard.
const GATE_TIMEOUT: Duration = Duration::from_secs(4);
/// What the first-launch window shows right now.
#[derive(Debug, PartialEq, Eq)]
pub(super) enum GateStep {
/// Still asking GitHub.
Checking,
/// A newer version exists: update or carry on.
Offer,
/// The update is downloading.
Updating,
/// Nothing to say: open the wizard.
Pass,
}
/// The step for the time spent, whether the answer is still awaited, whether
/// a newer version is known and whether its download has started.
pub(super) fn gate_step(
elapsed: Duration,
check_pending: bool,
update_available: bool,
updating: bool,
) -> GateStep {
if updating {
GateStep::Updating
} else if update_available {
GateStep::Offer
} else if check_pending && elapsed < GATE_TIMEOUT {
GateStep::Checking
} else {
GateStep::Pass
}
}
/// Is this the first launch: the wizard never finished and no VPX settings file exists?
pub(super) fn is_first_run(wizard_completed: bool, ini_present: bool) -> bool {
!wizard_completed && !ini_present
}
impl App {
/// Draws the first-launch window while it is needed. Returns `true` when it
/// took the whole window, `false` when the wizard can be drawn.
pub(super) fn render_first_run_gate(&mut self, ui: &mut egui::Ui) -> bool {
let Some(start) = self.first_run_gate else {
return false;
};
let ctx = ui.ctx().clone();
self.process_pinready_update_check(&ctx);
let step = gate_step(
start.elapsed(),
self.pinready_update_check_rx.is_some(),
self.pinready_latest_release.is_some(),
self.pinready_updating,
);
if step == GateStep::Pass {
self.first_run_gate = None;
return false;
}
let release = self.pinready_latest_release.clone();
let mut carry_on = false;
let mut update = false;
egui::CentralPanel::default().show(ui, |ui| {
ui.vertical_centered(|ui| {
ui.add_space((ui.available_height() * 0.25).max(24.0));
theme::dmd_wordmark(ui, 3.0);
ui.add_space(24.0);
match step {
GateStep::Checking => {
ui.spinner();
ui.add_space(8.0);
ui.label(t!("first_run_update_checking").to_string());
ctx.request_repaint_after(Duration::from_millis(100));
}
GateStep::Offer => {
if let Some(release) = &release {
ui.heading(t!("first_run_update_title").to_string());
ui.add_space(8.0);
ui.set_max_width(520.0);
ui.label(
t!(
"first_run_update_text",
tag = release.tag.as_str(),
current = updater::CURRENT_PINREADY_VERSION
)
.to_string(),
);
ui.add_space(16.0);
let label = t!("pinready_update_button", tag = release.tag.as_str())
.to_string();
if ui.button(egui::RichText::new(label).strong()).clicked() {
update = true;
}
ui.add_space(6.0);
if ui
.button(t!("first_run_update_later").to_string())
.clicked()
{
carry_on = true;
}
if let Some(err) = self.pinready_update_error.clone() {
ui.add_space(8.0);
ui.colored_label(
theme::pal(ui.ctx()).error,
t!("pinready_update_error", msg = err.as_str()).to_string(),
);
}
}
}
GateStep::Updating => {
let (current, total) = self.pinready_update_progress;
let (progress, txt) = if total > 0 {
let pct = (current as f32 / total as f32 * 100.0) as u32;
(
current as f32 / total as f32,
t!(
"pinready_update_progress",
mb = current / (1024 * 1024),
total = total / (1024 * 1024),
pct = pct
)
.to_string(),
)
} else {
(0.0, t!("pinready_update_extracting").to_string())
};
ui.add(
egui::ProgressBar::new(progress)
.desired_width(420.0)
.text(txt),
);
ctx.request_repaint_after(Duration::from_millis(100));
}
GateStep::Pass => {}
}
});
});
if update && let Some(release) = release {
self.start_pinready_download(&release);
}
if carry_on {
self.first_run_gate = None;
return false;
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_first_launch_is_only_when_nothing_was_ever_set_up() {
assert!(is_first_run(false, false));
assert!(!is_first_run(true, true));
// The wizard was finished, or a settings file already exists: not new.
assert!(!is_first_run(true, false));
assert!(!is_first_run(false, true));
}
#[test]
fn the_wizard_never_waits_longer_than_the_timeout() {
let s = Duration::from_secs;
// Waiting for the answer, inside the delay.
assert_eq!(gate_step(s(1), true, false, false), GateStep::Checking);
// Still no answer after the delay: go on without it.
assert_eq!(gate_step(s(5), true, false, false), GateStep::Pass);
// Answered, nothing new (or the check failed): go on at once.
assert_eq!(gate_step(s(0), false, false, false), GateStep::Pass);
// A newer version is offered, even after the delay.
assert_eq!(gate_step(s(9), false, true, false), GateStep::Offer);
// Once the download started, the window stays until the restart.
assert_eq!(gate_step(s(9), false, true, true), GateStep::Updating);
}
}