use crate::components::animated_progress::AnimatedProgress;
use crate::core::flash_subscription::FlashProgress;
use crate::core::storage::Storage;
use crate::core::{update, Message};
use crate::domain::{DriveInfo, ImageInfo};
use crate::view;
use flashkraft_core::commands::watch_usb_events;
use futures::StreamExt as _;
use iced::Color;
use iced::{stream, Element, Subscription, Task, Theme};
use std::sync::{atomic::AtomicBool, Arc};
#[derive(Debug)]
pub struct FlashKraft {
pub selected_image: Option<ImageInfo>,
pub selected_target: Option<DriveInfo>,
pub available_drives: Vec<DriveInfo>,
pub flash_progress: Option<f32>,
pub flash_bytes_written: u64,
pub flash_speed_mb_s: f32,
pub flash_stage: String,
pub verify_progress: Option<f32>,
pub verify_speed_mb_s: f32,
pub verify_phase: &'static str,
pub error_message: Option<String>,
pub device_selection_open: bool,
pub flashing_active: bool,
pub flash_complete: bool,
pub flash_cancel_token: Arc<AtomicBool>,
pub flash_run_id: u64,
pub theme: Theme,
pub storage: Option<Storage>,
pub animated_progress: AnimatedProgress,
pub verify_animated_progress: AnimatedProgress,
pub animation_time: f32,
}
impl FlashKraft {
pub fn new() -> Self {
let storage = Storage::new().ok();
let theme = storage
.as_ref()
.and_then(|s| s.load_theme())
.unwrap_or(Theme::Dark);
let mut animated_progress = AnimatedProgress::new();
animated_progress.set_theme(theme.clone());
let verify_animated_progress =
AnimatedProgress::new_with_color(Color::from_rgb(0.18, 0.78, 0.45));
Self {
selected_image: None,
selected_target: None,
available_drives: Vec::new(),
flash_progress: None,
flash_bytes_written: 0,
flash_speed_mb_s: 0.0,
flash_stage: String::new(),
verify_progress: None,
verify_speed_mb_s: 0.0,
verify_phase: "",
error_message: None,
device_selection_open: false,
flashing_active: false,
flash_complete: false,
flash_cancel_token: Arc::new(AtomicBool::new(false)),
flash_run_id: 0,
theme,
storage,
animated_progress,
verify_animated_progress,
animation_time: 0.0,
}
}
pub fn is_ready_to_flash(&self) -> bool {
self.selected_image.is_some() && self.selected_target.is_some()
}
pub fn is_flashing(&self) -> bool {
self.flash_progress.is_some()
}
pub fn is_flash_complete(&self) -> bool {
self.flash_complete
}
pub fn has_error(&self) -> bool {
self.error_message.is_some()
}
pub fn reset(&mut self) {
self.selected_image = None;
self.selected_target = None;
self.flash_progress = None;
self.flash_bytes_written = 0;
self.flash_speed_mb_s = 0.0;
self.flash_stage = String::new();
self.verify_progress = None;
self.verify_speed_mb_s = 0.0;
self.verify_phase = "";
self.error_message = None;
self.device_selection_open = false;
self.flashing_active = false;
self.flash_complete = false;
self.flash_cancel_token = Arc::new(AtomicBool::new(false));
}
pub fn cancel_selections(&mut self) {
self.reset();
}
pub fn begin_flash_state(&mut self) {
self.flash_cancel_token = Arc::new(AtomicBool::new(false));
self.flash_run_id = self.flash_run_id.wrapping_add(1);
self.flash_progress = Some(0.0);
self.error_message = None;
self.flashing_active = true;
self.flash_complete = false;
}
}
impl Default for FlashKraft {
fn default() -> Self {
Self::new()
}
}
impl FlashKraft {
pub fn update(&mut self, message: Message) -> Task<Message> {
if !matches!(message, Message::AnimationTick) {
#[cfg(debug_assertions)]
println!("[DEBUG] Message: {:?}", message);
}
update::update(self, message)
}
pub fn view(&self) -> Element<'_, Message> {
view::view(self)
}
pub fn subscription(&self) -> Subscription<Message> {
let mut subscriptions = Vec::new();
let hotplug_sub = Subscription::run(hotplug_stream);
subscriptions.push(hotplug_sub);
if self.flashing_active {
if let (Some(image), Some(target)) = (&self.selected_image, &self.selected_target) {
let flash_sub = crate::core::flash_subscription::flash_progress(
image.path.clone(),
target.device_path.clone().into(),
self.flash_cancel_token.clone(),
self.flash_run_id,
)
.map(|progress| match progress {
FlashProgress::Progress {
progress,
bytes_written,
speed_mb_s,
} => Message::FlashProgressUpdate(progress, bytes_written, speed_mb_s),
FlashProgress::VerifyProgress {
phase,
overall,
bytes_read,
total_bytes,
speed_mb_s,
} => Message::VerifyProgressUpdate(
overall,
phase,
bytes_read,
total_bytes,
speed_mb_s,
),
FlashProgress::Message(msg) => Message::Status(msg),
FlashProgress::Completed => Message::FlashCompleted(Ok(())),
FlashProgress::Failed(err) => Message::FlashCompleted(Err(err)),
});
subscriptions.push(flash_sub);
}
let animation_sub = iced::window::frames().map(|_| Message::AnimationTick);
subscriptions.push(animation_sub);
} else {
let animation_sub = iced::window::frames().map(|_| Message::AnimationTick);
subscriptions.push(animation_sub);
}
Subscription::batch(subscriptions)
}
}
fn hotplug_stream() -> impl futures::Stream<Item = Message> {
stream::channel(4, async |mut output| {
use futures::SinkExt as _;
match watch_usb_events() {
Ok(mut events) => {
while let Some(_event) = events.next().await {
let _ = output.send(Message::UsbHotplugDetected).await;
}
}
Err(e) => {
eprintln!("[hotplug] watch_usb_events failed: {e}");
std::future::pending::<()>().await;
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_new_state() {
let state = FlashKraft::new();
assert!(state.selected_image.is_none());
assert!(state.selected_target.is_none());
assert!(state.available_drives.is_empty());
assert!(!state.is_ready_to_flash());
assert!(!state.device_selection_open);
}
#[test]
fn test_is_ready_to_flash() {
let mut state = FlashKraft::new();
assert!(!state.is_ready_to_flash());
state.selected_image = Some(ImageInfo {
path: PathBuf::from("/tmp/test.img"),
name: "test.img".to_string(),
size_mb: 100.0,
});
assert!(!state.is_ready_to_flash());
state.selected_target = Some(DriveInfo::new(
"USB".to_string(),
"/media/usb".to_string(),
32.0,
"/dev/sdb".to_string(),
));
assert!(state.is_ready_to_flash());
}
#[test]
fn test_is_flashing() {
let mut state = FlashKraft::new();
assert!(!state.is_flashing());
state.flash_progress = Some(0.5);
assert!(state.is_flashing());
}
#[test]
fn test_reset() {
let mut state = FlashKraft::new();
state.selected_image = Some(ImageInfo {
path: PathBuf::from("/tmp/test.img"),
name: "test.img".to_string(),
size_mb: 100.0,
});
state.flash_progress = Some(0.5);
state.error_message = Some("Error".to_string());
state.device_selection_open = true;
state.reset();
assert!(state.selected_image.is_none());
assert!(state.flash_progress.is_none());
assert!(state.error_message.is_none());
assert!(!state.device_selection_open);
}
}