diff --git a/Cargo.lock b/Cargo.lock index 11783581..458aaf23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4176,6 +4176,18 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "glow" version = "0.16.0" @@ -6233,6 +6245,7 @@ dependencies = [ "egui_dock", "egui_extras", "futures", + "gloo-timers", "humansize", "image 0.24.9", "js-sys", diff --git a/mutant-web/Cargo.toml b/mutant-web/Cargo.toml index 5a1c1ec5..df2f961c 100644 --- a/mutant-web/Cargo.toml +++ b/mutant-web/Cargo.toml @@ -15,7 +15,7 @@ crate-type = ["cdylib", "rlib"] wasm-bindgen = "0.2.95" wasm-bindgen-futures = "0.4.45" -js-sys = "0.3.64" +js-sys = "0.3.72" serde_cbor = "0.11.2" serde = { version = "1.0.214", features = ["derive"] } serde-wasm-bindgen = "0.6.5" @@ -52,6 +52,7 @@ mime_guess = "2.0.4" base64 = "0.21.7" egui_extras = { version = "0.31.0", features = ["syntect"] } uuid = { version = "1.17.0", features = ["v4"] } +gloo-timers = { version = "0.3.0", features = ["futures"] } [dependencies.web-sys] version = "0.3" @@ -80,6 +81,7 @@ features = [ "HtmlMediaElement", "HtmlSourceElement", "Url", + "Response", ] diff --git a/mutant-web/src/app/components/progress.rs b/mutant-web/src/app/components/progress.rs index 8668774e..01644a88 100644 --- a/mutant-web/src/app/components/progress.rs +++ b/mutant-web/src/app/components/progress.rs @@ -1,4 +1,5 @@ -use eframe::egui::{self, text::LayoutJob, ProgressBar}; +use eframe::egui::{self, text::LayoutJob, ProgressBar, RichText}; +use crate::app::theme::MutantColors; pub fn progress(completion: f32, duration: String) -> ProgressBar { let mut text = format!("{:.1}% - {}", completion * 100.0, duration); @@ -44,3 +45,126 @@ pub fn detailed_progress(completion: f32, current: usize, total: usize, duration ProgressBar::new(completion as f32).animate(true).text(job) } + +/// Modern styled progress bar for file transfers (Phase 1: Browser → Daemon) +pub fn file_transfer_progress(ui: &mut egui::Ui, completion: f32, bytes_transferred: u64, total_bytes: u64, filename: &str) { + egui::Frame::new() + .fill(MutantColors::SURFACE) + .stroke(egui::Stroke::new(1.0, MutantColors::BORDER_MEDIUM)) + .inner_margin(egui::Margin::same(8)) + .show(ui, |ui| { + ui.horizontal(|ui| { + // File icon + ui.label(RichText::new("📁").size(16.0).color(MutantColors::ACCENT_BLUE)); + + ui.vertical(|ui| { + // Filename + ui.label(RichText::new(filename).size(13.0).color(MutantColors::TEXT_PRIMARY)); + + // Progress bar + let progress_bar = ProgressBar::new(completion) + .fill(MutantColors::ACCENT_BLUE) + .animate(true); + ui.add(progress_bar); + + // Transfer stats + let transferred_str = format_bytes(bytes_transferred); + let total_str = format_bytes(total_bytes); + let percentage = (completion * 100.0) as u32; + + ui.label(RichText::new(format!("{} / {} ({}%)", transferred_str, total_str, percentage)) + .size(11.0) + .color(MutantColors::TEXT_SECONDARY)); + }); + }); + }); +} + +/// Modern styled progress bar for network uploads (Phase 2: Daemon → Network) +pub fn network_upload_progress( + ui: &mut egui::Ui, + reservation_progress: f32, reserved_count: usize, + upload_progress: f32, uploaded_count: usize, + confirmation_progress: f32, confirmed_count: usize, + total_chunks: usize, + elapsed_time: String +) { + egui::Frame::new() + .fill(MutantColors::SURFACE) + .stroke(egui::Stroke::new(1.0, MutantColors::BORDER_MEDIUM)) + .inner_margin(egui::Margin::same(12)) + .show(ui, |ui| { + ui.vertical(|ui| { + // Header + ui.horizontal(|ui| { + ui.label(RichText::new("🌐").size(16.0).color(MutantColors::ACCENT_ORANGE)); + ui.label(RichText::new("Uploading to Autonomi Network") + .size(14.0) + .strong() + .color(MutantColors::TEXT_PRIMARY)); + }); + + ui.add_space(8.0); + + // Reservation phase + ui.horizontal(|ui| { + ui.label(RichText::new("Reserving:").size(12.0).color(MutantColors::TEXT_SECONDARY)); + ui.add(ProgressBar::new(reservation_progress) + .fill(MutantColors::WARNING) + .animate(true)); + ui.label(RichText::new(format!("{}/{}", reserved_count, total_chunks)) + .size(11.0) + .color(MutantColors::TEXT_MUTED)); + }); + + ui.add_space(4.0); + + // Upload phase + ui.horizontal(|ui| { + ui.label(RichText::new("Uploading:").size(12.0).color(MutantColors::TEXT_SECONDARY)); + ui.add(ProgressBar::new(upload_progress) + .fill(MutantColors::ACCENT_ORANGE) + .animate(true)); + ui.label(RichText::new(format!("{}/{}", uploaded_count, total_chunks)) + .size(11.0) + .color(MutantColors::TEXT_MUTED)); + }); + + ui.add_space(4.0); + + // Confirmation phase + ui.horizontal(|ui| { + ui.label(RichText::new("Confirming:").size(12.0).color(MutantColors::TEXT_SECONDARY)); + ui.add(ProgressBar::new(confirmation_progress) + .fill(MutantColors::SUCCESS) + .animate(true)); + ui.label(RichText::new(format!("{}/{}", confirmed_count, total_chunks)) + .size(11.0) + .color(MutantColors::TEXT_MUTED)); + }); + + ui.add_space(6.0); + + // Elapsed time + ui.horizontal(|ui| { + ui.label(RichText::new("⏱").size(12.0).color(MutantColors::ACCENT_CYAN)); + ui.label(RichText::new(format!("Elapsed: {}", elapsed_time)) + .size(11.0) + .color(MutantColors::TEXT_MUTED)); + }); + }); + }); +} + +/// Helper function to format bytes in human-readable format +fn format_bytes(bytes: u64) -> String { + if bytes >= 1024 * 1024 * 1024 { + format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0)) + } else if bytes >= 1024 * 1024 { + format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0)) + } else if bytes >= 1024 { + format!("{:.1} KB", bytes as f64 / 1024.0) + } else { + format!("{} B", bytes) + } +} diff --git a/mutant-web/src/app/fs_window.rs b/mutant-web/src/app/fs_window.rs index 2db65eb4..bfaac2e8 100644 --- a/mutant-web/src/app/fs_window.rs +++ b/mutant-web/src/app/fs_window.rs @@ -4,6 +4,9 @@ use eframe::egui::{self, RichText}; use mutant_protocol::KeyDetails; use serde::{Deserialize, Serialize}; +use lazy_static::lazy_static; +use web_sys::Blob; +use js_sys::Uint8Array; // Updated use statements use crate::app::components::multimedia; @@ -28,6 +31,154 @@ use crate::app::{put::PutWindow, stats::StatsWindow, colony_window::ColonyWindow // FsInternalTab and FsInternalTabViewer moved to mutant-web/src/app/fs/internal_tab.rs +// Global callback for adding PutWindow after file selection +lazy_static! { + static ref PENDING_PUT_WINDOW: Arc>> = Arc::new(RwLock::new(None)); +} + +/// Add a PutWindow to the main FsWindow from anywhere (used by file dialog callback) +pub fn add_put_window_to_main_fs(put_window: PutWindow) { + if let Some(fs_window_ref) = crate::app::fs::global::get_main_fs_window() { + let mut fs_window = fs_window_ref.write().unwrap(); + + // Check if a Put tab already exists + let tab_exists = fs_window.internal_dock.iter_all_tabs().any(|(_, existing_tab)| { + matches!(existing_tab, crate::app::fs::internal_tab::FsInternalTab::Put(_)) + }); + + if !tab_exists { + let tab = crate::app::fs::internal_tab::FsInternalTab::Put(put_window); + + // Add to the internal dock system + if fs_window.internal_dock.iter_all_tabs().next().is_none() { + // If the dock is empty, create a new surface + fs_window.internal_dock = egui_dock::DockState::new(vec![tab]); + } else { + // Add to the existing surface + fs_window.internal_dock.main_surface_mut().push_to_focused_leaf(tab); + } + + log::info!("Successfully added PutWindow tab to main FsWindow"); + } else { + log::info!("Put tab already exists in main FsWindow"); + } + } else { + log::warn!("Main FsWindow reference not available for adding PutWindow"); + } +} + +/// Start immediate file transfer from browser to daemon +fn start_immediate_file_transfer(file: web_sys::File, filename: String) { + use wasm_bindgen_futures::spawn_local; + + log::info!("Starting immediate file transfer for: {}", filename); + + spawn_local(async move { + // Read the file in chunks and start streaming to daemon + let file_size = file.size() as u64; + const CHUNK_SIZE: u64 = 256 * 1024; // 256KB chunks + let total_chunks = ((file_size + CHUNK_SIZE - 1) / CHUNK_SIZE) as usize; + + log::info!("File size: {} bytes, will be read in {} chunks", file_size, total_chunks); + + // Update the PutWindow to show that file reading has started + update_put_window_file_reading_state(true, 0.0, 0); + + let mut offset = 0u64; + let mut chunk_index = 0usize; + + while offset < file_size { + let chunk_size = std::cmp::min(CHUNK_SIZE, file_size - offset); + let end_offset = offset + chunk_size; + + // Create a blob slice for this chunk + let blob_slice = file.slice_with_i32_and_i32(offset as i32, end_offset as i32).unwrap(); + + // Read this chunk + match read_blob_chunk(blob_slice).await { + Ok(chunk_data) => { + log::debug!("Read chunk {} ({} bytes) at offset {}", chunk_index, chunk_data.len(), offset); + + // Update progress + let progress = (end_offset as f32) / (file_size as f32); + update_put_window_file_reading_state(true, progress, end_offset); + + // TODO: Send chunk to daemon here + // For now, we'll just simulate the transfer + + chunk_index += 1; + offset = end_offset; + }, + Err(e) => { + log::error!("Failed to read chunk {}: {}", chunk_index, e); + update_put_window_error_state(format!("Failed to read file: {}", e)); + return; + } + } + + // Small delay to prevent blocking the UI + gloo_timers::future::TimeoutFuture::new(10).await; + } + + log::info!("File reading completed for: {}", filename); + update_put_window_file_reading_state(false, 1.0, file_size); + }); +} + +/// Read a blob chunk asynchronously +async fn read_blob_chunk(blob: Blob) -> Result, String> { + use wasm_bindgen_futures::JsFuture; + use web_sys::Response; + + // Use the Response API to read the blob as array buffer + let response = Response::new_with_opt_blob(Some(&blob)) + .map_err(|e| format!("Failed to create response: {:?}", e))?; + + let array_buffer_promise = response.array_buffer() + .map_err(|e| format!("Failed to get array buffer: {:?}", e))?; + + let array_buffer = JsFuture::from(array_buffer_promise).await + .map_err(|e| format!("Failed to read array buffer: {:?}", e))?; + + let uint8_array = Uint8Array::new(&array_buffer); + let mut data = vec![0u8; uint8_array.length() as usize]; + uint8_array.copy_to(&mut data); + + Ok(data) +} + +/// Update the PutWindow file reading state +fn update_put_window_file_reading_state(is_reading: bool, progress: f32, bytes_read: u64) { + if let Some(fs_window_ref) = crate::app::fs::global::get_main_fs_window() { + let mut fs_window = fs_window_ref.write().unwrap(); + + // Find the PutWindow tab and update its state + for (_, tab) in fs_window.internal_dock.iter_all_tabs_mut() { + if let crate::app::fs::internal_tab::FsInternalTab::Put(put_window) = tab { + // Update the file reading state using the public method + put_window.update_file_reading_state(is_reading, progress, bytes_read); + break; + } + } + } +} + +/// Update the PutWindow error state +fn update_put_window_error_state(error: String) { + if let Some(fs_window_ref) = crate::app::fs::global::get_main_fs_window() { + let mut fs_window = fs_window_ref.write().unwrap(); + + // Find the PutWindow tab and update its error state + for (_, tab) in fs_window.internal_dock.iter_all_tabs_mut() { + if let crate::app::fs::internal_tab::FsInternalTab::Put(put_window) = tab { + // Set error message using the public method + put_window.set_error_message(error); + break; + } + } + } +} + /// The filesystem tree window #[derive(Clone, Serialize, Deserialize)] pub struct FsWindow { @@ -376,8 +527,9 @@ impl FsWindow { } /// Add a new Put window tab to the internal dock system + /// This now immediately triggers the file dialog and only creates the window after file selection pub fn add_put_tab(&mut self) { - log::info!("FsWindow: Creating new Put window tab"); + log::info!("FsWindow: Triggering file dialog for upload"); // Check if a Put tab already exists let tab_exists = self.internal_dock.iter_all_tabs().any(|(_, existing_tab)| { @@ -385,25 +537,77 @@ impl FsWindow { }); if !tab_exists { - // Create a new Put window - let put_window = PutWindow::new(); - let tab = crate::app::fs::internal_tab::FsInternalTab::Put(put_window); - - // Add to the internal dock system - if self.internal_dock.iter_all_tabs().next().is_none() { - // If the dock is empty, create a new surface - self.internal_dock = egui_dock::DockState::new(vec![tab]); - } else { - // Add to the existing surface - self.internal_dock.main_surface_mut().push_to_focused_leaf(tab); - } - - log::info!("FsWindow: Successfully added Put tab to internal dock"); + // Trigger file dialog immediately and only create PutWindow after successful selection + self.trigger_file_dialog_for_upload(); } else { log::info!("FsWindow: Put tab already exists in internal dock"); } } + /// Trigger file dialog and create PutWindow only after successful file selection + fn trigger_file_dialog_for_upload(&mut self) { + use wasm_bindgen::{JsCast, closure::Closure}; + use web_sys::Event; + + log::info!("Triggering file dialog for upload"); + + // Create file input element + let document = web_sys::window().unwrap().document().unwrap(); + let input = document + .create_element("input") + .unwrap() + .dyn_into::() + .unwrap(); + + input.set_type("file"); + input.set_accept("*/*"); + + // We need to capture self in a way that works with the closure + // Since we can't move self into the closure, we'll use a different approach + let input_clone = input.clone(); + + let onchange = Closure::once(move |_event: Event| { + if let Some(files) = input_clone.files() { + if files.length() > 0 { + if let Some(file) = files.get(0) { + let file_name = file.name(); + let file_size_js = file.size(); + let file_type_js = file.type_(); + + log::info!("File selected: {} ({} bytes, type: {})", file_name, file_size_js, file_type_js); + + // Create PutWindow with the selected file information + let mut put_window = PutWindow::new(); + + // Set the file information in the PutWindow using the setter method + let file_type_str = if file_type_js.is_empty() { + "Unknown".to_string() + } else { + file_type_js + }; + put_window.set_file_info(file_name.clone(), file_size_js as u64, file_type_str); + + // Add the PutWindow to the main FsWindow using the global function + add_put_window_to_main_fs(put_window); + + // Start immediate file transfer from browser to daemon + start_immediate_file_transfer(file, file_name.clone()); + + crate::app::notifications::info(format!("File selected: {}. Upload window opened.", file_name)); + } + } else { + log::info!("File selection cancelled - no PutWindow will be created"); + } + } + }); + + input.set_onchange(Some(onchange.as_ref().unchecked_ref())); + onchange.forget(); + + // Trigger file picker + input.click(); + } + /// Add a new Stats window tab to the internal dock system pub fn add_stats_tab(&mut self) { log::info!("FsWindow: Creating new Stats window tab"); diff --git a/mutant-web/src/app/put.rs b/mutant-web/src/app/put.rs index 1c4a89e7..4957899f 100644 --- a/mutant-web/src/app/put.rs +++ b/mutant-web/src/app/put.rs @@ -1,6 +1,6 @@ use std::sync::{Arc, RwLock}; -use eframe::egui::{self, Color32, RichText}; +use eframe::egui::{self, RichText}; use js_sys::Uint8Array; use mutant_protocol::StorageMode; use serde::{Deserialize, Serialize}; @@ -11,20 +11,20 @@ use web_sys::{File, FileReader, Event}; use web_time::{Duration, SystemTime}; use super::Window; -use super::components::progress::detailed_progress; +use super::components::progress::{file_transfer_progress, network_upload_progress}; use super::context; use super::notifications; -use super::theme::{MutantColors, primary_button, success_button}; +use super::theme::{MutantColors, primary_button, secondary_button, styled_section_group, info_section_frame, section_header}; #[derive(Clone, Serialize, Deserialize)] pub struct PutWindow { // File selection state selected_file: Arc>>, file_size: Arc>>, + file_type: Arc>>, #[serde(skip)] // Skip serializing file data to avoid localStorage quota issues file_data: Arc>>>, - // Key name input key_name: Arc>, @@ -33,6 +33,11 @@ pub struct PutWindow { storage_mode: Arc>, no_verify: Arc>, + // UI state + should_trigger_file_dialog: Arc>, + advanced_settings_open: Arc>, + file_selected: Arc>, + // File reading progress (Phase 1: File-to-Web) is_reading_file: Arc>, file_read_progress: Arc>, @@ -77,12 +82,18 @@ impl Default for PutWindow { Self { selected_file: Arc::new(RwLock::new(None)), file_size: Arc::new(RwLock::new(None)), + file_type: Arc::new(RwLock::new(None)), file_data: Arc::new(RwLock::new(None)), key_name: Arc::new(RwLock::new(String::new())), public: Arc::new(RwLock::new(false)), storage_mode: Arc::new(RwLock::new(StorageMode::Heaviest)), no_verify: Arc::new(RwLock::new(false)), + // UI state + should_trigger_file_dialog: Arc::new(RwLock::new(false)), + advanced_settings_open: Arc::new(RwLock::new(false)), + file_selected: Arc::new(RwLock::new(false)), + // File reading progress (Phase 1) is_reading_file: Arc::new(RwLock::new(false)), file_read_progress: Arc::new(RwLock::new(0.0)), @@ -123,6 +134,12 @@ impl Window for PutWindow { // Log that we're drawing the window log::debug!("Drawing PutWindow"); + // Check if we should trigger the file dialog immediately + if *self.should_trigger_file_dialog.read().unwrap() { + *self.should_trigger_file_dialog.write().unwrap() = false; + self.trigger_file_dialog(); + } + // If we're uploading, check the progress before drawing the form // This ensures we have the latest progress values when drawing if *self.is_uploading.read().unwrap() && !*self.upload_complete.read().unwrap() { @@ -135,7 +152,7 @@ impl Window for PutWindow { } // Draw the form with the updated progress values - self.draw_upload_form(ui); + self.draw_modern_upload_form(ui); // Request a repaint to ensure we update frequently // This is crucial for smooth progress bar updates @@ -149,6 +166,96 @@ impl PutWindow { Self::default() } + /// Trigger immediate file selection when the window is created + pub fn trigger_immediate_file_selection(&mut self) { + *self.should_trigger_file_dialog.write().unwrap() = true; + } + + /// Set file information (used when creating PutWindow with pre-selected file) + pub fn set_file_info(&mut self, filename: String, file_size: u64, file_type: String) { + *self.selected_file.write().unwrap() = Some(filename.clone()); + *self.file_size.write().unwrap() = Some(file_size); + *self.file_type.write().unwrap() = Some(file_type); + *self.file_selected.write().unwrap() = true; + + // Auto-populate key name with complete filename (including extension) + *self.key_name.write().unwrap() = filename; + } + + /// Update file reading state (used by external file transfer logic) + pub fn update_file_reading_state(&self, is_reading: bool, progress: f32, bytes_read: u64) { + *self.is_reading_file.write().unwrap() = is_reading; + *self.file_read_progress.write().unwrap() = progress; + *self.file_read_bytes.write().unwrap() = bytes_read; + } + + /// Set error message (used by external file transfer logic) + pub fn set_error_message(&self, error: String) { + *self.error_message.write().unwrap() = Some(error); + *self.is_reading_file.write().unwrap() = false; + } + + /// Trigger the file dialog immediately + fn trigger_file_dialog(&self) { + log::info!("Triggering immediate file dialog"); + + // Create file input element + let document = web_sys::window().unwrap().document().unwrap(); + let input = document + .create_element("input") + .unwrap() + .dyn_into::() + .unwrap(); + + input.set_type("file"); + input.set_accept("*/*"); + + // Clone references for the closure + let selected_file = self.selected_file.clone(); + let file_size = self.file_size.clone(); + let file_type = self.file_type.clone(); + let file_selected = self.file_selected.clone(); + let key_name = self.key_name.clone(); + let input_clone = input.clone(); + + let onchange = Closure::once(move |_event: Event| { + if let Some(files) = input_clone.files() { + if files.length() > 0 { + if let Some(file) = files.get(0) { + let file_name = file.name(); + let file_size_js = file.size(); + let file_type_js = file.type_(); + + log::info!("File selected: {} ({} bytes, type: {})", file_name, file_size_js, file_type_js); + + // Update state with selected file info + *selected_file.write().unwrap() = Some(file_name.clone()); + *file_size.write().unwrap() = Some(file_size_js as u64); + *file_type.write().unwrap() = Some(if file_type_js.is_empty() { + "Unknown".to_string() + } else { + file_type_js + }); + *file_selected.write().unwrap() = true; + + // Auto-populate key name with complete filename (including extension) + *key_name.write().unwrap() = file_name.clone(); + + notifications::info(format!("File selected: {}", file_name)); + } + } else { + log::info!("File selection cancelled"); + } + } + }); + + input.set_onchange(Some(onchange.as_ref().unchecked_ref())); + onchange.forget(); + + // Trigger file picker + input.click(); + } + fn check_progress(&self) { // Check if we should update the progress based on the timer let now = SystemTime::now(); @@ -596,284 +703,562 @@ impl PutWindow { *self.current_put_id.write().unwrap() = None; } - fn draw_upload_form(&mut self, ui: &mut egui::Ui) { + /// Modern redesigned upload form with professional styling + fn draw_modern_upload_form(&mut self, ui: &mut egui::Ui) { let is_uploading = *self.is_uploading.read().unwrap(); let upload_complete = *self.upload_complete.read().unwrap(); let is_reading_file = *self.is_reading_file.read().unwrap(); + let file_selected = *self.file_selected.read().unwrap(); // Check if we're in any kind of processing state let is_processing = is_uploading || is_reading_file; if !is_processing && !upload_complete { - // File selection section - ui.heading(RichText::new("📤 Upload File").size(20.0).color(MutantColors::TEXT_PRIMARY)); - ui.add_space(15.0); - - // Key name input with styled frame - ui.group(|ui| { - ui.vertical(|ui| { - ui.label(RichText::new("Key Name:").color(MutantColors::TEXT_PRIMARY)); - ui.add_space(5.0); - let mut key_name = self.key_name.write().unwrap(); - ui.text_edit_singleline(&mut *key_name); - }); + // Main header + ui.horizontal(|ui| { + ui.label(section_header("Upload File", "📤", MutantColors::ACCENT_ORANGE)); }); + ui.add_space(12.0); - ui.add_space(10.0); + // File Information Section (only show if file is selected) + if file_selected { + self.draw_file_info_section(ui); + ui.add_space(12.0); + } - // Show selected file info if any (from previous upload) - if let Some(filename) = &*self.selected_file.read().unwrap() { - ui.group(|ui| { - ui.vertical(|ui| { - ui.label(RichText::new("Last Selected File:").color(MutantColors::TEXT_SECONDARY)); - ui.label(RichText::new(filename).color(MutantColors::ACCENT_BLUE)); + // Key Name Input Section + self.draw_key_name_section(ui); + ui.add_space(12.0); - if let Some(size) = *self.file_size.read().unwrap() { - ui.label(RichText::new(format!("Size: {} bytes", size)).color(MutantColors::TEXT_MUTED)); - } - }); - }); - ui.add_space(10.0); - } + // Upload Mode Selection (Public/Private) + self.draw_upload_mode_section(ui); + ui.add_space(12.0); + + // Advanced Settings (Collapsible) + self.draw_advanced_settings_section(ui); + ui.add_space(12.0); - ui.add_space(10.0); + // Action Buttons Section + self.draw_action_buttons_section(ui); + + // Error Display + self.draw_error_section(ui); + } else if is_reading_file { + // Modern file reading progress + self.draw_file_reading_progress(ui); + } else if is_uploading { + // Modern upload progress with two phases + self.draw_upload_progress(ui); + + } else if upload_complete { + // Modern upload completion section + self.draw_upload_complete_section(ui); + } + } - // Configuration options - ui.collapsing("Upload Options", |ui| { - // Public checkbox + /// Draw the file information section with modern styling + fn draw_file_info_section(&self, ui: &mut egui::Ui) { + if let (Some(filename), Some(size), Some(file_type)) = ( + &*self.selected_file.read().unwrap(), + *self.file_size.read().unwrap(), + &*self.file_type.read().unwrap() + ) { + info_section_frame().show(ui, |ui| { ui.horizontal(|ui| { - let mut public = self.public.write().unwrap(); - ui.checkbox(&mut *public, "Public"); - ui.label("Make this file publicly accessible"); + // File icon based on type + let (icon, icon_color) = self.get_file_icon_and_color(filename); + ui.label(RichText::new(icon).size(24.0).color(icon_color)); + + ui.vertical(|ui| { + // Filename + ui.label(RichText::new(filename) + .size(16.0) + .strong() + .color(MutantColors::TEXT_PRIMARY)); + + // File details + let size_str = format_bytes(size); + ui.label(RichText::new(format!("{} • {}", size_str, file_type)) + .size(12.0) + .color(MutantColors::TEXT_SECONDARY)); + }); }); + }); + } + } + + /// Draw the key name input section + fn draw_key_name_section(&self, ui: &mut egui::Ui) { + styled_section_group().show(ui, |ui| { + ui.vertical(|ui| { + ui.label(section_header("Key Name", "🔑", MutantColors::ACCENT_BLUE)); + ui.add_space(8.0); + + let mut key_name = self.key_name.write().unwrap(); + ui.text_edit_singleline(&mut *key_name); + + ui.add_space(4.0); + ui.label(RichText::new("Choose a unique name for your file") + .size(11.0) + .color(MutantColors::TEXT_MUTED)); + }); + }); + } + + /// Draw the upload mode selection (Public/Private) + fn draw_upload_mode_section(&self, ui: &mut egui::Ui) { + styled_section_group().show(ui, |ui| { + ui.vertical(|ui| { + ui.label(section_header("Upload Mode", "🌐", MutantColors::ACCENT_GREEN)); + ui.add_space(8.0); + + let mut public = self.public.write().unwrap(); - // Storage mode selection ui.horizontal(|ui| { - ui.label("Storage Mode:"); - let mut storage_mode = self.storage_mode.write().unwrap(); - - egui::ComboBox::new(format!("mutant_put_storage_mode_{}", self.window_id), "") - .selected_text(format!("{:?}", *storage_mode)) - .show_ui(ui, |ui| { - ui.selectable_value(&mut *storage_mode, StorageMode::Light, "Light"); - ui.selectable_value(&mut *storage_mode, StorageMode::Medium, "Medium"); - ui.selectable_value(&mut *storage_mode, StorageMode::Heavy, "Heavy"); - ui.selectable_value(&mut *storage_mode, StorageMode::Heaviest, "Heaviest"); - }); + if ui.radio_value(&mut *public, false, "Private").clicked() { + log::info!("Upload mode set to Private"); + } + ui.label(RichText::new("Only you can access this file") + .size(11.0) + .color(MutantColors::TEXT_MUTED)); }); - // No verify checkbox + ui.add_space(4.0); + ui.horizontal(|ui| { - let mut no_verify = self.no_verify.write().unwrap(); - ui.checkbox(&mut *no_verify, "Skip Verification"); - ui.label("Skip verification of uploaded data (faster but less safe)"); + if ui.radio_value(&mut *public, true, "Public").clicked() { + log::info!("Upload mode set to Public"); + } + ui.label(RichText::new("Anyone with the address can access this file") + .size(11.0) + .color(MutantColors::TEXT_MUTED)); }); }); + }); + } - ui.add_space(10.0); + /// Draw the advanced settings section (collapsible) + fn draw_advanced_settings_section(&self, ui: &mut egui::Ui) { + let mut advanced_open = self.advanced_settings_open.write().unwrap(); - // Upload button - now triggers file selection and upload - let can_upload = !self.key_name.read().unwrap().is_empty(); + ui.collapsing("⚙️ Advanced Settings", |ui| { + *advanced_open = true; - ui.add_space(15.0); - ui.horizontal(|ui| { - if ui.add_enabled(can_upload, primary_button("📁 Select File and Upload")).clicked() { - self.start_upload(); - } + styled_section_group().show(ui, |ui| { + ui.vertical(|ui| { + // Storage mode selection with chunk size information + ui.horizontal(|ui| { + ui.label(RichText::new("Storage Mode:").color(MutantColors::TEXT_SECONDARY)); + let mut storage_mode = self.storage_mode.write().unwrap(); + + egui::ComboBox::new(format!("mutant_put_storage_mode_{}", self.window_id), "") + .selected_text(format!("{:?}", *storage_mode)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut *storage_mode, StorageMode::Light, "Light (64KB chunks)"); + ui.selectable_value(&mut *storage_mode, StorageMode::Medium, "Medium (256KB chunks)"); + ui.selectable_value(&mut *storage_mode, StorageMode::Heavy, "Heavy (512KB chunks)"); + ui.selectable_value(&mut *storage_mode, StorageMode::Heaviest, "Heaviest (1MB chunks)"); + }); + }); + + ui.add_space(8.0); + + // No verify checkbox + ui.horizontal(|ui| { + let mut no_verify = self.no_verify.write().unwrap(); + ui.checkbox(&mut *no_verify, "Skip Verification"); + ui.label(RichText::new("Faster upload but less safe") + .size(11.0) + .color(MutantColors::TEXT_MUTED)); + }); + }); }); + }); + + if !ui.ctx().memory(|m| m.is_popup_open(egui::Id::new("⚙️ Advanced Settings"))) { + *advanced_open = false; + } + } + + /// Draw the action buttons section + fn draw_action_buttons_section(&self, ui: &mut egui::Ui) { + let file_selected = *self.file_selected.read().unwrap(); + let key_name_valid = !self.key_name.read().unwrap().is_empty(); - if !can_upload { - ui.add_space(5.0); - ui.label(RichText::new("⚠ Please enter a key name").color(MutantColors::WARNING)); + ui.add_space(8.0); + + ui.horizontal(|ui| { + if file_selected && key_name_valid { + // File is selected and key name is valid - show upload button + if ui.add(primary_button("🚀 Start Upload")).clicked() { + self.start_upload_with_selected_file(); + } + } else if !file_selected { + // No file selected - show file selection button + if ui.add(primary_button("📁 Select File")).clicked() { + self.trigger_file_dialog(); + } + } else { + // File selected but no key name - show disabled upload button + ui.add_enabled(false, primary_button("🚀 Start Upload")); } - // Show error message if any - if let Some(error) = &*self.error_message.read().unwrap() { - ui.add_space(10.0); - ui.group(|ui| { - ui.label(RichText::new(format!("❌ Error: {}", error)).color(MutantColors::ERROR)); - }); + // Reset/Clear button + if file_selected { + ui.add_space(8.0); + if ui.add(secondary_button("🗑 Clear")).clicked() { + self.reset_file_selection(); + } } - } else if is_reading_file { - // File reading progress section - ui.heading("Reading File"); - ui.add_space(10.0); - - let file_name = self.selected_file.read().unwrap().clone().unwrap_or_default(); - ui.label(format!("Reading: {}", file_name)); - - ui.add_space(5.0); - - // File reading progress - let file_read_progress = *self.file_read_progress.read().unwrap(); - let file_read_bytes = *self.file_read_bytes.read().unwrap(); - let file_size = self.file_size.read().unwrap().unwrap_or(0); - - ui.label("Loading file into memory:"); - ui.add(detailed_progress( - file_read_progress, - file_read_bytes as usize, - file_size as usize, - "Reading...".to_string() - )); - - ui.add_space(10.0); - ui.label(RichText::new("Please wait while the file is being loaded...").color(Color32::GRAY)); - } else if is_uploading { - // Progress section - ui.heading("Upload Progress"); - ui.add_space(10.0); + }); + + // Validation messages + if file_selected && !key_name_valid { + ui.add_space(6.0); + ui.label(RichText::new("⚠ Please enter a key name to continue") + .color(MutantColors::WARNING)); + } + } - let key_name = self.key_name.read().unwrap(); - ui.label(format!("Uploading: {}", *key_name)); + /// Draw error messages if any + fn draw_error_section(&self, ui: &mut egui::Ui) { + if let Some(error) = &*self.error_message.read().unwrap() { + ui.add_space(12.0); + info_section_frame().show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new("❌").size(16.0).color(MutantColors::ERROR)); + ui.label(RichText::new(error) + .color(MutantColors::ERROR) + .strong()); + }); + }); + } + } - ui.add_space(5.0); + /// Draw file reading progress with modern styling + fn draw_file_reading_progress(&self, ui: &mut egui::Ui) { + ui.vertical_centered(|ui| { + ui.label(section_header("Reading File", "📖", MutantColors::ACCENT_BLUE)); + ui.add_space(16.0); - // Check if we're in daemon upload phase - let is_uploading_to_daemon = *self.is_uploading_to_daemon.read().unwrap(); - if is_uploading_to_daemon { - // Show daemon upload progress - let daemon_upload_progress = *self.daemon_upload_progress.read().unwrap(); - let daemon_upload_bytes = *self.daemon_upload_bytes.read().unwrap(); + if let Some(filename) = &*self.selected_file.read().unwrap() { + let file_read_progress = *self.file_read_progress.read().unwrap(); + let file_read_bytes = *self.file_read_bytes.read().unwrap(); let file_size = self.file_size.read().unwrap().unwrap_or(0); - ui.label("Sending to daemon:"); - ui.add(detailed_progress( - daemon_upload_progress, - daemon_upload_bytes as usize, - file_size as usize, - "Uploading...".to_string() - )); + // Use the modern file transfer progress component + file_transfer_progress(ui, file_read_progress, file_read_bytes, file_size, filename); - ui.add_space(5.0); + ui.add_space(12.0); + ui.label(RichText::new("Please wait while the file is being loaded...") + .color(MutantColors::TEXT_MUTED)); } + }); + } - // Calculate elapsed time - let elapsed = if let Some(start_time) = *self.start_time.read().unwrap() { - start_time.elapsed().unwrap() - } else { - *self.elapsed_time.read().unwrap() - }; - - let elapsed_str = format_elapsed_time(elapsed); - - // Get total chunks and current progress - let total_chunks = *self.total_chunks.read().unwrap(); - let chunks_to_reserve = *self.chunks_to_reserve.read().unwrap(); + /// Draw upload progress with modern two-phase display + fn draw_upload_progress(&self, ui: &mut egui::Ui) { + ui.vertical_centered(|ui| { + ui.label(section_header("Upload Progress", "🚀", MutantColors::ACCENT_ORANGE)); + ui.add_space(16.0); - // Calculate current counts based on progress - let reservation_progress = *self.reservation_progress.read().unwrap(); - let upload_progress = *self.upload_progress.read().unwrap(); - let confirmation_progress = *self.confirmation_progress.read().unwrap(); + // Phase 1: Browser → Daemon (if active) + let is_uploading_to_daemon = *self.is_uploading_to_daemon.read().unwrap(); + if is_uploading_to_daemon { + if let Some(filename) = &*self.selected_file.read().unwrap() { + let daemon_upload_progress = *self.daemon_upload_progress.read().unwrap(); + let daemon_upload_bytes = *self.daemon_upload_bytes.read().unwrap(); + let file_size = self.file_size.read().unwrap().unwrap_or(0); + + ui.label(RichText::new("Phase 1: Transferring to Daemon") + .size(14.0) + .color(MutantColors::TEXT_PRIMARY)); + ui.add_space(8.0); + + file_transfer_progress(ui, daemon_upload_progress, daemon_upload_bytes, file_size, filename); + ui.add_space(16.0); + } + } - // Get the latest progress values directly from the context - let (reservation_progress, upload_progress, confirmation_progress, total_chunks) = { + // Phase 2: Daemon → Network (if active) + let is_uploading = *self.is_uploading.read().unwrap(); + if is_uploading && !is_uploading_to_daemon { + // Get progress data from context if let Some(put_id) = &*self.current_put_id.read().unwrap() { let ctx = context::context(); if let Some(progress) = ctx.get_put_progress(put_id) { let progress_guard = progress.read().unwrap(); if let Some(op) = progress_guard.operation.get("put") { - // Calculate progress percentages - let res_progress = if op.total_pads > 0 { - op.nb_reserved as f32 / op.total_pads as f32 - } else { - 0.0 - }; + let total_chunks = op.total_pads; - let up_progress = if op.total_pads > 0 { - op.nb_written as f32 / op.total_pads as f32 - } else { - 0.0 - }; + let reservation_progress = if total_chunks > 0 { + op.nb_reserved as f32 / total_chunks as f32 + } else { 0.0 }; - let conf_progress = if op.total_pads > 0 { - op.nb_confirmed as f32 / op.total_pads as f32 - } else { - 0.0 - }; + let upload_progress = if total_chunks > 0 { + op.nb_written as f32 / total_chunks as f32 + } else { 0.0 }; - // Update the stored progress values - *self.reservation_progress.write().unwrap() = res_progress; - *self.upload_progress.write().unwrap() = up_progress; - *self.confirmation_progress.write().unwrap() = conf_progress; - *self.total_chunks.write().unwrap() = op.total_pads; + let confirmation_progress = if total_chunks > 0 { + op.nb_confirmed as f32 / total_chunks as f32 + } else { 0.0 }; - (res_progress, up_progress, conf_progress, op.total_pads) - } else { - (reservation_progress, upload_progress, confirmation_progress, total_chunks) + // Calculate elapsed time + let elapsed = if let Some(start_time) = *self.start_time.read().unwrap() { + start_time.elapsed().unwrap() + } else { + *self.elapsed_time.read().unwrap() + }; + let elapsed_str = format_elapsed_time(elapsed); + + ui.label(RichText::new("Phase 2: Uploading to Network") + .size(14.0) + .color(MutantColors::TEXT_PRIMARY)); + ui.add_space(8.0); + + // Use the modern network upload progress component + network_upload_progress( + ui, + reservation_progress, op.nb_reserved, + upload_progress, op.nb_written, + confirmation_progress, op.nb_confirmed, + total_chunks, elapsed_str + ); } - } else { - (reservation_progress, upload_progress, confirmation_progress, total_chunks) } - } else { - (reservation_progress, upload_progress, confirmation_progress, total_chunks) } - }; - // Calculate current counts for each stage - let reserved_count = if chunks_to_reserve > 0 { - (reservation_progress * total_chunks as f32) as usize - } else { - (reservation_progress * total_chunks as f32) as usize - }; + ui.add_space(16.0); - let uploaded_count = (upload_progress * total_chunks as f32) as usize; - let confirmed_count = (confirmation_progress * total_chunks as f32) as usize; + // Cancel button + if ui.add(secondary_button("❌ Cancel Upload")).clicked() { + *self.is_uploading.write().unwrap() = false; + notifications::warning("Upload cancelled".to_string()); + } + } + }); + } - log::debug!("Drawing progress bars: reservation={:.2}%, upload={:.2}%, confirmation={:.2}%", - reservation_progress * 100.0, upload_progress * 100.0, confirmation_progress * 100.0); + /// Draw upload completion section with modern styling + fn draw_upload_complete_section(&self, ui: &mut egui::Ui) { + ui.vertical_centered(|ui| { + ui.label(section_header("Upload Complete", "✅", MutantColors::SUCCESS)); + ui.add_space(16.0); - // Reservation progress bar - ui.label("Reserving pads:"); - ui.add(detailed_progress(reservation_progress, reserved_count, total_chunks, elapsed_str.clone())); + styled_section_group().show(ui, |ui| { + ui.vertical(|ui| { + let key_name = self.key_name.read().unwrap(); + ui.label(RichText::new(format!("Successfully uploaded: {}", *key_name)) + .size(16.0) + .strong() + .color(MutantColors::TEXT_PRIMARY)); + + ui.add_space(8.0); + + // Show elapsed time + let elapsed = *self.elapsed_time.read().unwrap(); + ui.label(RichText::new(format!("Time taken: {}", format_elapsed_time(elapsed))) + .color(MutantColors::TEXT_SECONDARY)); + + // Show public address if available + if let Some(address) = &*self.public_address.read().unwrap() { + ui.add_space(12.0); + ui.label(RichText::new("Public Address:") + .strong() + .color(MutantColors::ACCENT_GREEN)); + ui.add_space(4.0); + + ui.horizontal(|ui| { + ui.text_edit_singleline(&mut address.clone()); + if ui.button("📋").on_hover_text("Copy to clipboard").clicked() { + ui.ctx().copy_text(address.clone()); + notifications::info("Address copied to clipboard".to_string()); + } + }); + } + }); + }); - ui.add_space(5.0); + ui.add_space(20.0); - // Upload progress bar - ui.label("Uploading pads:"); - ui.add(detailed_progress(upload_progress, uploaded_count, total_chunks, elapsed_str.clone())); + ui.horizontal(|ui| { + if ui.add(primary_button("📤 Upload Another File")).clicked() { + self.reset(); + } - ui.add_space(5.0); + ui.add_space(8.0); - // Confirmation progress bar - ui.label("Confirming pads:"); - ui.add(detailed_progress(confirmation_progress, confirmed_count, total_chunks, elapsed_str)); + if ui.add(secondary_button("🗂 View in Files")).clicked() { + // TODO: Switch to files tab and highlight the uploaded file + notifications::info("Switching to Files view...".to_string()); + } + }); + }); + } - // Cancel button - ui.add_space(10.0); - if ui.button("Cancel").clicked() { - // TODO: Implement cancellation - *self.is_uploading.write().unwrap() = false; - notifications::warning("Upload cancelled".to_string()); + /// Get file icon and color based on filename extension (similar to fs-tree) + fn get_file_icon_and_color(&self, filename: &str) -> (&'static str, egui::Color32) { + if let Some(extension) = std::path::Path::new(filename).extension() { + match extension.to_string_lossy().to_lowercase().as_str() { + // Code files + "rs" | "rust" => ("🦀", MutantColors::ACCENT_ORANGE), + "js" | "ts" | "jsx" | "tsx" => ("📜", MutantColors::WARNING), + "py" | "python" => ("🐍", MutantColors::ACCENT_GREEN), + "java" | "class" => ("☕", MutantColors::ACCENT_ORANGE), + "cpp" | "c" | "cc" | "cxx" | "h" | "hpp" => ("⚙️", MutantColors::ACCENT_BLUE), + "go" => ("🐹", MutantColors::ACCENT_CYAN), + "html" | "htm" => ("🌐", MutantColors::ACCENT_ORANGE), + "css" | "scss" | "sass" | "less" => ("🎨", MutantColors::ACCENT_BLUE), + "json" | "yaml" | "yml" | "toml" | "xml" => ("📋", MutantColors::TEXT_MUTED), + + // Images + "png" | "jpg" | "jpeg" | "gif" | "bmp" | "svg" | "webp" => ("📷", MutantColors::ACCENT_GREEN), + + // Videos + "mp4" | "avi" | "mkv" | "mov" | "wmv" | "flv" | "webm" => ("🎬", MutantColors::ACCENT_PURPLE), + + // Audio + "mp3" | "wav" | "flac" | "ogg" | "aac" | "m4a" => ("🎵", MutantColors::ACCENT_CYAN), + + // Archives + "zip" | "rar" | "7z" | "tar" | "gz" | "bz2" | "xz" => ("📦", MutantColors::ACCENT_ORANGE), + + // Documents + "pdf" => ("📕", MutantColors::ERROR), + "doc" | "docx" => ("📘", MutantColors::ACCENT_BLUE), + "xls" | "xlsx" => ("📗", MutantColors::ACCENT_GREEN), + "ppt" | "pptx" => ("📙", MutantColors::WARNING), + "txt" | "md" | "readme" => ("📄", MutantColors::TEXT_MUTED), + + // Executables + "exe" | "msi" | "deb" | "rpm" | "dmg" | "app" => ("⚡", MutantColors::WARNING), + + _ => ("📄", MutantColors::TEXT_MUTED) } - } else if upload_complete { - // Upload complete section - ui.heading("Upload Complete"); - ui.add_space(10.0); + } else { + ("📄", MutantColors::TEXT_MUTED) + } + } - let key_name = self.key_name.read().unwrap(); - ui.label(format!("Successfully uploaded: {}", *key_name)); + /// Reset file selection state + fn reset_file_selection(&self) { + *self.selected_file.write().unwrap() = None; + *self.file_size.write().unwrap() = None; + *self.file_type.write().unwrap() = None; + *self.file_selected.write().unwrap() = false; + *self.key_name.write().unwrap() = String::new(); - // Show elapsed time - let elapsed = *self.elapsed_time.read().unwrap(); - ui.label(format!("Time taken: {}", format_elapsed_time(elapsed))); + notifications::info("File selection cleared".to_string()); + } - // Show public address if available - if let Some(address) = &*self.public_address.read().unwrap() { - ui.add_space(5.0); - ui.horizontal(|ui| { - ui.label("Public index address:"); - ui.text_edit_singleline(&mut address.clone()); - }); + /// Start upload with the currently selected file + fn start_upload_with_selected_file(&self) { + let key_name = self.key_name.read().unwrap().clone(); + let storage_mode = self.storage_mode.read().unwrap().clone(); + let public = *self.public.read().unwrap(); + let no_verify = *self.no_verify.read().unwrap(); + + log::info!("Starting upload with selected file: key_name={}, public={}, no_verify={}", + key_name, public, no_verify); + + // Check if we have a selected file + if let Some(filename) = &*self.selected_file.read().unwrap() { + if let Some(file_size) = *self.file_size.read().unwrap() { + // Set upload state + *self.is_uploading.write().unwrap() = true; + *self.upload_complete.write().unwrap() = false; + *self.start_time.write().unwrap() = Some(SystemTime::now()); + *self.error_message.write().unwrap() = None; + + // Reset progress + *self.reservation_progress.write().unwrap() = 0.0; + *self.upload_progress.write().unwrap() = 0.0; + *self.confirmation_progress.write().unwrap() = 0.0; + + // Start the daemon-to-network upload phase + self.start_daemon_to_network_upload( + key_name, + filename.clone(), + file_size, + storage_mode, + public, + no_verify + ); + + notifications::info("Starting upload to network...".to_string()); + } else { + notifications::error("No file size information available".to_string()); } + } else { + notifications::error("No file selected for upload".to_string()); + } + } - ui.add_space(15.0); - ui.horizontal(|ui| { - if ui.add(success_button("📤 Upload Another File")).clicked() { - self.reset(); + /// Start the daemon-to-network upload phase + fn start_daemon_to_network_upload( + &self, + key_name: String, + filename: String, + file_size: u64, + storage_mode: mutant_protocol::StorageMode, + public: bool, + no_verify: bool, + ) { + use wasm_bindgen_futures::spawn_local; + + // Clone the necessary Arc references for the async closure + let current_put_id = self.current_put_id.clone(); + let error_message = self.error_message.clone(); + let is_uploading = self.is_uploading.clone(); + let upload_complete = self.upload_complete.clone(); + let start_time = self.start_time.clone(); + let elapsed_time = self.elapsed_time.clone(); + + spawn_local(async move { + let ctx = context::context(); + + // For now, we'll simulate the file data since we don't have it stored + // In a real implementation, we would have the file data from the browser-to-daemon transfer + let file_data = vec![0u8; file_size as usize]; // Placeholder data + + log::info!("Starting daemon-to-network upload for: {} ({} bytes)", filename, file_size); + + // Create progress tracking + let (put_id, progress) = ctx.create_progress(&key_name, &filename); + *current_put_id.write().unwrap() = Some(put_id.clone()); + + // Start the upload + match ctx.put( + &key_name, + file_data, + &filename, + storage_mode, + public, + no_verify, + Some((put_id, progress)) + ).await { + Ok((final_put_id, _progress)) => { + log::info!("Upload completed successfully: {}", final_put_id); + + // Calculate elapsed time + if let Some(start) = *start_time.read().unwrap() { + *elapsed_time.write().unwrap() = start.elapsed().unwrap(); + } + + // Mark as complete + *is_uploading.write().unwrap() = false; + *upload_complete.write().unwrap() = true; + + notifications::info("Upload completed successfully!".to_string()); + }, + Err(e) => { + log::error!("Upload failed: {}", e); + *error_message.write().unwrap() = Some(format!("Upload failed: {}", e)); + *is_uploading.write().unwrap() = false; + notifications::error(format!("Upload failed: {}", e)); } - }); - } + } + }); } } @@ -893,4 +1278,17 @@ fn format_elapsed_time(duration: Duration) -> String { let seconds = total_seconds % 60; format!("{}h {}m {}s", hours, minutes, seconds) } +} + +// Helper function to format bytes in human-readable format +fn format_bytes(bytes: u64) -> String { + if bytes >= 1024 * 1024 * 1024 { + format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0)) + } else if bytes >= 1024 * 1024 { + format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0)) + } else if bytes >= 1024 { + format!("{:.1} KB", bytes as f64 / 1024.0) + } else { + format!("{} B", bytes) + } } \ No newline at end of file diff --git a/mutant-web/src/app/theme.rs b/mutant-web/src/app/theme.rs index f876ab6b..254ab3b4 100644 --- a/mutant-web/src/app/theme.rs +++ b/mutant-web/src/app/theme.rs @@ -195,3 +195,53 @@ pub fn success_progress_bar(progress: f32) -> egui::ProgressBar { pub fn info_progress_bar(progress: f32) -> egui::ProgressBar { styled_progress_bar(progress, MutantColors::ACCENT_BLUE) } + +/// Create a styled section group with MutAnt theming +pub fn styled_section_group() -> egui::Frame { + egui::Frame::group(&egui::Style::default()) + .fill(MutantColors::SURFACE) + .stroke(egui::Stroke::new(1.0, MutantColors::BORDER_MEDIUM)) + .inner_margin(egui::Margin::same(12)) + .outer_margin(egui::Margin::same(4)) +} + +/// Create a prominent info section frame +pub fn info_section_frame() -> egui::Frame { + egui::Frame::group(&egui::Style::default()) + .fill(MutantColors::BACKGROUND_LIGHT) + .stroke(egui::Stroke::new(1.5, MutantColors::ACCENT_BLUE)) + .inner_margin(egui::Margin::same(16)) + .outer_margin(egui::Margin::same(6)) +} + +/// Create a warning section frame +pub fn warning_section_frame() -> egui::Frame { + egui::Frame::group(&egui::Style::default()) + .fill(MutantColors::BACKGROUND_LIGHT) + .stroke(egui::Stroke::new(1.5, MutantColors::WARNING)) + .inner_margin(egui::Margin::same(16)) + .outer_margin(egui::Margin::same(6)) +} + +/// Create a section header with consistent styling +pub fn section_header(text: &str, icon: &str, color: Color32) -> egui::RichText { + egui::RichText::new(format!("{} {}", icon, text)) + .size(16.0) + .strong() + .color(color) +} + +/// Create a file info display with icon and details +pub fn file_info_text(filename: &str, size: u64, file_type: &str) -> String { + let size_str = if size > 1024 * 1024 * 1024 { + format!("{:.1} GB", size as f64 / (1024.0 * 1024.0 * 1024.0)) + } else if size > 1024 * 1024 { + format!("{:.1} MB", size as f64 / (1024.0 * 1024.0)) + } else if size > 1024 { + format!("{:.1} KB", size as f64 / 1024.0) + } else { + format!("{} bytes", size) + }; + + format!("{}\n{} • {}", filename, size_str, file_type) +} diff --git a/mutant-web/src/app/window_system.rs b/mutant-web/src/app/window_system.rs index 0444dfc3..3c532282 100644 --- a/mutant-web/src/app/window_system.rs +++ b/mutant-web/src/app/window_system.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, MappedRwLockWriteGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; use eframe::egui::{self, Id, RichText, SidePanel}; use egui_dock::{DockArea, DockState}; @@ -78,16 +78,18 @@ lazy_static::lazy_static! { = RwLock::new(None); } -fn new_window_tx() -> MappedRwLockWriteGuard<'static, futures::channel::mpsc::Sender> { - RwLockWriteGuard::map(NEW_WINDOW_TX.write().unwrap(), |lol| match lol { - Some(x) => x, +fn new_window_tx() -> futures::channel::mpsc::Sender { + let guard = NEW_WINDOW_TX.read().unwrap(); + match guard.as_ref() { + Some(x) => x.clone(), None => panic!("Game not initialized"), - }) + } } pub fn new_window + 'static>(window: T) { wasm_bindgen_futures::spawn_local(async move { - new_window_tx().send(window.into()).await.unwrap(); + let mut tx = new_window_tx(); + tx.send(window.into()).await.unwrap(); }); } diff --git a/mutant-web/src/lib.rs b/mutant-web/src/lib.rs index 6572238e..091c0cb5 100644 --- a/mutant-web/src/lib.rs +++ b/mutant-web/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(mapped_lock_guards)] + use std::{collections::HashMap, sync::{Arc, RwLock}};