瀏覽代碼

implement file events on desktop

Evan Almloff 2 年之前
父節點
當前提交
c1e2592299

+ 2 - 2
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 {

+ 2 - 1
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"

+ 74 - 0
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<PathBuf> {
+    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<Self, Self::Err> {
+        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())),
+            }
+        }
+    }
+}

+ 26 - 2
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<P: 'static>(root: Component<P>, props: P, cfg: Config)
                     }
                 }
 
+                EventData::Ipc(msg) if msg.method() == "file_diolog" => {
+                    if let Ok(file_diolog) =
+                        serde_json::from_value::<file_upload::FileDiologRequest>(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),

+ 19 - 9
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!(