| use serde::{Deserialize, Serialize};
|
| use tauri::{AppHandle, Manager};
|
|
|
|
|
| #[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<String>, format: String) -> Result<ColorExportResult, String> {
|
| let content = match format.as_str() {
|
| "hex" => colors.join("\n"),
|
| "css" => colors.iter().enumerate()
|
| .map(|(i, c)| format!(" --color-{}: {};", i + 1, c))
|
| .collect::<Vec<_>>()
|
| .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<serde_json::Value> = 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<Vec<crate::library::LibraryItem>, String> {
|
| let state = app.state::<crate::library::LibraryState>();
|
| 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) } }
|
|
|