455 lines
16 KiB
Rust
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);
|
|
}
|
|
}
|