diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index ee54234..a673c84 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -377,6 +377,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; mod queue; +mod parity; pub struct AppState { pub tasks: Arc>>, @@ -1341,7 +1342,8 @@ pub fn run() { request_automation_permission, open_automation_settings, set_keychain_password, get_keychain_password, delete_keychain_password, check_file_exists, delete_file, toggle_tray_icon, set_extension_pairing_token, - set_extension_frontend_ready + set_extension_frontend_ready, + parity::get_system_proxy, parity::get_file_category, parity::check_for_updates ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/apps/desktop/src-tauri/src/parity.rs b/apps/desktop/src-tauri/src/parity.rs new file mode 100644 index 0000000..4fc0134 --- /dev/null +++ b/apps/desktop/src-tauri/src/parity.rs @@ -0,0 +1,169 @@ +use serde::{Deserialize, Serialize}; + +#[tauri::command] +pub async fn get_system_proxy() -> Result, String> { + #[cfg(target_os = "macos")] + { + use std::process::Command; + let output = Command::new("scutil").arg("--proxy").output().map_err(|e| e.to_string())?; + let stdout = String::from_utf8_lossy(&output.stdout); + + let mut http_enable = false; + let mut http_proxy = String::new(); + let mut http_port = String::new(); + let mut https_enable = false; + let mut https_proxy = String::new(); + let mut https_port = String::new(); + + for line in stdout.lines() { + let parts: Vec<&str> = line.splitn(2, ':').collect(); + if parts.len() == 2 { + let key = parts[0].trim(); + let value = parts[1].trim(); + match key { + "HTTPEnable" => http_enable = value == "1", + "HTTPProxy" => http_proxy = value.to_string(), + "HTTPPort" => http_port = value.to_string(), + "HTTPSEnable" => https_enable = value == "1", + "HTTPSProxy" => https_proxy = value.to_string(), + "HTTPSPort" => https_port = value.to_string(), + _ => {} + } + } + } + + if https_enable && !https_proxy.is_empty() { + return Ok(Some(format!("http://{}:{}", https_proxy, https_port))); // Often https proxy is HTTP + } else if http_enable && !http_proxy.is_empty() { + return Ok(Some(format!("http://{}:{}", http_proxy, http_port))); + } + Ok(None) + } + #[cfg(not(target_os = "macos"))] + { + Ok(None) + } +} + +#[tauri::command] +pub fn get_file_category(filename: String) -> String { + let ext = std::path::Path::new(&filename) + .extension() + .and_then(|s| s.to_str()) + .map(|s| s.to_lowercase()) + .unwrap_or_default(); + + let music_exts = ["aac", "aif", "aiff", "alac", "amr", "ape", "au", "caf", "flac", "m4a", "m4b", "mid", "midi", "mp3", "oga", "ogg", "opus", "ra", "wav", "weba", "wma"]; + let movie_exts = ["3g2", "3gp", "avi", "divx", "f4v", "flv", "m2ts", "m4v", "mkv", "mov", "mp4", "mpeg", "mpg", "mts", "ogm", "ogv", "rm", "rmvb", "ts", "vob", "webm", "wmv"]; + let compressed_exts = ["7z", "ace", "alz", "apk", "appx", "ar", "arc", "arj", "bz", "bz2", "cab", "cpio", "deb", "dmg", "gz", "gzip", "iso", "jar", "lha", "lzh", "lz", "lz4", "lzip", "lzma", "pak", "pkg", "rar", "rpm", "sit", "sitx", "tar", "tbz", "tbz2", "tgz", "tlz", "txz", "war", "whl", "xar", "xz", "z", "zip", "zipx", "zst"]; + let picture_exts = ["ai", "apng", "avif", "bmp", "cr2", "cr3", "dng", "emf", "eps", "gif", "heic", "heif", "ico", "indd", "jfif", "jpeg", "jpg", "jxl", "nef", "orf", "pbm", "pgm", "png", "pnm", "ppm", "psd", "raw", "rw2", "svg", "tga", "tif", "tiff", "webp", "wmf"]; + let document_exts = ["azw", "azw3", "csv", "djvu", "doc", "docm", "docx", "dot", "dotx", "epub", "fb2", "htm", "html", "ics", "key", "log", "md", "mobi", "pdf", "numbers", "odp", "ods", "odt", "pages", "pot", "potx", "pps", "ppsx", "ppt", "pptm", "pptx", "rtf", "tex", "txt", "vcf", "xls", "xlsm", "xlsx", "xml", "xps", "yaml", "yml"]; + + if music_exts.contains(&ext.as_str()) { + "musics".to_string() + } else if movie_exts.contains(&ext.as_str()) { + "movies".to_string() + } else if compressed_exts.contains(&ext.as_str()) { + "compressed".to_string() + } else if picture_exts.contains(&ext.as_str()) { + "pictures".to_string() + } else if document_exts.contains(&ext.as_str()) { + "documents".to_string() + } else { + "other".to_string() + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AvailableReleaseUpdate { + pub version: String, + pub tag_name: String, + pub title: String, + pub release_notes: String, + pub release_url: String, + pub published_at: Option, +} + +#[derive(Serialize)] +#[serde(tag = "type")] +pub enum ReleaseCheckOutcome { + UpdateAvailable { update: AvailableReleaseUpdate }, + UpToDate { latest_version: String, local_version: String }, +} + +#[derive(Deserialize)] +struct GitHubRelease { + tag_name: String, + name: Option, + body: Option, + html_url: String, + draft: bool, + prerelease: bool, + published_at: Option, +} + +#[tauri::command] +pub async fn check_for_updates(app_handle: tauri::AppHandle) -> Result { + let current_version = app_handle.package_info().version.to_string(); + + let client = reqwest::Client::new(); + let res = client.get("https://api.github.com/repos/nimbold/Firelink/releases?per_page=30") + .header("User-Agent", "Firelink") + .header("Accept", "application/vnd.github+json") + .send() + .await + .map_err(|e| e.to_string())?; + + if !res.status().is_success() { + return Err(format!("GitHub returned HTTP {}", res.status().as_u16())); + } + + let releases: Vec = res.json().await.map_err(|e| e.to_string())?; + + let latest_stable = releases.into_iter() + .filter(|r| !r.draft && !r.prerelease) + .max_by(|a, b| cmp_versions(&a.tag_name, &b.tag_name)); + + let release = match latest_stable { + Some(r) => r, + None => return Err("No stable release was found.".to_string()), + }; + + let latest_version = release.tag_name.trim_start_matches(|c| c == 'v' || c == 'V').to_string(); + + if cmp_versions(&latest_version, ¤t_version) == std::cmp::Ordering::Greater { + Ok(ReleaseCheckOutcome::UpdateAvailable { + update: AvailableReleaseUpdate { + version: latest_version.clone(), + tag_name: release.tag_name.clone(), + title: release.name.unwrap_or(release.tag_name), + release_notes: release.body.unwrap_or_else(|| "No release notes were provided for this version.".to_string()), + release_url: release.html_url, + published_at: release.published_at, + } + }) + } else { + Ok(ReleaseCheckOutcome::UpToDate { + latest_version, + local_version: current_version, + }) + } +} + +fn cmp_versions(a: &str, b: &str) -> std::cmp::Ordering { + let a_clean = a.trim_start_matches(|c| c == 'v' || c == 'V'); + let b_clean = b.trim_start_matches(|c| c == 'v' || c == 'V'); + + let a_parts: Vec = a_clean.split('.').filter_map(|s| s.parse().ok()).collect(); + let b_parts: Vec = b_clean.split('.').filter_map(|s| s.parse().ok()).collect(); + + let len = std::cmp::max(a_parts.len(), b_parts.len()); + for i in 0..len { + let a_val = a_parts.get(i).unwrap_or(&0); + let b_val = b_parts.get(i).unwrap_or(&0); + if a_val != b_val { + return a_val.cmp(b_val); + } + } + std::cmp::Ordering::Equal +}