From 3280afd7890e0ecba9bd382cd28ab8fadffe0b50 Mon Sep 17 00:00:00 2001 From: Caelan Date: Sun, 29 Sep 2024 10:37:03 -0600 Subject: [PATCH] Add UI logging output to RustIced sample --- samples/RustIced/Cargo.lock | 1 + samples/RustIced/Cargo.toml | 3 +- samples/RustIced/src/logger.rs | 55 +++++++++++ samples/RustIced/src/main.rs | 167 +++++++++++++++++++++------------ 4 files changed, 167 insertions(+), 59 deletions(-) create mode 100644 samples/RustIced/src/logger.rs diff --git a/samples/RustIced/Cargo.lock b/samples/RustIced/Cargo.lock index c0141997..c9045574 100644 --- a/samples/RustIced/Cargo.lock +++ b/samples/RustIced/Cargo.lock @@ -3698,6 +3698,7 @@ dependencies = [ "async-std", "directories-next", "iced", + "log", "once_cell", "serde", "serde_json", diff --git a/samples/RustIced/Cargo.toml b/samples/RustIced/Cargo.toml index 326cb358..248a7c1d 100644 --- a/samples/RustIced/Cargo.toml +++ b/samples/RustIced/Cargo.toml @@ -14,4 +14,5 @@ serde_json = "1.0" uuid = { version = "1.0", features = ["v4", "fast-rng", "serde"] } directories-next = "2.0" tracing-subscriber = "0.3" -velopack = { path = "../../src/lib-rust", features = ["async"] } \ No newline at end of file +velopack = { path = "../../src/lib-rust", features = ["async"] } +log = "0.4" diff --git a/samples/RustIced/src/logger.rs b/samples/RustIced/src/logger.rs new file mode 100644 index 00000000..cd5514d1 --- /dev/null +++ b/samples/RustIced/src/logger.rs @@ -0,0 +1,55 @@ +use crate::{AppState, Message}; +use iced::futures::{channel::mpsc, sink::SinkExt, StreamExt}; +use iced::{stream, Subscription}; +use log::{Level, Log, Metadata, Record}; +use std::sync::Mutex; + +static LOG_RECEIVER: Mutex>> = Mutex::new(None); + +pub struct IcedLogger { + sender: mpsc::UnboundedSender, +} + +impl IcedLogger { + pub fn init() + { + let (sender, receiver) = mpsc::unbounded(); + log::set_boxed_logger(Box::new(IcedLogger { sender })).unwrap(); + log::set_max_level(log::LevelFilter::Info); + + let mut log_receiver = LOG_RECEIVER.lock().unwrap(); + *log_receiver = Some(receiver); + } + + fn take_receiver() -> mpsc::UnboundedReceiver { + let mut log_receiver = LOG_RECEIVER.lock().unwrap(); + log_receiver.take().unwrap() + } + + pub fn subscription(_: &AppState) -> Subscription { + Subscription::run(|| { + let mut log_receiver = Self::take_receiver(); + stream::channel(100, |mut output| async move { + loop { + let message = log_receiver.select_next_some().await; + let _ = output.send(Message::LogReceived(message)).await; + } + }) + }) + } +} + +impl Log for IcedLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= Level::Info + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + let log_msg = format!("{}", record.args()); + let _ = self.sender.unbounded_send(log_msg); + } + } + + fn flush(&self) {} +} \ No newline at end of file diff --git a/samples/RustIced/src/main.rs b/samples/RustIced/src/main.rs index 12d05bfe..d0002fc1 100644 --- a/samples/RustIced/src/main.rs +++ b/samples/RustIced/src/main.rs @@ -1,7 +1,10 @@ #![windows_subsystem = "windows"] -use anyhow::Result; -use iced::widget::{Button, Column, Text}; +mod logger; + +use anyhow::Result; +use iced::widget::{button, column, container, scrollable, text, vertical_space}; +use iced::Task; use velopack::*; #[derive(Debug, Clone)] @@ -12,18 +15,45 @@ pub enum Message { DownloadProgress(i16), DownloadComplete, Restart, + LogReceived(String), } -pub struct GUI { +pub struct AppState { update_manager: Option, - state: GUIState, + status: AppStatus, current_version: Option, update_info: Option, download_progress: i16, + logs: Vec, +} + +impl AppState { + pub fn new() -> (Self, Task) { + let source = sources::FileSource::new(env!("RELEASES_DIR")); + let um = UpdateManager::new(source, None, None); + let mut version: Option = None; + let mut state = AppStatus::NotInstalled; + if um.is_ok() { + state = AppStatus::Idle; + version = Some(um.as_ref().unwrap().current_version().unwrap()); + } + + ( + AppState { + logs: Vec::new(), + update_manager: um.ok(), + status: state, + current_version: version, + update_info: None, + download_progress: 0, + }, + Task::none(), + ) + } } #[derive(Debug, Clone)] -pub enum GUIState { +pub enum AppStatus { NotInstalled, Idle, Checking, @@ -33,38 +63,30 @@ pub enum GUIState { } fn main() -> Result<()> { + logger::IcedLogger::init(); VelopackApp::build().run(); - let source = sources::FileSource::new(env!("RELEASES_DIR")); - let um = UpdateManager::new(source, None, None); - let mut version: Option = None; - let mut state = GUIState::NotInstalled; - if um.is_ok() { - state = GUIState::Idle; - version = Some(um.as_ref().unwrap().current_version().unwrap()); - } - - let gui = GUI { update_manager: um.ok(), state, current_version: version, update_info: None, download_progress: 0 }; + iced::application("Velopack Rust Sample", update, view) + .window_size(iced::Size::new(600.0, 400.0)) + .centered() + .subscription(logger::IcedLogger::subscription) + .run_with(|| AppState::new())?; - iced::application("A cool application", update, view) - .window_size(iced::Size::new(400.0, 200.0)) - .run_with(move || (gui, iced::Task::none()))?; - Ok(()) } -fn update(gui: &mut GUI, message: Message) -> iced::Task { +fn update(state: &mut AppState, message: Message) -> Task { match message { Message::CheckForUpdates => { - gui.state = GUIState::Checking; - iced::Task::perform(gui.update_manager.as_ref().unwrap().check_for_updates_async(), |result| match result { + state.status = AppStatus::Checking; + Task::perform(state.update_manager.as_ref().unwrap().check_for_updates_async(), |result| match result { Ok(update_info) => { match update_info { UpdateCheck::RemoteIsEmpty => Message::UpdatesFound(None), UpdateCheck::NoUpdateAvailable => Message::UpdatesFound(None), UpdateCheck::UpdateAvailable(updates) => Message::UpdatesFound(Some(updates)), } - }, + } Err(_) => { // Handle the error case, perhaps by logging or setting an error state // For simplicity, we're sending a None update here, but you should handle errors appropriately @@ -73,55 +95,84 @@ fn update(gui: &mut GUI, message: Message) -> iced::Task { }) } Message::UpdatesFound(update) => { - gui.update_info = update; - gui.state = match gui.update_info { - Some(_) => GUIState::UpdatesAvailable, - None => GUIState::Idle, + state.update_info = update; + state.status = match state.update_info { + Some(_) => AppStatus::UpdatesAvailable, + None => AppStatus::Idle, }; - iced::Task::none() + Task::none() } Message::DownloadUpdates => { - gui.state = GUIState::Downloading; - let update_info = gui.update_info.clone().unwrap(); // Ensure you handle this safely in your actual code - iced::Task::perform(gui.update_manager.as_ref().unwrap().download_updates_async(&update_info, None), |_| Message::DownloadComplete) + state.status = AppStatus::Downloading; + let update_info = state.update_info.clone().unwrap(); // Ensure you handle this safely in your actual code + Task::perform(state.update_manager.as_ref().unwrap().download_updates_async(&update_info, None), |_| Message::DownloadComplete) } Message::DownloadProgress(progress) => { - gui.download_progress = progress; - iced::Task::none() + state.download_progress = progress; + Task::none() } Message::DownloadComplete => { - gui.state = GUIState::ReadyToRestart; - iced::Task::none() + state.status = AppStatus::ReadyToRestart; + Task::none() } Message::Restart => { - let update_info = gui.update_info.clone().unwrap(); // Ensure you handle this safely in your actual code - gui.update_manager.as_ref().unwrap().apply_updates_and_restart(update_info).unwrap(); - iced::Task::none() + let update_info = state.update_info.clone().unwrap(); // Ensure you handle this safely in your actual code + state.update_manager.as_ref().unwrap().apply_updates_and_restart(update_info).unwrap(); + Task::none() + } + Message::LogReceived(log) => { + state.logs.push(log); + Task::none() } } } -fn view(gui: &GUI) -> iced::Element { - let content = match gui.state { - GUIState::NotInstalled => Column::new() - .push(Text::new("Can't check for updates if not installed")), - GUIState::Idle => Column::new() - .push(Text::new(format!("Current version: {}", gui.current_version.as_ref().unwrap_or(&"Unknown".to_string())))) - .push(Button::new(Text::new("Check for updates")).on_press(Message::CheckForUpdates)), - GUIState::Checking => Column::new() - .push(Text::new("Checking for updates...")), - GUIState::UpdatesAvailable => { - let update_version = gui.update_info.as_ref().map_or("Unknown", |info| &info.TargetFullRelease.Version); - Column::new() - .push(Text::new(format!("Update available: {}", update_version))) - .push(Button::new(Text::new("Download updates")).on_press(Message::DownloadUpdates)) +fn view(state: &AppState) -> iced::Element { + let content = match state.status { + AppStatus::NotInstalled => + column![text("Can't check for updates if not installed")], + AppStatus::Idle => + column![ + text(format!("Current version: {}", state.current_version.as_ref().unwrap_or(&"Unknown".to_string()))), + button("Check for updates").on_press(Message::CheckForUpdates), + ], + AppStatus::Checking => + column![text("Checking for updates...")], + AppStatus::UpdatesAvailable => { + let update_version = state.update_info.as_ref().map_or("Unknown", |info| &info.TargetFullRelease.Version); + column![ + text(format!("Update available: {}", update_version)), + button("Download updates").on_press(Message::DownloadUpdates), + ] } - GUIState::Downloading => Column::new() - .push(Text::new(format!("Downloading updates... Progress: {}%", gui.download_progress))), - GUIState::ReadyToRestart => Column::new() - .push(Text::new("Updates downloaded. Ready to restart.")) - .push(Button::new(Text::new("Restart")).on_press(Message::Restart)), + AppStatus::Downloading => + column![text(format!("Downloading updates... Progress: {}%", state.download_progress))], + AppStatus::ReadyToRestart => + column![ + text("Updates downloaded. Ready to restart."), + button("Restart").on_press(Message::Restart), + ], }; + + let log_area = scrollable(text(state.logs.join("\n"))) + .height(iced::Length::Fill) + .width(iced::Length::Fill); + + let log_container = container(log_area) + .padding(10) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .style(|_| { + container::Style { + background: Some(iced::Color::from_rgb8(0x77, 0x1d, 0x1d).into()), + text_color: Some(iced::Color::WHITE), + ..Default::default() + } + }); - content.into() + column![ + content, + vertical_space().height(20), + log_container, + ].into() }