use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager}; /// Export palette colors to various artist application formats #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ColorExportResult { pub format: String, pub content: String, pub filename: String, } #[tauri::command] pub fn color_export(colors: Vec, format: String) -> Result { let content = match format.as_str() { "hex" => colors.join("\n"), "css" => colors.iter().enumerate() .map(|(i, c)| format!(" --color-{}: {};", i + 1, c)) .collect::>() .join("\n") .pipe(|body| format!(":root {{\n{body}\n}}")), "gpl" => { let mut lines = vec!["GIMP Palette".to_string(), "Name: Muse Export".to_string(), "#".to_string()]; for color in &colors { if let Some((r, g, b)) = parse_hex(color) { lines.push(format!("{r:3} {g:3} {b:3}\t{color}")); } } lines.join("\n") } "ase" => { let mut lines = vec!["// Adobe Swatch Exchange (text preview)".to_string()]; for color in &colors { if let Some((r, g, b)) = parse_hex(color) { lines.push(format!("RGB {:.4} {:.4} {:.4} // {color}", r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)); } } lines.join("\n") } "procreate" => { let swatches: Vec = colors.iter().filter_map(|c| { parse_hex(c).map(|(r, g, b)| serde_json::json!({ "hue": 0, "saturation": 0, "brightness": 0, "alpha": 1, "colorSpace": 0, "components": [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0, 1.0] })) }).collect(); serde_json::to_string_pretty(&serde_json::json!({ "name": "Muse Export", "swatches": swatches })).unwrap_or_default() } _ => colors.join("\n"), }; let ext = match format.as_str() { "css" => "css", "gpl" => "gpl", "ase" => "ase", "procreate" => "swatches", _ => "txt" }; Ok(ColorExportResult { format: format.clone(), content, filename: format!("muse-palette.{ext}"), }) } #[tauri::command] pub fn color_search_library(app: AppHandle, hue_min: f32, hue_max: f32) -> Result, String> { let state = app.state::(); let items = state.items.lock().map_err(|_| "library lock poisoned")?; Ok(items.iter().filter(|item| { item.colors.iter().any(|c| { if let Some((r, g, b)) = parse_hex(c) { let hue = rgb_to_hue(r, g, b); if hue_min <= hue_max { hue >= hue_min && hue <= hue_max } else { hue >= hue_min || hue <= hue_max } } else { false } }) }).cloned().collect()) } fn parse_hex(hex: &str) -> Option<(u8, u8, u8)> { let h = hex.trim_start_matches('#'); if h.len() != 6 { return None; } let r = u8::from_str_radix(&h[0..2], 16).ok()?; let g = u8::from_str_radix(&h[2..4], 16).ok()?; let b = u8::from_str_radix(&h[4..6], 16).ok()?; Some((r, g, b)) } fn rgb_to_hue(r: u8, g: u8, b: u8) -> f32 { let rf = r as f32 / 255.0; let gf = g as f32 / 255.0; let bf = b as f32 / 255.0; let max = rf.max(gf).max(bf); let min = rf.min(gf).min(bf); if max == min { return 0.0; } let d = max - min; let h = if max == rf { ((gf - bf) / d) % 6.0 } else if max == gf { (bf - rf) / d + 2.0 } else { (rf - gf) / d + 4.0 }; ((h * 60.0) + 360.0) % 360.0 } trait PipeExt { fn pipe(self, f: impl FnOnce(String) -> String) -> String; } impl PipeExt for String { fn pipe(self, f: impl FnOnce(String) -> String) -> String { f(self) } }