use std::path::{Path, PathBuf};
pub fn clipboard_text() -> Option<String> {
corundum_framework_platform::clipboard::text()
}
pub fn window_insets() -> Option<[i32; 4]> {
corundum_framework_platform::insets::window_insets()
}
#[cfg(all(target_os = "android", feature = "android"))]
pub(crate) fn set_android_activity(activity: *mut std::ffi::c_void) {
corundum_framework_platform::android::set_activity(activity);
}
pub fn needs_paste_button() -> bool {
cfg!(target_os = "android")
}
const DESKTOP: bool = cfg!(not(any(target_os = "android", target_os = "ios")));
pub const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "bmp", "heic", "avif"];
pub const AUDIO_EXTENSIONS: &[&str] = &["mp3", "m4a", "aac", "flac", "ogg", "opus", "wav"];
pub const VIDEO_EXTENSIONS: &[&str] = &["mp4", "m4v", "mov", "mkv", "webm"];
pub fn file_picker_available() -> bool {
DESKTOP
}
pub fn folder_picker_available() -> bool {
DESKTOP
}
pub fn pick_folder(title: &str) -> Option<PathBuf> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
rfd::FileDialog::new().set_title(title).pick_folder()
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
let _ = title;
None
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FilePicker {
title: Option<String>,
filters: Vec<(String, Vec<String>)>,
directory: Option<PathBuf>,
}
impl FilePicker {
pub fn new() -> FilePicker {
FilePicker::default()
}
pub fn title(mut self, title: impl Into<String>) -> FilePicker {
self.title = Some(title.into());
self
}
pub fn filter(mut self, name: impl Into<String>, extensions: &[&str]) -> FilePicker {
let extensions = extensions.iter().map(|e| e.to_string()).collect();
self.filters.push((name.into(), extensions));
self
}
pub fn directory(mut self, dir: impl Into<PathBuf>) -> FilePicker {
self.directory = Some(dir.into());
self
}
pub fn pick_file(self) -> Option<PathBuf> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
self.dialog().pick_file()
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
None
}
}
pub fn pick_files(self) -> Vec<PathBuf> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
self.dialog().pick_files().unwrap_or_default()
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
Vec::new()
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn dialog(self) -> rfd::FileDialog {
let mut dialog = rfd::FileDialog::new();
if let Some(title) = self.title {
dialog = dialog.set_title(title);
}
for (name, extensions) in &self.filters {
dialog = dialog.add_filter(name, extensions);
}
if let Some(dir) = self.directory {
dialog = dialog.set_directory(dir);
}
dialog
}
}
#[cfg(feature = "media-picker")]
pub use corundum_framework_platform::media::MediaKind;
#[cfg(any(feature = "media-picker", feature = "qr-scan"))]
pub use corundum_framework_platform::pending::Pending;
#[cfg(feature = "media-picker")]
pub fn media_picker_available() -> bool {
DESKTOP || corundum_framework_platform::media::available()
}
#[cfg(feature = "media-picker")]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MediaPicker {
kind: MediaKind,
limit: usize,
title: Option<String>,
}
#[cfg(feature = "media-picker")]
impl MediaPicker {
pub fn new() -> MediaPicker {
MediaPicker::default()
}
pub fn kind(mut self, kind: MediaKind) -> MediaPicker {
self.kind = kind;
self
}
pub fn limit(mut self, limit: usize) -> MediaPicker {
self.limit = limit;
self
}
pub fn title(mut self, title: impl Into<String>) -> MediaPicker {
self.title = Some(title.into());
self
}
pub fn pick(self, ctx: &egui::Context) -> Pending<Vec<PathBuf>> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
Pending::ready(self.pick_on_desktop(ctx))
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
let ctx = ctx.clone();
corundum_framework_platform::media::pick(self.kind, self.limit, move || {
ctx.request_repaint()
})
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn pick_on_desktop(self, ctx: &egui::Context) -> Vec<PathBuf> {
let mut dialog = FilePicker::new();
if let Some(title) = self.title {
dialog = dialog.title(title);
}
let images = crate::i18n::tr(ctx, "corundum-media-images");
let videos = crate::i18n::tr(ctx, "corundum-media-videos");
dialog = match self.kind {
MediaKind::Images => dialog.filter(images, IMAGE_EXTENSIONS),
MediaKind::Videos => dialog.filter(videos, VIDEO_EXTENSIONS),
MediaKind::ImagesAndVideos => {
let both: Vec<&str> = IMAGE_EXTENSIONS
.iter()
.chain(VIDEO_EXTENSIONS)
.copied()
.collect();
dialog
.filter(
crate::i18n::tr(ctx, "corundum-media-images-and-videos"),
&both,
)
.filter(images, IMAGE_EXTENSIONS)
.filter(videos, VIDEO_EXTENSIONS)
}
};
let mut picked = if self.limit == 1 {
dialog.pick_file().into_iter().collect()
} else {
dialog.pick_files()
};
if self.limit > 0 {
picked.truncate(self.limit);
}
picked
}
}
#[cfg(feature = "qr-scan")]
pub fn qr_scanner_available() -> bool {
corundum_framework_platform::qr::available()
}
#[cfg(feature = "qr-scan")]
pub fn scan_qr(ctx: &egui::Context) -> Pending<Option<String>> {
let ctx = ctx.clone();
corundum_framework_platform::qr::scan(move || ctx.request_repaint())
}
pub fn file_manager_available() -> bool {
DESKTOP
}
pub fn open_in_file_manager(path: &Path) {
#[cfg(windows)]
let program = Some("explorer");
#[cfg(target_os = "macos")]
let program = Some("open");
#[cfg(all(
unix,
not(any(target_os = "macos", target_os = "android", target_os = "ios"))
))]
let program = Some("xdg-open");
#[cfg(any(target_os = "android", target_os = "ios"))]
let program: Option<&str> = None;
if let Some(program) = program {
let _ = std::process::Command::new(program).arg(path).spawn();
}
}
#[cfg(all(test, not(target_os = "android")))]
mod tests {
use super::*;
#[test]
fn off_android_there_is_no_clipboard_insets_or_paste_button() {
assert_eq!(clipboard_text(), None);
assert_eq!(window_insets(), None);
assert!(!needs_paste_button());
}
#[cfg(not(target_os = "ios"))]
#[test]
fn a_desktop_has_the_dialogs_and_a_file_manager() {
assert!(file_picker_available());
assert!(folder_picker_available());
assert!(file_manager_available());
}
#[test]
fn the_picker_keeps_its_filters_in_order() {
let picker = FilePicker::new()
.title("Upload")
.filter("Images", IMAGE_EXTENSIONS)
.filter("Audio", &["mp3"])
.directory("/tmp");
assert_eq!(picker.title.as_deref(), Some("Upload"));
let names: Vec<_> = picker.filters.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(names, ["Images", "Audio"]);
assert_eq!(picker.filters[1].1, ["mp3"]);
assert_eq!(picker.directory, Some(PathBuf::from("/tmp")));
}
#[cfg(feature = "media-picker")]
#[test]
fn the_media_picker_takes_any_number_of_both_kinds_by_default() {
let picker = MediaPicker::new();
assert_eq!(picker.kind, MediaKind::ImagesAndVideos);
assert_eq!(picker.limit, 0);
let picker = picker.kind(MediaKind::Videos).limit(1).title("Video");
assert_eq!(picker.kind, MediaKind::Videos);
assert_eq!(picker.limit, 1);
assert_eq!(picker.title.as_deref(), Some("Video"));
}
#[cfg(all(feature = "media-picker", not(target_os = "ios")))]
#[test]
fn a_desktop_has_the_media_picker() {
assert!(media_picker_available());
}
#[cfg(all(feature = "qr-scan", not(target_os = "ios")))]
#[test]
fn a_desktop_has_no_qr_scanner_and_answers_at_once() {
assert!(!qr_scanner_available());
let ctx = egui::Context::default();
assert_eq!(scan_qr(&ctx).poll(), Some(None));
}
}