diff --git a/locales/en.yml b/locales/en.yml index 2f8c1e8..928efbc 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -12,3 +12,12 @@ welcome.menu: "Quick Menu(use ↑/↓ keys to navigate, press enter to select an welcome.menu_items.0: "Open Omega Code Console Panel, to configure the application." welcome.menu_items.1: "Start a new project." welcome.menu_items.2: "Quit" +welcome.install_message1: "Step 1: Java Runtime Environment..." +welcome.install_message2: "Step 2: Python Runtime Environment..." +welcome.install_message3: "Java runtime environment installation failed." +welcome.install_message4: "Python runtime environment installation failed." +welcome.install_message5: "Downloading JRE..." +welcome.install_message6: "Extracting JRE..." +welcome.install_message7: "Downloading Python..." +welcome.install_message8: "Extracting Python..." +welcome.install_message9: "Environment prepared" diff --git a/src/components/welcome.rs b/src/components/welcome.rs index 0ef5f9e..51a0530 100644 --- a/src/components/welcome.rs +++ b/src/components/welcome.rs @@ -1,12 +1,4 @@ -use ratatui_kit::{ - Component, - ComponentDrawer, - ComponentUpdater, - Hooks, - Props, - UseEvents, - UseState, -}; +use ratatui_kit::{Component, ComponentDrawer, ComponentUpdater, Hooks, Props, UseEvents, UseFuture, UseState}; use ratatui_kit::crossterm::event::{Event, KeyCode}; @@ -15,6 +7,9 @@ use ratatui_kit::ratatui::{ text::{Line, Span}, widgets::{Paragraph, Widget}, }; +use crate::runtime::download::DownloadProgress; +use crate::runtime::extract::ExtractProgress; +use crate::runtime::runtime::{ensure_jre, ensure_python}; #[derive(Props)] pub struct WelcomeProps { @@ -38,11 +33,21 @@ impl Default for WelcomeProps { pub struct Welcome { selected_index: usize, - // props cache style: Style, menu_style: Style, version: Option, show_menu: bool, + + jre_ready: bool, + python_ready: bool, + is_installing: bool, + install_message: String, + + stage: String, + percent: f64, + downloaded: u64, + total: u64, + speed: f64, } impl Component for Welcome { @@ -56,6 +61,17 @@ impl Component for Welcome { menu_style: props.menu_style, version: props.version.clone(), show_menu: props.show_menu, + + jre_ready: false, + python_ready: false, + is_installing: false, + install_message: "".to_string(), + + stage: String::new(), + percent: 0.0, + downloaded: 0, + total: 0, + speed: 0.0, } } @@ -65,62 +81,163 @@ impl Component for Welcome { mut hooks: Hooks, _updater: &mut ComponentUpdater, ) { - // sync props self.style = props.style; self.menu_style = props.menu_style; self.version = props.version.clone(); - self.show_menu = props.show_menu; - - let mut state_index = hooks.use_state(|| self.selected_index); - hooks.use_events(move |event| { - if let Event::Key(key) = event { - match key.code { - KeyCode::Up => { - state_index.set( - state_index - .get() - .saturating_sub(1), - ); - } + let mut jre_ready = hooks.use_state(|| self.jre_ready); + let mut python_ready = hooks.use_state(|| self.python_ready); + let mut is_installing = hooks.use_state(|| self.is_installing); + let mut install_message = hooks.use_state(|| self.install_message.clone()); + let mut install_started = hooks.use_state(|| false); - KeyCode::Down => { - let current = state_index.get(); - - state_index.set((current + 1).min(2)); - } + let mut stage = hooks.use_state(|| self.stage.clone()); + let mut percent = hooks.use_state(|| self.percent); + let mut downloaded = hooks.use_state(|| self.downloaded); + let mut total = hooks.use_state(|| self.total); + let mut speed = hooks.use_state(|| self.speed); - KeyCode::Enter => { - match state_index.get() { - 0 => { - todo!("打开配置"); - } + let current_show_menu_for_events = self.jre_ready && self.python_ready; - 1 => { - todo!("开启新项目"); - } + let mut state_index = hooks.use_state(|| self.selected_index); + hooks.use_events(move |event| { + if jre_ready.get() && python_ready.get() { + if let Event::Key(key) = event { + match key.code { + KeyCode::Up => { + state_index.set( + state_index.get().saturating_sub(1) + ); + } - 2 => { - todo!("退出程序"); - } + KeyCode::Down => { + let curr = state_index.get(); + state_index.set((curr + 1).min(2)); + } + KeyCode::Enter => match state_index.get() { + 0 => todo!("打开配置"), + 1 => todo!("开启新项目"), + 2 => todo!("退出程序"), _ => {} - } - } + }, - _ => {} + _ => {} + } } } }); + if !install_started.get() && !jre_ready.get() && !is_installing.get() { + install_started.set(true); + + let mut jre_ready_clone = jre_ready.clone(); + let mut python_ready_clone = python_ready.clone(); + let mut is_installing_clone = is_installing.clone(); + let mut install_message_clone = install_message.clone(); + + let mut dl_stage = stage.clone(); + let mut dl_percent = percent.clone(); + let mut dl_downloaded = downloaded.clone(); + let mut dl_total = total.clone(); + let mut dl_speed = speed.clone(); + + let mut ex_stage = stage.clone(); + let mut ex_percent = percent.clone(); + let mut ex_downloaded = downloaded.clone(); + let mut ex_total = total.clone(); + + hooks.use_future(async move { + // JRE + install_message_clone.set(t!("welcome.install_message1").to_string()); + let jre_ok = ensure_jre( + |p: DownloadProgress| { + dl_stage.set(t!("welcome.install_message5").to_string()); + dl_percent.set(p.percent); + dl_downloaded.set(p.downloaded); + dl_total.set(p.total); + dl_speed.set(p.speed); + }, + |p: ExtractProgress| { + ex_stage.set(t!("welcome.install_message6").to_string()); + let pct = if p.total > 0 { + p.current as f64 / p.total as f64 * 100.0 + } else { + 0.0 + }; + ex_percent.set(pct); + ex_downloaded.set(p.current); + ex_total.set(p.total); + }, + ) + .await + .is_ok(); + + jre_ready_clone.set(jre_ok); + if !jre_ok { + install_message_clone.set(t!("welcome.install_message3").to_string()); + is_installing_clone.set(false); + return; + } + + // Python + install_message_clone.set(t!("welcome.install_message2").to_string()); + let python_ok = ensure_python( + |p: DownloadProgress| { + dl_stage.set(t!("welcome.install_message7").to_string()); + dl_percent.set(p.percent); + dl_downloaded.set(p.downloaded); + dl_total.set(p.total); + dl_speed.set(p.speed); + }, + |p: ExtractProgress| { + ex_stage.set(t!("welcome.install_message8").to_string()); + let pct = if p.total > 0 { + p.current as f64 / p.total as f64 * 100.0 + } else { + 0.0 + }; + ex_percent.set(pct); + ex_downloaded.set(p.current); + ex_total.set(p.total); + }, + ) + .await + .is_ok(); + + python_ready_clone.set(python_ok); + if python_ok { + install_message_clone.set(t!("welcome.install_message9").to_string()); + } else { + install_message_clone.set(t!("welcome.install_message4").to_string()); + } + + is_installing_clone.set(false); + }); + } + + + // 更新 self 的字段 + self.jre_ready = jre_ready.get(); + self.python_ready = python_ready.get(); + self.is_installing = is_installing.get(); + self.install_message = install_message.read().clone(); + // 正确更新 show_menu + self.show_menu = jre_ready.get() && python_ready.get(); + self.selected_index = state_index.get(); + + self.stage = stage.read().clone(); + self.percent = percent.get(); + self.downloaded = downloaded.get(); + self.total = total.get(); + self.speed = speed.get(); } fn draw(&mut self, drawer: &mut ComponentDrawer<'_, '_>) { let area = drawer.area; let buf = drawer.buffer_mut(); - // ASCII LOGO let ascii_lines = [ " ██████╗ ███╗ ███╗███████╗ ██████╗ █████╗", "██╔═══██╗████╗ ████║██╔════╝██╔════╝ ██╔══██╗", @@ -137,15 +254,8 @@ impl Component for Welcome { " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝", ]; - let version = self - .version - .clone() - .unwrap_or_else(|| { - env!("CARGO_PKG_VERSION").to_string() - }); - + let version = self.version.clone().unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); let welcome_text = t!("welcome.text"); - let menu_items = [ t!("welcome.menu_items.0"), t!("welcome.menu_items.1"), @@ -153,93 +263,82 @@ impl Component for Welcome { ]; let mut lines = Vec::new(); - - // logo for line in ascii_lines { - lines.push( - Line::from( - Span::styled( - line, - self.style, - ) - ) - ); + lines.push(Line::from(Span::styled(line, self.style))); } + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + t!("welcome.version", version = version), + self.style.add_modifier(Modifier::DIM), + ))); + lines.push(Line::from("")); + lines.push(Line::from(Span::styled(welcome_text, self.style))); lines.push(Line::from("")); - // version - lines.push( - Line::from( - Span::styled( - t!("welcome.version", version = version), - self.style.add_modifier(Modifier::DIM), - ) - ) - ); + if !self.show_menu { + lines.push(Line::from(Span::styled( + &self.install_message, + self.style.add_modifier(Modifier::BOLD), + ))); + lines.push(Line::from("")); - lines.push(Line::from("")); + if !self.stage.is_empty() { + lines.push(Line::from(Span::styled( + &self.stage, + self.style.add_modifier(Modifier::BOLD), + ))); - // welcome text - lines.push( - Line::from( - Span::styled( - welcome_text, - self.style, - ) - ) - ); + let percent = self.percent.clamp(0.0, 100.0); + let w = 30; + let fill = ((percent / 100.0 * w as f64) as usize).min(w); + let bar = format!("[{}{}]", "█".repeat(fill), "░".repeat(w - fill)); - lines.push(Line::from("")); + let mut spans = vec![ + Span::styled(bar, self.menu_style), + Span::raw(format!(" {:.2}%", percent)), + ]; - // menu title - lines.push( - Line::from( - Span::styled( - t!("welcome.menu"), - self.style.add_modifier(Modifier::BOLD), - ) - ) - ); + if self.stage.contains("Downloading") { + let mb = self.downloaded as f64 / 1024.0 / 1024.0; + let total_mb = self.total as f64 / 1024.0 / 1024.0; + let speed_mb = self.speed / 1024.0 / 1024.0; + + spans.push(Span::raw(format!(" {:.2} MB/{:.2} MB", mb, total_mb))); + spans.push(Span::raw(format!(" {:.2} MB/s", speed_mb))); + } else { + spans.push(Span::raw(format!(" {} / {}", self.downloaded, self.total))); + } + + lines.push(Line::from(spans)); + } + } lines.push(Line::from("")); - // menu if self.show_menu { - let max_index = menu_items - .len() - .saturating_sub(1); + lines.push(Line::from(Span::styled( + t!("welcome.menu"), + self.style.add_modifier(Modifier::BOLD), + ))); + lines.push(Line::from("")); - self.selected_index = - self.selected_index.min(max_index); + let max = menu_items.len() - 1; + self.selected_index = self.selected_index.min(max); for (i, item) in menu_items.iter().enumerate() { - let selected = - i == self.selected_index; - - let prefix = if selected { - "→ " - } else { - " " - }; - + let selected = i == self.selected_index; + let prefix = if selected { "→ " } else { " " }; let style = if selected { - self.menu_style.add_modifier( - Modifier::BOLD - | Modifier::REVERSED, - ) + self.menu_style.add_modifier(Modifier::BOLD | Modifier::REVERSED) } else { self.style }; - lines.push( - Line::from( - Span::styled( - format!("{prefix}• {item}"), - style, - ) - ) - ); + lines.push(Line::from(Span::styled( + format!("{prefix}• {item}"), + style, + ))); } } diff --git a/src/main.rs b/src/main.rs index 90ce7d0..a993874 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ fn main() { } fn init_logger() { - let _logger = Logger::try_with_str("trace") + let _logger = Logger::try_with_str("OmegaCode=trace,reqwest=warn,hyper=warn") .unwrap() .log_to_file( FileSpec::default() @@ -38,7 +38,6 @@ fn init_logger() { Cleanup::KeepLogFiles(3), ) .write_mode(WriteMode::Direct) - .duplicate_to_stderr(Duplicate::Debug) // Logger level, production env should be Info level .format_for_files(detailed_format) .start() .unwrap(); diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 1375bed..ad11938 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -1,4 +1,16 @@ -mod manifest; -mod download; -mod extract; -mod runtime; \ No newline at end of file +pub mod manifest; +pub mod download; +pub mod extract; +pub mod runtime; + +#[derive(Debug, Clone)] +pub struct DownloadProgress { + pub downloaded: u64, + pub total: u64, +} + +#[derive(Debug, Clone)] +pub struct ExtractProgress { + pub extracted: Option, + pub total: Option, +} \ No newline at end of file