More fullstack fixes (#4737)
* allow `--server` only builds! * rollback example * capture `/:route` routes * fix: properly implement file uploads in forms * implement desktop form uploads * Performant serialization of form bytes * finish up file stuff * cleanups * add request headers to streaming context * remove some unwraps
This commit is contained in:
1 parent
fc01a002fd
commit
92792aded9
47 files changed
+1241
-703
No files matched your search
Generated
+1
@@ -14089,6 +14089,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
|||||||
@@ -52,11 +52,14 @@ fn app() -> Element {
|
|||||||
#[post("/api/upload-multipart")]
|
#[post("/api/upload-multipart")]
|
||||||
async fn upload(mut form: MultipartFormData) -> Result<()> {
|
async fn upload(mut form: MultipartFormData) -> Result<()> {
|
||||||
while let Ok(Some(field)) = form.next_field().await {
|
while let Ok(Some(field)) = form.next_field().await {
|
||||||
|
let name = field.name().unwrap_or("<none>").to_string();
|
||||||
|
let file_name = field.file_name().unwrap_or("<none>").to_string();
|
||||||
|
let content_type = field.content_type().unwrap_or("<none>").to_string();
|
||||||
|
let size = field.bytes().await.unwrap().len();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Field name: {:?}, filename: {:?}, content_type: {:?}",
|
"Field name: {:?}, filename: {:?}, content_type: {:?}, size: {:?}",
|
||||||
field.name().to_owned(),
|
name, file_name, content_type, size
|
||||||
field.file_name(),
|
|
||||||
field.content_type(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Option 1:
|
|||||||
For simple, synchronous, thread-safe data, we can use statics with atomic types or mutexes.
|
For simple, synchronous, thread-safe data, we can use statics with atomic types or mutexes.
|
||||||
The `LazyLock` type from the standard library is a great choice for simple, synchronous data
|
The `LazyLock` type from the standard library is a great choice for simple, synchronous data
|
||||||
*/
|
*/
|
||||||
|
#[cfg(feature = "server")]
|
||||||
static MESSAGES: LazyLock<Mutex<Vec<String>>> = LazyLock::new(|| Mutex::new(Vec::new()));
|
static MESSAGES: LazyLock<Mutex<Vec<String>>> = LazyLock::new(|| Mutex::new(Vec::new()));
|
||||||
|
|
||||||
#[post("/api/messages")]
|
#[post("/api/messages")]
|
||||||
|
|||||||
@@ -4,15 +4,14 @@
|
|||||||
//! in the "values" field.
|
//! in the "values" field.
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::launch(app);
|
dioxus::launch(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app() -> Element {
|
fn app() -> Element {
|
||||||
let mut values = use_signal(HashMap::new);
|
let mut values = use_signal(Vec::new);
|
||||||
let mut submitted_values = use_signal(HashMap::new);
|
let mut submitted_values = use_signal(Vec::new);
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { style: "display: flex",
|
div { style: "display: flex",
|
||||||
@@ -63,7 +62,7 @@ fn app() -> Element {
|
|||||||
input { r#type: "text", name: "full-name" }
|
input { r#type: "text", name: "full-name" }
|
||||||
|
|
||||||
label { r#for: "email", "Email (matching <name>@example.com)" }
|
label { r#for: "email", "Email (matching <name>@example.com)" }
|
||||||
input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" }
|
input { r#type: "email", size: "30", id: "email", name: "email" }
|
||||||
|
|
||||||
label { r#for: "password", "Password" }
|
label { r#for: "password", "Password" }
|
||||||
input { r#type: "password", name: "password" }
|
input { r#type: "password", name: "password" }
|
||||||
@@ -122,6 +121,9 @@ fn app() -> Element {
|
|||||||
input { r#type: "checkbox", name: "cbox", value: "yellow", id: "cbox-yellow" }
|
input { r#type: "checkbox", name: "cbox", value: "yellow", id: "cbox-yellow" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File input
|
||||||
|
label { r#for: "headshot", "Headshot" }
|
||||||
|
input { r#type: "file", name: "headshot", id: "headshot", multiple: true, accept: ".png,.jpg,.jpeg" }
|
||||||
|
|
||||||
// Buttons will submit your form by default.
|
// Buttons will submit your form by default.
|
||||||
button { r#type: "submit", value: "Submit", "Submit the form" }
|
button { r#type: "submit", value: "Submit", "Submit the form" }
|
||||||
|
|||||||
@@ -154,19 +154,20 @@ impl AppBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new `AppBuilder` and immediately start a build process.
|
/// Create a new `AppBuilder` and immediately start a build process.
|
||||||
pub fn started(request: &BuildRequest, mode: BuildMode) -> Result<Self> {
|
pub fn started(request: &BuildRequest, mode: BuildMode, build_id: BuildId) -> Result<Self> {
|
||||||
let mut builder = Self::new(request)?;
|
let mut builder = Self::new(request)?;
|
||||||
builder.start(mode);
|
builder.start(mode, build_id);
|
||||||
Ok(builder)
|
Ok(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn start(&mut self, mode: BuildMode) {
|
pub(crate) fn start(&mut self, mode: BuildMode, build_id: BuildId) {
|
||||||
self.build_task = tokio::spawn({
|
self.build_task = tokio::spawn({
|
||||||
let request = self.build.clone();
|
let request = self.build.clone();
|
||||||
let tx = self.tx.clone();
|
let tx = self.tx.clone();
|
||||||
async move {
|
async move {
|
||||||
let ctx = BuildContext {
|
let ctx = BuildContext {
|
||||||
mode,
|
mode,
|
||||||
|
build_id,
|
||||||
tx: tx.clone(),
|
tx: tx.clone(),
|
||||||
};
|
};
|
||||||
request.verify_tooling(&ctx).await?;
|
request.verify_tooling(&ctx).await?;
|
||||||
@@ -290,10 +291,12 @@ impl AppBuilder {
|
|||||||
update
|
update
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn patch_rebuild(&mut self, changed_files: Vec<PathBuf>) {
|
pub(crate) fn patch_rebuild(&mut self, changed_files: Vec<PathBuf>, build_id: BuildId) {
|
||||||
// We need the rustc args from the original build to pass to the new build
|
// We need the rustc args from the original build to pass to the new build
|
||||||
let Some(artifacts) = self.artifacts.as_ref().cloned() else {
|
let Some(artifacts) = self.artifacts.as_ref().cloned() else {
|
||||||
tracing::warn!("Ignoring patch rebuild since there is no existing build.");
|
tracing::warn!(
|
||||||
|
"Ignoring patch rebuild for {build_id:?} since there is no existing build."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -327,6 +330,7 @@ impl AppBuilder {
|
|||||||
self.build_task = tokio::spawn({
|
self.build_task = tokio::spawn({
|
||||||
let request = self.build.clone();
|
let request = self.build.clone();
|
||||||
let ctx = BuildContext {
|
let ctx = BuildContext {
|
||||||
|
build_id,
|
||||||
tx: self.tx.clone(),
|
tx: self.tx.clone(),
|
||||||
mode: BuildMode::Thin {
|
mode: BuildMode::Thin {
|
||||||
changed_files,
|
changed_files,
|
||||||
@@ -340,7 +344,7 @@ impl AppBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Restart this builder with new build arguments.
|
/// Restart this builder with new build arguments.
|
||||||
pub(crate) fn start_rebuild(&mut self, mode: BuildMode) {
|
pub(crate) fn start_rebuild(&mut self, mode: BuildMode, build_id: BuildId) {
|
||||||
// Abort all the ongoing builds, cleaning up any loose artifacts and waiting to cleanly exit
|
// Abort all the ongoing builds, cleaning up any loose artifacts and waiting to cleanly exit
|
||||||
// And then start a new build, resetting our progress/stage to the beginning and replacing the old tokio task
|
// And then start a new build, resetting our progress/stage to the beginning and replacing the old tokio task
|
||||||
self.abort_all(BuildStage::Restarting);
|
self.abort_all(BuildStage::Restarting);
|
||||||
@@ -351,6 +355,7 @@ impl AppBuilder {
|
|||||||
let ctx = BuildContext {
|
let ctx = BuildContext {
|
||||||
tx: self.tx.clone(),
|
tx: self.tx.clone(),
|
||||||
mode,
|
mode,
|
||||||
|
build_id,
|
||||||
};
|
};
|
||||||
async move { request.build(&ctx).await }
|
async move { request.build(&ctx).await }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use std::{path::PathBuf, process::ExitStatus};
|
|||||||
pub struct BuildContext {
|
pub struct BuildContext {
|
||||||
pub tx: ProgressTx,
|
pub tx: ProgressTx,
|
||||||
pub mode: BuildMode,
|
pub mode: BuildMode,
|
||||||
|
pub build_id: BuildId,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ProgressTx = UnboundedSender<BuilderUpdate>;
|
pub type ProgressTx = UnboundedSender<BuilderUpdate>;
|
||||||
@@ -24,8 +25,8 @@ pub type ProgressRx = UnboundedReceiver<BuilderUpdate>;
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
pub struct BuildId(pub(crate) usize);
|
pub struct BuildId(pub(crate) usize);
|
||||||
impl BuildId {
|
impl BuildId {
|
||||||
pub const CLIENT: Self = Self(0);
|
pub const PRIMARY: Self = Self(0);
|
||||||
pub const SERVER: Self = Self(1);
|
pub const SECONDARY: Self = Self(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
@@ -78,6 +79,12 @@ pub enum BuilderUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BuildContext {
|
impl BuildContext {
|
||||||
|
/// Returns true if this is a client build - basically, is this the primary build?
|
||||||
|
/// We try not to duplicate work between client and server builds, like asset copying.
|
||||||
|
pub(crate) fn is_primary_build(&self) -> bool {
|
||||||
|
self.build_id == BuildId::PRIMARY
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn status_wasm_bindgen_start(&self) {
|
pub(crate) fn status_wasm_bindgen_start(&self) {
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::RunningBindgen,
|
stage: BuildStage::RunningBindgen,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ pub(crate) async fn pre_render_static_routes(
|
|||||||
devserver_ip,
|
devserver_ip,
|
||||||
Some(fullstack_address),
|
Some(fullstack_address),
|
||||||
false,
|
false,
|
||||||
BuildId::SERVER,
|
BuildId::SECONDARY,
|
||||||
);
|
);
|
||||||
// Run the server executable
|
// Run the server executable
|
||||||
let _child = Command::new(&server_exe)
|
let _child = Command::new(&server_exe)
|
||||||
|
|||||||
@@ -320,9 +320,9 @@
|
|||||||
//! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
|
//! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AndroidTools, BuildContext, BundleFormat, DioxusConfig, Error, LinkAction, LinkerFlavor,
|
AndroidTools, BuildContext, BuildId, BundleFormat, DioxusConfig, Error, LinkAction,
|
||||||
Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig,
|
LinkerFlavor, Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc, WasmBindgen,
|
||||||
Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
|
WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use cargo_metadata::diagnostic::Diagnostic;
|
use cargo_metadata::diagnostic::Diagnostic;
|
||||||
@@ -445,7 +445,6 @@ pub enum BuildMode {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct BuildArtifacts {
|
pub struct BuildArtifacts {
|
||||||
pub(crate) root_dir: PathBuf,
|
pub(crate) root_dir: PathBuf,
|
||||||
pub(crate) bundle: BundleFormat,
|
|
||||||
pub(crate) exe: PathBuf,
|
pub(crate) exe: PathBuf,
|
||||||
pub(crate) direct_rustc: RustcArgs,
|
pub(crate) direct_rustc: RustcArgs,
|
||||||
pub(crate) time_start: SystemTime,
|
pub(crate) time_start: SystemTime,
|
||||||
@@ -454,6 +453,7 @@ pub struct BuildArtifacts {
|
|||||||
pub(crate) mode: BuildMode,
|
pub(crate) mode: BuildMode,
|
||||||
pub(crate) patch_cache: Option<Arc<HotpatchModuleCache>>,
|
pub(crate) patch_cache: Option<Arc<HotpatchModuleCache>>,
|
||||||
pub(crate) depinfo: RustcDepInfo,
|
pub(crate) depinfo: RustcDepInfo,
|
||||||
|
pub(crate) build_id: BuildId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuildRequest {
|
impl BuildRequest {
|
||||||
@@ -989,10 +989,10 @@ impl BuildRequest {
|
|||||||
_ = std::fs::File::create_new(self.windows_command_file());
|
_ = std::fs::File::create_new(self.windows_command_file());
|
||||||
|
|
||||||
if !matches!(ctx.mode, BuildMode::Thin { .. }) {
|
if !matches!(ctx.mode, BuildMode::Thin { .. }) {
|
||||||
self.prepare_build_dir()?;
|
self.prepare_build_dir(ctx)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.bundle == BundleFormat::Server {
|
if !ctx.is_primary_build() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1250,7 +1250,6 @@ impl BuildRequest {
|
|||||||
let assets = self.collect_assets(&exe, ctx).await?;
|
let assets = self.collect_assets(&exe, ctx).await?;
|
||||||
let time_end = SystemTime::now();
|
let time_end = SystemTime::now();
|
||||||
let mode = ctx.mode.clone();
|
let mode = ctx.mode.clone();
|
||||||
let bundle = self.bundle;
|
|
||||||
let depinfo = RustcDepInfo::from_file(&exe.with_extension("d")).unwrap_or_default();
|
let depinfo = RustcDepInfo::from_file(&exe.with_extension("d")).unwrap_or_default();
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
@@ -1261,7 +1260,6 @@ impl BuildRequest {
|
|||||||
|
|
||||||
Ok(BuildArtifacts {
|
Ok(BuildArtifacts {
|
||||||
time_end,
|
time_end,
|
||||||
bundle,
|
|
||||||
exe,
|
exe,
|
||||||
direct_rustc,
|
direct_rustc,
|
||||||
time_start,
|
time_start,
|
||||||
@@ -1270,6 +1268,7 @@ impl BuildRequest {
|
|||||||
depinfo,
|
depinfo,
|
||||||
root_dir: self.root_dir(),
|
root_dir: self.root_dir(),
|
||||||
patch_cache: None,
|
patch_cache: None,
|
||||||
|
build_id: ctx.build_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1460,7 +1459,7 @@ impl BuildRequest {
|
|||||||
/// Should be the same on all platforms - just copy over the assets from the manifest into the output directory
|
/// Should be the same on all platforms - just copy over the assets from the manifest into the output directory
|
||||||
async fn write_assets(&self, ctx: &BuildContext, assets: &AssetManifest) -> Result<()> {
|
async fn write_assets(&self, ctx: &BuildContext, assets: &AssetManifest) -> Result<()> {
|
||||||
// Server doesn't need assets - web will provide them
|
// Server doesn't need assets - web will provide them
|
||||||
if self.bundle == BundleFormat::Server {
|
if !ctx.is_primary_build() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4320,14 +4319,14 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
|||||||
/// - extra scaffolding
|
/// - extra scaffolding
|
||||||
///
|
///
|
||||||
/// It's not guaranteed that they're different from any other folder
|
/// It's not guaranteed that they're different from any other folder
|
||||||
pub(crate) fn prepare_build_dir(&self) -> Result<()> {
|
pub(crate) fn prepare_build_dir(&self, ctx: &BuildContext) -> Result<()> {
|
||||||
use std::fs::{create_dir_all, remove_dir_all};
|
use std::fs::{create_dir_all, remove_dir_all};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
static INITIALIZED: OnceLock<Result<()>> = OnceLock::new();
|
static INITIALIZED: OnceLock<Result<()>> = OnceLock::new();
|
||||||
|
|
||||||
let success = INITIALIZED.get_or_init(|| {
|
let success = INITIALIZED.get_or_init(|| {
|
||||||
if self.bundle != BundleFormat::Server {
|
if ctx.is_primary_build() {
|
||||||
_ = remove_dir_all(self.exe_dir());
|
_ = remove_dir_all(self.exe_dir());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use dioxus_dx_wire_format::StructuredBuildArtifacts;
|
use dioxus_dx_wire_format::StructuredBuildArtifacts;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::*, Anonymized, AppBuilder, BuildArtifacts, BuildMode, BuildRequest, BundleFormat,
|
cli::*, Anonymized, AppBuilder, BuildArtifacts, BuildId, BuildMode, BuildRequest, BundleFormat,
|
||||||
TargetArgs, Workspace,
|
Platform, TargetArgs, Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Build the Rust Dioxus app and all of its assets.
|
/// Build the Rust Dioxus app and all of its assets.
|
||||||
@@ -128,6 +128,10 @@ impl CommandWithPlatformOverrides<BuildArgs> {
|
|||||||
BuildRequest::new(&server_args.build_arguments, workspace.clone()).await?,
|
BuildRequest::new(&server_args.build_arguments, workspace.clone()).await?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
None if client_args.platform == Platform::Server => {
|
||||||
|
// If the user requests a server build with `--server`, then we don't need to build a separate server binary.
|
||||||
|
// There's no client to use, so even though fullstack is true, we only build the server.
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
let mut args = self.shared.build_arguments.clone();
|
let mut args = self.shared.build_arguments.clone();
|
||||||
args.platform = crate::Platform::Server;
|
args.platform = crate::Platform::Server;
|
||||||
@@ -171,7 +175,7 @@ impl CommandWithPlatformOverrides<BuildArgs> {
|
|||||||
request: &BuildRequest,
|
request: &BuildRequest,
|
||||||
mode: BuildMode,
|
mode: BuildMode,
|
||||||
) -> Result<BuildArtifacts> {
|
) -> Result<BuildArtifacts> {
|
||||||
AppBuilder::started(request, mode)?
|
AppBuilder::started(request, mode, BuildId::PRIMARY)?
|
||||||
.finish_build()
|
.finish_build()
|
||||||
.await
|
.await
|
||||||
.inspect(|_| {
|
.inspect(|_| {
|
||||||
@@ -189,7 +193,7 @@ impl CommandWithPlatformOverrides<BuildArgs> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// If the server is present, we need to build it as well
|
// If the server is present, we need to build it as well
|
||||||
let mut server_build = AppBuilder::started(server, mode)?;
|
let mut server_build = AppBuilder::started(server, mode, BuildId::SECONDARY)?;
|
||||||
let server_artifacts = server_build.finish_build().await?;
|
let server_artifacts = server_build.finish_build().await?;
|
||||||
|
|
||||||
// Run SSG and cache static routes
|
// Run SSG and cache static routes
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{AppBuilder, BuildArgs, BuildMode, BuildRequest, BundleFormat};
|
use crate::{AppBuilder, BuildArgs, BuildId, BuildMode, BuildRequest, BundleFormat};
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use path_absolutize::Absolutize;
|
use path_absolutize::Absolutize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -40,16 +40,17 @@ impl Bundle {
|
|||||||
let BuildTargets { client, server } = self.args.into_targets().await?;
|
let BuildTargets { client, server } = self.args.into_targets().await?;
|
||||||
|
|
||||||
let mut server_artifacts = None;
|
let mut server_artifacts = None;
|
||||||
let client_artifacts = AppBuilder::started(&client, BuildMode::Base { run: false })?
|
let client_artifacts =
|
||||||
.finish_build()
|
AppBuilder::started(&client, BuildMode::Base { run: false }, BuildId::PRIMARY)?
|
||||||
.await?;
|
.finish_build()
|
||||||
|
.await?;
|
||||||
|
|
||||||
tracing::info!(path = ?client.root_dir(), "Client build completed successfully! 🚀");
|
tracing::info!(path = ?client.root_dir(), "Client build completed successfully! 🚀");
|
||||||
|
|
||||||
if let Some(server) = server.as_ref() {
|
if let Some(server) = server.as_ref() {
|
||||||
// If the server is present, we need to build it as well
|
// If the server is present, we need to build it as well
|
||||||
server_artifacts = Some(
|
server_artifacts = Some(
|
||||||
AppBuilder::started(server, BuildMode::Base { run: false })?
|
AppBuilder::started(server, BuildMode::Base { run: false }, BuildId::SECONDARY)?
|
||||||
.finish_build()
|
.finish_build()
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
platform_override::CommandWithPlatformOverrides, AppBuilder, BuildArgs, BuildMode,
|
platform_override::CommandWithPlatformOverrides, AppBuilder, BuildArgs, BuildId, BuildMode,
|
||||||
HotpatchModuleCache, Result, StructuredOutput,
|
HotpatchModuleCache, Result, StructuredOutput,
|
||||||
};
|
};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
@@ -34,8 +34,16 @@ impl HotpatchTip {
|
|||||||
pub async fn run(self) -> Result<StructuredOutput> {
|
pub async fn run(self) -> Result<StructuredOutput> {
|
||||||
let targets = self.build_args.into_targets().await?;
|
let targets = self.build_args.into_targets().await?;
|
||||||
|
|
||||||
|
let patch_server = self.patch_server.unwrap_or(false);
|
||||||
|
|
||||||
|
let build_id = if patch_server {
|
||||||
|
BuildId::SECONDARY
|
||||||
|
} else {
|
||||||
|
BuildId::PRIMARY
|
||||||
|
};
|
||||||
|
|
||||||
// Select which target to patch
|
// Select which target to patch
|
||||||
let request = if self.patch_server.unwrap_or_default() {
|
let request = if patch_server {
|
||||||
targets.server.as_ref().context("No server to patch!")?
|
targets.server.as_ref().context("No server to patch!")?
|
||||||
} else {
|
} else {
|
||||||
&targets.client
|
&targets.client
|
||||||
@@ -68,7 +76,9 @@ impl HotpatchTip {
|
|||||||
cache: cache.clone(),
|
cache: cache.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let artifacts = AppBuilder::started(request, mode)?.finish_build().await?;
|
let artifacts = AppBuilder::started(request, mode, build_id)?
|
||||||
|
.finish_build()
|
||||||
|
.await?;
|
||||||
let patch_exe = request.patch_exe(artifacts.time_start);
|
let patch_exe = request.patch_exe(artifacts.time_start);
|
||||||
|
|
||||||
Ok(StructuredOutput::Hotpatch {
|
Ok(StructuredOutput::Hotpatch {
|
||||||
|
|||||||
@@ -109,12 +109,12 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &TraceController) -> Resu
|
|||||||
pid,
|
pid,
|
||||||
} => {
|
} => {
|
||||||
devserver
|
devserver
|
||||||
.send_hotreload(builder.applied_hot_reload_changes(BuildId::CLIENT))
|
.send_hotreload(builder.applied_hot_reload_changes(BuildId::PRIMARY))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if builder.server.is_some() {
|
if builder.server.is_some() {
|
||||||
devserver
|
devserver
|
||||||
.send_hotreload(builder.applied_hot_reload_changes(BuildId::SERVER))
|
.send_hotreload(builder.applied_hot_reload_changes(BuildId::SECONDARY))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -268,12 +268,12 @@ impl Output {
|
|||||||
}
|
}
|
||||||
KeyCode::Char('D') => {
|
KeyCode::Char('D') => {
|
||||||
return Ok(Some(ServeUpdate::OpenDebugger {
|
return Ok(Some(ServeUpdate::OpenDebugger {
|
||||||
id: BuildId::SERVER,
|
id: BuildId::SECONDARY,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
KeyCode::Char('d') => {
|
KeyCode::Char('d') => {
|
||||||
return Ok(Some(ServeUpdate::OpenDebugger {
|
return Ok(Some(ServeUpdate::OpenDebugger {
|
||||||
id: BuildId::CLIENT,
|
id: BuildId::PRIMARY,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
KeyCode::Char('c') => {
|
KeyCode::Char('c') => {
|
||||||
|
|||||||
@@ -234,9 +234,9 @@ impl AppServer {
|
|||||||
false => BuildMode::Base { run: true },
|
false => BuildMode::Base { run: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
self.client.start(build_mode.clone());
|
self.client.start(build_mode.clone(), BuildId::PRIMARY);
|
||||||
if let Some(server) = self.server.as_mut() {
|
if let Some(server) = self.server.as_mut() {
|
||||||
server.start(build_mode);
|
server.start(build_mode, BuildId::SECONDARY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,14 +273,14 @@ impl AppServer {
|
|||||||
// Wait for the client to finish
|
// Wait for the client to finish
|
||||||
client_update = client_wait => {
|
client_update = client_wait => {
|
||||||
ServeUpdate::BuilderUpdate {
|
ServeUpdate::BuilderUpdate {
|
||||||
id: BuildId::CLIENT,
|
id: BuildId::PRIMARY,
|
||||||
update: client_update,
|
update: client_update,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(server_update) = server_wait => {
|
Some(server_update) = server_wait => {
|
||||||
ServeUpdate::BuilderUpdate {
|
ServeUpdate::BuilderUpdate {
|
||||||
id: BuildId::SERVER,
|
id: BuildId::SECONDARY,
|
||||||
update: server_update,
|
update: server_update,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -491,17 +491,18 @@ impl AppServer {
|
|||||||
// A full rebuild is required when the user modifies static initializers which we haven't wired up yet.
|
// A full rebuild is required when the user modifies static initializers which we haven't wired up yet.
|
||||||
if needs_full_rebuild && self.automatic_rebuilds {
|
if needs_full_rebuild && self.automatic_rebuilds {
|
||||||
if self.use_hotpatch_engine {
|
if self.use_hotpatch_engine {
|
||||||
self.client.patch_rebuild(files.to_vec());
|
self.client.patch_rebuild(files.to_vec(), BuildId::PRIMARY);
|
||||||
if let Some(server) = self.server.as_mut() {
|
if let Some(server) = self.server.as_mut() {
|
||||||
server.patch_rebuild(files.to_vec());
|
server.patch_rebuild(files.to_vec(), BuildId::SECONDARY);
|
||||||
}
|
}
|
||||||
self.clear_hot_reload_changes();
|
self.clear_hot_reload_changes();
|
||||||
self.clear_cached_rsx();
|
self.clear_cached_rsx();
|
||||||
server.send_patch_start().await;
|
server.send_patch_start().await;
|
||||||
} else {
|
} else {
|
||||||
self.client.start_rebuild(BuildMode::Base { run: true });
|
self.client
|
||||||
|
.start_rebuild(BuildMode::Base { run: true }, BuildId::PRIMARY);
|
||||||
if let Some(server) = self.server.as_mut() {
|
if let Some(server) = self.server.as_mut() {
|
||||||
server.start_rebuild(BuildMode::Base { run: true });
|
server.start_rebuild(BuildMode::Base { run: true }, BuildId::SECONDARY);
|
||||||
}
|
}
|
||||||
self.clear_hot_reload_changes();
|
self.clear_hot_reload_changes();
|
||||||
self.clear_cached_rsx();
|
self.clear_cached_rsx();
|
||||||
@@ -560,13 +561,14 @@ impl AppServer {
|
|||||||
devserver: &mut WebServer,
|
devserver: &mut WebServer,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Make sure to save artifacts regardless of if we're opening the app or not
|
// Make sure to save artifacts regardless of if we're opening the app or not
|
||||||
match artifacts.bundle {
|
match artifacts.build_id {
|
||||||
BundleFormat::Server => {
|
BuildId::PRIMARY => self.client.artifacts = Some(artifacts.clone()),
|
||||||
|
BuildId::SECONDARY => {
|
||||||
if let Some(server) = self.server.as_mut() {
|
if let Some(server) = self.server.as_mut() {
|
||||||
server.artifacts = Some(artifacts.clone());
|
server.artifacts = Some(artifacts.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => self.client.artifacts = Some(artifacts.clone()),
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let should_open = self.client.stage == BuildStage::Success
|
let should_open = self.client.stage == BuildStage::Success
|
||||||
@@ -634,7 +636,7 @@ impl AppServer {
|
|||||||
fullstack_address,
|
fullstack_address,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
BuildId::SERVER,
|
BuildId::SECONDARY,
|
||||||
&self.server_args,
|
&self.server_args,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -649,7 +651,7 @@ impl AppServer {
|
|||||||
fullstack_address,
|
fullstack_address,
|
||||||
open_browser,
|
open_browser,
|
||||||
self.always_on_top,
|
self.always_on_top,
|
||||||
BuildId::CLIENT,
|
BuildId::PRIMARY,
|
||||||
&self.client_args,
|
&self.client_args,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -696,9 +698,10 @@ impl AppServer {
|
|||||||
false => BuildMode::Base { run: true },
|
false => BuildMode::Base { run: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
self.client.start_rebuild(build_mode.clone());
|
self.client
|
||||||
|
.start_rebuild(build_mode.clone(), BuildId::PRIMARY);
|
||||||
if let Some(s) = self.server.as_mut() {
|
if let Some(s) = self.server.as_mut() {
|
||||||
s.start_rebuild(build_mode)
|
s.start_rebuild(build_mode, BuildId::SECONDARY);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.clear_hot_reload_changes();
|
self.clear_hot_reload_changes();
|
||||||
@@ -719,8 +722,8 @@ impl AppServer {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let jump_table = match id {
|
let jump_table = match id {
|
||||||
BuildId::CLIENT => self.client.hotpatch(bundle, cache).await,
|
BuildId::PRIMARY => self.client.hotpatch(bundle, cache).await,
|
||||||
BuildId::SERVER => {
|
BuildId::SECONDARY => {
|
||||||
self.server
|
self.server
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.context("Server not found")?
|
.context("Server not found")?
|
||||||
@@ -730,7 +733,7 @@ impl AppServer {
|
|||||||
_ => bail!("Invalid build id"),
|
_ => bail!("Invalid build id"),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
if id == BuildId::CLIENT {
|
if id == BuildId::PRIMARY {
|
||||||
self.applied_client_hot_reload_message.jump_table = self.client.patches.last().cloned();
|
self.applied_client_hot_reload_message.jump_table = self.client.patches.last().cloned();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,11 +762,16 @@ impl AppServer {
|
|||||||
.context("Missing server jump table")?;
|
.context("Missing server jump table")?;
|
||||||
|
|
||||||
devserver
|
devserver
|
||||||
.send_patch(server_jump_table, elapsed, BuildId::SERVER, server.pid)
|
.send_patch(server_jump_table, elapsed, BuildId::SECONDARY, server.pid)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
devserver
|
devserver
|
||||||
.send_patch(client_jump_table, elapsed, BuildId::CLIENT, self.client.pid)
|
.send_patch(
|
||||||
|
client_jump_table,
|
||||||
|
elapsed,
|
||||||
|
BuildId::PRIMARY,
|
||||||
|
self.client.pid,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,8 +780,8 @@ impl AppServer {
|
|||||||
|
|
||||||
pub(crate) fn get_build(&self, id: BuildId) -> Option<&AppBuilder> {
|
pub(crate) fn get_build(&self, id: BuildId) -> Option<&AppBuilder> {
|
||||||
match id {
|
match id {
|
||||||
BuildId::CLIENT => Some(&self.client),
|
BuildId::PRIMARY => Some(&self.client),
|
||||||
BuildId::SERVER => self.server.as_ref(),
|
BuildId::SECONDARY => self.server.as_ref(),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -791,18 +799,18 @@ impl AppServer {
|
|||||||
pub(crate) fn applied_hot_reload_changes(&mut self, build: BuildId) -> HotReloadMsg {
|
pub(crate) fn applied_hot_reload_changes(&mut self, build: BuildId) -> HotReloadMsg {
|
||||||
let mut msg = self.applied_client_hot_reload_message.clone();
|
let mut msg = self.applied_client_hot_reload_message.clone();
|
||||||
|
|
||||||
if build == BuildId::CLIENT {
|
if build == BuildId::PRIMARY {
|
||||||
msg.jump_table = self.client.patches.last().cloned();
|
msg.jump_table = self.client.patches.last().cloned();
|
||||||
msg.for_build_id = Some(BuildId::CLIENT.0 as _);
|
msg.for_build_id = Some(BuildId::PRIMARY.0 as _);
|
||||||
if let Some(lib) = msg.jump_table.as_mut() {
|
if let Some(lib) = msg.jump_table.as_mut() {
|
||||||
lib.lib = PathBuf::from("/").join(lib.lib.clone());
|
lib.lib = PathBuf::from("/").join(lib.lib.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if build == BuildId::SERVER {
|
if build == BuildId::SECONDARY {
|
||||||
if let Some(server) = self.server.as_mut() {
|
if let Some(server) = self.server.as_mut() {
|
||||||
msg.jump_table = server.patches.last().cloned();
|
msg.jump_table = server.patches.last().cloned();
|
||||||
msg.for_build_id = Some(BuildId::SERVER.0 as _);
|
msg.for_build_id = Some(BuildId::SECONDARY.0 as _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,7 +836,7 @@ impl AppServer {
|
|||||||
pid: Option<u32>,
|
pid: Option<u32>,
|
||||||
) {
|
) {
|
||||||
match build_id {
|
match build_id {
|
||||||
BuildId::CLIENT => {
|
BuildId::PRIMARY => {
|
||||||
// multiple tabs on web can cause this to be called incorrectly, and it doesn't
|
// multiple tabs on web can cause this to be called incorrectly, and it doesn't
|
||||||
// make any sense anyways
|
// make any sense anyways
|
||||||
if self.client.build.bundle != BundleFormat::Web {
|
if self.client.build.bundle != BundleFormat::Web {
|
||||||
@@ -840,7 +848,7 @@ impl AppServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BuildId::SERVER => {
|
BuildId::SECONDARY => {
|
||||||
if let Some(server) = self.server.as_mut() {
|
if let Some(server) = self.server.as_mut() {
|
||||||
server.aslr_reference = aslr_reference;
|
server.aslr_reference = aslr_reference;
|
||||||
}
|
}
|
||||||
@@ -1146,10 +1154,10 @@ impl AppServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match build {
|
match build {
|
||||||
BuildId::CLIENT => {
|
BuildId::PRIMARY => {
|
||||||
_ = self.client.open_debugger(dev).await;
|
_ = self.client.open_debugger(dev).await;
|
||||||
}
|
}
|
||||||
BuildId::SERVER => {
|
BuildId::SECONDARY => {
|
||||||
if let Some(server) = self.server.as_mut() {
|
if let Some(server) = self.server.as_mut() {
|
||||||
_ = server.open_debugger(dev).await;
|
_ = server.open_debugger(dev).await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ impl WebServer {
|
|||||||
if let Some(new_socket) = new_hot_reload_socket {
|
if let Some(new_socket) = new_hot_reload_socket {
|
||||||
let aslr_reference = new_socket.aslr_reference;
|
let aslr_reference = new_socket.aslr_reference;
|
||||||
let pid = new_socket.pid;
|
let pid = new_socket.pid;
|
||||||
let id = new_socket.build_id.unwrap_or(BuildId::CLIENT);
|
let id = new_socket.build_id.unwrap_or(BuildId::PRIMARY);
|
||||||
|
|
||||||
drop(new_message);
|
drop(new_message);
|
||||||
self.hot_reload_sockets.push(new_socket);
|
self.hot_reload_sockets.push(new_socket);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use dioxus::html::geometry::euclid::Vector3D;
|
use dioxus::html::geometry::euclid::Vector3D;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_desktop::DesktopContext;
|
use dioxus_desktop::DesktopContext;
|
||||||
@@ -448,7 +446,7 @@ fn test_focus_out_div() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn test_form_input() -> Element {
|
fn test_form_input() -> Element {
|
||||||
let mut values = use_signal(HashMap::new);
|
let mut values = use_signal(Vec::new);
|
||||||
|
|
||||||
utils::mock_event_with_extra(
|
utils::mock_event_with_extra(
|
||||||
"form-username",
|
"form-username",
|
||||||
@@ -464,10 +462,10 @@ fn test_form_input() -> Element {
|
|||||||
|
|
||||||
// And then the value the form gives us should also match
|
// And then the value the form gives us should also match
|
||||||
values.with_mut(|x| {
|
values.with_mut(|x| {
|
||||||
assert_eq!(x.get("username").unwrap(), "hello");
|
assert_eq!(x.iter().find(|f| f.0 == "username").unwrap().1, "hello");
|
||||||
assert_eq!(x.get("full-name").unwrap(), "lorem");
|
assert_eq!(x.iter().find(|f| f.0 == "full-name").unwrap().1, "lorem");
|
||||||
assert_eq!(x.get("password").unwrap(), "ipsum");
|
assert_eq!(x.iter().find(|f| f.0 == "password").unwrap().1, "ipsum");
|
||||||
assert_eq!(x.get("color").unwrap(), "red");
|
assert_eq!(x.iter().find(|f| f.0 == "color").unwrap().1, "red");
|
||||||
});
|
});
|
||||||
RECEIVED_EVENTS.with_mut(|x| *x += 1);
|
RECEIVED_EVENTS.with_mut(|x| *x += 1);
|
||||||
};
|
};
|
||||||
@@ -505,7 +503,7 @@ fn test_form_input() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn test_form_submit() -> Element {
|
fn test_form_submit() -> Element {
|
||||||
let mut values = use_signal(HashMap::new);
|
let mut values = use_signal(Vec::new);
|
||||||
|
|
||||||
utils::mock_event_with_extra(
|
utils::mock_event_with_extra(
|
||||||
"form-submitter",
|
"form-submitter",
|
||||||
@@ -516,10 +514,10 @@ fn test_form_submit() -> Element {
|
|||||||
let set_values = move |ev: FormEvent| {
|
let set_values = move |ev: FormEvent| {
|
||||||
values.set(ev.values());
|
values.set(ev.values());
|
||||||
values.with_mut(|x| {
|
values.with_mut(|x| {
|
||||||
assert_eq!(x.get("username").unwrap(), "goodbye");
|
assert_eq!(x.iter().find(|f| f.0 == "username").unwrap().1, "goodbye");
|
||||||
assert_eq!(x.get("full-name").unwrap(), "lorem");
|
assert_eq!(x.iter().find(|f| f.0 == "full-name").unwrap().1, "lorem");
|
||||||
assert_eq!(x.get("password").unwrap(), "ipsum");
|
assert_eq!(x.iter().find(|f| f.0 == "password").unwrap().1, "ipsum");
|
||||||
assert_eq!(x.get("color").unwrap(), "red");
|
assert_eq!(x.iter().find(|f| f.0 == "color").unwrap().1, "red");
|
||||||
});
|
});
|
||||||
RECEIVED_EVENTS.with_mut(|x| *x += 1);
|
RECEIVED_EVENTS.with_mut(|x| *x += 1);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ use crate::{
|
|||||||
config::{Config, WindowCloseBehaviour},
|
config::{Config, WindowCloseBehaviour},
|
||||||
edits::EditWebsocket,
|
edits::EditWebsocket,
|
||||||
event_handlers::WindowEventHandlers,
|
event_handlers::WindowEventHandlers,
|
||||||
file_upload::{DesktopFileUploadForm, FileDialogRequest},
|
|
||||||
ipc::{IpcMessage, UserWindowEvent},
|
ipc::{IpcMessage, UserWindowEvent},
|
||||||
query::QueryResult,
|
query::QueryResult,
|
||||||
shortcut::ShortcutRegistry,
|
shortcut::ShortcutRegistry,
|
||||||
webview::{PendingWebview, WebviewInstance},
|
webview::{PendingWebview, WebviewInstance},
|
||||||
};
|
};
|
||||||
use dioxus_core::{consume_context, ElementId, ScopeId, VirtualDom};
|
use dioxus_core::{consume_context, ScopeId, VirtualDom};
|
||||||
use dioxus_history::History;
|
use dioxus_history::History;
|
||||||
use dioxus_html::PlatformEventData;
|
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
@@ -409,35 +406,6 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) {
|
|
||||||
let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let id = ElementId(file_dialog.target);
|
|
||||||
let event_name = &file_dialog.event;
|
|
||||||
let event_bubbles = file_dialog.bubbles;
|
|
||||||
let files = file_dialog.get_file_event();
|
|
||||||
|
|
||||||
let as_any = Box::new(DesktopFileUploadForm { files });
|
|
||||||
|
|
||||||
let data = Rc::new(PlatformEventData::new(as_any));
|
|
||||||
|
|
||||||
let Some(view) = self.webviews.get_mut(&window) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let event = dioxus_core::Event::new(data as Rc<dyn Any>, event_bubbles);
|
|
||||||
|
|
||||||
let runtime = view.dom.runtime();
|
|
||||||
if event_name == "change&input" {
|
|
||||||
runtime.handle_event("input", event.clone(), id);
|
|
||||||
runtime.handle_event("change", event, id);
|
|
||||||
} else {
|
|
||||||
runtime.handle_event(event_name, event, id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Poll the virtualdom until it's pending
|
/// Poll the virtualdom until it's pending
|
||||||
///
|
///
|
||||||
/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
|
/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
|
||||||
|
|||||||
@@ -2,13 +2,34 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
element::DesktopElement,
|
element::DesktopElement,
|
||||||
file_upload::{DesktopFileDragEvent, DesktopFileUploadForm},
|
file_upload::{DesktopFileDragEvent, DesktopFormData},
|
||||||
};
|
};
|
||||||
use dioxus_html::*;
|
use dioxus_html::*;
|
||||||
|
|
||||||
pub(crate) struct SerializedHtmlEventConverter;
|
pub(crate) struct SerializedHtmlEventConverter;
|
||||||
|
|
||||||
impl HtmlEventConverter for SerializedHtmlEventConverter {
|
impl HtmlEventConverter for SerializedHtmlEventConverter {
|
||||||
|
/// This impl is special because it relies on the renderer to convert from serialized data to native data
|
||||||
|
/// as an intermediate step.
|
||||||
|
fn convert_drag_data(&self, event: &PlatformEventData) -> DragData {
|
||||||
|
event
|
||||||
|
.downcast::<DesktopFileDragEvent>()
|
||||||
|
.cloned()
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This impl is special because it relies on the renderer to convert from serialized data to native data
|
||||||
|
/// as an intermediate step.
|
||||||
|
fn convert_form_data(&self, event: &PlatformEventData) -> FormData {
|
||||||
|
event.downcast::<DesktopFormData>().cloned().unwrap().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This impl is special because it leverages the `DesktopElement` ref between the virtualdom and webview
|
||||||
|
fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData {
|
||||||
|
event.downcast::<DesktopElement>().cloned().unwrap().into()
|
||||||
|
}
|
||||||
|
|
||||||
fn convert_animation_data(&self, event: &PlatformEventData) -> AnimationData {
|
fn convert_animation_data(&self, event: &PlatformEventData) -> AnimationData {
|
||||||
event
|
event
|
||||||
.downcast::<SerializedAnimationData>()
|
.downcast::<SerializedAnimationData>()
|
||||||
@@ -41,20 +62,6 @@ impl HtmlEventConverter for SerializedHtmlEventConverter {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_drag_data(&self, event: &PlatformEventData) -> DragData {
|
|
||||||
// Attempt a simple serialized data conversion
|
|
||||||
if let Some(_data) = event.downcast::<SerializedDragData>() {
|
|
||||||
return _data.clone().into();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If that failed then it's a file drag form
|
|
||||||
event
|
|
||||||
.downcast::<DesktopFileDragEvent>()
|
|
||||||
.cloned()
|
|
||||||
.unwrap()
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData {
|
fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData {
|
||||||
event
|
event
|
||||||
.downcast::<SerializedFocusData>()
|
.downcast::<SerializedFocusData>()
|
||||||
@@ -63,20 +70,6 @@ impl HtmlEventConverter for SerializedHtmlEventConverter {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_form_data(&self, event: &PlatformEventData) -> FormData {
|
|
||||||
// Attempt a simple serialized form data conversion
|
|
||||||
if let Some(_data) = event.downcast::<SerializedFormData>() {
|
|
||||||
return _data.clone().into();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If that failed then it's a file upload form
|
|
||||||
event
|
|
||||||
.downcast::<DesktopFileUploadForm>()
|
|
||||||
.cloned()
|
|
||||||
.unwrap()
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn convert_image_data(&self, event: &PlatformEventData) -> ImageData {
|
fn convert_image_data(&self, event: &PlatformEventData) -> ImageData {
|
||||||
event
|
event
|
||||||
.downcast::<SerializedImageData>()
|
.downcast::<SerializedImageData>()
|
||||||
@@ -101,10 +94,6 @@ impl HtmlEventConverter for SerializedHtmlEventConverter {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData {
|
|
||||||
event.downcast::<DesktopElement>().cloned().unwrap().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData {
|
fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData {
|
||||||
event
|
event
|
||||||
.downcast::<SerializedMouseData>()
|
.downcast::<SerializedMouseData>()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
|
|
||||||
use std::any::Any;
|
use std::{any::Any, collections::HashMap};
|
||||||
|
|
||||||
#[cfg(feature = "tokio_runtime")]
|
#[cfg(feature = "tokio_runtime")]
|
||||||
use tokio::{fs::File, io::AsyncReadExt};
|
use tokio::{fs::File, io::AsyncReadExt};
|
||||||
@@ -11,11 +11,11 @@ use dioxus_html::{
|
|||||||
point_interaction::{
|
point_interaction::{
|
||||||
InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
|
InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
|
||||||
},
|
},
|
||||||
FileData, HasDragData, HasFileData, HasFormData, HasMouseData, NativeFileData,
|
FileData, FormValue, HasDragData, HasFileData, HasFormData, HasMouseData, NativeFileData,
|
||||||
SerializedMouseData, SerializedPointInteraction,
|
SerializedFormData, SerializedFormObject, SerializedMouseData, SerializedPointInteraction,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
@@ -25,7 +25,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
use wry::DragDropEvent;
|
use wry::DragDropEvent;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub(crate) struct FileDialogRequest {
|
pub(crate) struct FileDialogRequest {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
accept: Option<String>,
|
accept: Option<String>,
|
||||||
@@ -34,6 +34,8 @@ pub(crate) struct FileDialogRequest {
|
|||||||
pub event: String,
|
pub event: String,
|
||||||
pub target: usize,
|
pub target: usize,
|
||||||
pub bubbles: bool,
|
pub bubbles: bool,
|
||||||
|
pub target_name: String,
|
||||||
|
pub values: Vec<SerializedFormObject>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
@@ -61,57 +63,69 @@ impl FileDialogRequest {
|
|||||||
target_os = "openbsd"
|
target_os = "openbsd"
|
||||||
))]
|
))]
|
||||||
pub(crate) fn get_file_event(&self) -> Vec<PathBuf> {
|
pub(crate) fn get_file_event(&self) -> Vec<PathBuf> {
|
||||||
fn get_file_event_for_folder(
|
|
||||||
request: &FileDialogRequest,
|
|
||||||
dialog: rfd::FileDialog,
|
|
||||||
) -> Vec<PathBuf> {
|
|
||||||
if request.multiple {
|
|
||||||
dialog.pick_folders().into_iter().flatten().collect()
|
|
||||||
} else {
|
|
||||||
dialog.pick_folder().into_iter().collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_file_event_for_file(
|
|
||||||
request: &FileDialogRequest,
|
|
||||||
mut dialog: rfd::FileDialog,
|
|
||||||
) -> Vec<PathBuf> {
|
|
||||||
let filters: Vec<_> = request
|
|
||||||
.accept
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or(".*")
|
|
||||||
.split(',')
|
|
||||||
.filter_map(|s| Filters::from_str(s.trim()).ok())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let file_extensions: Vec<_> = filters
|
|
||||||
.iter()
|
|
||||||
.flat_map(|f| f.as_extensions().into_iter())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let filter_name = file_extensions
|
|
||||||
.iter()
|
|
||||||
.map(|extension| format!("*.{extension}"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
dialog = dialog.add_filter(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
|
|
||||||
}
|
|
||||||
|
|
||||||
let dialog = rfd::FileDialog::new();
|
let dialog = rfd::FileDialog::new();
|
||||||
|
|
||||||
if self.directory {
|
if self.directory {
|
||||||
get_file_event_for_folder(self, dialog)
|
self.get_file_event_for_folder(dialog)
|
||||||
} else {
|
} else {
|
||||||
get_file_event_for_file(self, dialog)
|
self.get_file_event_for_file(dialog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
target_os = "windows",
|
||||||
|
target_os = "macos",
|
||||||
|
target_os = "linux",
|
||||||
|
target_os = "dragonfly",
|
||||||
|
target_os = "freebsd",
|
||||||
|
target_os = "netbsd",
|
||||||
|
target_os = "openbsd"
|
||||||
|
))]
|
||||||
|
fn get_file_event_for_file(&self, mut dialog: rfd::FileDialog) -> Vec<PathBuf> {
|
||||||
|
let filters: Vec<_> = self
|
||||||
|
.accept
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(".*")
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|s| Filters::from_str(s.trim()).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let file_extensions: Vec<_> = filters
|
||||||
|
.iter()
|
||||||
|
.flat_map(|f| f.as_extensions().into_iter())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let filter_name = file_extensions
|
||||||
|
.iter()
|
||||||
|
.map(|extension| format!("*.{extension}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
dialog = dialog.add_filter(filter_name, file_extensions.as_slice());
|
||||||
|
|
||||||
|
let files: Vec<_> = if self.multiple {
|
||||||
|
dialog.pick_files().into_iter().flatten().collect()
|
||||||
|
} else {
|
||||||
|
dialog.pick_file().into_iter().collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
files
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
target_os = "windows",
|
||||||
|
target_os = "macos",
|
||||||
|
target_os = "linux",
|
||||||
|
target_os = "dragonfly",
|
||||||
|
target_os = "freebsd",
|
||||||
|
target_os = "netbsd",
|
||||||
|
target_os = "openbsd"
|
||||||
|
))]
|
||||||
|
fn get_file_event_for_folder(&self, dialog: rfd::FileDialog) -> Vec<PathBuf> {
|
||||||
|
if self.multiple {
|
||||||
|
dialog.pick_folders().into_iter().flatten().collect()
|
||||||
|
} else {
|
||||||
|
dialog.pick_folder().into_iter().collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,24 +168,53 @@ impl FromStr for Filters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct DesktopFileUploadForm {
|
pub(crate) struct DesktopFormData {
|
||||||
pub files: Vec<PathBuf>,
|
pub value: String,
|
||||||
|
pub valid: bool,
|
||||||
|
pub values: Vec<(String, FormValue)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasFileData for DesktopFileUploadForm {
|
impl DesktopFormData {
|
||||||
|
pub fn new(values: Vec<(String, FormValue)>) -> Self {
|
||||||
|
Self {
|
||||||
|
value: String::new(),
|
||||||
|
valid: true,
|
||||||
|
values,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasFileData for DesktopFormData {
|
||||||
fn files(&self) -> Vec<FileData> {
|
fn files(&self) -> Vec<FileData> {
|
||||||
self.files
|
self.values
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.filter_map(|(_, v)| {
|
||||||
.map(|f| FileData::new(DesktopFileData(f)))
|
if let FormValue::File(Some(f)) = v {
|
||||||
|
Some(f.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasFormData for DesktopFileUploadForm {
|
impl HasFormData for DesktopFormData {
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn value(&self) -> String {
|
||||||
|
self.value.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid(&self) -> bool {
|
||||||
|
self.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn values(&self) -> Vec<(String, FormValue)> {
|
||||||
|
self.values.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ pub struct IpcMessage {
|
|||||||
/// A set of known messages that we need to respond to
|
/// A set of known messages that we need to respond to
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
pub enum IpcMethod<'a> {
|
pub enum IpcMethod<'a> {
|
||||||
FileDialog,
|
|
||||||
UserEvent,
|
UserEvent,
|
||||||
Query,
|
Query,
|
||||||
BrowserOpen,
|
BrowserOpen,
|
||||||
@@ -68,7 +67,6 @@ pub enum IpcMethod<'a> {
|
|||||||
impl IpcMessage {
|
impl IpcMessage {
|
||||||
pub(crate) fn method(&self) -> IpcMethod<'_> {
|
pub(crate) fn method(&self) -> IpcMethod<'_> {
|
||||||
match self.method.as_str() {
|
match self.method.as_str() {
|
||||||
"file_dialog" => IpcMethod::FileDialog,
|
|
||||||
"user_event" => IpcMethod::UserEvent,
|
"user_event" => IpcMethod::UserEvent,
|
||||||
"query" => IpcMethod::Query,
|
"query" => IpcMethod::Query,
|
||||||
"browser_open" => IpcMethod::BrowserOpen,
|
"browser_open" => IpcMethod::BrowserOpen,
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, mut desktop_config:
|
|||||||
|
|
||||||
UserWindowEvent::Ipc { id, msg } => match msg.method() {
|
UserWindowEvent::Ipc { id, msg } => match msg.method() {
|
||||||
IpcMethod::Initialize => app.handle_initialize_msg(id),
|
IpcMethod::Initialize => app.handle_initialize_msg(id),
|
||||||
IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id),
|
|
||||||
IpcMethod::UserEvent => {}
|
IpcMethod::UserEvent => {}
|
||||||
IpcMethod::Query => app.handle_query_msg(msg, id),
|
IpcMethod::Query => app.handle_query_msg(msg, id),
|
||||||
IpcMethod::BrowserOpen => app.handle_browser_open(msg),
|
IpcMethod::BrowserOpen => app.handle_browser_open(msg),
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use crate::document::NATIVE_EVAL_JS;
|
|
||||||
use crate::{assets::*, webview::WebviewEdits};
|
use crate::{assets::*, webview::WebviewEdits};
|
||||||
|
use crate::{document::NATIVE_EVAL_JS, file_upload::FileDialogRequest};
|
||||||
|
use base64::prelude::BASE64_STANDARD;
|
||||||
|
use dioxus_core::AnyhowContext;
|
||||||
|
use dioxus_html::{SerializedFileData, SerializedFormObject};
|
||||||
use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
|
use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
|
||||||
use dioxus_interpreter_js::NATIVE_JS;
|
use dioxus_interpreter_js::NATIVE_JS;
|
||||||
use wry::{
|
use wry::{
|
||||||
@@ -8,13 +11,13 @@ use wry::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
const EVENTS_PATH: &str = "https://dioxus.index.html/__events";
|
const BASE_URI: &str = "https://dioxus.index.html/";
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
const EVENTS_PATH: &str = "http://dioxus.index.html/__events";
|
const BASE_URI: &str = "http://dioxus.index.html/";
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "windows")))]
|
#[cfg(not(any(target_os = "android", target_os = "windows")))]
|
||||||
const EVENTS_PATH: &str = "dioxus://index.html/__events";
|
const BASE_URI: &str = "dioxus://index.html/";
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
static DEFAULT_INDEX: &str = include_str!("./assets/dev.index.html");
|
static DEFAULT_INDEX: &str = include_str!("./assets/dev.index.html");
|
||||||
@@ -58,6 +61,14 @@ pub(super) fn desktop_handler(
|
|||||||
return edit_state.handle_event(request, responder);
|
return edit_state.handle_event(request, responder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the request is asking for a file dialog, handle that, returning the list of files selected
|
||||||
|
if trimmed_uri == "__file_dialog" {
|
||||||
|
if let Err(err) = file_dialog_responder(request, responder) {
|
||||||
|
tracing::error!("Failed to handle file dialog request: {err:?}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// todo: we want to move the custom assets onto a different protocol or something
|
// todo: we want to move the custom assets onto a different protocol or something
|
||||||
if let Some(name) = request.uri().path().split('/').nth(1) {
|
if let Some(name) = request.uri().path().split('/').nth(1) {
|
||||||
if asset_handlers.has_handler(name) {
|
if asset_handlers.has_handler(name) {
|
||||||
@@ -104,7 +115,6 @@ fn index_request(
|
|||||||
|
|
||||||
// Insert a custom head if provided
|
// Insert a custom head if provided
|
||||||
// We look just for the closing head tag. If a user provided a custom index with weird syntax, this might fail
|
// We look just for the closing head tag. If a user provided a custom index with weird syntax, this might fail
|
||||||
|
|
||||||
if let Some(head) = custom_head {
|
if let Some(head) = custom_head {
|
||||||
index.insert_str(index.find("</head>").expect("Head element to exist"), &head);
|
index.insert_str(index.find("</head>").expect("Head element to exist"), &head);
|
||||||
}
|
}
|
||||||
@@ -147,14 +157,14 @@ fn module_loader(root_id: &str, headless: bool, edit_state: &WebviewEdits) -> St
|
|||||||
{NATIVE_JS}
|
{NATIVE_JS}
|
||||||
|
|
||||||
// The native interpreter extends the sledgehammer interpreter with a few extra methods that we use for IPC
|
// The native interpreter extends the sledgehammer interpreter with a few extra methods that we use for IPC
|
||||||
window.interpreter = new NativeInterpreter("{EVENTS_PATH}", {headless});
|
window.interpreter = new NativeInterpreter("{BASE_URI}", {headless});
|
||||||
|
|
||||||
// Wait for the page to load before sending the initialize message
|
// Wait for the page to load before sending the initialize message
|
||||||
window.onload = function() {{
|
window.onload = function() {{
|
||||||
let root_element = window.document.getElementById("{root_id}");
|
let root_element = window.document.getElementById("{root_id}");
|
||||||
if (root_element != null) {{
|
if (root_element != null) {{
|
||||||
window.interpreter.initialize(root_element);
|
window.interpreter.initialize(root_element);
|
||||||
window.ipc.postMessage(window.interpreter.serializeIpcMessage("initialize"));
|
window.interpreter.sendIpcMessage("initialize");
|
||||||
}}
|
}}
|
||||||
window.interpreter.waitForRequest("{edits_path}", "{expected_key}");
|
window.interpreter.waitForRequest("{edits_path}", "{expected_key}");
|
||||||
}}
|
}}
|
||||||
@@ -166,3 +176,77 @@ fn module_loader(root_id: &str, headless: bool, edit_state: &WebviewEdits) -> St
|
|||||||
"#
|
"#
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn file_dialog_responder(
|
||||||
|
request: wry::http::Request<Vec<u8>>,
|
||||||
|
responder: wry::RequestAsyncResponder,
|
||||||
|
) -> dioxus_core::Result<()> {
|
||||||
|
// Handle the file dialog request
|
||||||
|
// We can't use the body, just the headers
|
||||||
|
let header = request
|
||||||
|
.headers()
|
||||||
|
.get("x-dioxus-data")
|
||||||
|
.context("Failed to get x-dioxus-data header")?;
|
||||||
|
|
||||||
|
let data_from_header = base64::Engine::decode(&BASE64_STANDARD, header.as_bytes())
|
||||||
|
.context("Failed to decode x-dioxus-data header from base64")?;
|
||||||
|
|
||||||
|
let mut file_dialog: FileDialogRequest = serde_json::from_slice(&data_from_header)
|
||||||
|
.context("Failed to parse x-dioxus-data header as JSON")?;
|
||||||
|
|
||||||
|
let file_list = file_dialog.get_file_event();
|
||||||
|
|
||||||
|
let position_of_entry = file_dialog
|
||||||
|
.values
|
||||||
|
.iter()
|
||||||
|
.position(|x| x.key == file_dialog.target_name)
|
||||||
|
.context("Failed to find target_name in values")?;
|
||||||
|
|
||||||
|
// Remove any existing entries
|
||||||
|
file_dialog
|
||||||
|
.values
|
||||||
|
.retain(|x| x.key != file_dialog.target_name);
|
||||||
|
|
||||||
|
// And then insert the new ones
|
||||||
|
for path in file_list {
|
||||||
|
let file = std::fs::metadata(&path).context("Failed to get file metadata")?;
|
||||||
|
file_dialog.values.insert(
|
||||||
|
position_of_entry,
|
||||||
|
SerializedFormObject {
|
||||||
|
key: file_dialog.target_name.clone(),
|
||||||
|
text: None,
|
||||||
|
file: Some(SerializedFileData {
|
||||||
|
size: file.len(),
|
||||||
|
last_modified: file
|
||||||
|
.modified()
|
||||||
|
.context("Failed to get file modified time")?
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_millis())
|
||||||
|
.unwrap_or_default() as _,
|
||||||
|
content_type: Some(
|
||||||
|
dioxus_asset_resolver::native::get_mime_from_ext(
|
||||||
|
path.extension().and_then(|s| s.to_str()),
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
contents: Default::default(),
|
||||||
|
path,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// And then respond with the updated file dialog
|
||||||
|
let response_data = serde_json::to_vec(&file_dialog)
|
||||||
|
.context("Failed to serialize FileDialogRequest to JSON")?;
|
||||||
|
|
||||||
|
responder.respond(
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(response_data)
|
||||||
|
.context("Failed to build response")?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::element::DesktopElement;
|
use crate::file_upload::{DesktopFileData, DesktopFileDragEvent};
|
||||||
use crate::file_upload::DesktopFileDragEvent;
|
|
||||||
use crate::menubar::DioxusMenu;
|
use crate::menubar::DioxusMenu;
|
||||||
use crate::PendingDesktopContext;
|
use crate::PendingDesktopContext;
|
||||||
use crate::WindowCloseBehaviour;
|
use crate::WindowCloseBehaviour;
|
||||||
@@ -9,12 +8,13 @@ use crate::{
|
|||||||
DesktopContext, DesktopService,
|
DesktopContext, DesktopService,
|
||||||
};
|
};
|
||||||
use crate::{document::DesktopDocument, WeakDesktopContext};
|
use crate::{document::DesktopDocument, WeakDesktopContext};
|
||||||
|
use crate::{element::DesktopElement, file_upload::DesktopFormData};
|
||||||
use base64::prelude::BASE64_STANDARD;
|
use base64::prelude::BASE64_STANDARD;
|
||||||
use dioxus_core::{consume_context, provide_context, Runtime, ScopeId, VirtualDom};
|
use dioxus_core::{consume_context, provide_context, Runtime, ScopeId, VirtualDom};
|
||||||
use dioxus_document::Document;
|
use dioxus_document::Document;
|
||||||
use dioxus_history::{History, MemoryHistory};
|
use dioxus_history::{History, MemoryHistory};
|
||||||
use dioxus_hooks::to_owned;
|
use dioxus_hooks::to_owned;
|
||||||
use dioxus_html::{HtmlEvent, PlatformEventData};
|
use dioxus_html::{FileData, FormValue, HtmlEvent, PlatformEventData};
|
||||||
use futures_util::{pin_mut, FutureExt};
|
use futures_util::{pin_mut, FutureExt};
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::{cell::OnceCell, time::Duration};
|
use std::{cell::OnceCell, time::Duration};
|
||||||
@@ -115,7 +115,7 @@ impl WebviewEdits {
|
|||||||
let desktop_context = desktop_context.upgrade().unwrap();
|
let desktop_context = desktop_context.upgrade().unwrap();
|
||||||
|
|
||||||
let query = desktop_context.query.clone();
|
let query = desktop_context.query.clone();
|
||||||
let recent_file = desktop_context.file_hover.clone();
|
let hovered_file = desktop_context.file_hover.clone();
|
||||||
|
|
||||||
// check for a mounted event placeholder and replace it with a desktop specific element
|
// check for a mounted event placeholder and replace it with a desktop specific element
|
||||||
let as_any = match data {
|
let as_any = match data {
|
||||||
@@ -123,9 +123,39 @@ impl WebviewEdits {
|
|||||||
let element = DesktopElement::new(element, desktop_context.clone(), query.clone());
|
let element = DesktopElement::new(element, desktop_context.clone(), query.clone());
|
||||||
Rc::new(PlatformEventData::new(Box::new(element)))
|
Rc::new(PlatformEventData::new(Box::new(element)))
|
||||||
}
|
}
|
||||||
|
dioxus_html::EventData::Form(form) => {
|
||||||
|
Rc::new(PlatformEventData::new(Box::new(DesktopFormData {
|
||||||
|
value: form.value,
|
||||||
|
valid: form.valid,
|
||||||
|
values: form
|
||||||
|
.values
|
||||||
|
.into_iter()
|
||||||
|
.map(|obj| {
|
||||||
|
if let Some(text) = obj.text {
|
||||||
|
return (obj.key, FormValue::Text(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(file_data) = obj.file {
|
||||||
|
if file_data.path.capacity() == 0 {
|
||||||
|
return (obj.key, FormValue::File(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
obj.key,
|
||||||
|
FormValue::File(Some(FileData::new(DesktopFileData(
|
||||||
|
file_data.path,
|
||||||
|
)))),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
(obj.key, FormValue::Text(String::new()))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})))
|
||||||
|
}
|
||||||
dioxus_html::EventData::Drag(ref drag) => {
|
dioxus_html::EventData::Drag(ref drag) => {
|
||||||
// we want to override this with a native file engine, provided by the most recent drag event
|
// we want to override this with a native file engine, provided by the most recent drag event
|
||||||
let file_event = recent_file.current().unwrap();
|
let file_event = hovered_file.current().unwrap();
|
||||||
let paths = match file_event {
|
let paths = match file_event {
|
||||||
wry::DragDropEvent::Enter { paths, .. } => paths,
|
wry::DragDropEvent::Enter { paths, .. } => paths,
|
||||||
wry::DragDropEvent::Drop { paths, .. } => paths,
|
wry::DragDropEvent::Drop { paths, .. } => paths,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use axum_core::response::IntoResponse;
|
use axum_core::response::IntoResponse;
|
||||||
|
use futures_util::TryStreamExt;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
@@ -91,6 +92,28 @@ impl ServerFnError {
|
|||||||
code: None,
|
code: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new server error (status code 500) with a message and details.
|
||||||
|
pub async fn from_axum_response(resp: axum_core::response::Response) -> Self {
|
||||||
|
let status = resp.status();
|
||||||
|
let message = resp
|
||||||
|
.into_body()
|
||||||
|
.into_data_stream()
|
||||||
|
.try_fold(Vec::new(), |mut acc, chunk| async move {
|
||||||
|
acc.extend_from_slice(&chunk);
|
||||||
|
Ok(acc)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||||
|
.unwrap_or_else(|| status.canonical_reason().unwrap_or("").to_string());
|
||||||
|
|
||||||
|
ServerFnError::ServerError {
|
||||||
|
message,
|
||||||
|
code: Some(status.as_u16()),
|
||||||
|
details: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<anyhow::Error> for ServerFnError {
|
impl From<anyhow::Error> for ServerFnError {
|
||||||
@@ -159,7 +182,64 @@ impl From<HttpError> for ServerFnError {
|
|||||||
|
|
||||||
impl IntoResponse for ServerFnError {
|
impl IntoResponse for ServerFnError {
|
||||||
fn into_response(self) -> axum_core::response::Response {
|
fn into_response(self) -> axum_core::response::Response {
|
||||||
todo!()
|
match self {
|
||||||
|
Self::ServerError {
|
||||||
|
message,
|
||||||
|
code,
|
||||||
|
details,
|
||||||
|
} => {
|
||||||
|
let status = code
|
||||||
|
.and_then(|c| StatusCode::from_u16(c).ok())
|
||||||
|
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
let body = if let Some(details) = details {
|
||||||
|
serde_json::json!({
|
||||||
|
"error": message,
|
||||||
|
"details": details,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
serde_json::json!({
|
||||||
|
"error": message,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let body = axum_core::body::Body::from(
|
||||||
|
serde_json::to_string(&body)
|
||||||
|
.unwrap_or_else(|_| "{\"error\":\"Internal Server Error\"}".to_string()),
|
||||||
|
);
|
||||||
|
axum_core::response::Response::builder()
|
||||||
|
.status(status)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(body)
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
axum_core::response::Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(axum_core::body::Body::from(
|
||||||
|
"{\"error\":\"Internal Server Error\"}",
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let status: StatusCode = self.clone().into();
|
||||||
|
let body = axum_core::body::Body::from(
|
||||||
|
serde_json::json!({
|
||||||
|
"error": self.to_string(),
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
axum_core::response::Response::builder()
|
||||||
|
.status(status)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(body)
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
axum_core::response::Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(axum_core::body::Body::from(
|
||||||
|
"{\"error\":\"Internal Server Error\"}",
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
use crate::ServerFnError;
|
||||||
|
use axum_core::{extract::FromRequest, response::IntoResponse};
|
||||||
use dioxus_core::try_consume_context;
|
use dioxus_core::try_consume_context;
|
||||||
use dioxus_signals::{ReadableExt, Signal, WritableExt};
|
use dioxus_signals::{ReadableExt, Signal, WritableExt};
|
||||||
|
use http::request::Parts;
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
/// The status of the streaming response
|
/// The status of the streaming response
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
@@ -13,23 +17,26 @@ pub enum StreamingStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The context dioxus fullstack provides for the status of streaming responses on the server
|
/// The context dioxus fullstack provides for the status of streaming responses on the server
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct StreamingContext {
|
pub struct StreamingContext {
|
||||||
current_status: Signal<StreamingStatus>,
|
current_status: Signal<StreamingStatus>,
|
||||||
|
request_headers: Rc<RefCell<http::request::Parts>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for StreamingContext {
|
impl PartialEq for StreamingContext {
|
||||||
fn default() -> Self {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
Self::new()
|
self.current_status == other.current_status
|
||||||
|
&& Rc::ptr_eq(&self.request_headers, &other.request_headers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StreamingContext {
|
impl StreamingContext {
|
||||||
/// Create a new streaming context. You should not need to call this directly. Dioxus fullstack will
|
/// Create a new streaming context. You should not need to call this directly. Dioxus fullstack will
|
||||||
/// provide this context for you.
|
/// provide this context for you.
|
||||||
pub fn new() -> Self {
|
pub fn new(parts: Parts) -> Self {
|
||||||
Self {
|
Self {
|
||||||
current_status: Signal::new(StreamingStatus::RenderingInitialChunk),
|
current_status: Signal::new(StreamingStatus::RenderingInitialChunk),
|
||||||
|
request_headers: Rc::new(RefCell::new(parts)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +55,42 @@ impl StreamingContext {
|
|||||||
pub fn current_status(&self) -> StreamingStatus {
|
pub fn current_status(&self) -> StreamingStatus {
|
||||||
*self.current_status.read()
|
*self.current_status.read()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the http request parts mutably. This will allow you to modify headers and other parts of the request.
|
||||||
|
pub fn parts_mut(&self) -> std::cell::RefMut<'_, http::request::Parts> {
|
||||||
|
self.request_headers.borrow_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract an axum extractor from the current request. This will always use an empty body for the request,
|
||||||
|
/// since it's assumed that rendering the app is done under a `GET` request.
|
||||||
|
pub async fn extract<T: FromRequest<(), M>, M>() -> Result<T, ServerFnError> {
|
||||||
|
let this = Self::current()
|
||||||
|
.ok_or_else(|| ServerFnError::new("No StreamingContext found".to_string()))?;
|
||||||
|
|
||||||
|
let parts = this.request_headers.borrow_mut().clone();
|
||||||
|
let request =
|
||||||
|
axum_core::extract::Request::from_parts(parts, axum_core::body::Body::empty());
|
||||||
|
match T::from_request(request, &()).await {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => {
|
||||||
|
let resp = err.into_response();
|
||||||
|
Err(ServerFnError::from_axum_response(resp).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current `StreamingContext` if it exists. This will return `None` if called on the client
|
||||||
|
/// or outside of a streaming response on the server.
|
||||||
|
pub fn current() -> Option<Self> {
|
||||||
|
if let Some(rt) = dioxus_core::Runtime::try_current() {
|
||||||
|
let id = rt.try_current_scope_id()?;
|
||||||
|
if let Some(ctx) = rt.consume_context::<StreamingContext>(id) {
|
||||||
|
return Some(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Commit the initial chunk of the response. This will be called automatically if you are using the
|
/// Commit the initial chunk of the response. This will be called automatically if you are using the
|
||||||
|
|||||||
@@ -305,6 +305,14 @@ fn route_impl_with_route(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let server_types = server_args
|
||||||
|
.iter()
|
||||||
|
.map(|pat_type| match pat_type {
|
||||||
|
FnArg::Receiver(_) => parse_quote! { () },
|
||||||
|
FnArg::Typed(pat_type) => (*pat_type.ty).clone(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let body_struct_impl = {
|
let body_struct_impl = {
|
||||||
let tys = body_json_types
|
let tys = body_json_types
|
||||||
.iter()
|
.iter()
|
||||||
@@ -334,28 +342,13 @@ fn route_impl_with_route(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// there's no active request on the server, so we just create a dummy one
|
// there's no active request on the server, so we just create a dummy one
|
||||||
let server_defaults = server_args.iter().map(|arg| {
|
let server_defaults = if server_args.is_empty() {
|
||||||
let name = match arg {
|
quote! {}
|
||||||
FnArg::Receiver(_) => panic!("Server args cannot be receiver"),
|
} else {
|
||||||
FnArg::Typed(pat_type) => match pat_type.pat.as_ref() {
|
|
||||||
Pat::Ident(pat_ident) => &pat_ident.ident,
|
|
||||||
_ => panic!("Expected Pat::Ident"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let ty = match arg {
|
|
||||||
FnArg::Receiver(_) => panic!("Server args cannot be receiver"),
|
|
||||||
FnArg::Typed(pat_type) => (*pat_type.ty).clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
let #name = {
|
let (#(#server_names,)*) = dioxus_fullstack::StreamingContext::extract::<(#(#server_types,)*), _>().await?;
|
||||||
use dioxus_fullstack::axum::extract::FromRequest;
|
|
||||||
let __request = dioxus_fullstack::axum::extract::Request::new(dioxus_fullstack::axum::body::Body::empty());
|
|
||||||
<#ty as FromRequest<_, _>>::from_request(__request, &()).await.unwrap()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
let as_axum_path = route.to_axum_path_string();
|
let as_axum_path = route.to_axum_path_string();
|
||||||
|
|
||||||
@@ -519,7 +512,7 @@ fn route_impl_with_route(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#(#server_defaults)*
|
#server_defaults
|
||||||
|
|
||||||
return #fn_name #ty_generics(
|
return #fn_name #ty_generics(
|
||||||
#(#extracted_idents,)*
|
#(#extracted_idents,)*
|
||||||
@@ -1214,6 +1207,15 @@ impl PathParam {
|
|||||||
ty,
|
ty,
|
||||||
Brace(span),
|
Brace(span),
|
||||||
)
|
)
|
||||||
|
} else if str.starts_with(':') && str.len() > 1 {
|
||||||
|
let str = str.strip_prefix(':').unwrap();
|
||||||
|
Self::Capture(
|
||||||
|
LitStr::new(str, span),
|
||||||
|
Brace(span),
|
||||||
|
Ident::new(str, span),
|
||||||
|
ty,
|
||||||
|
Brace(span),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Self::Static(LitStr::new(str, span))
|
Self::Static(LitStr::new(str, span))
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use dioxus_core::{Element, VirtualDom};
|
use dioxus_core::{Element, VirtualDom};
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use http::header::*;
|
use http::{header::*, request::Parts};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tower::util::MapResponse;
|
use tower::util::MapResponse;
|
||||||
@@ -297,13 +297,8 @@ impl RenderHandleState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (parts, _) = request.into_parts();
|
let (parts, _) = request.into_parts();
|
||||||
let url = parts
|
|
||||||
.uri
|
|
||||||
.path_and_query()
|
|
||||||
.ok_or(StatusCode::BAD_REQUEST)?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
match state.render(url, cfg, build_virtual_dom).await {
|
match state.render(parts, cfg, build_virtual_dom).await {
|
||||||
Ok((freshness, rx)) => {
|
Ok((freshness, rx)) => {
|
||||||
let mut response =
|
let mut response =
|
||||||
axum::response::Html::from(Body::from_stream(rx)).into_response();
|
axum::response::Html::from(Body::from_stream(rx)).into_response();
|
||||||
@@ -338,7 +333,7 @@ impl RenderHandleState {
|
|||||||
/// Render the application to HTML.
|
/// Render the application to HTML.
|
||||||
pub async fn render<'a>(
|
pub async fn render<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
route: String,
|
parts: Parts,
|
||||||
cfg: &'a ServeConfig,
|
cfg: &'a ServeConfig,
|
||||||
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
@@ -350,7 +345,7 @@ impl RenderHandleState {
|
|||||||
> {
|
> {
|
||||||
self.renderers
|
self.renderers
|
||||||
.clone()
|
.clone()
|
||||||
.render_to(cfg, route, virtual_dom_factory)
|
.render_to(parts, cfg, virtual_dom_factory)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use dioxus_router::ParseRouteError;
|
|||||||
use dioxus_ssr::Renderer;
|
use dioxus_ssr::Renderer;
|
||||||
use futures_channel::mpsc::Sender;
|
use futures_channel::mpsc::Sender;
|
||||||
use futures_util::{Stream, StreamExt};
|
use futures_util::{Stream, StreamExt};
|
||||||
use http::StatusCode;
|
use http::{request::Parts, StatusCode};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
@@ -98,8 +98,8 @@ impl SsrRendererPool {
|
|||||||
/// The streaming is canceled when the stream the function returns is dropped
|
/// The streaming is canceled when the stream the function returns is dropped
|
||||||
pub(crate) async fn render_to(
|
pub(crate) async fn render_to(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
|
parts: Parts,
|
||||||
cfg: &ServeConfig,
|
cfg: &ServeConfig,
|
||||||
route: String,
|
|
||||||
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
@@ -133,6 +133,15 @@ impl SsrRendererPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let route = parts
|
||||||
|
.uri
|
||||||
|
.path_and_query()
|
||||||
|
.ok_or_else(|| SSRError::HttpError {
|
||||||
|
status: StatusCode::BAD_REQUEST,
|
||||||
|
message: None,
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let (mut into, rx) =
|
let (mut into, rx) =
|
||||||
futures_channel::mpsc::channel::<Result<String, IncrementalRendererError>>(1000);
|
futures_channel::mpsc::channel::<Result<String, IncrementalRendererError>>(1000);
|
||||||
|
|
||||||
@@ -177,9 +186,10 @@ impl SsrRendererPool {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Provide the document and streaming context to the root of the app
|
// Provide the document and streaming context to the root of the app
|
||||||
let streaming_context = virtual_dom.in_scope(ScopeId::ROOT, StreamingContext::new);
|
let streaming_context =
|
||||||
|
virtual_dom.in_scope(ScopeId::ROOT, || StreamingContext::new(parts));
|
||||||
virtual_dom.provide_root_context(document.clone() as Rc<dyn dioxus_document::Document>);
|
virtual_dom.provide_root_context(document.clone() as Rc<dyn dioxus_document::Document>);
|
||||||
virtual_dom.provide_root_context(streaming_context);
|
virtual_dom.provide_root_context(streaming_context.clone());
|
||||||
|
|
||||||
virtual_dom.in_scope(ScopeId::ROOT, || {
|
virtual_dom.in_scope(ScopeId::ROOT, || {
|
||||||
// Wrap the memory history in a fullstack history provider to provide the initial route for hydration
|
// Wrap the memory history in a fullstack history provider to provide the initial route for hydration
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ serde_json = { workspace = true }
|
|||||||
pin-project = { workspace = true }
|
pin-project = { workspace = true }
|
||||||
serde_qs = { workspace = true, default-features = true}
|
serde_qs = { workspace = true, default-features = true}
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
reqwest = { workspace = true, features = ["json", "rustls-tls", "cookies", "stream"] }
|
reqwest = { workspace = true, features = ["json", "rustls-tls", "cookies", "stream", "multipart"] }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
serde_urlencoded = { workspace = true }
|
serde_urlencoded = { workspace = true }
|
||||||
form_urlencoded = { workspace = true }
|
form_urlencoded = { workspace = true }
|
||||||
|
|||||||
@@ -161,6 +161,49 @@ impl ClientRequest {
|
|||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends the request with multipart/form-data body constructed from the given FormData.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub async fn send_multipart(
|
||||||
|
self,
|
||||||
|
form: &dioxus_html::FormData,
|
||||||
|
) -> Result<ClientResponse, RequestError> {
|
||||||
|
let mut outgoing = reqwest::multipart::Form::new();
|
||||||
|
|
||||||
|
for (key, value) in form.values() {
|
||||||
|
match value {
|
||||||
|
dioxus_html::FormValue::Text(text) => {
|
||||||
|
outgoing = outgoing.text(key.to_string(), text.to_string());
|
||||||
|
}
|
||||||
|
dioxus_html::FormValue::File(Some(file_data)) => {
|
||||||
|
outgoing = outgoing
|
||||||
|
.file(key.to_string(), file_data.path())
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
RequestError::Builder(format!(
|
||||||
|
"Failed to add file to multipart form: {e}",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
dioxus_html::FormValue::File(None) => {
|
||||||
|
// No file was selected for this input, so we skip it.
|
||||||
|
outgoing = outgoing.part(key.to_string(), reqwest::multipart::Part::bytes(b""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = self
|
||||||
|
.new_reqwest_request()
|
||||||
|
.multipart(outgoing)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(reqwest_error_to_request_error)?;
|
||||||
|
|
||||||
|
Ok(ClientResponse {
|
||||||
|
response: Box::new(res),
|
||||||
|
extensions: self.extensions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_form(self, data: &impl Serialize) -> Result<ClientResponse, RequestError> {
|
pub async fn send_form(self, data: &impl Serialize) -> Result<ClientResponse, RequestError> {
|
||||||
// For GET and HEAD requests, we encode the form data as query parameters.
|
// For GET and HEAD requests, we encode the form data as query parameters.
|
||||||
// For other request methods, we encode the form data as the request body.
|
// For other request methods, we encode the form data as the request body.
|
||||||
|
|||||||
@@ -95,7 +95,11 @@ impl<S> IntoRequest for MultipartFormData<S> {
|
|||||||
// and construct a multipart form body manually.
|
// and construct a multipart form body manually.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
todo!("Properly implement multipart form data for non-web wasm32 clients");
|
let data = self._client.clone().ok_or_else(|| {
|
||||||
|
RequestError::Builder("Failed to get FormData from event".into())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return _req.send_multipart(&data).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
unimplemented!("Non web wasm32 clients are not supported yet")
|
unimplemented!("Non web wasm32 clients are not supported yet")
|
||||||
|
|||||||
+241
-198
@@ -1,77 +1,16 @@
|
|||||||
use crate::file_data::HasFileData;
|
use crate::file_data::HasFileData;
|
||||||
use crate::FileData;
|
use crate::FileData;
|
||||||
use std::{collections::HashMap, fmt::Debug, ops::Deref};
|
use std::fmt::Debug;
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
use crate::SerializedFileData;
|
|
||||||
|
|
||||||
use dioxus_core::Event;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type FormEvent = Event<FormData>;
|
pub type FormEvent = Event<FormData>;
|
||||||
|
|
||||||
/// A form value that may either be a list of values or a single value
|
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq)]
|
|
||||||
pub struct FormValue(pub Vec<String>);
|
|
||||||
|
|
||||||
impl Deref for FormValue {
|
|
||||||
type Target = [String];
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.as_slice()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FormValue {
|
|
||||||
/// Convenient way to represent Value as slice
|
|
||||||
pub fn as_slice(&self) -> &[String] {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the first value, panicking if there are none
|
|
||||||
pub fn as_value(&self) -> String {
|
|
||||||
self.0.first().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert into [`Vec<String>`]
|
|
||||||
pub fn to_vec(self) -> Vec<String> {
|
|
||||||
self.0.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq<str> for FormValue {
|
|
||||||
fn eq(&self, other: &str) -> bool {
|
|
||||||
self.0.len() == 1 && self.0.first().map(|s| s.as_str()) == Some(other)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* DOMEvent: Send + SyncTarget relatedTarget */
|
/* DOMEvent: Send + SyncTarget relatedTarget */
|
||||||
pub struct FormData {
|
pub struct FormData {
|
||||||
inner: Box<dyn HasFormData>,
|
inner: Box<dyn HasFormData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: HasFormData> From<E> for FormData {
|
|
||||||
fn from(e: E) -> Self {
|
|
||||||
Self { inner: Box::new(e) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for FormData {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.value() == other.value() && self.values() == other.values()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for FormData {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("FormEvent")
|
|
||||||
.field("value", &self.value())
|
|
||||||
.field("values", &self.values())
|
|
||||||
.field("valid", &self.valid())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FormData {
|
impl FormData {
|
||||||
/// Create a new form event
|
/// Create a new form event
|
||||||
pub fn new(event: impl HasFormData + 'static) -> Self {
|
pub fn new(event: impl HasFormData + 'static) -> Self {
|
||||||
@@ -104,10 +43,25 @@ impl FormData {
|
|||||||
/// Collect all the named form values from the containing form.
|
/// Collect all the named form values from the containing form.
|
||||||
///
|
///
|
||||||
/// Every input must be named!
|
/// Every input must be named!
|
||||||
pub fn values(&self) -> HashMap<String, FormValue> {
|
pub fn values(&self) -> Vec<(String, FormValue)> {
|
||||||
self.inner.values()
|
self.inner.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the first value with the given name
|
||||||
|
pub fn get_first(&self, name: &str) -> Option<FormValue> {
|
||||||
|
self.values()
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|(k, v)| if k == name { Some(v) } else { None })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all values with the given name
|
||||||
|
pub fn get(&self, name: &str) -> Vec<FormValue> {
|
||||||
|
self.values()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(k, v)| if k == name { Some(v) } else { None })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the files of the form event
|
/// Get the files of the form event
|
||||||
pub fn files(&self) -> Vec<FileData> {
|
pub fn files(&self) -> Vec<FileData> {
|
||||||
self.inner.files()
|
self.inner.files()
|
||||||
@@ -125,146 +79,69 @@ impl FormData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An object that has all the data for a form event
|
|
||||||
pub trait HasFormData: HasFileData + std::any::Any {
|
|
||||||
fn value(&self) -> String {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn valid(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn values(&self) -> HashMap<String, FormValue> {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// return self as Any
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FormData {
|
impl FormData {
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
/// Parse the values into a struct with one field per value
|
/// Parse the values into a struct with one field per value
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
pub fn parsed_values<T>(&self) -> Result<T, serde_json::Error>
|
pub fn parsed_values<T>(&self) -> Result<T, serde_json::Error>
|
||||||
where
|
where
|
||||||
T: serde::de::DeserializeOwned,
|
T: serde::de::DeserializeOwned,
|
||||||
{
|
{
|
||||||
|
use crate::SerializedFileData;
|
||||||
|
|
||||||
let values = &self.values();
|
let values = &self.values();
|
||||||
|
|
||||||
let mut map = serde_json::Map::new();
|
let mut map = serde_json::Map::new();
|
||||||
for (key, value) in values {
|
for (key, value) in values {
|
||||||
if value.0.len() == 1 {
|
let entry = map
|
||||||
map.insert(key.clone(), serde_json::Value::String(value.0[0].clone()));
|
.entry(key.clone())
|
||||||
} else {
|
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
|
||||||
let arr = value
|
|
||||||
.0
|
match value {
|
||||||
.iter()
|
FormValue::Text(text) => {
|
||||||
.cloned()
|
entry
|
||||||
.map(serde_json::Value::String)
|
.as_array_mut()
|
||||||
.collect();
|
.expect("entry should be an array")
|
||||||
map.insert(key.clone(), serde_json::Value::Array(arr));
|
.push(serde_json::Value::String(text.clone()));
|
||||||
|
}
|
||||||
|
// we create the serialized variant with no bytes
|
||||||
|
// SerializedFileData, if given a real path, will read the bytes from disk (synchronously)
|
||||||
|
FormValue::File(Some(file_data)) => {
|
||||||
|
let serialized = SerializedFileData {
|
||||||
|
path: file_data.path().to_owned(),
|
||||||
|
size: file_data.size(),
|
||||||
|
last_modified: file_data.last_modified(),
|
||||||
|
content_type: file_data.content_type(),
|
||||||
|
contents: None,
|
||||||
|
};
|
||||||
|
entry
|
||||||
|
.as_array_mut()
|
||||||
|
.expect("entry should be an array")
|
||||||
|
.push(serde_json::to_value(&serialized).unwrap_or(serde_json::Value::Null));
|
||||||
|
}
|
||||||
|
FormValue::File(None) => {
|
||||||
|
entry
|
||||||
|
.as_array_mut()
|
||||||
|
.expect("entry should be an array")
|
||||||
|
.push(
|
||||||
|
serde_json::to_value(SerializedFileData::empty())
|
||||||
|
.unwrap_or(serde_json::Value::Null),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serde_json::from_value(serde_json::Value::Object(map))
|
// Go through the map and convert single-element arrays to just the element
|
||||||
}
|
let map = map
|
||||||
}
|
.into_iter()
|
||||||
|
.map(|(k, v)| match v {
|
||||||
#[cfg(feature = "serialize")]
|
serde_json::Value::Array(arr) if arr.len() == 1 => {
|
||||||
/// A serialized form data object
|
(k, arr.into_iter().next().unwrap())
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
|
|
||||||
pub struct SerializedFormData {
|
|
||||||
#[serde(default)]
|
|
||||||
value: String,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
values: HashMap<String, FormValue>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
valid: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
files: Vec<SerializedFileData>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
impl SerializedFormData {
|
|
||||||
/// Create a new serialized form data object
|
|
||||||
pub fn new(value: String, values: HashMap<String, FormValue>) -> Self {
|
|
||||||
Self {
|
|
||||||
value,
|
|
||||||
values,
|
|
||||||
valid: true,
|
|
||||||
files: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add files to the serialized form data object
|
|
||||||
pub fn with_files(mut self, files: Vec<SerializedFileData>) -> Self {
|
|
||||||
self.files = files;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new serialized form data object from a traditional form data object
|
|
||||||
pub async fn async_from(data: &FormData) -> Result<Self, dioxus_core::Error> {
|
|
||||||
Ok(Self {
|
|
||||||
value: data.value(),
|
|
||||||
values: data.values(),
|
|
||||||
valid: data.valid(),
|
|
||||||
files: {
|
|
||||||
let mut out = vec![];
|
|
||||||
for file in data.files() {
|
|
||||||
out.push(SerializedFileData {
|
|
||||||
name: file.name(),
|
|
||||||
size: file.size(),
|
|
||||||
content_type: file.content_type(),
|
|
||||||
last_modified: file.last_modified(),
|
|
||||||
path: file.pathbuf(),
|
|
||||||
contents: file.read_bytes().await?,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
out
|
_ => (k, v),
|
||||||
},
|
})
|
||||||
})
|
.collect::<serde_json::Map<String, serde_json::Value>>();
|
||||||
}
|
|
||||||
|
|
||||||
fn from_lossy(data: &FormData) -> Self {
|
serde_json::from_value(serde_json::Value::Object(map))
|
||||||
Self {
|
|
||||||
value: data.value(),
|
|
||||||
values: data.values(),
|
|
||||||
valid: data.valid(),
|
|
||||||
files: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
impl HasFormData for SerializedFormData {
|
|
||||||
fn value(&self) -> String {
|
|
||||||
self.value.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn values(&self) -> HashMap<String, FormValue> {
|
|
||||||
self.values.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn valid(&self) -> bool {
|
|
||||||
self.valid
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
impl HasFileData for SerializedFormData {
|
|
||||||
fn files(&self) -> Vec<FileData> {
|
|
||||||
self.files
|
|
||||||
.iter()
|
|
||||||
.map(|f| FileData::new(f.clone()))
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,20 +151,186 @@ impl HasFileData for FormData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
impl<E: HasFormData> From<E> for FormData {
|
||||||
impl serde::Serialize for FormData {
|
fn from(e: E) -> Self {
|
||||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
Self { inner: Box::new(e) }
|
||||||
SerializedFormData::from_lossy(self).serialize(serializer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for FormData {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.value() == other.value() && self.values() == other.values()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for FormData {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("FormEvent")
|
||||||
|
.field("value", &self.value())
|
||||||
|
.field("values", &self.values())
|
||||||
|
.field("valid", &self.valid())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A value in a form, either text or a file
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum FormValue {
|
||||||
|
Text(String),
|
||||||
|
File(Option<FileData>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<str> for FormValue {
|
||||||
|
fn eq(&self, other: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
FormValue::Text(s) => s == other,
|
||||||
|
FormValue::File(_f) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<&str> for FormValue {
|
||||||
|
fn eq(&self, other: &&str) -> bool {
|
||||||
|
match self {
|
||||||
|
FormValue::Text(s) => s == other,
|
||||||
|
FormValue::File(_f) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An object that has all the data for a form event
|
||||||
|
pub trait HasFormData: HasFileData + std::any::Any {
|
||||||
|
fn value(&self) -> String;
|
||||||
|
|
||||||
|
fn valid(&self) -> bool;
|
||||||
|
|
||||||
|
fn values(&self) -> Vec<(String, FormValue)>;
|
||||||
|
|
||||||
|
/// return self as Any
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any;
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
impl<'de> serde::Deserialize<'de> for FormData {
|
pub use serialize::*;
|
||||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
|
||||||
let data = SerializedFormData::deserialize(deserializer)?;
|
#[cfg(feature = "serialize")]
|
||||||
Ok(Self {
|
mod serialize {
|
||||||
inner: Box::new(data),
|
use crate::SerializedFileData;
|
||||||
})
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A serialized form data object
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
|
||||||
|
pub struct SerializedFormData {
|
||||||
|
#[serde(default)]
|
||||||
|
pub value: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub values: Vec<SerializedFormObject>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub valid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
|
||||||
|
pub struct SerializedFormObject {
|
||||||
|
pub key: String,
|
||||||
|
pub text: Option<String>,
|
||||||
|
pub file: Option<SerializedFileData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
|
impl SerializedFormData {
|
||||||
|
/// Create a new serialized form data object
|
||||||
|
pub fn new(value: String, values: Vec<SerializedFormObject>) -> Self {
|
||||||
|
Self {
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
valid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a serialized form data object from a form data object
|
||||||
|
fn from_form_lossy(data: &FormData) -> Self {
|
||||||
|
if let Some(data) = data.downcast::<SerializedFormData>() {
|
||||||
|
return data.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let values = data
|
||||||
|
.values()
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| match value {
|
||||||
|
FormValue::Text(s) => SerializedFormObject {
|
||||||
|
key: key.clone(),
|
||||||
|
text: Some(s.to_string()),
|
||||||
|
file: None,
|
||||||
|
},
|
||||||
|
FormValue::File(f) => SerializedFormObject {
|
||||||
|
key: key.clone(),
|
||||||
|
text: None,
|
||||||
|
file: if let Some(f) = f {
|
||||||
|
Some(SerializedFileData {
|
||||||
|
path: f.path(),
|
||||||
|
size: f.size(),
|
||||||
|
last_modified: f.last_modified(),
|
||||||
|
content_type: f.content_type(),
|
||||||
|
contents: None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Some(SerializedFileData::empty())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
values,
|
||||||
|
value: data.value(),
|
||||||
|
valid: data.valid(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasFormData for SerializedFormData {
|
||||||
|
fn value(&self) -> String {
|
||||||
|
self.value.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn values(&self) -> Vec<(String, FormValue)> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid(&self) -> bool {
|
||||||
|
self.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasFileData for SerializedFormData {
|
||||||
|
fn files(&self) -> Vec<FileData> {
|
||||||
|
self.values
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.file.as_ref().map(|f| FileData::new(f.clone())))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl serde::Serialize for FormData {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
SerializedFormData::from_form_lossy(self).serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for FormData {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
let data = SerializedFormData::deserialize(deserializer)?;
|
||||||
|
Ok(Self {
|
||||||
|
inner: Box::new(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+134
-60
@@ -1,7 +1,6 @@
|
|||||||
use std::{path::PathBuf, pin::Pin, prelude::rust_2024::Future};
|
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::Stream;
|
use futures_util::Stream;
|
||||||
|
use std::{path::PathBuf, pin::Pin, prelude::rust_2024::Future};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FileData {
|
pub struct FileData {
|
||||||
@@ -49,11 +48,20 @@ impl FileData {
|
|||||||
self.inner.inner()
|
self.inner.inner()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pathbuf(&self) -> PathBuf {
|
pub fn path(&self) -> PathBuf {
|
||||||
self.inner.path()
|
self.inner.path()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for FileData {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.name() == other.name()
|
||||||
|
&& self.size() == other.size()
|
||||||
|
&& self.last_modified() == other.last_modified()
|
||||||
|
&& self.path() == other.path()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait NativeFileData: Send + Sync {
|
pub trait NativeFileData: Send + Sync {
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
fn size(&self) -> u64;
|
fn size(&self) -> u64;
|
||||||
@@ -82,75 +90,141 @@ impl std::fmt::Debug for FileData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::cmp::PartialEq for FileData {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.inner.name() == other.inner.name()
|
|
||||||
&& self.inner.size() == other.inner.size()
|
|
||||||
&& self.inner.last_modified() == other.inner.last_modified()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasFileData: std::any::Any {
|
pub trait HasFileData: std::any::Any {
|
||||||
fn files(&self) -> Vec<FileData>;
|
fn files(&self) -> Vec<FileData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A serializable representation of file data\
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
|
pub use serialize::*;
|
||||||
pub struct SerializedFileData {
|
|
||||||
pub name: String,
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub size: u64,
|
|
||||||
pub last_modified: u64,
|
|
||||||
pub content_type: Option<String>,
|
|
||||||
pub contents: bytes::Bytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
impl NativeFileData for SerializedFileData {
|
mod serialize {
|
||||||
fn name(&self) -> String {
|
use super::*;
|
||||||
self.name.clone()
|
|
||||||
|
/// A serializable representation of file data
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
|
||||||
|
pub struct SerializedFileData {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub size: u64,
|
||||||
|
pub last_modified: u64,
|
||||||
|
pub content_type: Option<String>,
|
||||||
|
pub contents: Option<bytes::Bytes>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size(&self) -> u64 {
|
impl SerializedFileData {
|
||||||
self.size
|
/// Create a new empty serialized file data object
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
path: PathBuf::new(),
|
||||||
|
size: 0,
|
||||||
|
last_modified: 0,
|
||||||
|
content_type: None,
|
||||||
|
contents: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last_modified(&self) -> u64 {
|
impl NativeFileData for SerializedFileData {
|
||||||
self.last_modified
|
fn name(&self) -> String {
|
||||||
|
self.path()
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> u64 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_modified(&self) -> u64 {
|
||||||
|
self.last_modified
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bytes(
|
||||||
|
&self,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<Bytes, dioxus_core::Error>> + 'static>> {
|
||||||
|
let contents = self.contents.clone();
|
||||||
|
let path = self.path.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
if let Some(contents) = contents {
|
||||||
|
return Ok(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
if path.exists() {
|
||||||
|
return Ok(std::fs::read(path).map(Bytes::from)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(dioxus_core::Error::msg("File contents not available"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_string(
|
||||||
|
&self,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<String, dioxus_core::Error>> + 'static>> {
|
||||||
|
let contents = self.contents.clone();
|
||||||
|
let path = self.path.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
if let Some(contents) = contents {
|
||||||
|
return Ok(String::from_utf8(contents.to_vec())?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
if path.exists() {
|
||||||
|
return Ok(std::fs::read_to_string(path)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(dioxus_core::Error::msg("File contents not available"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn byte_stream(
|
||||||
|
&self,
|
||||||
|
) -> Pin<
|
||||||
|
Box<
|
||||||
|
dyn futures_util::Stream<Item = Result<Bytes, dioxus_core::Error>> + 'static + Send,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
let contents = self.contents.clone();
|
||||||
|
let path = self.path.clone();
|
||||||
|
|
||||||
|
Box::pin(futures_util::stream::once(async move {
|
||||||
|
if let Some(contents) = contents {
|
||||||
|
return Ok(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
if path.exists() {
|
||||||
|
return Ok(std::fs::read(path).map(Bytes::from)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(dioxus_core::Error::msg("File contents not available"))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> PathBuf {
|
||||||
|
self.path.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content_type(&self) -> Option<String> {
|
||||||
|
self.content_type.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_bytes(
|
impl<'de> serde::Deserialize<'de> for FileData {
|
||||||
&self,
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
) -> Pin<Box<dyn Future<Output = Result<Bytes, dioxus_core::Error>> + 'static>> {
|
where
|
||||||
let contents = self.contents.clone();
|
D: serde::Deserializer<'de>,
|
||||||
Box::pin(async move { Ok(contents) })
|
{
|
||||||
}
|
let sfd = SerializedFileData::deserialize(deserializer)?;
|
||||||
|
Ok(FileData::new(sfd))
|
||||||
fn read_string(
|
}
|
||||||
&self,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Result<String, dioxus_core::Error>> + 'static>> {
|
|
||||||
let contents = self.contents.clone();
|
|
||||||
Box::pin(async move { Ok(String::from_utf8(contents.to_vec())?) })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn byte_stream(
|
|
||||||
&self,
|
|
||||||
) -> Pin<Box<dyn futures_util::Stream<Item = Result<Bytes, dioxus_core::Error>> + 'static + Send>>
|
|
||||||
{
|
|
||||||
let contents = self.contents.clone();
|
|
||||||
Box::pin(futures_util::stream::once(async move { Ok(contents) }))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inner(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn path(&self) -> PathBuf {
|
|
||||||
self.path.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn content_type(&self) -> Option<String> {
|
|
||||||
self.content_type.clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ fn main() {
|
|||||||
// If any TS files change, re-run the build script
|
// If any TS files change, re-run the build script
|
||||||
lazy_js_bundle::LazyTypeScriptBindings::new()
|
lazy_js_bundle::LazyTypeScriptBindings::new()
|
||||||
.with_watching("./src/ts")
|
.with_watching("./src/ts")
|
||||||
.with_binding("./src/ts/common.ts", "./src/js/common.js")
|
.with_binding("./src/ts/set_attribute.ts", "./src/js/set_attribute.js")
|
||||||
.with_binding("./src/ts/native.ts", "./src/js/native.js")
|
.with_binding("./src/ts/native.ts", "./src/js/native.js")
|
||||||
.with_binding("./src/ts/core.ts", "./src/js/core.js")
|
.with_binding("./src/ts/core.ts", "./src/js/core.js")
|
||||||
.with_binding("./src/ts/hydrate.ts", "./src/js/hydrate.js")
|
.with_binding("./src/ts/hydrate.ts", "./src/js/hydrate.js")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.tagName==="OPTION")setAttributeDefault(node,field,value);else if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;case"style":let existingStyles={};for(let i=0;i<node.style.length;i++){let prop=node.style[i];existingStyles[prop]=node.style.getPropertyValue(prop)}node.setAttribute(field,value);for(let prop in existingStyles)if(!node.style.getPropertyValue(prop))node.style.setProperty(prop,existingStyles[prop]);break;case"multiple":if(setAttributeDefault(node,field,value),node.options!==null&&node.options!==void 0){let options=node.options;for(let option of options)option.selected=option.defaultSelected}break;default:setAttributeDefault(node,field,value)}}function setAttributeDefault(node,field,value){if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}function truthy(val){return val==="true"||val===!0}function isBoolAttr(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}}function retrieveFormValues(form){let formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(value instanceof File){contents[key]=[value.name];return}if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}export{setAttributeInner,retrieveFormValues};
|
function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.tagName==="OPTION")setAttributeDefault(node,field,value);else if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;case"style":let existingStyles={};for(let i=0;i<node.style.length;i++){let prop=node.style[i];existingStyles[prop]=node.style.getPropertyValue(prop)}node.setAttribute(field,value);for(let prop in existingStyles)if(!node.style.getPropertyValue(prop))node.style.setProperty(prop,existingStyles[prop]);break;case"multiple":if(setAttributeDefault(node,field,value),node.options!==null&&node.options!==void 0){let options=node.options;for(let option of options)option.selected=option.defaultSelected}break;default:setAttributeDefault(node,field,value)}}function setAttributeDefault(node,field,value){if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}function truthy(val){return val==="true"||val===!0}function isBoolAttr(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}}function retrieveFormValues(form){let formData=new FormData(form),contents=[];return formData.forEach((value,key)=>{if(value instanceof File){let fileData={name:value.name,path:value.webkitRelativePath,size:value.size,last_modified:value.lastModified,content_type:value.type};contents.push([key,fileData])}else contents.push([key,value])}),{valid:form.checkValidity(),values:contents}}export{setAttributeInner,retrieveFormValues};
|
||||||
@@ -1 +1 @@
|
|||||||
[6449103750905854967, 17669692872757955279, 4701826235737143966, 11420464406527728232, 3770103091118609057, 5444526391971481782, 14692042304579321130, 5052021921702764563, 7041565893588933645, 11339769846046015954]
|
[17669692872757955279, 11420464406527728232, 3770103091118609057, 5444526391971481782, 8889858244860485542, 5052021921702764563, 5113938988622536969, 11339769846046015954]
|
||||||
@@ -1 +1 @@
|
|||||||
function retrieveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){let formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(value instanceof File){contents[key]=[value.name];return}if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);if(event.type==="scroll")extend(serializeScrollEvent(event));return contents}function toSerializableResizeObserverSize(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]}function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}function serializeIntersectionEventDetail(detail){return{bounding_client_rect:detail.boundingClientRect,intersection_ratio:detail.intersectionRatio,intersection_rect:detail.intersectionRect,is_intersecting:detail.isIntersecting,root_bounds:detail.rootBounds,time_ms:Math.floor(Date.now()+detail.time)}}function serializeInputEvent(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents}function serializeWheelEvent(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}}function serializeTouchEvent(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:serializeTouchList(event.changedTouches),target_touches:serializeTouchList(event.targetTouches),touches:serializeTouchList(event.touches)}}function serializePointerEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}}function serializeTouchList(touchList){let serializedTouches=[];for(let i=0;i<touchList.length;i++){let touch=touchLisLine truncated
|
function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent<"u"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent){let files=[];if(event.dataTransfer&&event.dataTransfer.files)for(let i=0;i<event.dataTransfer.files.length;i++){let file=event.dataTransfer.files[i],data={path:file.name,size:file.size,last_modified:file.lastModified,content_type:file.type};files.push({key:file.name,file:data})}extend({files})}if(event.type==="scroll")extend(serializeScrollEvent(event));return contents}function toSerializableResizeObserverSize(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]}function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}function serializeIntersectionEventDetail(detail){return{bounding_client_rect:detail.boundingClientRect,intersection_ratio:detail.intersectionRatio,intersection_rect:detail.intersectionRect,is_intersecting:detail.isIntersecting,root_bounds:detail.rootBounds,time_ms:Math.floor(Date.now()+detail.time)}}function serializeInputEvent(event,target){let contents={};if(target instanceof HTMLElement){let values=extractSerializedFormValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents}function serializeWheelEvent(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}}function serializeTouchEvent(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:serializeTouchList(event.changedTouches),target_touches:serializeTouchList(event.targetTouches),touches:serializeTouchList(event.touches)}}function serializePointerEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}}function serializeTouchList(touchList){let serializedTouches=[];for(let i=0;i<touchList.length;i++){let touch=touchList[i];serializedTouches.push({identifier:touch.identifier,client_x:touch.clientX,client_y:touch.clientY,page_x:touch.pageX,page_y:touch.pageY,screen_x:touch.screenX,screen_y:touch.screenY,radius_x:touch.radiusX,radius_y:touch.radiusY,rotation_angle:touch.rotationAngle,force:touch.force})}return serializedTouches}function serializeMouseEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttonsLine truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.tagName==="OPTION")setAttributeDefault(node,field,value);else if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;case"style":let existingStyles={};for(let i=0;i<node.style.length;i++){let prop=node.style[i];existingStyles[prop]=node.style.getPropertyValue(prop)}node.setAttribute(field,value);for(let prop in existingStyles)if(!node.style.getPropertyValue(prop))node.style.setProperty(prop,existingStyles[prop]);break;case"multiple":if(setAttributeDefault(node,field,value),node.options!==null&&node.options!==void 0){let options=node.options;for(let option of options)option.selected=option.defaultSelected}break;default:setAttributeDefault(node,field,value)}}function setAttributeDefault(node,field,value){if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}function truthy(val){return val==="true"||val===!0}function isBoolAttr(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}}export{setAttributeInner};
|
||||||
@@ -32,13 +32,10 @@ pub mod minimal_bindings {
|
|||||||
/// Some useful snippets that we use to share common functionality between the different platforms we support.
|
/// Some useful snippets that we use to share common functionality between the different platforms we support.
|
||||||
///
|
///
|
||||||
/// This maintains some sort of consistency between web, desktop, and liveview
|
/// This maintains some sort of consistency between web, desktop, and liveview
|
||||||
#[wasm_bindgen(module = "/src/js/common.js")]
|
#[wasm_bindgen(module = "/src/js/set_attribute.js")]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
/// Set the attribute of the node
|
/// Set the attribute of the node
|
||||||
pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>);
|
pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>);
|
||||||
|
|
||||||
/// Roll up all the values from the node into a JS object that we can deserialize
|
|
||||||
pub fn retrieveFormValues(node: JsValue) -> JsValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(module = "/src/js/hydrate.js")]
|
#[wasm_bindgen(module = "/src/js/hydrate.js")]
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export { setAttributeInner } from "./set_attribute";
|
|
||||||
export { retrieveFormValues } from "./form";
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
// Consistently deserialize forms and form elements for use across web/desktop/mobile
|
|
||||||
|
|
||||||
type FormValues = {
|
|
||||||
valid?: boolean;
|
|
||||||
values: { [key: string]: FormDataEntryValue[] };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function retrieveValues(event: Event, target: HTMLElement): FormValues {
|
|
||||||
let contents: FormValues = {
|
|
||||||
values: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// If there's a form...
|
|
||||||
let form = target.closest("form");
|
|
||||||
|
|
||||||
// If the target is an input, and the event is input or change, we want to get the value without going through the form
|
|
||||||
if (form) {
|
|
||||||
if (
|
|
||||||
event.type === "input"
|
|
||||||
|| event.type === "change"
|
|
||||||
|| event.type === "submit"
|
|
||||||
|| event.type === "reset"
|
|
||||||
|| event.type === "click"
|
|
||||||
) {
|
|
||||||
contents = retrieveFormValues(form);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: maybe encode spaces or something?
|
|
||||||
// We encode select multiple as a comma separated list which breaks... when there's commas in the values
|
|
||||||
export function retrieveFormValues(form: HTMLFormElement): FormValues {
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const contents: { [key: string]: FormDataEntryValue[] } = {};
|
|
||||||
formData.forEach((value, key) => {
|
|
||||||
if (value instanceof File) {
|
|
||||||
contents[key] = [value.name];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contents[key]) {
|
|
||||||
contents[key].push(value);
|
|
||||||
} else {
|
|
||||||
contents[key] = [value];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
valid: form.checkValidity(),
|
|
||||||
values: contents
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function retrieveSelectValue(target: HTMLSelectElement): string[] {
|
|
||||||
// there might be multiple...
|
|
||||||
let options = target.selectedOptions;
|
|
||||||
let values = [];
|
|
||||||
for (let i = 0; i < options.length; i++) {
|
|
||||||
values.push(options[i].value);
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
// provide since it doesn't have access to the dom.
|
// provide since it doesn't have access to the dom.
|
||||||
|
|
||||||
import { BaseInterpreter, NodeId } from "./core";
|
import { BaseInterpreter, NodeId } from "./core";
|
||||||
import { SerializedEvent, serializeEvent } from "./serialize";
|
import { SerializedEvent, serializeEvent, SerializedFileData, extractSerializedFormValues, SerializedFormObject } from "./serialize";
|
||||||
|
|
||||||
// okay so, we've got this JSChannel thing from sledgehammer, implicitly imported into our scope
|
// okay so, we've got this JSChannel thing from sledgehammer, implicitly imported into our scope
|
||||||
// we want to extend it, and it technically extends base interpreter. To make typescript happy,
|
// we want to extend it, and it technically extends base interpreter. To make typescript happy,
|
||||||
@@ -21,6 +21,7 @@ export class NativeInterpreter extends JSChannel_ {
|
|||||||
intercept_link_redirects: boolean;
|
intercept_link_redirects: boolean;
|
||||||
ipc: any;
|
ipc: any;
|
||||||
edits: WebSocket;
|
edits: WebSocket;
|
||||||
|
baseUri: string;
|
||||||
eventsPath: string;
|
eventsPath: string;
|
||||||
headless: boolean;
|
headless: boolean;
|
||||||
kickStylesheets: boolean;
|
kickStylesheets: boolean;
|
||||||
@@ -30,9 +31,10 @@ export class NativeInterpreter extends JSChannel_ {
|
|||||||
// however, for now we need to support it since WebSockets in fullstack doesn't exist yet
|
// however, for now we need to support it since WebSockets in fullstack doesn't exist yet
|
||||||
liveview: boolean;
|
liveview: boolean;
|
||||||
|
|
||||||
constructor(eventsPath: string, headless: boolean) {
|
constructor(baseUri: string, headless: boolean) {
|
||||||
super();
|
super();
|
||||||
this.eventsPath = eventsPath;
|
this.baseUri = baseUri;
|
||||||
|
this.eventsPath = `${baseUri}/__events`;
|
||||||
this.kickStylesheets = false;
|
this.kickStylesheets = false;
|
||||||
this.headless = headless;
|
this.headless = headless;
|
||||||
}
|
}
|
||||||
@@ -79,20 +81,76 @@ export class NativeInterpreter extends JSChannel_ {
|
|||||||
// Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it
|
// Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it
|
||||||
let target_id = getTargetId(target);
|
let target_id = getTargetId(target);
|
||||||
if (target_id !== null) {
|
if (target_id !== null) {
|
||||||
const message = this.serializeIpcMessage("file_dialog", {
|
// Handle file inputs specifically, since we want to get the real file inputs from the native
|
||||||
event: "change&input",
|
// dialog and then set the form inputs accordingly.
|
||||||
accept: target.getAttribute("accept"),
|
if (
|
||||||
directory: target.getAttribute("webkitdirectory") === "true",
|
target instanceof HTMLInputElement &&
|
||||||
multiple: target.hasAttribute("multiple"),
|
target.getAttribute("type") === "file"
|
||||||
target: target_id,
|
) {
|
||||||
bubbles: event.bubbles,
|
// Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it
|
||||||
});
|
event.preventDefault();
|
||||||
this.ipc.postMessage(message);
|
|
||||||
event.preventDefault();
|
const contents = serializeEvent(event, target);
|
||||||
|
|
||||||
|
const target_name = target.getAttribute("name") || "";
|
||||||
|
|
||||||
|
let requestData = {
|
||||||
|
event: "change&input",
|
||||||
|
accept: target.getAttribute("accept"),
|
||||||
|
directory: target.getAttribute("webkitdirectory") === "true",
|
||||||
|
multiple: target.hasAttribute("multiple"),
|
||||||
|
target: target_id,
|
||||||
|
bubbles: event.bubbles,
|
||||||
|
target_name,
|
||||||
|
values: contents.values,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.fetchAgainstHost("__file_dialog", requestData).then(response => response.json()).then(resp => {
|
||||||
|
const formObjects: SerializedFormObject[] = resp.values;
|
||||||
|
|
||||||
|
// Create a new DataTransfer to hold the files
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
|
||||||
|
// We name the file the path, so we can just use the path as the name later on.
|
||||||
|
for (let formObject of formObjects) {
|
||||||
|
if (formObject.key == target_name && formObject.file != null) {
|
||||||
|
const file = new File([], formObject.file.path, {
|
||||||
|
type: formObject.file.content_type,
|
||||||
|
lastModified: formObject.file.last_modified,
|
||||||
|
});
|
||||||
|
dataTransfer.items.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the files on the input
|
||||||
|
target.files = dataTransfer.files;
|
||||||
|
|
||||||
|
let body = {
|
||||||
|
data: contents,
|
||||||
|
element: target_id,
|
||||||
|
bubbles: event.bubbles,
|
||||||
|
};
|
||||||
|
|
||||||
|
// And then dispatch the actual event against the dom
|
||||||
|
contents.values = formObjects;
|
||||||
|
this.sendSerializedEvent({
|
||||||
|
...body,
|
||||||
|
name: "input",
|
||||||
|
});
|
||||||
|
this.sendSerializedEvent({
|
||||||
|
...body,
|
||||||
|
name: "change",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// @ts-ignore - wry gives us this
|
// @ts-ignore - wry gives us this
|
||||||
this.ipc = window.ipc;
|
this.ipc = window.ipc;
|
||||||
|
|
||||||
@@ -103,8 +161,23 @@ export class NativeInterpreter extends JSChannel_ {
|
|||||||
super.initialize(root, handler);
|
super.initialize(root, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeIpcMessage(method: string, params = {}) {
|
fetchAgainstHost(path: string, data: { [key: string]: any }): Promise<Response> {
|
||||||
return JSON.stringify({ method, params });
|
let encoded_data = new TextEncoder().encode(JSON.stringify(data));
|
||||||
|
const base64data = btoa(
|
||||||
|
String.fromCharCode.apply(null, Array.from(encoded_data))
|
||||||
|
);
|
||||||
|
|
||||||
|
return fetch(`${this.baseUri}/${path}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"x-dioxus-data": base64data
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendIpcMessage(method: string, params = {}) {
|
||||||
|
const body = JSON.stringify({ method, params });
|
||||||
|
this.ipc.postMessage(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollTo(id: NodeId, options: ScrollIntoViewOptions): boolean {
|
scrollTo(id: NodeId, options: ScrollIntoViewOptions): boolean {
|
||||||
@@ -181,11 +254,14 @@ export class NativeInterpreter extends JSChannel_ {
|
|||||||
|
|
||||||
// Windows drag-n-drop fix code. Called by wry drag-n-drop handler over the event loop.
|
// Windows drag-n-drop fix code. Called by wry drag-n-drop handler over the event loop.
|
||||||
handleWindowsDragDrop() {
|
handleWindowsDragDrop() {
|
||||||
|
// @ts-ignore
|
||||||
if (window.dxDragLastElement) {
|
if (window.dxDragLastElement) {
|
||||||
const dragLeaveEvent = new DragEvent("dragleave", {
|
const dragLeaveEvent = new DragEvent("dragleave", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
window.dxDragLastElement.dispatchEvent(dragLeaveEvent);
|
window.dxDragLastElement.dispatchEvent(dragLeaveEvent);
|
||||||
|
|
||||||
let data = new DataTransfer();
|
let data = new DataTransfer();
|
||||||
@@ -199,23 +275,29 @@ export class NativeInterpreter extends JSChannel_ {
|
|||||||
cancelable: true,
|
cancelable: true,
|
||||||
dataTransfer: data,
|
dataTransfer: data,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
window.dxDragLastElement.dispatchEvent(dragDropEvent);
|
window.dxDragLastElement.dispatchEvent(dragDropEvent);
|
||||||
|
// @ts-ignore
|
||||||
window.dxDragLastElement = null;
|
window.dxDragLastElement = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowsDragOver(xPos, yPos) {
|
handleWindowsDragOver(xPos: number, yPos: number) {
|
||||||
const displayScaleFactor = window.devicePixelRatio || 1;
|
const displayScaleFactor = window.devicePixelRatio || 1;
|
||||||
xPos /= displayScaleFactor;
|
xPos /= displayScaleFactor;
|
||||||
yPos /= displayScaleFactor;
|
yPos /= displayScaleFactor;
|
||||||
const element = document.elementFromPoint(xPos, yPos);
|
const element = document.elementFromPoint(xPos, yPos);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
if (element != window.dxDragLastElement) {
|
if (element != window.dxDragLastElement) {
|
||||||
|
// @ts-ignore
|
||||||
if (window.dxDragLastElement) {
|
if (window.dxDragLastElement) {
|
||||||
const dragLeaveEvent = new DragEvent("dragleave", {
|
const dragLeaveEvent = new DragEvent("dragleave", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
});
|
});
|
||||||
|
// @ts-ignore
|
||||||
window.dxDragLastElement.dispatchEvent(dragLeaveEvent);
|
window.dxDragLastElement.dispatchEvent(dragLeaveEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,17 +306,21 @@ export class NativeInterpreter extends JSChannel_ {
|
|||||||
cancelable: true,
|
cancelable: true,
|
||||||
});
|
});
|
||||||
element.dispatchEvent(dragOverEvent);
|
element.dispatchEvent(dragOverEvent);
|
||||||
|
// @ts-ignore
|
||||||
window.dxDragLastElement = element;
|
window.dxDragLastElement = element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowsDragLeave() {
|
handleWindowsDragLeave() {
|
||||||
|
// @ts-ignore
|
||||||
if (window.dxDragLastElement) {
|
if (window.dxDragLastElement) {
|
||||||
const dragLeaveEvent = new DragEvent("dragleave", {
|
const dragLeaveEvent = new DragEvent("dragleave", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
});
|
});
|
||||||
|
// @ts-ignore
|
||||||
window.dxDragLastElement.dispatchEvent(dragLeaveEvent);
|
window.dxDragLastElement.dispatchEvent(dragLeaveEvent);
|
||||||
|
// @ts-ignore
|
||||||
window.dxDragLastElement = null;
|
window.dxDragLastElement = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,31 +352,30 @@ export class NativeInterpreter extends JSChannel_ {
|
|||||||
|
|
||||||
handleEvent(event: Event, name: string, bubbles: boolean) {
|
handleEvent(event: Event, name: string, bubbles: boolean) {
|
||||||
const target = event.target!;
|
const target = event.target!;
|
||||||
const realId = getTargetId(target)!;
|
const element = getTargetId(target)!;
|
||||||
const contents = serializeEvent(event, target);
|
const contents = serializeEvent(event, target);
|
||||||
|
|
||||||
// Handle the event on the virtualdom and then preventDefault if it also preventsDefault
|
// Handle the event on the virtualdom and then preventDefault if it also preventsDefault
|
||||||
// Some listeners
|
// Some listeners
|
||||||
let body = {
|
let body = {
|
||||||
name: name,
|
name,
|
||||||
data: contents,
|
data: contents,
|
||||||
element: realId,
|
element,
|
||||||
bubbles,
|
bubbles,
|
||||||
};
|
};
|
||||||
|
|
||||||
// liveview does not have synchronous event handling, so we need to send the event to the host
|
// liveview does not have synchronous event handling, so we need to send the event to the host
|
||||||
if (this.liveview) {
|
if (
|
||||||
// Okay, so the user might've requested some files to be read
|
this.liveview &&
|
||||||
if (
|
target instanceof HTMLInputElement &&
|
||||||
target instanceof HTMLInputElement &&
|
(event.type === "change" || event.type === "input")
|
||||||
(event.type === "change" || event.type === "input")
|
) {
|
||||||
) {
|
if (target.getAttribute("type") === "file") {
|
||||||
if (target.getAttribute("type") === "file") {
|
this.readFiles(target, contents, bubbles, element, name);
|
||||||
this.readFiles(target, contents, bubbles, realId, name);
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = this.sendSerializedEvent(body);
|
const response = this.sendSerializedEvent(body);
|
||||||
// capture/prevent default of the event if the virtualdom wants to
|
// capture/prevent default of the event if the virtualdom wants to
|
||||||
if (response) {
|
if (response) {
|
||||||
@@ -316,8 +401,7 @@ export class NativeInterpreter extends JSChannel_ {
|
|||||||
bubbles: boolean;
|
bubbles: boolean;
|
||||||
}): EventSyncResult | void {
|
}): EventSyncResult | void {
|
||||||
if (this.liveview) {
|
if (this.liveview) {
|
||||||
const message = this.serializeIpcMessage("user_event", body);
|
this.sendIpcMessage("user_event", body);
|
||||||
this.ipc.postMessage(message);
|
|
||||||
} else {
|
} else {
|
||||||
// Run the event handler on the virtualdom
|
// Run the event handler on the virtualdom
|
||||||
return handleVirtualdomEventSync(this.eventsPath, JSON.stringify(body));
|
return handleVirtualdomEventSync(this.eventsPath, JSON.stringify(body));
|
||||||
@@ -337,9 +421,7 @@ export class NativeInterpreter extends JSChannel_ {
|
|||||||
|
|
||||||
const href = a_element.getAttribute("href");
|
const href = a_element.getAttribute("href");
|
||||||
if (href !== "" && href !== null && href !== undefined) {
|
if (href !== "" && href !== null && href !== undefined) {
|
||||||
this.ipc.postMessage(
|
this.sendIpcMessage("browser_open", { href })
|
||||||
this.serializeIpcMessage("browser_open", { href })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// Handle serialization of the event data across the IPC boundarytype SerializedEvent = {};
|
// Handle serialization of the event data across the IPC boundary
|
||||||
|
//
|
||||||
import { retrieveSelectValue, retrieveValues } from "./form";
|
// Note that for files, we don't serialize their contents, expecting the renderer itself to handle
|
||||||
|
// reading the file contents if needed. Reading files is an async operation, and should be done lazily
|
||||||
|
// if the app needs it.
|
||||||
|
|
||||||
export type AppTouchEvent = TouchEvent;
|
export type AppTouchEvent = TouchEvent;
|
||||||
|
|
||||||
export type SerializedEvent = {
|
export type SerializedEvent = {
|
||||||
values?: { [key: string]: FormDataEntryValue[] };
|
values?: SerializedFormObject[];
|
||||||
value?: string;
|
value?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
@@ -17,7 +19,7 @@ export function serializeEvent(
|
|||||||
let contents = {};
|
let contents = {};
|
||||||
|
|
||||||
// merge the object into the contents
|
// merge the object into the contents
|
||||||
let extend = (obj: any) => (contents = { ...contents, ...obj });
|
let extend = (obj: SerializedEvent) => (contents = { ...contents, ...obj });
|
||||||
|
|
||||||
if (event instanceof WheelEvent) {
|
if (event instanceof WheelEvent) {
|
||||||
extend(serializeWheelEvent(event));
|
extend(serializeWheelEvent(event));
|
||||||
@@ -84,22 +86,20 @@ export function serializeEvent(
|
|||||||
|
|
||||||
// If there's any files, we need to serialize them
|
// If there's any files, we need to serialize them
|
||||||
if (event instanceof DragEvent) {
|
if (event instanceof DragEvent) {
|
||||||
// let files: { [key: string]: Uint8Array } = {};
|
let files: SerializedFormObject[] = [];
|
||||||
// if (event.dataTransfer && event.dataTransfer.files) {
|
if (event.dataTransfer && event.dataTransfer.files) {
|
||||||
// files["a"] = new Uint8Array(0);
|
for (let i = 0; i < event.dataTransfer.files.length; i++) {
|
||||||
// // files = {
|
let file = event.dataTransfer.files[i];
|
||||||
// // entries: Array.from(event.dataTransfer.files).map((file) => {
|
let data: SerializedFileData = {
|
||||||
// // return {
|
path: file.name,
|
||||||
// // name: file.name,
|
size: file.size,
|
||||||
// // type: file.type,
|
last_modified: file.lastModified,
|
||||||
// // size: file.size,
|
content_type: file.type,
|
||||||
// // last_modified: file.lastModified,
|
}
|
||||||
// // };
|
files.push({ key: file.name, file: data });
|
||||||
// // }
|
}
|
||||||
// // };
|
}
|
||||||
// // files = await serializeFileList(event.dataTransfer.files);
|
extend({ files });
|
||||||
// }
|
|
||||||
// extend({ files: files });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "scroll") {
|
if (event.type === "scroll") {
|
||||||
@@ -173,7 +173,7 @@ function serializeInputEvent(
|
|||||||
|
|
||||||
// Attempt to retrieve the values from the form
|
// Attempt to retrieve the values from the form
|
||||||
if (target instanceof HTMLElement) {
|
if (target instanceof HTMLElement) {
|
||||||
let values = retrieveValues(event, target);
|
let values = extractSerializedFormValues(event, target);
|
||||||
contents.values = values.values;
|
contents.values = values.values;
|
||||||
contents.valid = values.valid;
|
contents.valid = values.valid;
|
||||||
}
|
}
|
||||||
@@ -376,4 +376,85 @@ function serializeScrollEvent(event: Event): SerializedEvent {
|
|||||||
client_width: clientWidth,
|
client_width: clientWidth,
|
||||||
client_height: clientHeight,
|
client_height: clientHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type SerializedFormData = {
|
||||||
|
valid?: boolean;
|
||||||
|
values: SerializedFormObject[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SerializedFormObject = {
|
||||||
|
key: string;
|
||||||
|
text?: string;
|
||||||
|
file?: SerializedFileData;
|
||||||
|
}
|
||||||
|
export type SerializedFileData = {
|
||||||
|
// we store the real path of the file as the name.
|
||||||
|
// This makes it possible to simply read the file during event handling
|
||||||
|
path: string;
|
||||||
|
size?: number;
|
||||||
|
last_modified?: number;
|
||||||
|
content_type?: string;
|
||||||
|
contents?: string; // base64 encoded, if present. not required to br present
|
||||||
|
};
|
||||||
|
|
||||||
|
export function extractSerializedFormValues(event: Event, target: HTMLElement): SerializedFormData {
|
||||||
|
let contents: SerializedFormData = {
|
||||||
|
values: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// If there's a form...
|
||||||
|
let form = target.closest("form");
|
||||||
|
|
||||||
|
// If the target is an input, and the event is input or change, we want to get the value without going through the form
|
||||||
|
if (form) {
|
||||||
|
if (
|
||||||
|
event.type === "input"
|
||||||
|
|| event.type === "change"
|
||||||
|
|| event.type === "submit"
|
||||||
|
|| event.type === "reset"
|
||||||
|
|| event.type === "click"
|
||||||
|
) {
|
||||||
|
contents = retrieveFormValues(form);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: maybe encode spaces or something?
|
||||||
|
// We encode select multiple as a comma separated list which breaks... when there's commas in the values
|
||||||
|
function retrieveFormValues(form: HTMLFormElement): SerializedFormData {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const contents: SerializedFormObject[] = [];
|
||||||
|
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
if (value instanceof File) {
|
||||||
|
let fileData: SerializedFileData = {
|
||||||
|
path: value.name,
|
||||||
|
size: value.size,
|
||||||
|
last_modified: value.lastModified,
|
||||||
|
content_type: value.type,
|
||||||
|
};
|
||||||
|
contents.push({ key, file: fileData });
|
||||||
|
} else {
|
||||||
|
contents.push({ key, text: value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: form.checkValidity(),
|
||||||
|
values: contents
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function retrieveSelectValue(target: HTMLSelectElement): string[] {
|
||||||
|
// there might be multiple...
|
||||||
|
let options = target.selectedOptions;
|
||||||
|
let values = [];
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
values.push(options[i].value);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ use futures_util::{pin_mut, FutureExt};
|
|||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::task::{Context as TaskContext, Waker};
|
use std::task::{Context as TaskContext, Waker};
|
||||||
use std::{any::Any, collections::HashMap, rc::Rc};
|
use std::{any::Any, rc::Rc};
|
||||||
|
|
||||||
fn wrap_event_data<T: Any>(value: T) -> Rc<dyn Any> {
|
fn wrap_event_data<T: Any>(value: T) -> Rc<dyn Any> {
|
||||||
Rc::new(PlatformEventData::new(Box::new(value)))
|
Rc::new(PlatformEventData::new(Box::new(value)))
|
||||||
@@ -262,7 +262,7 @@ impl EventHandler for DioxusEventHandler<'_> {
|
|||||||
|
|
||||||
DomEventData::Input(data) => Some(wrap_event_data(NativeFormData {
|
DomEventData::Input(data) => Some(wrap_event_data(NativeFormData {
|
||||||
value: data.value.clone(),
|
value: data.value.clone(),
|
||||||
values: HashMap::new(),
|
values: vec![],
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// TODO: Implement IME handling
|
// TODO: Implement IME handling
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use dioxus_html::{
|
|||||||
};
|
};
|
||||||
use keyboard_types::{Code, Key, Location, Modifiers};
|
use keyboard_types::{Code, Key, Location, Modifiers};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub struct NativeConverter {}
|
pub struct NativeConverter {}
|
||||||
|
|
||||||
@@ -110,7 +109,7 @@ impl HtmlEventConverter for NativeConverter {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct NativeFormData {
|
pub struct NativeFormData {
|
||||||
pub value: String,
|
pub value: String,
|
||||||
pub values: HashMap<String, FormValue>,
|
pub values: Vec<(String, FormValue)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasFormData for NativeFormData {
|
impl HasFormData for NativeFormData {
|
||||||
@@ -122,9 +121,13 @@ impl HasFormData for NativeFormData {
|
|||||||
self.value.clone()
|
self.value.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn values(&self) -> HashMap<String, FormValue> {
|
fn values(&self) -> Vec<(String, FormValue)> {
|
||||||
self.values.clone()
|
self.values.clone()
|
||||||
}
|
}
|
||||||
|
fn valid(&self) -> bool {
|
||||||
|
// todo: actually implement validation here.
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasFileData for NativeFormData {
|
impl HasFileData for NativeFormData {
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ features = [
|
|||||||
"File",
|
"File",
|
||||||
"FileList",
|
"FileList",
|
||||||
"FileReader",
|
"FileReader",
|
||||||
"ReadableStream"
|
"ReadableStream",
|
||||||
|
"FormData"
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
use std::{any::Any, collections::HashMap};
|
use super::WebEventExt;
|
||||||
|
use crate::WebFileData;
|
||||||
use dioxus_html::{FileData, FormValue, HasFileData, HasFormData};
|
use dioxus_html::{FileData, FormValue, HasFileData, HasFormData};
|
||||||
use js_sys::Array;
|
use js_sys::Array;
|
||||||
use wasm_bindgen::JsValue;
|
use std::any::Any;
|
||||||
use wasm_bindgen::{prelude::wasm_bindgen, JsCast};
|
use wasm_bindgen::{prelude::wasm_bindgen, JsCast};
|
||||||
use web_sys::{Element, Event};
|
use web_sys::{Element, Event, FileReader};
|
||||||
|
|
||||||
use super::WebEventExt;
|
|
||||||
|
|
||||||
pub(crate) struct WebFormData {
|
pub(crate) struct WebFormData {
|
||||||
element: Element,
|
element: Element,
|
||||||
@@ -67,26 +65,29 @@ impl HasFormData for WebFormData {
|
|||||||
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener")
|
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn values(&self) -> HashMap<String, FormValue> {
|
fn values(&self) -> Vec<(String, FormValue)> {
|
||||||
let mut values = HashMap::new();
|
let mut values = Vec::new();
|
||||||
|
|
||||||
fn insert_value(map: &mut HashMap<String, FormValue>, key: String, new_value: String) {
|
|
||||||
map.entry(key.clone()).or_default().0.push(new_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to fill in form values
|
// try to fill in form values
|
||||||
if let Some(form) = self.element.dyn_ref::<web_sys::HtmlFormElement>() {
|
if let Some(form) = self.element.dyn_ref::<web_sys::HtmlFormElement>() {
|
||||||
let form_data = get_form_data(form);
|
let form_data = web_sys::FormData::new_with_form(form).unwrap();
|
||||||
for value in form_data.entries().into_iter().flatten() {
|
|
||||||
if let Ok(array) = value.dyn_into::<Array>() {
|
for entry in form_data.entries().into_iter().flatten() {
|
||||||
|
if let Ok(array) = entry.dyn_into::<Array>() {
|
||||||
if let Some(name) = array.get(0).as_string() {
|
if let Some(name) = array.get(0).as_string() {
|
||||||
if let Ok(item_values) = array.get(1).dyn_into::<Array>() {
|
let value = array.get(1);
|
||||||
item_values
|
if let Some(file) = value.dyn_ref::<web_sys::File>() {
|
||||||
.iter()
|
if file.name().is_empty() {
|
||||||
.filter_map(|v| v.as_string())
|
values.push((name, FormValue::File(None)));
|
||||||
.for_each(|v| insert_value(&mut values, name.clone(), v));
|
} else {
|
||||||
} else if let Ok(item_value) = array.get(1).dyn_into::<JsValue>() {
|
let data =
|
||||||
insert_value(&mut values, name, item_value.as_string().unwrap());
|
WebFileData::new(file.clone(), FileReader::new().unwrap());
|
||||||
|
let as_file = FileData::new(data);
|
||||||
|
|
||||||
|
values.push((name, FormValue::File(Some(as_file))));
|
||||||
|
}
|
||||||
|
} else if let Some(s) = value.as_string() {
|
||||||
|
values.push((name, FormValue::Text(s)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +95,9 @@ impl HasFormData for WebFormData {
|
|||||||
} else if let Some(select) = self.element.dyn_ref::<web_sys::HtmlSelectElement>() {
|
} else if let Some(select) = self.element.dyn_ref::<web_sys::HtmlSelectElement>() {
|
||||||
// try to fill in select element values
|
// try to fill in select element values
|
||||||
let options = get_select_data(select);
|
let options = get_select_data(select);
|
||||||
values.insert("options".to_string(), FormValue(options));
|
for option in &options {
|
||||||
|
values.push((select.name(), FormValue::Text(option.clone())));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
values
|
values
|
||||||
@@ -103,6 +106,14 @@ impl HasFormData for WebFormData {
|
|||||||
fn as_any(&self) -> &dyn Any {
|
fn as_any(&self) -> &dyn Any {
|
||||||
&self.event as &dyn Any
|
&self.event as &dyn Any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn valid(&self) -> bool {
|
||||||
|
self.event
|
||||||
|
.target()
|
||||||
|
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
|
||||||
|
.map(|input| input.check_validity())
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasFileData for WebFormData {
|
impl HasFileData for WebFormData {
|
||||||
@@ -135,20 +146,3 @@ export function get_select_data(select) {
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
fn get_select_data(select: &web_sys::HtmlSelectElement) -> Vec<String>;
|
fn get_select_data(select: &web_sys::HtmlSelectElement) -> Vec<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// web-sys does not expose the keys api for form data, so we need to manually bind to it
|
|
||||||
#[wasm_bindgen(inline_js = r#"
|
|
||||||
export function get_form_data(form) {
|
|
||||||
let values = new Map();
|
|
||||||
const formData = new FormData(form);
|
|
||||||
|
|
||||||
for (let name of formData.keys()) {
|
|
||||||
values.set(name, formData.getAll(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
"#)]
|
|
||||||
extern "C" {
|
|
||||||
fn get_form_data(form: &web_sys::HtmlFormElement) -> js_sys::Map;
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user