Add UI logging output to RustIced sample

This commit is contained in:
Caelan
2024-09-29 10:37:03 -06:00
parent ffbdd343c7
commit 3280afd789
4 changed files with 167 additions and 59 deletions

View File

@@ -3698,6 +3698,7 @@ dependencies = [
"async-std", "async-std",
"directories-next", "directories-next",
"iced", "iced",
"log",
"once_cell", "once_cell",
"serde", "serde",
"serde_json", "serde_json",

View File

@@ -14,4 +14,5 @@ serde_json = "1.0"
uuid = { version = "1.0", features = ["v4", "fast-rng", "serde"] } uuid = { version = "1.0", features = ["v4", "fast-rng", "serde"] }
directories-next = "2.0" directories-next = "2.0"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
velopack = { path = "../../src/lib-rust", features = ["async"] } velopack = { path = "../../src/lib-rust", features = ["async"] }
log = "0.4"

View File

@@ -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<Option<mpsc::UnboundedReceiver<String>>> = Mutex::new(None);
pub struct IcedLogger {
sender: mpsc::UnboundedSender<String>,
}
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<String> {
let mut log_receiver = LOG_RECEIVER.lock().unwrap();
log_receiver.take().unwrap()
}
pub fn subscription(_: &AppState) -> Subscription<Message> {
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) {}
}

View File

@@ -1,7 +1,10 @@
#![windows_subsystem = "windows"] #![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::*; use velopack::*;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -12,18 +15,45 @@ pub enum Message {
DownloadProgress(i16), DownloadProgress(i16),
DownloadComplete, DownloadComplete,
Restart, Restart,
LogReceived(String),
} }
pub struct GUI { pub struct AppState {
update_manager: Option<UpdateManager>, update_manager: Option<UpdateManager>,
state: GUIState, status: AppStatus,
current_version: Option<String>, current_version: Option<String>,
update_info: Option<UpdateInfo>, update_info: Option<UpdateInfo>,
download_progress: i16, download_progress: i16,
logs: Vec<String>,
}
impl AppState {
pub fn new() -> (Self, Task<Message>) {
let source = sources::FileSource::new(env!("RELEASES_DIR"));
let um = UpdateManager::new(source, None, None);
let mut version: Option<String> = 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)] #[derive(Debug, Clone)]
pub enum GUIState { pub enum AppStatus {
NotInstalled, NotInstalled,
Idle, Idle,
Checking, Checking,
@@ -33,38 +63,30 @@ pub enum GUIState {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
logger::IcedLogger::init();
VelopackApp::build().run(); VelopackApp::build().run();
let source = sources::FileSource::new(env!("RELEASES_DIR")); iced::application("Velopack Rust Sample", update, view)
let um = UpdateManager::new(source, None, None); .window_size(iced::Size::new(600.0, 400.0))
let mut version: Option<String> = None; .centered()
let mut state = GUIState::NotInstalled; .subscription(logger::IcedLogger::subscription)
if um.is_ok() { .run_with(|| AppState::new())?;
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("A cool application", update, view)
.window_size(iced::Size::new(400.0, 200.0))
.run_with(move || (gui, iced::Task::none()))?;
Ok(()) Ok(())
} }
fn update(gui: &mut GUI, message: Message) -> iced::Task<Message> { fn update(state: &mut AppState, message: Message) -> Task<Message> {
match message { match message {
Message::CheckForUpdates => { Message::CheckForUpdates => {
gui.state = GUIState::Checking; state.status = AppStatus::Checking;
iced::Task::perform(gui.update_manager.as_ref().unwrap().check_for_updates_async(), |result| match result { Task::perform(state.update_manager.as_ref().unwrap().check_for_updates_async(), |result| match result {
Ok(update_info) => { Ok(update_info) => {
match update_info { match update_info {
UpdateCheck::RemoteIsEmpty => Message::UpdatesFound(None), UpdateCheck::RemoteIsEmpty => Message::UpdatesFound(None),
UpdateCheck::NoUpdateAvailable => Message::UpdatesFound(None), UpdateCheck::NoUpdateAvailable => Message::UpdatesFound(None),
UpdateCheck::UpdateAvailable(updates) => Message::UpdatesFound(Some(updates)), UpdateCheck::UpdateAvailable(updates) => Message::UpdatesFound(Some(updates)),
} }
}, }
Err(_) => { Err(_) => {
// Handle the error case, perhaps by logging or setting an error state // 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 // 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> {
}) })
} }
Message::UpdatesFound(update) => { Message::UpdatesFound(update) => {
gui.update_info = update; state.update_info = update;
gui.state = match gui.update_info { state.status = match state.update_info {
Some(_) => GUIState::UpdatesAvailable, Some(_) => AppStatus::UpdatesAvailable,
None => GUIState::Idle, None => AppStatus::Idle,
}; };
iced::Task::none() Task::none()
} }
Message::DownloadUpdates => { Message::DownloadUpdates => {
gui.state = GUIState::Downloading; state.status = AppStatus::Downloading;
let update_info = gui.update_info.clone().unwrap(); // Ensure you handle this safely in your actual code let update_info = state.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) Task::perform(state.update_manager.as_ref().unwrap().download_updates_async(&update_info, None), |_| Message::DownloadComplete)
} }
Message::DownloadProgress(progress) => { Message::DownloadProgress(progress) => {
gui.download_progress = progress; state.download_progress = progress;
iced::Task::none() Task::none()
} }
Message::DownloadComplete => { Message::DownloadComplete => {
gui.state = GUIState::ReadyToRestart; state.status = AppStatus::ReadyToRestart;
iced::Task::none() Task::none()
} }
Message::Restart => { Message::Restart => {
let update_info = gui.update_info.clone().unwrap(); // Ensure you handle this safely in your actual code let update_info = state.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(); state.update_manager.as_ref().unwrap().apply_updates_and_restart(update_info).unwrap();
iced::Task::none() Task::none()
}
Message::LogReceived(log) => {
state.logs.push(log);
Task::none()
} }
} }
} }
fn view(gui: &GUI) -> iced::Element<Message> { fn view(state: &AppState) -> iced::Element<Message> {
let content = match gui.state { let content = match state.status {
GUIState::NotInstalled => Column::new() AppStatus::NotInstalled =>
.push(Text::new("Can't check for updates if not installed")), column![text("Can't check for updates if not installed")],
GUIState::Idle => Column::new() AppStatus::Idle =>
.push(Text::new(format!("Current version: {}", gui.current_version.as_ref().unwrap_or(&"Unknown".to_string())))) column![
.push(Button::new(Text::new("Check for updates")).on_press(Message::CheckForUpdates)), text(format!("Current version: {}", state.current_version.as_ref().unwrap_or(&"Unknown".to_string()))),
GUIState::Checking => Column::new() button("Check for updates").on_press(Message::CheckForUpdates),
.push(Text::new("Checking for updates...")), ],
GUIState::UpdatesAvailable => { AppStatus::Checking =>
let update_version = gui.update_info.as_ref().map_or("Unknown", |info| &info.TargetFullRelease.Version); column![text("Checking for updates...")],
Column::new() AppStatus::UpdatesAvailable => {
.push(Text::new(format!("Update available: {}", update_version))) let update_version = state.update_info.as_ref().map_or("Unknown", |info| &info.TargetFullRelease.Version);
.push(Button::new(Text::new("Download updates")).on_press(Message::DownloadUpdates)) column![
text(format!("Update available: {}", update_version)),
button("Download updates").on_press(Message::DownloadUpdates),
]
} }
GUIState::Downloading => Column::new() AppStatus::Downloading =>
.push(Text::new(format!("Downloading updates... Progress: {}%", gui.download_progress))), column![text(format!("Downloading updates... Progress: {}%", state.download_progress))],
GUIState::ReadyToRestart => Column::new() AppStatus::ReadyToRestart =>
.push(Text::new("Updates downloaded. Ready to restart.")) column![
.push(Button::new(Text::new("Restart")).on_press(Message::Restart)), 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()
} }