From 9cfb655478d3f12d18faaf9d955d8ca4739cf4b0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 25 Apr 2023 14:26:12 -0500 Subject: [PATCH] implement the file engine for liveview --- packages/desktop/src/protocol.rs | 11 +++--- packages/html/src/events/form.rs | 47 +++++++++++++++++++++++- packages/interpreter/src/interpreter.js | 21 +++++++---- packages/liveview/Cargo.toml | 1 + packages/liveview/src/lib.rs | 49 ++++++++++++++++++++++++- 5 files changed, 113 insertions(+), 16 deletions(-) diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 5b0cd1cc7..f6725c428 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -23,11 +23,12 @@ fn module_loader(root_name: &str) -> String { 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); + const send = (event_name) => { + 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 }); + window.ipc.postMessage(message); + }; + send("change"); + send("input"); } event.preventDefault(); } diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 6edf60448..ec65ffe03 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -12,10 +12,55 @@ pub struct FormData { pub values: HashMap>, - #[cfg_attr(feature = "serialize", serde(skip))] + #[cfg_attr( + feature = "serialize", + serde(skip_serializing, deserialize_with = "deserialize_file_engine") + )] pub files: Option>, } +#[cfg(feature = "serialize")] +#[derive(serde::Serialize, serde::Deserialize)] +struct SerializedFileEngine { + files: HashMap>, +} + +#[cfg(feature = "serialize")] +#[async_trait::async_trait(?Send)] +impl FileEngine for SerializedFileEngine { + fn files(&self) -> Vec { + self.files.keys().cloned().collect() + } + + async fn read_file(&self, file: &str) -> Option> { + self.files.get(file).cloned() + } + + async fn read_file_to_string(&self, file: &str) -> Option { + self.read_file(file) + .await + .map(|bytes| String::from_utf8_lossy(&bytes).to_string()) + } +} + +#[cfg(feature = "serialize")] +fn deserialize_file_engine<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + + let Ok(file_engine) = + SerializedFileEngine::deserialize(deserializer) else{ + return Ok(None); + }; + + let file_engine = std::sync::Arc::new(file_engine); + Ok(Some(file_engine)) +} + impl PartialEq for FormData { fn eq(&self, other: &Self) -> bool { self.value == other.value && self.values == other.values diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 162d5c8f5..81639b778 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -344,17 +344,19 @@ class Interpreter { this.RemoveEventListener(edit.id, edit.name); break; case "NewEventListener": - let bubbles = event_bubbles(edit.name); + const bubbles = event_bubbles(edit.name); - this.NewEventListener(edit.name, edit.id, bubbles, handler); + this.NewEventListener(edit.name, edit.id, bubbles, (event) => + handler(event, edit.name, bubbles) + ); break; } } } -// this handler is only provided on desktop implementations since this +// this handler is only provided on the desktop and liveview implementations since this // method is not used by the web implementation -function handler(event) { +function handler(event, name, bubbles) { let target = event.target; if (target != null) { let shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); @@ -387,8 +389,6 @@ function handler(event) { shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - let contents = serialize_event(event); - if (shouldPreventDefault === `on${event.type}`) { event.preventDefault(); } @@ -397,6 +397,10 @@ function handler(event) { event.preventDefault(); } + let contents = serialize_event(event); + + /*POST_EVENT_SERIALIZATION*/ + if ( target.tagName === "FORM" && (event.type === "submit" || event.type === "input") @@ -408,7 +412,8 @@ function handler(event) { const formData = new FormData(target); for (let name of formData.keys()) { - contents.values[name] = formData.getAll(name); + let value = formData.getAll(name); + contents.values[name] = value; } } } @@ -418,7 +423,7 @@ function handler(event) { } window.ipc.postMessage( serializeIpcMessage("user_event", { - name: edit.name, + name: name, element: parseInt(realId), data: contents, bubbles, diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 5269927b0..61fbe0887 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -36,6 +36,7 @@ axum = { version = "0.6.1", optional = true, features = ["ws"] } # salvo salvo = { version = "0.37.7", optional = true, features = ["ws"] } +once_cell = "1.17.1" # actix is ... complicated? # actix-files = { version = "0.6.2", optional = true } diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index ffddf0272..bea21a71b 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -34,7 +34,51 @@ pub enum LiveViewError { SendingFailed, } -use dioxus_interpreter_js::INTERPRETER_JS; +use once_cell::sync::Lazy; + +static INTERPRETER_JS: Lazy = Lazy::new(|| { + let interpreter = dioxus_interpreter_js::INTERPRETER_JS; + let serialize_file_uploads = r#"if ( + target.tagName === "INPUT" && + (event.type === "change" || event.type === "input") + ) { + const type = target.getAttribute("type"); + if (type === "file") { + async function read_files() { + const files = target.files; + const file_contents = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + let file_engine = { + files: file_contents, + }; + contents.files = file_engine; + + if (realId === null) { + return; + } + const message = serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }); + window.ipc.postMessage(message); + } + read_files(); + return; + } + }"#; + + interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads) +}); + static MAIN_JS: &str = include_str!("./main.js"); /// This script that gets injected into your app connects this page to the websocket endpoint @@ -42,11 +86,12 @@ static MAIN_JS: &str = include_str!("./main.js"); /// Once the endpoint is connected, it will send the initial state of the app, and then start /// processing user events and returning edits to the liveview instance pub fn interpreter_glue(url: &str) -> String { + let js = &*INTERPRETER_JS; format!( r#"