tauri-plugin-deeprespond-im 1.0.0-alpha

深应智能 IM 的 Tauri 插件:本地存储、安全存储、窗口间的锁与消息、系统事件、通知、角标、文件
//! 系统的打开、保存对话框(desktop.pickFiles、pickSavePath,《2.功能概述文档》第 9.1 节)。
//!
//! 对话框由插件自己打开,返回的路径才能登记为允许读取或写入;Tauri 的 dialog 插件返回了哪些路径插件看不到。
//! 用 rfd 的异步对话框:在主线程上创建(macOS 的面板必须在主线程;Linux 上 rfd 把 GTK 的调用投递到主循环),
//! 在命令的异步任务中等待结果,不阻塞主线程。

use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;

use serde::Deserialize;
use tauri::{AppHandle, Runtime, Window};

use crate::error::{Error, Result};

#[derive(Debug, Clone, Deserialize)]
pub struct Filter {
    pub name: String,
    pub extensions: Vec<String>,
}

type Pending<T> = Pin<Box<dyn Future<Output = T> + Send>>;

/// 在主线程上创建对话框并取得它的 Future,再在当前任务中等待。
async fn on_main<R: Runtime, T: Send + 'static>(app: &AppHandle<R>, make: impl FnOnce() -> Pending<T> + Send + 'static) -> Result<T> {
    let (tx, rx) = tokio::sync::oneshot::channel();
    app.run_on_main_thread(move || {
        let _ = tx.send(make());
    })?;
    let pending = rx.await.map_err(|_| Error::internal("对话框没有打开"))?;
    Ok(pending.await)
}

#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
pub async fn pick<R: Runtime>(app: &AppHandle<R>, parent: Option<Window<R>>, multiple: bool, filters: Vec<Filter>) -> Result<Vec<PathBuf>> {
    on_main(app, move || {
        let mut dialog = rfd::AsyncFileDialog::new();
        for filter in &filters {
            let extensions: Vec<&str> = filter.extensions.iter().map(|e| e.trim_start_matches('.')).collect();
            dialog = dialog.add_filter(filter.name.clone(), &extensions);
        }
        if let Some(window) = &parent {
            dialog = dialog.set_parent(window);
        }
        if multiple {
            let picked = dialog.pick_files();
            Box::pin(async move { picked.await.map(|files| files.into_iter().map(|f| f.path().to_path_buf()).collect()).unwrap_or_default() })
        } else {
            let picked = dialog.pick_file();
            Box::pin(async move { picked.await.map(|f| vec![f.path().to_path_buf()]).unwrap_or_default() })
        }
    })
    .await
}

#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
pub async fn pick_save<R: Runtime>(app: &AppHandle<R>, parent: Option<Window<R>>, default_name: Option<String>, directory: Option<PathBuf>) -> Result<Option<PathBuf>> {
    on_main(app, move || {
        let mut dialog = rfd::AsyncFileDialog::new().set_can_create_directories(true);
        if let Some(name) = default_name.as_deref().filter(|n| !n.is_empty()) {
            dialog = dialog.set_file_name(super::transfer::sanitize_file_name(name, "download"));
        }
        if let Some(dir) = &directory {
            dialog = dialog.set_directory(dir);
        }
        if let Some(window) = &parent {
            dialog = dialog.set_parent(window);
        }
        let picked = dialog.save_file();
        Box::pin(async move { picked.await.map(|f| f.path().to_path_buf()) })
    })
    .await
}

#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
pub async fn pick<R: Runtime>(_app: &AppHandle<R>, _parent: Option<Window<R>>, _multiple: bool, _filters: Vec<Filter>) -> Result<Vec<PathBuf>> {
    Err(Error::unsupported("dialog_unavailable"))
}

#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
pub async fn pick_save<R: Runtime>(_app: &AppHandle<R>, _parent: Option<Window<R>>, _default_name: Option<String>, _directory: Option<PathBuf>) -> Result<Option<PathBuf>> {
    Err(Error::unsupported("dialog_unavailable"))
}