// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // Standard library imports use std::path::Path; use std::env; // Serde for serialization/deserialization // Tauri related imports // Internal modules pub mod models; pub mod services; pub mod commands; pub mod crawlers; pub mod platform_matcher; // Import our models pub use models::server::{ServerType, ServerInfo, ScanResult, ScanProgress}; pub use models::plugin::{Plugin, PluginMeta}; pub use models::repository::{RepositorySource, RepositoryPlugin, PotentialPluginMatch}; // Import our services pub use services::http::HttpClient; pub use services::plugin_scanner::{scan_server_directory, perform_scan, extract_plugin_metadata, calculate_file_hash, is_file_locked}; pub use services::update_manager::{check_for_plugin_updates, check_single_plugin_update, backup_plugin, replace_plugin, normalize_version, compare_plugin_versions}; // Import our commands pub use commands::plugin_commands::*; pub use commands::scan_commands::*; // Import our crawlers pub use crawlers::HangarCrawler; pub use crawlers::SpigotMCCrawler; pub use crawlers::ModrinthCrawler; pub use crawlers::GitHubCrawler; pub use crawlers::Repository; // Import platform matchers pub use platform_matcher::{get_compatible_modrinth_loaders, is_version_compatible_with_server}; use futures::future::{BoxFuture, FutureExt}; // Import necessary future types /// Search for plugins in repositories pub async fn lib_search_plugins_in_repositories( query: &str, repositories: Vec ) -> Result, String> { // Check for empty query if query.is_empty() { return Err("Search query cannot be empty".to_string()); } // Check for empty repository list if repositories.is_empty() { return Err("No repositories specified for search".to_string()); } // Create a list to store tasks (boxed futures) let mut tasks: Vec), (String, String)>>> = Vec::new(); // Add tasks for each repository for repo in repositories { match repo { RepositorySource::HangarMC => { let crawler = crawlers::HangarCrawler::new(); let query_owned = query.to_string(); let task = async move { match crawler.search(&query_owned).await { Ok(repo_results) => Ok((crawler.get_repository_name(), repo_results)), Err(e) => Err((crawler.get_repository_name(), e.to_string())) } }.boxed(); // Box the future tasks.push(task); }, RepositorySource::SpigotMC => { let crawler = crawlers::SpigotMCCrawler::new(); let query_owned = query.to_string(); let task = async move { match crawler.search(&query_owned).await { Ok(repo_results) => Ok((crawler.get_repository_name(), repo_results)), Err(e) => Err((crawler.get_repository_name(), e.to_string())) } }.boxed(); // Box the future tasks.push(task); }, RepositorySource::Modrinth => { let crawler = crawlers::ModrinthCrawler::new(); let query_owned = query.to_string(); let task = async move { match crawler.search(&query_owned).await { Ok(repo_results) => Ok((crawler.get_repository_name(), repo_results)), Err(e) => Err((crawler.get_repository_name(), e.to_string())) } }.boxed(); // Box the future tasks.push(task); }, RepositorySource::GitHub => { let crawler = crawlers::GitHubCrawler::new(); let query_owned = query.to_string(); let task = async move { match crawler.search(&query_owned).await { Ok(repo_results) => Ok((crawler.get_repository_name(), repo_results)), Err(e) => Err((crawler.get_repository_name(), e.to_string())) } }.boxed(); // Box the future tasks.push(task); }, _ => { eprintln!("Repository {:?} not supported for search", repo); } } } // Execute all tasks concurrently let results = futures::future::join_all(tasks).await; // Collect successful results let mut plugins = Vec::new(); let mut errors = Vec::new(); for result in results { match result { Ok((_, repo_plugins)) => { plugins.extend(repo_plugins); }, Err((repo, error)) => { errors.push(format!("[{}] {}", repo, error)); } } } // Check if we found any results if plugins.is_empty() { if !errors.is_empty() { return Err(errors.join("; ")); } else { return Ok(Vec::new()); } } Ok(plugins) } /// Generate search variations for a plugin name fn generate_search_variations(plugin_name: &str) -> Vec { let mut variations = Vec::new(); // Add original name variations.push(plugin_name.to_string()); // Convert to lowercase let name_lower = plugin_name.to_lowercase(); if name_lower != plugin_name { variations.push(name_lower.clone()); } // Add variations with common prefixes/suffixes removed let prefixes = ["plugin", "mc", "minecraft"]; let suffixes = ["plugin", "spigot", "bukkit", "paper", "mc"]; for prefix in prefixes.iter() { let prefix_str = format!("{} ", prefix); if name_lower.starts_with(&prefix_str) { variations.push(name_lower[prefix_str.len()..].to_string()); } } for suffix in suffixes.iter() { let suffix_str = format!(" {}", suffix); if name_lower.ends_with(&suffix_str) { variations.push(name_lower[0..name_lower.len() - suffix_str.len()].to_string()); } } // Remove duplicates variations.sort(); variations.dedup(); variations } /// Search for plugin variations pub async fn search_with_variations(plugin_name: &str, repositories: &[RepositorySource]) -> Result, String> { let variations = generate_search_variations(plugin_name); let mut all_results = Vec::new(); for variation in variations { match lib_search_plugins_in_repositories(&variation, repositories.to_vec()).await { Ok(results) => { all_results.extend(results); }, Err(e) => { println!("Error searching for variation '{}': {}", variation, e); // Continue with other variations even if this one fails } } } // Remove duplicates by plugin ID and repository all_results.sort_by(|a, b| { let a_key = format!("{:?}:{}", a.repository, a.id); let b_key = format!("{:?}:{}", b.repository, b.id); a_key.cmp(&b_key) }); all_results.dedup_by(|a, b| { a.id == b.id && a.repository == b.repository }); Ok(all_results) } /// Get plugin details from a repository pub async fn lib_get_plugin_details_from_repository( plugin_id: &str, repository: RepositorySource, server_type: Option<&ServerType> ) -> Result { match repository { RepositorySource::HangarMC => { let crawler = crawlers::HangarCrawler::new(); crawler.get_plugin_details(plugin_id).await .map_err(|e| format!("Failed to get plugin details from HangarMC: {}", e)) }, RepositorySource::SpigotMC => { let crawler = crawlers::SpigotMCCrawler::new(); crawler.get_plugin_details(plugin_id).await .map_err(|e| format!("Failed to get plugin details from SpigotMC: {}", e)) }, RepositorySource::Modrinth => { let crawler = crawlers::ModrinthCrawler::new(); // Use server type aware version if provided if let Some(server_type) = server_type { crawler.get_plugin_details_with_server_type(plugin_id, Some(server_type)).await .map_err(|e| format!("Failed to get plugin details from Modrinth: {}", e)) } else { crawler.get_plugin_details(plugin_id).await .map_err(|e| format!("Failed to get plugin details from Modrinth: {}", e)) } }, RepositorySource::GitHub => { let crawler = crawlers::GitHubCrawler::new(); crawler.get_plugin_details(plugin_id).await .map_err(|e| format!("Failed to get plugin details from GitHub: {}", e)) }, _ => Err(format!("Repository source {:?} not supported for plugin details", repository)) } } /// Download a plugin from a repository pub async fn lib_download_plugin_from_repository( plugin_id: &str, version: &str, repository: RepositorySource, destination: &str, server_type: Option<&ServerType> ) -> Result { match repository { RepositorySource::HangarMC => { let crawler = crawlers::HangarCrawler::new(); crawler.download_plugin(plugin_id, version, Path::new(destination)).await .map_err(|e| format!("Failed to download plugin from HangarMC: {}", e)) }, RepositorySource::SpigotMC => { let crawler = crawlers::SpigotMCCrawler::new(); crawler.download_plugin(plugin_id, version, Path::new(destination)).await .map_err(|e| format!("Failed to download plugin from SpigotMC: {}", e)) }, RepositorySource::Modrinth => { let crawler = crawlers::ModrinthCrawler::new(); // Use server type aware version if provided if let Some(server_type) = server_type { crawler.download_plugin_with_server_type(plugin_id, version, Path::new(destination), Some(server_type)).await .map_err(|e| format!("Failed to download plugin from Modrinth: {}", e)) } else { crawler.download_plugin(plugin_id, version, Path::new(destination)).await .map_err(|e| format!("Failed to download plugin from Modrinth: {}", e)) } }, RepositorySource::GitHub => { let crawler = crawlers::GitHubCrawler::new(); crawler.download_plugin(plugin_id, version, Path::new(destination)).await .map_err(|e| format!("Failed to download plugin from GitHub: {}", e)) }, _ => Err(format!("Repository source {:?} not supported for downloads", repository)) } } /// Configure and run the Tauri application pub fn run() { // Build the Tauri application tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ // Plugin discovery commands scan_server_dir, scan_server_dir_sync, // Plugin repository commands search_plugins, get_plugin_details, // Update commands update_plugin, check_plugin_updates, check_single_plugin_update_command, backup_plugin_command, // Plugin management commands download_plugin, set_plugin_repository, get_plugin_versions, load_plugin_data, save_plugin_data, // Utility commands get_potential_plugin_matches, compare_versions, is_plugin_compatible, greet ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }