mrmime/src/app.rs
2025-07-09 18:45:45 -04:00

455 lines
16 KiB
Rust

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<MimeTypeEntry>,
pub(crate) display_entries: Vec<DisplayEntry>,
pub(crate) filtered_entries: Vec<DisplayEntry>,
pub(crate) search_filter: String,
pub(crate) last_search_filter: String,
pub(crate) loading: bool,
pub(crate) error_message: Option<String>,
pub(crate) mime_receiver: Option<mpsc::Receiver<Result<Vec<MimeTypeEntry>, String>>>,
pub(crate) editing_mime_type: Option<String>,
pub(crate) editing_applications: Vec<String>,
pub(crate) new_application: String,
pub(crate) show_edit_dialog: bool,
pub(crate) selected_mime_type: Option<String>,
pub(crate) available_applications: Vec<String>,
pub(crate) selected_app_index: usize,
pub(crate) current_theme: Theme,
pub(crate) last_applied_theme: Option<Theme>,
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<VerificationEntry>,
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);
}
}