node_launchpad/components/popup/
upgrade_launchpad.rs1use super::super::Component;
10use super::super::utils::centered_rect_fixed;
11use crate::{
12 action::{Action, UpgradeLaunchpadActions},
13 mode::{InputMode, Scene},
14 style::{EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VIVID_SKY_BLUE, clear_area},
15 widgets::hyperlink::Hyperlink,
16};
17use ant_releases::{AntReleaseRepoActions, ReleaseType};
18use color_eyre::Result;
19use crossterm::event::{KeyCode, KeyEvent};
20use ratatui::{prelude::*, widgets::*};
21use semver::Version;
22use std::time::Duration;
23
24#[derive(Debug, Default)]
25pub struct UpgradeLaunchpadPopup {
26 active: bool,
27 current_version: Option<String>,
28 latest_version: Option<String>,
29}
30
31impl Component for UpgradeLaunchpadPopup {
32 fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
33 if !self.active {
34 return Ok(vec![]);
35 }
36
37 match key.code {
38 KeyCode::Enter | KeyCode::Esc => {
39 info!("User dismissed the LP upgrade notification.");
40 self.active = false;
41 Ok(vec![
42 Action::SwitchInputMode(InputMode::Navigation),
43 Action::SwitchScene(Scene::Status),
44 ])
45 }
46 _ => Ok(vec![]),
47 }
48 }
49
50 fn update(&mut self, action: Action) -> Result<Option<Action>> {
51 let send_back = match action {
52 Action::SwitchScene(scene) => match scene {
53 Scene::UpgradeLaunchpadPopUp => {
54 self.active = true;
55 Some(Action::SwitchInputMode(InputMode::Entry))
56 }
57 _ => {
58 self.active = false;
59 None
60 }
61 },
62 Action::UpgradeLaunchpadActions(update_launchpad_actions) => {
63 match update_launchpad_actions {
64 UpgradeLaunchpadActions::UpdateAvailable {
65 current_version,
66 latest_version,
67 } => {
68 info!(
69 "Received UpdateAvailable action with current version: {current_version} and latest version: {latest_version}. Switching to UpgradeLaunchpadPopUp scene."
70 );
71 self.current_version = Some(current_version);
72 self.latest_version = Some(latest_version);
73 Some(Action::SwitchScene(Scene::UpgradeLaunchpadPopUp))
74 }
75 }
76 }
77 _ => None,
78 };
79 Ok(send_back)
80 }
81
82 fn register_action_handler(
83 &mut self,
84 tx: tokio::sync::mpsc::UnboundedSender<Action>,
85 ) -> Result<()> {
86 info!("We've received the action sender. Spawning task to check for updates.");
87 tokio::spawn(async move {
88 loop {
89 match check_for_update().await {
90 Ok(Some((latest_version, current_version))) => {
91 if let Err(err) = tx.send(Action::UpgradeLaunchpadActions(
92 UpgradeLaunchpadActions::UpdateAvailable {
93 current_version: current_version.to_string(),
94 latest_version: latest_version.to_string(),
95 },
96 )) {
97 error!(
98 "Error sending UpgradeLaunchpadActions::UpdateAvailable action: {err}"
99 );
100 }
101 }
102 _ => {
103 info!("No new launchpad version available.");
104 }
105 };
106 info!("Checking for LP update in 12 hours..");
107 tokio::time::sleep(Duration::from_secs(12 * 60 * 60)).await;
108 }
109 });
110
111 Ok(())
112 }
113
114 fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
115 if !self.active {
116 return Ok(());
117 }
118
119 let Some(current_version) = self.current_version.as_ref() else {
120 error!(
121 "Current version is not set, even though the upgrade popup is active. This is unexpected."
122 );
123 return Ok(());
124 };
125 let Some(latest_version) = self.latest_version.as_ref() else {
126 error!(
127 "Latest version is not set, even though the upgrade popup is active. This is unexpected."
128 );
129 return Ok(());
130 };
131
132 let layer_zero = centered_rect_fixed(60, 15, area);
133 let layer_one = Layout::new(
134 Direction::Vertical,
135 [
136 Constraint::Length(2),
137 Constraint::Min(1),
138 Constraint::Length(1),
139 ],
140 )
141 .split(layer_zero);
142
143 let pop_up_border = Paragraph::new("").block(
144 Block::default()
145 .borders(Borders::ALL)
146 .title(" Update Available ")
147 .bold()
148 .title_style(Style::new().fg(VIVID_SKY_BLUE))
149 .padding(Padding::uniform(2))
150 .border_style(Style::new().fg(VIVID_SKY_BLUE)),
151 );
152 clear_area(f, layer_zero);
153
154 let layer_two = Layout::new(
155 Direction::Vertical,
156 [
157 Constraint::Length(6),
158 Constraint::Length(2),
159 Constraint::Length(3),
160 Constraint::Length(1),
161 ],
162 )
163 .split(layer_one[1]);
164
165 let text = Paragraph::new(vec![
166 Line::from(Span::styled("\n", Style::default())),
167 Line::from(vec![Span::styled(
168 "A new version of Node Launchpad is available:".to_string(),
169 Style::default().fg(LIGHT_PERIWINKLE),
170 )]),
171 Line::from(vec![Span::styled(
172 format!("v{current_version} → v{latest_version}"),
173 Style::default().fg(LIGHT_PERIWINKLE),
174 )]),
175 Line::from(Span::styled("\n", Style::default())),
176 Line::from(vec![Span::styled(
177 "To update, please download the latest version from:",
178 Style::default().fg(GHOST_WHITE),
179 )]),
180 ])
181 .block(Block::default().padding(Padding::horizontal(2)))
182 .alignment(Alignment::Center)
183 .wrap(Wrap { trim: true });
184
185 f.render_widget(text, layer_two[0]);
186
187 let link_layout = Layout::default()
189 .direction(Direction::Horizontal)
190 .constraints([
191 Constraint::Percentage(28), Constraint::Percentage(44), Constraint::Percentage(28), ])
195 .split(layer_two[1]);
196
197 let link = Hyperlink::new(
199 Span::styled(
200 "https://autonomi.com/node",
201 Style::default().fg(VIVID_SKY_BLUE),
202 ),
203 "https://autonomi.com/node",
204 );
205 f.render_widget_ref(link, link_layout[1]);
207
208 let dash = Block::new()
209 .borders(Borders::BOTTOM)
210 .border_style(Style::new().fg(GHOST_WHITE));
211 f.render_widget(dash, layer_two[2]);
212
213 let buttons_layer =
214 Layout::horizontal(vec![Constraint::Percentage(100)]).split(layer_two[3]);
215
216 let button_ok = Paragraph::new(Line::from(vec![Span::styled(
217 "Press [Enter] to continue",
218 Style::default().fg(EUCALYPTUS),
219 )]))
220 .alignment(Alignment::Center);
221 f.render_widget(button_ok, buttons_layer[0]);
222
223 f.render_widget(pop_up_border, layer_zero);
224
225 Ok(())
226 }
227}
228
229pub async fn check_for_update() -> Result<Option<(Version, Version)>> {
232 let release_repo = <dyn AntReleaseRepoActions>::default_config();
233 let current_version = Version::parse(env!("CARGO_PKG_VERSION"))?;
234
235 match release_repo
236 .get_latest_version(&ReleaseType::NodeLaunchpad)
237 .await
238 {
239 Ok(latest_version) => {
240 info!("Current version: {current_version} and latest version: {latest_version}");
241 if latest_version > current_version {
242 Ok(Some((latest_version, current_version)))
243 } else {
244 Ok(None)
245 }
246 }
247 Err(e) => {
248 debug!("Failed to check for updates: {}", e);
249 Ok(None)
250 }
251 }
252}