diff --git a/examples/file_upload.rs b/examples/file_upload.rs index c3771f7b0..c43e6e6ef 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -11,11 +11,11 @@ fn App(cx: Scope) -> Element { cx.render(rsx! { input { r#type: "file", - accept: "text/*", + accept: ".txt", + multiple: true, onchange: |evt| { to_owned![files_uploaded]; async move { - println!("files uploaded: {:?}", evt); if let Some(file_engine) = &evt.files { let files = file_engine.files(); for file_name in &files { diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index beb971ca9..8b41019e1 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] } -dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" } +dioxus-html = { path = "../html", features = ["serialize", "native-bind"], version = "^0.3.0" } dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" } dioxus-hot-reload = { path = "../hot-reload", optional = true } @@ -37,6 +37,7 @@ dunce = "1.0.2" slab = "0.4" futures-util = "0.3.25" +rfd = "0.11.3" [target.'cfg(target_os = "ios")'.dependencies] objc = "0.2.7" diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs new file mode 100644 index 000000000..ae4a81cc2 --- /dev/null +++ b/packages/desktop/src/file_upload.rs @@ -0,0 +1,74 @@ +use std::{path::PathBuf, str::FromStr}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub(crate) struct FileDiologRequest { + accept: String, + multiple: bool, + pub event: String, + pub target: usize, + pub bubbles: bool, +} + +pub(crate) fn get_file_event(request: &FileDiologRequest) -> Vec { + let mut dialog = rfd::FileDialog::new(); + + let filters: Vec<_> = request + .accept + .split(',') + .filter_map(|s| Filters::from_str(s).ok()) + .collect(); + + let file_extensions: Vec<_> = filters + .iter() + .flat_map(|f| f.as_extensions().into_iter()) + .collect(); + + dialog = dialog.add_filter("name", file_extensions.as_slice()); + + let files: Vec<_> = if request.multiple { + dialog.pick_files().into_iter().flatten().collect() + } else { + dialog.pick_file().into_iter().collect() + }; + + files +} + +enum Filters { + Extension(String), + Mime(String), + Audio, + Video, + Image, +} + +impl Filters { + fn as_extensions(&self) -> Vec<&str> { + match self { + Filters::Extension(extension) => vec![extension.as_str()], + Filters::Mime(_) => vec![], + Filters::Audio => vec!["mp3", "wav", "ogg"], + Filters::Video => vec!["mp4", "webm"], + Filters::Image => vec!["png", "jpg", "jpeg", "gif", "webp"], + } + } +} + +impl FromStr for Filters { + type Err = String; + + fn from_str(s: &str) -> Result { + if let Some(extension) = s.strip_prefix('.') { + Ok(Filters::Extension(extension.to_string())) + } else { + match s { + "audio/*" => Ok(Filters::Audio), + "video/*" => Ok(Filters::Video), + "image/*" => Ok(Filters::Image), + _ => Ok(Filters::Mime(s.to_string())), + } + } + } +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index cdb592f72..4d904e948 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -8,6 +8,7 @@ mod desktop_context; mod escape; mod eval; mod events; +mod file_upload; mod protocol; mod shortcut; mod waker; @@ -19,14 +20,14 @@ pub use desktop_context::{ }; use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use dioxus_core::*; -use dioxus_html::HtmlEvent; +use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; use shortcut::ShortcutRegistry; pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; -use std::collections::HashMap; use std::rc::Rc; use std::task::Waker; +use std::{collections::HashMap, sync::Arc}; pub use tao::dpi::{LogicalSize, PhysicalSize}; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use tao::window::WindowBuilder; @@ -264,6 +265,29 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) } } + EventData::Ipc(msg) if msg.method() == "file_diolog" => { + if let Ok(file_diolog) = + serde_json::from_value::(msg.params()) + { + let id = ElementId(file_diolog.target); + let event_name = &file_diolog.event; + let event_bubbles = file_diolog.bubbles; + let files = file_upload::get_file_event(&file_diolog); + let data = FormData { + value: Default::default(), + values: Default::default(), + files: Some(Arc::new(NativeFileEngine::new(files))), + }; + + let view = webviews.get_mut(&event.1).unwrap(); + + view.dom + .handle_event(event_name, Rc::new(data), id, event_bubbles); + + send_edits(view.dom.render_immediate(), &view.webview); + } + } + _ => {} }, Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id), diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 3d4246987..5b0cd1cc7 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -14,15 +14,25 @@ fn module_loader(root_name: &str) -> String { r#"// Prevent file inputs from opening the file dialog on click let inputs = document.querySelectorAll("input"); for (let input of inputs) { - input.addEventListener("click", (event) => { - let target = event.target; - // prevent file inputs from opening the file dialog on click - const type = target.getAttribute("type"); - if (type === "file") { - window.ipc.postMessage(serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), multiple: target.hasAttribute("multiple") })); - event.preventDefault(); - } - }); + if (!input.getAttribute("data-dioxus-file-listener")) { + input.setAttribute("data-dioxus-file-listener", true); + input.addEventListener("click", (event) => { + let target = event.target; + // prevent file inputs from opening the file dialog on click + const type = target.getAttribute("type"); + if (type === "file") { + let target_id = find_real_id(target); + if (target_id !== null) { + const event_name = "change"; + const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); + console.log(message); + console.log(event); + window.ipc.postMessage(message); + } + event.preventDefault(); + } + }); + } }"#, ); format!(