use crate::mime_parsing; use crate::theme::{self, Theme}; use crate::types::{DisplayEntry, MimeTypeEntry, VerificationEntry}; use eframe::egui; use std::sync::mpsc; use std::thread; pub struct MimeExplorerApp { pub(crate) mime_types: Vec, pub(crate) display_entries: Vec, pub(crate) filtered_entries: Vec, pub(crate) search_filter: String, pub(crate) last_search_filter: String, pub(crate) loading: bool, pub(crate) error_message: Option, pub(crate) mime_receiver: Option, String>>>, pub(crate) editing_mime_type: Option, pub(crate) editing_applications: Vec, pub(crate) new_application: String, pub(crate) show_edit_dialog: bool, pub(crate) selected_mime_type: Option, pub(crate) available_applications: Vec, pub(crate) selected_app_index: usize, pub(crate) current_theme: Theme, pub(crate) last_applied_theme: Option, pub(crate) row_height: f32, pub(crate) visible_start: usize, pub(crate) visible_count: usize, pub(crate) show_help_dialog: bool, pub(crate) show_verify_dialog: bool, pub(crate) verification_entries: Vec, pub(crate) show_entries_with_no_applications: bool, } impl Default for MimeExplorerApp { fn default() -> Self { Self { mime_types: Vec::new(), display_entries: Vec::new(), filtered_entries: Vec::new(), search_filter: String::new(), last_search_filter: String::new(), loading: false, error_message: None, mime_receiver: None, editing_mime_type: None, editing_applications: Vec::new(), new_application: String::new(), show_edit_dialog: false, selected_mime_type: None, available_applications: Vec::new(), selected_app_index: 0, current_theme: Theme::System, last_applied_theme: None, row_height: 0.0, visible_start: 0, visible_count: 0, show_help_dialog: false, show_verify_dialog: false, verification_entries: Vec::new(), show_entries_with_no_applications: true, } } } impl MimeExplorerApp { pub(crate) fn load_mime_types(&mut self) { self.loading = true; self.error_message = None; let (tx, rx) = mpsc::channel(); self.mime_receiver = Some(rx); thread::spawn(move || { let result = mime_parsing::discover_mime_types() .map_err(|e| format!("Error loading MIME types: {e}")); let _ = tx.send(result); }); if self.available_applications.is_empty() { self.load_applications(); } self.last_search_filter = self.search_filter.clone(); } fn load_applications(&mut self) { match mime_parsing::discover_all_applications() { Ok(apps) => { self.available_applications = apps; } Err(e) => { eprintln!("Warning: Error loading applications: {e}"); self.available_applications = vec!["Failed to load applications".to_string()]; } } } fn update_display_cache(&mut self) { self.display_entries = self.mime_types.iter().map(DisplayEntry::from).collect(); self.update_filtered_cache(); } pub(crate) fn update_filtered_cache(&mut self) { let search_term = if self.search_filter.is_empty() { None } else { Some(self.search_filter.to_lowercase()) }; self.filtered_entries = self .display_entries .iter() .zip(self.mime_types.iter()) .filter(|(display_entry, mime_entry)| { if !self.show_entries_with_no_applications && mime_entry.applications.is_empty() { return false; } if let Some(ref search_term) = search_term { display_entry.mime_type.to_lowercase().contains(search_term) || display_entry .extensions_text .to_lowercase() .contains(search_term) || display_entry .applications_text .to_lowercase() .contains(search_term) || mime_entry .applications .iter() .any(|app| app.to_lowercase().contains(search_term)) || mime_entry .extensions .iter() .any(|ext| ext.to_lowercase().contains(search_term)) } else { true } }) .map(|(display_entry, _)| display_entry.clone()) .collect(); self.last_search_filter = self.search_filter.clone(); } pub(crate) fn calculate_visible_range(&mut self, scroll_offset: f32, viewport_height: f32) { // Estimate row height if not set if self.row_height == 0.0 { self.row_height = 24.0; // Reasonable default } self.visible_start = ((scroll_offset / self.row_height) as usize).saturating_sub(5); // 5 item buffer self.visible_count = ((viewport_height / self.row_height) as usize + 15).min(self.filtered_entries.len()); // 15 item buffer } pub(crate) fn start_editing(&mut self, mime_type: &str) { if let Some(entry) = self.mime_types.iter().find(|e| e.mime_type == mime_type) { self.editing_mime_type = Some(mime_type.to_string()); self.editing_applications = entry.applications.clone(); self.new_application = String::new(); self.selected_app_index = 0; self.show_edit_dialog = true; } } pub(crate) fn save_applications(&mut self) { if let Some(mime_type) = &self.editing_mime_type { let mime_type_clone = mime_type.clone(); let applications_clone = self.editing_applications.clone(); if let Some(entry) = self .mime_types .iter_mut() .find(|e| e.mime_type == mime_type_clone) { entry.applications = applications_clone.clone(); } self.update_display_cache(); if let Err(e) = mime_parsing::save_mime_type_applications(&mime_type_clone, &applications_clone) { self.error_message = Some(format!("Error saving changes: {e}")); } } self.cancel_editing(); } pub(crate) fn cancel_editing(&mut self) { self.editing_mime_type = None; self.editing_applications.clear(); self.new_application.clear(); self.selected_app_index = 0; self.show_edit_dialog = false; } pub(crate) fn start_verification(&mut self) { self.verification_entries.clear(); for entry in &self.mime_types { let mut missing_applications = Vec::new(); let mut duplicate_applications = Vec::new(); for app in &entry.applications { if app == "No default application" || app == "System" { continue; } if !self.available_applications.contains(app) && !mime_parsing::application_command_exists(app) { missing_applications.push(app.clone()); } } let mut seen_apps = std::collections::HashSet::new(); let mut duplicates_set = std::collections::HashSet::new(); for app in &entry.applications { if !seen_apps.insert(app.clone()) { duplicates_set.insert(app.clone()); } } duplicate_applications.extend(duplicates_set.into_iter()); duplicate_applications.sort(); if !missing_applications.is_empty() || !duplicate_applications.is_empty() { let selected_for_removal = vec![true; missing_applications.len()]; let replacement_applications = vec![None; missing_applications.len()]; let replacement_indices = vec![0; missing_applications.len()]; let selected_duplicates_for_removal = vec![true; duplicate_applications.len()]; self.verification_entries.push(VerificationEntry { mime_type: entry.mime_type.clone(), missing_applications, selected_for_removal, replacement_applications, replacement_indices, duplicate_applications, selected_duplicates_for_removal, }); } } self.show_verify_dialog = true; } pub(crate) fn clean_missing_applications(&mut self) { for verification_entry in &self.verification_entries { if let Some(mime_entry) = self .mime_types .iter_mut() .find(|e| e.mime_type == verification_entry.mime_type) { let mut replacements = std::collections::HashMap::new(); let mut apps_to_remove = std::collections::HashSet::new(); for (i, missing_app) in verification_entry.missing_applications.iter().enumerate() { if *verification_entry .selected_for_removal .get(i) .unwrap_or(&false) { if let Some(replacement) = verification_entry .replacement_applications .get(i) .and_then(|r| r.as_ref()) { if !mime_entry.applications.contains(replacement) { replacements.insert(missing_app.clone(), replacement.clone()); } } else { apps_to_remove.insert(missing_app.clone()); } } } for (i, duplicate_app) in verification_entry.duplicate_applications.iter().enumerate() { if *verification_entry .selected_duplicates_for_removal .get(i) .unwrap_or(&false) { apps_to_remove.insert(duplicate_app.clone()); } } let mut new_applications = Vec::new(); let mut seen_apps = std::collections::HashSet::new(); for app in &mime_entry.applications { if let Some(replacement) = replacements.get(app) { if !seen_apps.contains(replacement) { new_applications.push(replacement.clone()); seen_apps.insert(replacement.clone()); } } else if !apps_to_remove.contains(app) { if !seen_apps.contains(app) { new_applications.push(app.clone()); seen_apps.insert(app.clone()); } } } mime_entry.applications = new_applications; if let Err(e) = mime_parsing::save_mime_type_applications( &mime_entry.mime_type, &mime_entry.applications, ) { self.error_message = Some(format!( "Error saving changes for {}: {}", mime_entry.mime_type, e )); } } } self.update_display_cache(); self.cancel_verification(); } pub(crate) fn cancel_verification(&mut self) { self.show_verify_dialog = false; self.verification_entries.clear(); } pub(crate) fn render_truncated_text_with_tooltip( &self, ui: &mut egui::Ui, rect: egui::Rect, text: &str, font_id: egui::FontId, color: egui::Color32, response: &egui::Response, ) { let full_text_size = ui .painter() .layout_no_wrap(text.to_string(), font_id.clone(), color) .size(); let available_width = rect.width() - 8.0; let (display_text, is_truncated) = if full_text_size.x > available_width { let ellipsis = "..."; let mut left = 0; let mut right = text.len(); let mut best_len = 0; while left <= right { let mid = (left + right) / 2; if mid > text.len() { break; } let mut boundary = mid; while boundary > 0 && !text.is_char_boundary(boundary) { boundary -= 1; } if boundary == 0 { break; } let prefix = &text[..boundary]; let test_text = format!("{prefix}{ellipsis}"); let test_width = ui .painter() .layout_no_wrap(test_text.clone(), font_id.clone(), color) .size() .x; if test_width <= available_width { best_len = boundary; left = mid + 1; } else { right = mid.saturating_sub(1); } } let truncated_text = if best_len > 0 { format!("{}{}", &text[..best_len], ellipsis) } else { ellipsis.to_string() }; (truncated_text, true) } else { (text.to_string(), false) }; ui.painter().text( rect.left_center() + egui::vec2(4.0, 0.0), egui::Align2::LEFT_CENTER, &display_text, font_id, color, ); if response.hovered() && is_truncated { response.clone().on_hover_text(text); } } } impl eframe::App for MimeExplorerApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { if self.last_applied_theme != Some(self.current_theme) { theme::apply(self.current_theme, ctx); self.last_applied_theme = Some(self.current_theme); } if let Some(receiver) = &self.mime_receiver { if let Ok(result) = receiver.try_recv() { match result { Ok(mime_types) => { self.mime_types = mime_types; self.update_display_cache(); self.loading = false; } Err(e) => { self.error_message = Some(e); self.loading = false; } } self.mime_receiver = None; } } if self.mime_types.is_empty() && !self.loading && self.mime_receiver.is_none() { self.load_mime_types(); } if self.search_filter != self.last_search_filter { self.update_filtered_cache(); } self.show_edit_dialog(ctx); self.show_help_dialog(ctx); self.show_verify_dialog(ctx); self.show_top_panel(ctx); self.show_mime_list(ctx); } }