zouni 0.2.1

Variety of functions that `std` does not offer or `std` offers but are not satisfiable
Documentation
use rfd::{AsyncFileDialog, AsyncMessageDialog, MessageButtons, MessageDialogResult, MessageLevel};
use serde::{Deserialize, Serialize};
use std::path::Path;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageDialogKind {
    Info,
    Warning,
    Error,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MessageDialogOptions {
    pub title: Option<String>,
    pub kind: Option<MessageDialogKind>,
    pub buttons: Vec<String>,
    pub message: String,
    pub cancel_id: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MessageResult {
    pub button: String,
    pub cancelled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum OpenProperty {
    OpenFile,
    OpenDirectory,
    MultiSelections,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OpenDialogOptions {
    pub title: Option<String>,
    pub default_path: Option<String>,
    pub filters: Option<Vec<FileFilter>>,
    pub properties: Option<Vec<OpenProperty>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SaveDialogOptions {
    pub title: Option<String>,
    pub default_path: Option<String>,
    pub filters: Option<Vec<FileFilter>>,
}

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

impl FileFilter {
    pub fn new(name: &str, extensions: &[&str]) -> Self {
        Self {
            name: name.to_string(),
            extensions: extensions.to_vec().iter().map(|s| s.to_string()).collect(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileDialogResult {
    pub canceled: bool,
    pub file_paths: Vec<String>,
}

impl Default for FileDialogResult {
    fn default() -> Self {
        Self {
            canceled: true,
            file_paths: Vec::new(),
        }
    }
}

fn get_level(kind: &Option<MessageDialogKind>) -> MessageLevel {
    if let Some(kind) = kind {
        match kind {
            MessageDialogKind::Info => MessageLevel::Info,
            MessageDialogKind::Warning => MessageLevel::Warning,
            MessageDialogKind::Error => MessageLevel::Error,
        }
    } else {
        MessageLevel::Info
    }
}

const CANCEL: &str = "Cancel";

#[derive(Debug, Default)]
struct CustomButtons {
    ok: Option<String>,
    cancel: Option<String>,
    yes: Option<String>,
    no: Option<String>,
}

fn parse_result(cancel_label: String, result: MessageDialogResult, buttons: CustomButtons) -> MessageResult {
    match result {
        MessageDialogResult::Ok => {
            let button = if let Some(ok) = buttons.ok {
                ok
            } else {
                "Ok".to_string()
            };
            let cancelled = button == cancel_label;
            MessageResult {
                button,
                cancelled,
            }
        }
        MessageDialogResult::Cancel => {
            let button = if let Some(cancel) = buttons.cancel {
                cancel
            } else {
                "Cancel".to_string()
            };
            let cancelled = button == cancel_label;
            MessageResult {
                button,
                cancelled,
            }
        }
        MessageDialogResult::Yes => {
            let button = if let Some(yes) = buttons.yes {
                yes
            } else {
                "Yes".to_string()
            };
            let cancelled = button == cancel_label;
            MessageResult {
                button,
                cancelled,
            }
        }
        MessageDialogResult::No => {
            let button = if let Some(no) = buttons.no {
                no
            } else {
                "No".to_string()
            };
            let cancelled = button == cancel_label;
            MessageResult {
                button,
                cancelled,
            }
        }
        MessageDialogResult::Custom(label) => MessageResult {
            button: label.clone(),
            cancelled: cancel_label == label,
        },
    }
}

pub async fn message(options: MessageDialogOptions) -> MessageResult {
    let dialog = AsyncMessageDialog::new().set_title(options.title.as_ref().unwrap_or(&String::new())).set_level(get_level(&options.kind)).set_description(&options.message);

    let cancel_label = if let Some(cancel_id) = options.cancel_id {
        options.buttons.get(cancel_id as usize).unwrap_or(&String::new()).to_string()
    } else {
        let mut label = String::new();
        for button in &options.buttons {
            if button.to_lowercase() == CANCEL.to_lowercase() {
                label = button.clone();
                break;
            }
        }
        label
    };

    let (dialog, buttons) = if options.buttons.len() == 1 {
        let buttons = CustomButtons {
            ok: Some(options.buttons.first().unwrap().to_string()),
            ..Default::default()
        };
        (dialog.set_buttons(MessageButtons::OkCustom(options.buttons.first().unwrap().to_string())), buttons)
    } else if options.buttons.len() == 2 {
        let buttons = CustomButtons {
            ok: Some(options.buttons.first().unwrap().to_string()),
            cancel: Some(options.buttons.get(1).unwrap().to_string()),
            ..Default::default()
        };
        (dialog.set_buttons(MessageButtons::OkCancelCustom(options.buttons.first().unwrap().to_string(), options.buttons.get(1).unwrap().to_string())), buttons)
    } else if options.buttons.len() == 3 {
        let buttons = CustomButtons {
            yes: Some(options.buttons.first().unwrap().to_string()),
            no: Some(options.buttons.get(1).unwrap().to_string()),
            cancel: Some(options.buttons.get(2).unwrap().to_string()),
            ..Default::default()
        };
        (
            dialog.set_buttons(MessageButtons::YesNoCancelCustom(
                options.buttons.first().unwrap().to_string(),
                options.buttons.get(1).unwrap().to_string(),
                options.buttons.get(2).unwrap().to_string(),
            )),
            buttons,
        )
    } else {
        (dialog.set_buttons(MessageButtons::Ok), CustomButtons::default())
    };

    let result = dialog.show().await;
    parse_result(cancel_label, result, buttons)
}

pub async fn open(options: OpenDialogOptions) -> FileDialogResult {
    let dialog = AsyncFileDialog::new().set_title(options.title.as_ref().unwrap_or(&String::new())).set_directory(options.default_path.as_ref().unwrap_or(&String::new()));
    let dialog = if let Some(filters) = options.filters {
        let mut dialog_result = dialog;
        for filter in filters {
            dialog_result = dialog_result.add_filter(filter.name, &filter.extensions);
        }
        dialog_result
    } else {
        dialog
    };

    if let Some(properties) = options.properties {
        if properties.contains(&OpenProperty::MultiSelections) {
            pick_multiple(dialog, properties.contains(&OpenProperty::OpenFile)).await
        } else {
            pick_single(dialog, properties.contains(&OpenProperty::OpenFile)).await
        }
    } else {
        pick_single(dialog, true).await
    }
}

async fn pick_multiple(dialog: AsyncFileDialog, pic_file: bool) -> FileDialogResult {
    let results = if pic_file {
        dialog.pick_files().await
    } else {
        dialog.pick_folders().await
    };

    if let Some(results) = results {
        let mut file_paths = Vec::new();
        for result in results {
            file_paths.push(result.path().to_string_lossy().to_string());
        }

        return FileDialogResult {
            canceled: false,
            file_paths,
        };
    }

    FileDialogResult::default()
}

async fn pick_single(dialog: AsyncFileDialog, pic_file: bool) -> FileDialogResult {
    let result = if pic_file {
        dialog.pick_file().await
    } else {
        dialog.pick_folder().await
    };

    if let Some(result) = result {
        return FileDialogResult {
            canceled: false,
            file_paths: vec![result.path().to_string_lossy().to_string()],
        };
    }

    FileDialogResult::default()
}

pub async fn save(options: SaveDialogOptions) -> FileDialogResult {
    let (directory, file_name) = if let Some(default_path) = &options.default_path {
        let path = Path::new(default_path);
        if path.is_dir() {
            (Some(path), None)
        } else {
            (Some(path.parent().unwrap_or(Path::new(""))), path.file_name().map(|s| s.to_string_lossy().to_string()))
        }
    } else {
        (None, None)
    };

    let dialog = AsyncFileDialog::new()
        .set_title(options.title.as_ref().unwrap_or(&String::new()))
        .set_directory(directory.as_ref().unwrap_or(&Path::new("")))
        .set_file_name(file_name.as_ref().unwrap_or(&String::new()));
    let dialog = if let Some(filters) = options.filters {
        let mut dialog_result = dialog;
        for filter in filters {
            dialog_result = dialog_result.add_filter(filter.name, &filter.extensions);
        }
        dialog_result
    } else {
        let extensions: Vec<String> = Vec::new();
        dialog.add_filter("All Files (*.*)", &extensions)
    };

    let result = dialog.save_file().await;

    if let Some(result) = result {
        return FileDialogResult {
            canceled: false,
            file_paths: vec![result.path().to_string_lossy().to_string()],
        };
    }

    FileDialogResult::default()
}