cueward-adapter-macos 0.3.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use std::thread;
use std::time::Duration;

use serde_json::Value;

use crate::MacosError;
use crate::safari_guard::with_safari_session;

use super::super::super::core::execute_js_for_profile;
use super::super::super::script::escape_js_string;
use super::super::SafariAiResponseResult;

pub fn gemini_save_images(
    conversation_url: &str,
    output_dir: &str,
    profile_filter: Option<&str>,
) -> Result<Vec<String>, MacosError> {
    with_safari_session(|| {
        let nav_js = format!(
            r#"(function() {{ window.location.href = "{url}"; return "true"; }})()"#,
            url = escape_js_string(conversation_url),
        );
        let _ = execute_js_for_profile(&nav_js, profile_filter, "safari_gemini_save_img_navigate")?;
        thread::sleep(Duration::from_millis(5000));

        let count_js = r#"(() => {
        const imgs = document.querySelectorAll('img[alt*="AI"], img[alt*="生成"]');
        return String(imgs.length);
    })()"#;
        let count: usize =
            execute_js_for_profile(count_js, profile_filter, "safari_gemini_img_count")?
                .trim()
                .parse()
                .unwrap_or(0);

        if count == 0 {
            return Ok(Vec::new());
        }

        let out_path = std::path::Path::new(output_dir);
        if !out_path.exists() {
            std::fs::create_dir_all(out_path)
                .map_err(|e| MacosError::Other(format!("failed to create output dir: {e}")))?;
        }

        use base64::Engine;
        let engine = base64::engine::general_purpose::STANDARD;
        let mut saved = Vec::new();

        for i in 0..count {
            let extract_js = format!(
                r#"(() => {{
                const imgs = document.querySelectorAll('img[alt*="AI"], img[alt*="生成"]');
                const img = imgs[{i}];
                if (!img) return "";
                const canvas = document.createElement("canvas");
                canvas.width = img.naturalWidth || img.width;
                canvas.height = img.naturalHeight || img.height;
                canvas.getContext("2d").drawImage(img, 0, 0);
                const dataUrl = canvas.toDataURL("image/png");
                const idx = dataUrl.indexOf(",");
                return idx >= 0 ? dataUrl.substring(idx + 1) : "";
            }})()"#,
            );
            let b64 =
                execute_js_for_profile(&extract_js, profile_filter, "safari_gemini_img_extract")?;
            let b64 = b64.trim();
            if b64.is_empty() {
                continue;
            }

            let bytes = engine.decode(b64).map_err(|e| {
                MacosError::Other(format!("base64 decode failed for image {i}: {e}"))
            })?;

            let filename = format!("gemini_image_{i}.png");
            let filepath = out_path.join(&filename);
            std::fs::write(&filepath, &bytes).map_err(|e| {
                MacosError::Other(format!("failed to write {}: {e}", filepath.display()))
            })?;
            saved.push(filepath.to_string_lossy().into_owned());
        }

        Ok(saved)
    })
}

pub fn gemini_save_media(
    conversation_url: &str,
    profile_filter: Option<&str>,
) -> Result<SafariAiResponseResult, MacosError> {
    with_safari_session(|| {
        let nav_js = format!(
            r#"(function() {{ window.location.href = "{url}"; return "true"; }})()"#,
            url = escape_js_string(conversation_url),
        );
        let _ = execute_js_for_profile(&nav_js, profile_filter, "safari_gemini_media_navigate")?;
        thread::sleep(Duration::from_millis(5000));

        let js = r#"(() => {
        const video = document.querySelector("video");
        if (!video) return JSON.stringify({ status: "error", response: "no media found" });
        const src = video.src || video.currentSrc || "";
        if (!src) return JSON.stringify({ status: "error", response: "no media source" });
        const a = document.createElement("a");
        a.href = src;
        a.download = "gemini_media.mp4";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        return JSON.stringify({ status: "downloading", response: src });
    })()"#;
        let raw = execute_js_for_profile(js, profile_filter, "safari_gemini_media_download")?;
        let value: Value = serde_json::from_str(&raw)
            .map_err(|e| MacosError::Other(format!("failed to parse media result: {e}")))?;
        let status = value
            .get("status")
            .and_then(|v| v.as_str())
            .unwrap_or("error");
        let response = value.get("response").and_then(|v| v.as_str()).unwrap_or("");
        Ok(SafariAiResponseResult {
            provider: "gemini".to_string(),
            status: status.to_string(),
            response: response.to_string(),
            conversation_url: None,
        })
    })
}