Merge branch 'master' into add-file-data-drag-event
This commit is contained in:
commit
56798b3d1c
407 files changed
+16602
-11298
No files matched your search
@@ -139,7 +139,6 @@ Missing Features
|
||||
Missing examples
|
||||
- Shared state
|
||||
- Root-less element groups
|
||||
- Spread props
|
||||
- Custom elements
|
||||
- Component Children: Pass children into child components
|
||||
- Render To string: Render a mounted virtualdom to a string
|
||||
|
||||
@@ -53,8 +53,7 @@ fn app(cx: Scope) -> Element {
|
||||
};
|
||||
|
||||
cx.render(rsx! (
|
||||
div {
|
||||
style: "{CONTAINER_STYLE}",
|
||||
div { style: "{CONTAINER_STYLE}",
|
||||
div {
|
||||
style: "{RECT_STYLE}",
|
||||
// focusing is necessary to catch keyboard events
|
||||
@@ -62,7 +61,7 @@ fn app(cx: Scope) -> Element {
|
||||
|
||||
onmousemove: move |event| log_event(Event::MouseMove(event)),
|
||||
onclick: move |event| log_event(Event::MouseClick(event)),
|
||||
ondblclick: move |event| log_event(Event::MouseDoubleClick(event)),
|
||||
ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event)),
|
||||
onmousedown: move |event| log_event(Event::MouseDown(event)),
|
||||
onmouseup: move |event| log_event(Event::MouseUp(event)),
|
||||
|
||||
@@ -77,9 +76,7 @@ fn app(cx: Scope) -> Element {
|
||||
|
||||
"Hover, click, type or scroll to see the info down below"
|
||||
}
|
||||
div {
|
||||
events.read().iter().map(|event| rsx!( div { "{event:?}" } ))
|
||||
},
|
||||
},
|
||||
div { events.read().iter().map(|event| rsx!( div { "{event:?}" } )) }
|
||||
}
|
||||
))
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
button {
|
||||
onclick: |_| async move {
|
||||
println!("hello, desktop!");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
println!("goodbye, desktop!");
|
||||
},
|
||||
"hello, desktop!"
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -6,13 +6,13 @@ This calculator version uses React-style state management. All state is held as
|
||||
use dioxus::events::*;
|
||||
use dioxus::html::input_data::keyboard_types::Key;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::{Config, WindowBuilder};
|
||||
use dioxus_desktop::{Config, LogicalSize, WindowBuilder};
|
||||
|
||||
fn main() {
|
||||
let config = Config::new().with_window(
|
||||
WindowBuilder::default()
|
||||
.with_title("Calculator")
|
||||
.with_inner_size(dioxus_desktop::LogicalSize::new(300.0, 500.0)),
|
||||
.with_inner_size(LogicalSize::new(300.0, 500.0)),
|
||||
);
|
||||
|
||||
dioxus_desktop::launch_cfg(app, config);
|
||||
@@ -62,6 +62,7 @@ fn app(cx: Scope) -> Element {
|
||||
div { id: "wrapper",
|
||||
div { class: "app",
|
||||
div { class: "calculator",
|
||||
tabindex: "0",
|
||||
onkeydown: handle_key_down_event,
|
||||
div { class: "calculator-display", val.to_string() }
|
||||
div { class: "calculator-keypad",
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element {
|
||||
|
||||
use_future!(cx, || async move {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
count += 1;
|
||||
println!("current: {count}");
|
||||
}
|
||||
|
||||
+4
-18
@@ -1,7 +1,6 @@
|
||||
//! This example shows how to create a popup window and send data back to the parent window.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::use_window;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
fn main() {
|
||||
@@ -9,7 +8,6 @@ fn main() {
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let window = use_window(cx);
|
||||
let emails_sent = use_ref(cx, Vec::new);
|
||||
|
||||
let tx = use_coroutine(cx, |mut rx: UnboundedReceiver<String>| {
|
||||
@@ -27,14 +25,8 @@ fn app(cx: Scope) -> Element {
|
||||
|
||||
button {
|
||||
onclick: move |_| {
|
||||
let dom = VirtualDom::new_with_props(compose, ComposeProps {
|
||||
app_tx: tx.clone()
|
||||
});
|
||||
|
||||
// this returns a weak reference to the other window
|
||||
// Be careful not to keep a strong reference to the other window or it will never be dropped
|
||||
// and the window will never close.
|
||||
window.new_window(dom, Default::default());
|
||||
let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() });
|
||||
dioxus_desktop::window().new_window(dom, Default::default());
|
||||
},
|
||||
"Click to compose a new email"
|
||||
}
|
||||
@@ -57,7 +49,6 @@ struct ComposeProps {
|
||||
|
||||
fn compose(cx: Scope<ComposeProps>) -> Element {
|
||||
let user_input = use_state(cx, String::new);
|
||||
let window = use_window(cx);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
@@ -66,17 +57,12 @@ fn compose(cx: Scope<ComposeProps>) -> Element {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
cx.props.app_tx.send(user_input.get().clone());
|
||||
window.close();
|
||||
dioxus_desktop::window().close();
|
||||
},
|
||||
"Click to send"
|
||||
}
|
||||
|
||||
input {
|
||||
oninput: move |e| {
|
||||
user_input.set(e.value.clone());
|
||||
},
|
||||
value: "{user_input}"
|
||||
}
|
||||
input { oninput: move |e| user_input.set(e.value()), value: "{user_input}" }
|
||||
}
|
||||
})
|
||||
}
|
||||
+1
-1
@@ -22,7 +22,7 @@ fn app(cx: Scope) -> Element {
|
||||
input {
|
||||
value: "{counter}",
|
||||
oninput: move |e| {
|
||||
if let Ok(value) = e.value.parse::<usize>() {
|
||||
if let Ok(value) = e.value().parse::<usize>() {
|
||||
counters.make_mut()[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
+27
-63
@@ -35,14 +35,16 @@ fn App(cx: Scope) -> Element {
|
||||
rel: "stylesheet",
|
||||
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
|
||||
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
|
||||
crossorigin: "anonymous",
|
||||
crossorigin: "anonymous"
|
||||
}
|
||||
|
||||
style { "
|
||||
style {
|
||||
"
|
||||
.red {{
|
||||
background-color: rgb(202, 60, 60) !important;
|
||||
}}
|
||||
" }
|
||||
"
|
||||
}
|
||||
|
||||
h1 { "Dioxus CRM Example" }
|
||||
|
||||
@@ -57,16 +59,8 @@ fn ClientList(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
h2 { "List of Clients" }
|
||||
|
||||
Link {
|
||||
to: Route::ClientAdd {},
|
||||
class: "pure-button pure-button-primary",
|
||||
"Add Client"
|
||||
}
|
||||
Link {
|
||||
to: Route::Settings {},
|
||||
class: "pure-button",
|
||||
"Settings"
|
||||
}
|
||||
Link { to: Route::ClientAdd {}, class: "pure-button pure-button-primary", "Add Client" }
|
||||
Link { to: Route::Settings {}, class: "pure-button", "Settings" }
|
||||
|
||||
clients.read().iter().map(|client| rsx! {
|
||||
div {
|
||||
@@ -87,8 +81,6 @@ fn ClientAdd(cx: Scope) -> Element {
|
||||
let last_name = use_state(cx, String::new);
|
||||
let description = use_state(cx, String::new);
|
||||
|
||||
let navigator = use_navigator(cx);
|
||||
|
||||
cx.render(rsx! {
|
||||
h2 { "Add new Client" }
|
||||
|
||||
@@ -96,79 +88,55 @@ fn ClientAdd(cx: Scope) -> Element {
|
||||
class: "pure-form pure-form-aligned",
|
||||
onsubmit: move |_| {
|
||||
let mut clients = clients.write();
|
||||
|
||||
clients.push(Client {
|
||||
first_name: first_name.to_string(),
|
||||
last_name: last_name.to_string(),
|
||||
description: description.to_string(),
|
||||
});
|
||||
|
||||
navigator.push(Route::ClientList {});
|
||||
clients
|
||||
.push(Client {
|
||||
first_name: first_name.to_string(),
|
||||
last_name: last_name.to_string(),
|
||||
description: description.to_string(),
|
||||
});
|
||||
dioxus_router::router().push(Route::ClientList {});
|
||||
},
|
||||
|
||||
fieldset {
|
||||
div {
|
||||
class: "pure-control-group",
|
||||
label {
|
||||
"for": "first_name",
|
||||
"First Name"
|
||||
}
|
||||
div { class: "pure-control-group",
|
||||
label { "for": "first_name", "First Name" }
|
||||
input {
|
||||
id: "first_name",
|
||||
"type": "text",
|
||||
placeholder: "First Name…",
|
||||
required: "",
|
||||
value: "{first_name}",
|
||||
oninput: move |e| first_name.set(e.value.clone())
|
||||
oninput: move |e| first_name.set(e.value())
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: "pure-control-group",
|
||||
label {
|
||||
"for": "last_name",
|
||||
"Last Name"
|
||||
}
|
||||
div { class: "pure-control-group",
|
||||
label { "for": "last_name", "Last Name" }
|
||||
input {
|
||||
id: "last_name",
|
||||
"type": "text",
|
||||
placeholder: "Last Name…",
|
||||
required: "",
|
||||
value: "{last_name}",
|
||||
oninput: move |e| last_name.set(e.value.clone())
|
||||
oninput: move |e| last_name.set(e.value())
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: "pure-control-group",
|
||||
label {
|
||||
"for": "description",
|
||||
"Description"
|
||||
}
|
||||
div { class: "pure-control-group",
|
||||
label { "for": "description", "Description" }
|
||||
textarea {
|
||||
id: "description",
|
||||
placeholder: "Description…",
|
||||
value: "{description}",
|
||||
oninput: move |e| description.set(e.value.clone())
|
||||
oninput: move |e| description.set(e.value())
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: "pure-controls",
|
||||
button {
|
||||
"type": "submit",
|
||||
class: "pure-button pure-button-primary",
|
||||
"Save"
|
||||
}
|
||||
Link {
|
||||
to: Route::ClientList {},
|
||||
class: "pure-button pure-button-primary red",
|
||||
"Cancel"
|
||||
}
|
||||
div { class: "pure-controls",
|
||||
button { "type": "submit", class: "pure-button pure-button-primary", "Save" }
|
||||
Link { to: Route::ClientList {}, class: "pure-button pure-button-primary red", "Cancel" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -189,10 +157,6 @@ fn Settings(cx: Scope) -> Element {
|
||||
"Remove all Clients"
|
||||
}
|
||||
|
||||
Link {
|
||||
to: Route::ClientList {},
|
||||
class: "pure-button",
|
||||
"Go back"
|
||||
}
|
||||
Link { to: Route::ClientList {}, class: "pure-button", "Go back" }
|
||||
})
|
||||
}
|
||||
@@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element {
|
||||
p {
|
||||
"This should show an image:"
|
||||
}
|
||||
img { src: "examples/assets/logo.png" }
|
||||
img { src: mg!(image("examples/assets/logo.png").format(ImageType::Avif)).to_string() }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::{use_asset_handler, wry::http::Response};
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
use_asset_handler(cx, "logos", |request, response| {
|
||||
// Note that the "logos" prefix is stripped from the URI
|
||||
//
|
||||
// However, the asset is absolute to its "virtual folder" - meaning it starts with a leading slash
|
||||
if request.uri().path() != "/logo.png" {
|
||||
return;
|
||||
}
|
||||
|
||||
response.respond(Response::new(include_bytes!("./assets/logo.png").to_vec()));
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
img {
|
||||
src: "/logos/logo.png"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
+11
-16
@@ -1,4 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::{core::CapturedError, prelude::*};
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(App);
|
||||
@@ -6,30 +6,25 @@ fn main() {
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> Element {
|
||||
let val = use_state(cx, || "0.0001");
|
||||
|
||||
let num = match val.parse::<f32>() {
|
||||
Err(_) => return cx.render(rsx!("Parsing failed")),
|
||||
Ok(num) => num,
|
||||
};
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "The parsed value is {num}" }
|
||||
button {
|
||||
onclick: move |_| val.set("invalid"),
|
||||
"Set an invalid number"
|
||||
ErrorBoundary {
|
||||
handle_error: |error: CapturedError| rsx! {"Found error {error}"},
|
||||
DemoC {
|
||||
x: 1
|
||||
}
|
||||
}
|
||||
(0..5).map(|i| rsx! {
|
||||
DemoC { x: i }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn DemoC(cx: Scope, x: i32) -> Element {
|
||||
let result = Err("Error");
|
||||
|
||||
result.throw()?;
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 {
|
||||
"asdasdasdasd {x}"
|
||||
"{x}"
|
||||
}
|
||||
})
|
||||
}
|
||||
+9
-14
@@ -5,26 +5,21 @@ fn main() {
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let eval_provider = use_eval(cx);
|
||||
|
||||
let future = use_future(cx, (), |_| {
|
||||
to_owned![eval_provider];
|
||||
async move {
|
||||
let eval = eval_provider(
|
||||
r#"
|
||||
let future = use_future(cx, (), |_| async move {
|
||||
let eval = eval(
|
||||
r#"
|
||||
dioxus.send("Hi from JS!");
|
||||
let msg = await dioxus.recv();
|
||||
console.log(msg);
|
||||
return "hello world";
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
eval.send("Hi from Rust!".into()).unwrap();
|
||||
let res = eval.recv().await.unwrap();
|
||||
println!("{:?}", eval.await);
|
||||
res
|
||||
}
|
||||
eval.send("Hi from Rust!".into()).unwrap();
|
||||
let res = eval.recv().await.unwrap();
|
||||
println!("{:?}", eval.await);
|
||||
res
|
||||
});
|
||||
|
||||
match future.value() {
|
||||
|
||||
@@ -18,13 +18,14 @@ fn main() {
|
||||
);
|
||||
}
|
||||
|
||||
const _STYLE: &str = mg!(file("./examples/assets/fileexplorer.css"));
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let files = use_ref(cx, Files::new);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", }
|
||||
style { include_str!("./assets/fileexplorer.css") }
|
||||
header {
|
||||
i { class: "material-icons icon-menu", "menu" }
|
||||
h1 { "Files: ", files.read().current() }
|
||||
|
||||
@@ -16,7 +16,7 @@ fn App(cx: Scope) -> Element {
|
||||
r#type: "checkbox",
|
||||
checked: "{enable_directory_upload}",
|
||||
oninput: move |evt| {
|
||||
enable_directory_upload.set(evt.value.parse().unwrap());
|
||||
enable_directory_upload.set(evt.value().parse().unwrap());
|
||||
},
|
||||
},
|
||||
"Enable directory upload"
|
||||
@@ -30,7 +30,7 @@ fn App(cx: Scope) -> Element {
|
||||
onchange: |evt| {
|
||||
to_owned![files_uploaded];
|
||||
async move {
|
||||
if let Some(file_engine) = &evt.files {
|
||||
if let Some(file_engine) = &evt.files() {
|
||||
let files = file_engine.files();
|
||||
for file_name in files {
|
||||
sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@ fn app(cx: Scope) -> Element {
|
||||
div {
|
||||
h1 { "Form" }
|
||||
form {
|
||||
onsubmit: move |ev| println!("Submitted {:?}", ev.values),
|
||||
oninput: move |ev| println!("Input {:?}", ev.values),
|
||||
onsubmit: move |ev| println!("Submitted {:?}", ev.values()),
|
||||
oninput: move |ev| println!("Input {:?}", ev.values()),
|
||||
input { r#type: "text", name: "username" }
|
||||
input { r#type: "text", name: "full-name" }
|
||||
input { r#type: "password", name: "password" }
|
||||
|
||||
+17
-20
@@ -8,33 +8,30 @@ fn main() {
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let onsubmit = move |evt: FormEvent| {
|
||||
cx.spawn(async move {
|
||||
let resp = reqwest::Client::new()
|
||||
.post("http://localhost:8080/login")
|
||||
.form(&[
|
||||
("username", &evt.values["username"]),
|
||||
("password", &evt.values["password"]),
|
||||
])
|
||||
.send()
|
||||
.await;
|
||||
let onsubmit = move |evt: FormEvent| async move {
|
||||
let resp = reqwest::Client::new()
|
||||
.post("http://localhost:8080/login")
|
||||
.form(&[
|
||||
("username", &evt.values()["username"]),
|
||||
("password", &evt.values()["password"]),
|
||||
])
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match resp {
|
||||
// Parse data from here, such as storing a response token
|
||||
Ok(_data) => println!("Login successful!"),
|
||||
match resp {
|
||||
// Parse data from here, such as storing a response token
|
||||
Ok(_data) => println!("Login successful!"),
|
||||
|
||||
//Handle any errors from the fetch here
|
||||
Err(_err) => {
|
||||
println!("Login failed - you need a login server running on localhost:8080.")
|
||||
}
|
||||
//Handle any errors from the fetch here
|
||||
Err(_err) => {
|
||||
println!("Login failed - you need a login server running on localhost:8080.")
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "Login" }
|
||||
form {
|
||||
onsubmit: onsubmit,
|
||||
form { onsubmit: onsubmit,
|
||||
input { r#type: "text", id: "username", name: "username" }
|
||||
label { "Username" }
|
||||
br {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
target/
|
||||
**/*.rs.bk
|
||||
|
||||
# tauri-mobile
|
||||
# cargo-mobile2
|
||||
.cargo/
|
||||
/gen
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ frameworks = ["WebKit"]
|
||||
[dependencies]
|
||||
anyhow = "1.0.56"
|
||||
log = "0.4.11"
|
||||
wry = "0.28.0"
|
||||
wry = "0.35.0"
|
||||
dioxus = { path = "../../packages/dioxus" }
|
||||
dioxus-desktop = { path = "../../packages/desktop", features = [
|
||||
"tokio_runtime",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Right now, Dioxus supports mobile targets including iOS and Android. However, our tooling is not mature enough to include the build commands directly.
|
||||
|
||||
This project was generated using [tauri-mobile](https://github.com/tauri-apps/tauri-mobile). We have yet to integrate this generation into the Dioxus-CLI. The open issue for this is [#1157](https://github.com/DioxusLabs/dioxus/issues/1157).
|
||||
This project was generated using [cargo-mobile2](https://github.com/tauri-apps/cargo-mobile2). We have yet to integrate this generation into the Dioxus-CLI. The open issue for this is [#1157](https://github.com/DioxusLabs/dioxus/issues/1157).
|
||||
|
||||
## Running on iOS
|
||||
|
||||
|
||||
@@ -5,14 +5,12 @@ fn main() {
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let window = dioxus_desktop::use_window(cx);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
let dom = VirtualDom::new(popup);
|
||||
window.new_window(dom, Default::default());
|
||||
dioxus_desktop::window().new_window(dom, Default::default());
|
||||
},
|
||||
"New Window"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
/dist
|
||||
.env
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "openid_auth_demo"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
dioxus-logger = "0.4.1"
|
||||
dioxus = { path = "../../packages/dioxus", version = "*" }
|
||||
dioxus-router = { path = "../../packages/router", version = "*" }
|
||||
dioxus-web = { path = "../../packages/web", version = "*" }
|
||||
fermi = { path = "../../packages/fermi", version = "*" }
|
||||
form_urlencoded = "1.2.0"
|
||||
gloo-storage = "0.3.0"
|
||||
log = "0.4"
|
||||
openidconnect = "3.4.0"
|
||||
reqwest = "0.11.20"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.105"
|
||||
thiserror = "1.0.48"
|
||||
uuid = "1.4"
|
||||
web-sys = { version = "0.3", features = ["Request", "Document"] }
|
||||
@@ -0,0 +1,47 @@
|
||||
[application]
|
||||
|
||||
# dioxus project name
|
||||
name = "OpenID Connect authentication demo"
|
||||
|
||||
# default platfrom
|
||||
# you can also use `dioxus serve/build --platform XXX` to use other platform
|
||||
# value: web | desktop
|
||||
default_platform = "web"
|
||||
|
||||
# Web `build` & `serve` dist path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (static) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "OpenID Connect authentication demo"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
index_on_404 = true
|
||||
|
||||
watch_path = ["src"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Javascript code file
|
||||
# serve: [dev-server] only
|
||||
script = []
|
||||
|
||||
[application.plugins]
|
||||
|
||||
available = true
|
||||
|
||||
required = []
|
||||
@@ -0,0 +1,13 @@
|
||||
# OpenID Connect example to show how to authenticate an user
|
||||
|
||||
The environment variables in `.cargo/config.toml` must be set in order for this example to work(if this example is just being compiled from the root workspace, the `.cargo/config.toml` from the root workspace must be set as stated in the [Cargo book](https://doc.rust-lang.org/cargo/reference/config.html)).
|
||||
|
||||
Once they are set, you can run `dx serve`
|
||||
|
||||
### Environment variables summary
|
||||
|
||||
```DIOXUS_FRONT_ISSUER_URL``` The openid-connect's issuer url
|
||||
|
||||
```DIOXUS_FRONT_CLIENT_ID``` The openid-connect's client id
|
||||
|
||||
```DIOXUS_FRONT_URL``` The url the frontend is supposed to be running on, it could be for example `http://localhost:8080`
|
||||
@@ -0,0 +1,2 @@
|
||||
pub const DIOXUS_FRONT_AUTH_TOKEN: &str = "auth_token";
|
||||
pub const DIOXUS_FRONT_AUTH_REQUEST: &str = "auth_request";
|
||||
@@ -0,0 +1,20 @@
|
||||
use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Discovery error: {0}")]
|
||||
OpenIdConnect(
|
||||
#[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>,
|
||||
),
|
||||
#[error("Parsing error: {0}")]
|
||||
Parse(#[from] url::ParseError),
|
||||
#[error("Request token error: {0}")]
|
||||
RequestToken(
|
||||
#[from]
|
||||
RequestTokenError<
|
||||
openidconnect::reqwest::Error<reqwest::Error>,
|
||||
StandardErrorResponse<CoreErrorResponseType>,
|
||||
>,
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#![allow(non_snake_case)]
|
||||
use dioxus::prelude::*;
|
||||
use fermi::*;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use log::LevelFilter;
|
||||
pub(crate) mod constants;
|
||||
pub(crate) mod errors;
|
||||
pub(crate) mod model;
|
||||
pub(crate) mod oidc;
|
||||
pub(crate) mod props;
|
||||
pub(crate) mod router;
|
||||
pub(crate) mod storage;
|
||||
pub(crate) mod views;
|
||||
use oidc::{AuthRequestState, AuthTokenState};
|
||||
use router::Route;
|
||||
|
||||
use dioxus_router::prelude::*;
|
||||
|
||||
use crate::{
|
||||
constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
|
||||
oidc::ClientState,
|
||||
};
|
||||
pub static FERMI_CLIENT: fermi::AtomRef<ClientState> = AtomRef(|_| ClientState::default());
|
||||
|
||||
// An option is required to prevent the component from being constantly refreshed
|
||||
pub static FERMI_AUTH_TOKEN: fermi::AtomRef<Option<AuthTokenState>> = AtomRef(|_| None);
|
||||
pub static FERMI_AUTH_REQUEST: fermi::AtomRef<Option<AuthRequestState>> = AtomRef(|_| None);
|
||||
|
||||
pub static DIOXUS_FRONT_ISSUER_URL: &str = env!("DIOXUS_FRONT_ISSUER_URL");
|
||||
pub static DIOXUS_FRONT_CLIENT_ID: &str = env!("DIOXUS_FRONT_CLIENT_ID");
|
||||
pub static DIOXUS_FRONT_URL: &str = env!("DIOXUS_FRONT_URL");
|
||||
|
||||
fn App(cx: Scope) -> Element {
|
||||
use_init_atom_root(cx);
|
||||
|
||||
// Retrieve the value stored in the browser's storage
|
||||
let stored_auth_token = LocalStorage::get(DIOXUS_FRONT_AUTH_TOKEN)
|
||||
.ok()
|
||||
.unwrap_or(AuthTokenState::default());
|
||||
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||
if fermi_auth_token.read().is_none() {
|
||||
*fermi_auth_token.write() = Some(stored_auth_token);
|
||||
}
|
||||
|
||||
let stored_auth_request = LocalStorage::get(DIOXUS_FRONT_AUTH_REQUEST)
|
||||
.ok()
|
||||
.unwrap_or(AuthRequestState::default());
|
||||
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||
if fermi_auth_request.read().is_none() {
|
||||
*fermi_auth_request.write() = Some(stored_auth_request);
|
||||
}
|
||||
render! { Router::<Route> {} }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
dioxus_logger::init(LevelFilter::Info).expect("failed to init logger");
|
||||
console_error_panic_hook::set_once();
|
||||
log::info!("starting app");
|
||||
dioxus_web::launch(App);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod user;
|
||||
@@ -0,0 +1,7 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use openidconnect::{
|
||||
core::{CoreClient, CoreErrorResponseType, CoreIdToken, CoreResponseType, CoreTokenResponse},
|
||||
reqwest::async_http_client,
|
||||
url::Url,
|
||||
AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, ClientId, CsrfToken, IssuerUrl,
|
||||
LogoutRequest, Nonce, ProviderMetadataWithLogout, RedirectUrl, RefreshToken, RequestTokenError,
|
||||
StandardErrorResponse,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{props::client::ClientProps, DIOXUS_FRONT_CLIENT_ID};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ClientState {
|
||||
pub oidc_client: Option<ClientProps>,
|
||||
}
|
||||
|
||||
/// State that holds the nonce and authorization url and the nonce generated to log in an user
|
||||
#[derive(Clone, Deserialize, Serialize, Default)]
|
||||
pub struct AuthRequestState {
|
||||
pub auth_request: Option<AuthRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct AuthRequest {
|
||||
pub nonce: Nonce,
|
||||
pub authorize_url: String,
|
||||
}
|
||||
|
||||
/// State the tokens returned once the user is authenticated
|
||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||
pub struct AuthTokenState {
|
||||
/// Token used to identify the user
|
||||
pub id_token: Option<CoreIdToken>,
|
||||
/// Token used to refresh the tokens if they expire
|
||||
pub refresh_token: Option<RefreshToken>,
|
||||
}
|
||||
|
||||
pub fn email(
|
||||
client: CoreClient,
|
||||
id_token: CoreIdToken,
|
||||
nonce: Nonce,
|
||||
) -> Result<String, ClaimsVerificationError> {
|
||||
match id_token.claims(&client.id_token_verifier(), &nonce) {
|
||||
Ok(claims) => Ok(claims.clone().email().unwrap().to_string()),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authorize_url(client: CoreClient) -> AuthRequest {
|
||||
let (authorize_url, _csrf_state, nonce) = client
|
||||
.authorize_url(
|
||||
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
||||
CsrfToken::new_random,
|
||||
Nonce::new_random,
|
||||
)
|
||||
.add_scope(openidconnect::Scope::new("email".to_string()))
|
||||
.add_scope(openidconnect::Scope::new("profile".to_string()))
|
||||
.url();
|
||||
AuthRequest {
|
||||
authorize_url: authorize_url.to_string(),
|
||||
nonce,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init_provider_metadata() -> Result<ProviderMetadataWithLogout, crate::errors::Error> {
|
||||
let issuer_url = IssuerUrl::new(crate::DIOXUS_FRONT_ISSUER_URL.to_string())?;
|
||||
Ok(ProviderMetadataWithLogout::discover_async(issuer_url, async_http_client).await?)
|
||||
}
|
||||
|
||||
pub async fn init_oidc_client() -> Result<(ClientId, CoreClient), crate::errors::Error> {
|
||||
let client_id = ClientId::new(crate::DIOXUS_FRONT_CLIENT_ID.to_string());
|
||||
let provider_metadata = init_provider_metadata().await?;
|
||||
let client_secret = None;
|
||||
let redirect_url = RedirectUrl::new(format!("{}/login", crate::DIOXUS_FRONT_URL))?;
|
||||
|
||||
Ok((
|
||||
client_id.clone(),
|
||||
CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret)
|
||||
.set_redirect_uri(redirect_url),
|
||||
))
|
||||
}
|
||||
|
||||
///TODO: Add pkce_pacifier
|
||||
pub async fn token_response(
|
||||
oidc_client: CoreClient,
|
||||
code: String,
|
||||
) -> Result<CoreTokenResponse, crate::errors::Error> {
|
||||
// let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
Ok(oidc_client
|
||||
.exchange_code(AuthorizationCode::new(code.clone()))
|
||||
// .set_pkce_verifier(pkce_verifier)
|
||||
.request_async(async_http_client)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn exchange_refresh_token(
|
||||
oidc_client: CoreClient,
|
||||
refresh_token: RefreshToken,
|
||||
) -> Result<
|
||||
CoreTokenResponse,
|
||||
RequestTokenError<
|
||||
openidconnect::reqwest::Error<reqwest::Error>,
|
||||
StandardErrorResponse<CoreErrorResponseType>,
|
||||
>,
|
||||
> {
|
||||
oidc_client
|
||||
.exchange_refresh_token(&refresh_token)
|
||||
.request_async(async_http_client)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn log_out_url(id_token_hint: CoreIdToken) -> Result<Url, crate::errors::Error> {
|
||||
let provider_metadata = init_provider_metadata().await?;
|
||||
let end_session_url = provider_metadata
|
||||
.additional_metadata()
|
||||
.clone()
|
||||
.end_session_endpoint
|
||||
.unwrap();
|
||||
let logout_request: LogoutRequest = LogoutRequest::from(end_session_url);
|
||||
Ok(logout_request
|
||||
.set_client_id(ClientId::new(DIOXUS_FRONT_CLIENT_ID.to_string()))
|
||||
.set_id_token_hint(&id_token_hint)
|
||||
.http_get_url())
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use dioxus::prelude::*;
|
||||
use openidconnect::{core::CoreClient, ClientId};
|
||||
|
||||
#[derive(Props, Clone, Debug)]
|
||||
pub struct ClientProps {
|
||||
pub client: CoreClient,
|
||||
pub client_id: ClientId,
|
||||
}
|
||||
|
||||
impl PartialEq for ClientProps {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.client_id == other.client_id
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientProps {
|
||||
pub fn new(client_id: ClientId, client: CoreClient) -> Self {
|
||||
ClientProps { client_id, client }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod client;
|
||||
@@ -0,0 +1,17 @@
|
||||
use crate::views::{header::AuthHeader, home::Home, login::Login, not_found::NotFound};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router::prelude::*;
|
||||
|
||||
#[derive(Routable, Clone)]
|
||||
pub enum Route {
|
||||
#[layout(AuthHeader)]
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
// https://dioxuslabs.com/learn/0.4/router/reference/routes#query-segments
|
||||
#[route("/login?:query_string")]
|
||||
Login { query_string: String },
|
||||
#[end_layout]
|
||||
#[route("/:..route")]
|
||||
NotFound { route: Vec<String> },
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use fermi::UseAtomRef;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
|
||||
oidc::{AuthRequestState, AuthTokenState},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct StorageEntry<T> {
|
||||
pub key: String,
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
pub trait PersistentWrite<T: Serialize + Clone> {
|
||||
fn persistent_set(atom_ref: &UseAtomRef<Option<T>>, entry: Option<T>);
|
||||
}
|
||||
|
||||
impl PersistentWrite<AuthTokenState> for AuthTokenState {
|
||||
fn persistent_set(
|
||||
atom_ref: &UseAtomRef<Option<AuthTokenState>>,
|
||||
entry: Option<AuthTokenState>,
|
||||
) {
|
||||
*atom_ref.write() = entry.clone();
|
||||
LocalStorage::set(DIOXUS_FRONT_AUTH_TOKEN, entry).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl PersistentWrite<AuthRequestState> for AuthRequestState {
|
||||
fn persistent_set(
|
||||
atom_ref: &UseAtomRef<Option<AuthRequestState>>,
|
||||
entry: Option<AuthRequestState>,
|
||||
) {
|
||||
*atom_ref.write() = entry.clone();
|
||||
LocalStorage::set(DIOXUS_FRONT_AUTH_REQUEST, entry).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
use crate::{
|
||||
oidc::{
|
||||
authorize_url, email, exchange_refresh_token, init_oidc_client, log_out_url,
|
||||
AuthRequestState, AuthTokenState, ClientState,
|
||||
},
|
||||
props::client::ClientProps,
|
||||
router::Route,
|
||||
storage::PersistentWrite,
|
||||
FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT,
|
||||
};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router::prelude::{Link, Outlet};
|
||||
use fermi::*;
|
||||
use openidconnect::{url::Url, OAuth2TokenResponse, TokenResponse};
|
||||
|
||||
#[component]
|
||||
pub fn LogOut(cx: Scope<ClientProps>) -> Element {
|
||||
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||
let fermi_auth_token_read = fermi_auth_token.read().clone();
|
||||
let log_out_url_state = use_state(cx, || None::<Option<Result<Url, crate::errors::Error>>>);
|
||||
cx.render(match fermi_auth_token_read {
|
||||
Some(fermi_auth_token_read) => match fermi_auth_token_read.id_token.clone() {
|
||||
Some(id_token) => match log_out_url_state.get() {
|
||||
Some(log_out_url_result) => match log_out_url_result {
|
||||
Some(uri) => match uri {
|
||||
Ok(uri) => {
|
||||
rsx! {
|
||||
Link {
|
||||
onclick: move |_| {
|
||||
{
|
||||
AuthTokenState::persistent_set(
|
||||
fermi_auth_token,
|
||||
Some(AuthTokenState::default()),
|
||||
);
|
||||
}
|
||||
},
|
||||
to: uri.to_string(),
|
||||
"Log out"
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
rsx! {
|
||||
div { format!{"Failed to load disconnection url: {:?}", error} }
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! { div { "Loading... Please wait" } }
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let logout_url_task = move || {
|
||||
cx.spawn({
|
||||
let log_out_url_state = log_out_url_state.to_owned();
|
||||
async move {
|
||||
let logout_url = log_out_url(id_token).await;
|
||||
let logout_url_option = Some(logout_url);
|
||||
log_out_url_state.set(Some(logout_url_option));
|
||||
}
|
||||
})
|
||||
};
|
||||
logout_url_task();
|
||||
rsx! { div{"Loading log out url... Please wait"}}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! {{}}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! {{}}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RefreshToken(cx: Scope<ClientProps>) -> Element {
|
||||
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||
let fermi_auth_token_read = fermi_auth_token.read().clone();
|
||||
cx.render(match fermi_auth_token_read {
|
||||
Some(fermi_auth_client_read) => match fermi_auth_client_read.refresh_token {
|
||||
Some(refresh_token) => {
|
||||
let fermi_auth_token = fermi_auth_token.to_owned();
|
||||
let fermi_auth_request = fermi_auth_request.to_owned();
|
||||
let client = cx.props.client.clone();
|
||||
let exchange_refresh_token_spawn = move || {
|
||||
cx.spawn({
|
||||
async move {
|
||||
let exchange_refresh_token =
|
||||
exchange_refresh_token(client, refresh_token).await;
|
||||
match exchange_refresh_token {
|
||||
Ok(response_token) => {
|
||||
AuthTokenState::persistent_set(
|
||||
&fermi_auth_token,
|
||||
Some(AuthTokenState {
|
||||
id_token: response_token.id_token().cloned(),
|
||||
refresh_token: response_token.refresh_token().cloned(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
Err(_error) => {
|
||||
AuthTokenState::persistent_set(
|
||||
&fermi_auth_token,
|
||||
Some(AuthTokenState::default()),
|
||||
);
|
||||
AuthRequestState::persistent_set(
|
||||
&fermi_auth_request,
|
||||
Some(AuthRequestState::default()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
exchange_refresh_token_spawn();
|
||||
rsx! { div { "Refreshing session, please wait" } }
|
||||
}
|
||||
None => {
|
||||
rsx! { div { "Id token expired and no refresh token found" } }
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! {{}}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoadClient(cx: Scope) -> Element {
|
||||
let init_client_future = use_future(cx, (), |_| async move { init_oidc_client().await });
|
||||
let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
|
||||
cx.render(match init_client_future.value() {
|
||||
Some(client_props) => match client_props {
|
||||
Ok((client_id, client)) => {
|
||||
*fermi_client.write() = ClientState {
|
||||
oidc_client: Some(ClientProps::new(client_id.clone(), client.clone())),
|
||||
};
|
||||
rsx! {
|
||||
div { "Client successfully loaded" }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
rsx! {
|
||||
div { format!{"Failed to load client: {:?}", error} }
|
||||
log::info!{"Failed to load client: {:?}", error},
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! {
|
||||
div {
|
||||
div { "Loading client, please wait" }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn AuthHeader(cx: Scope) -> Element {
|
||||
let auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||
let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
|
||||
let client = fermi_client.read().oidc_client.clone();
|
||||
let auth_request_read = fermi_auth_request.read().clone();
|
||||
let auth_token_read = auth_token.read().clone();
|
||||
cx.render(match (client, auth_request_read, auth_token_read) {
|
||||
// We have everything we need to attempt to authenticate the user
|
||||
(Some(client_props), Some(auth_request), Some(auth_token)) => {
|
||||
match auth_request.auth_request {
|
||||
Some(auth_request) => {
|
||||
match auth_token.id_token {
|
||||
Some(id_token) => {
|
||||
match email(
|
||||
client_props.client.clone(),
|
||||
id_token.clone(),
|
||||
auth_request.nonce.clone(),
|
||||
) {
|
||||
Ok(email) => {
|
||||
rsx! {
|
||||
div {
|
||||
div { email }
|
||||
LogOut { client_id: client_props.client_id, client: client_props.client }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Id token failed to be decoded
|
||||
Err(error) => match error {
|
||||
// Id token failed to be decoded because it expired, we refresh it
|
||||
openidconnect::ClaimsVerificationError::Expired(_message) => {
|
||||
log::info!("Token expired");
|
||||
rsx! {
|
||||
div {
|
||||
RefreshToken {client_id: client_props.client_id, client: client_props.client}
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Other issue with token decoding
|
||||
_ => {
|
||||
log::info!("Other issue with token");
|
||||
rsx! {
|
||||
div {
|
||||
div { error.to_string() }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
// User is not logged in
|
||||
None => {
|
||||
rsx! {
|
||||
div {
|
||||
Link { to: auth_request.authorize_url.clone(), "Log in" }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let auth_request = authorize_url(client_props.client);
|
||||
AuthRequestState::persistent_set(
|
||||
fermi_auth_request,
|
||||
Some(AuthRequestState {
|
||||
auth_request: Some(auth_request),
|
||||
}),
|
||||
);
|
||||
rsx! { div { "Loading nonce" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Client is not initialized yet, we need it for everything
|
||||
(None, _, _) => {
|
||||
rsx! { LoadClient {} }
|
||||
}
|
||||
// We need everything loaded before doing anything
|
||||
(_client, _auth_request, _auth_token) => {
|
||||
rsx! {{}}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub fn Home(cx: Scope) -> Element {
|
||||
render! { div { "Hello world" } }
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use crate::{
|
||||
oidc::{token_response, AuthRequestState, AuthTokenState},
|
||||
router::Route,
|
||||
storage::PersistentWrite,
|
||||
DIOXUS_FRONT_URL, FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT,
|
||||
};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router::prelude::{Link, NavigationTarget};
|
||||
use fermi::*;
|
||||
use openidconnect::{OAuth2TokenResponse, TokenResponse};
|
||||
|
||||
#[component]
|
||||
pub fn Login(cx: Scope, query_string: String) -> Element {
|
||||
let fermi_client = use_atom_ref(cx, &FERMI_CLIENT);
|
||||
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||
let home_url: NavigationTarget<Route> = DIOXUS_FRONT_URL.parse().unwrap();
|
||||
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||
let client = fermi_client.read().oidc_client.clone();
|
||||
let auth_token_read = fermi_auth_token.read().clone();
|
||||
cx.render(match (client, auth_token_read) {
|
||||
(Some(client_props), Some(auth_token_read)) => {
|
||||
match (auth_token_read.id_token, auth_token_read.refresh_token) {
|
||||
(Some(_id_token), Some(_refresh_token)) => {
|
||||
rsx! {
|
||||
div { "Sign in successful" }
|
||||
Link { to: home_url, "Go back home" }
|
||||
}
|
||||
}
|
||||
// If the refresh token is set but not the id_token, there was an error, we just go back home and reset their value
|
||||
(None, Some(_)) | (Some(_), None) => {
|
||||
rsx! {
|
||||
div { "Error while attempting to log in" }
|
||||
Link {
|
||||
to: home_url,
|
||||
onclick: move |_| {
|
||||
AuthTokenState::persistent_set(fermi_auth_token, Some(AuthTokenState::default()));
|
||||
AuthRequestState::persistent_set(
|
||||
fermi_auth_request,
|
||||
Some(AuthRequestState::default()),
|
||||
);
|
||||
},
|
||||
"Go back home"
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
let mut query_pairs = form_urlencoded::parse(query_string.as_bytes());
|
||||
let code_pair = query_pairs.find(|(key, _value)| key == "code");
|
||||
match code_pair {
|
||||
Some((_key, code)) => {
|
||||
let auth_code = code.to_string();
|
||||
let token_response_spawn = move ||{
|
||||
cx.spawn({
|
||||
let fermi_auth_token = fermi_auth_token.to_owned();
|
||||
async move {
|
||||
let token_response_result = token_response(client_props.client, auth_code).await;
|
||||
match token_response_result{
|
||||
Ok(token_response) => {
|
||||
let id_token = token_response.id_token().unwrap();
|
||||
AuthTokenState::persistent_set(&fermi_auth_token, Some(AuthTokenState {
|
||||
id_token: Some(id_token.clone()),
|
||||
refresh_token: token_response.refresh_token().cloned()
|
||||
}));
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!{"{error}"};
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
token_response_spawn();
|
||||
rsx!{ div {} }
|
||||
}
|
||||
None => {
|
||||
rsx! { div { "No code provided" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(_, _) => {
|
||||
rsx! {{}}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub(crate) mod header;
|
||||
pub(crate) mod home;
|
||||
pub(crate) mod login;
|
||||
pub(crate) mod not_found;
|
||||
@@ -0,0 +1,7 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn NotFound(cx: Scope, route: Vec<String>) -> Element {
|
||||
let routes = route.join("");
|
||||
render! {rsx! {div{routes}}}
|
||||
}
|
||||
@@ -16,8 +16,20 @@ fn app(cx: Scope) -> Element {
|
||||
a: "asd".to_string(),
|
||||
c: "asd".to_string(),
|
||||
d: Some("asd".to_string()),
|
||||
e: Some("asd".to_string()),
|
||||
}
|
||||
Button {
|
||||
a: "asd".to_string(),
|
||||
b: "asd".to_string(),
|
||||
c: "asd".to_string(),
|
||||
d: Some("asd".to_string()),
|
||||
e: "asd".to_string(),
|
||||
}
|
||||
Button {
|
||||
a: "asd".to_string(),
|
||||
c: "asd".to_string(),
|
||||
d: Some("asd".to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -1,13 +1,11 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::{tao::dpi::PhysicalPosition, use_window, LogicalSize, WindowBuilder};
|
||||
use dioxus_desktop::{tao::dpi::PhysicalPosition, LogicalSize, WindowBuilder};
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch_cfg(app, make_config());
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let window = use_window(cx);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
width: "100%",
|
||||
@@ -19,7 +17,7 @@ fn app(cx: Scope) -> Element {
|
||||
width: "100%",
|
||||
height: "10px",
|
||||
background_color: "black",
|
||||
onmousedown: move |_| window.drag(),
|
||||
onmousedown: move |_| dioxus_desktop::window().drag(),
|
||||
}
|
||||
|
||||
"This is an overlay!"
|
||||
|
||||
@@ -21,7 +21,7 @@ use dioxus::events::*;
|
||||
use dioxus::html::input_data::keyboard_types::Key;
|
||||
use dioxus::html::MouseEvent;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::wry::application::dpi::LogicalSize;
|
||||
use dioxus_desktop::tao::dpi::LogicalSize;
|
||||
use dioxus_desktop::{Config, WindowBuilder};
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "query_segments_demo"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
@@ -14,29 +14,35 @@ use dioxus_router::prelude::*;
|
||||
#[derive(Routable, Clone)]
|
||||
#[rustfmt::skip]
|
||||
enum Route {
|
||||
// segments that start with ?: are query segments
|
||||
#[route("/blog?:query_params")]
|
||||
// segments that start with ?:.. are query segments that capture the entire query
|
||||
#[route("/blog?:..query_params")]
|
||||
BlogPost {
|
||||
// You must include query segments in child variants
|
||||
query_params: BlogQuerySegments,
|
||||
query_params: ManualBlogQuerySegments,
|
||||
},
|
||||
// segments that follow the ?:field&:other_field syntax are query segments that follow the standard url query syntax
|
||||
#[route("/autoblog?:name&:surname")]
|
||||
AutomaticBlogPost {
|
||||
name: String,
|
||||
surname: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct BlogQuerySegments {
|
||||
struct ManualBlogQuerySegments {
|
||||
name: String,
|
||||
surname: String,
|
||||
}
|
||||
|
||||
/// The display impl needs to display the query in a way that can be parsed:
|
||||
impl Display for BlogQuerySegments {
|
||||
impl Display for ManualBlogQuerySegments {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "name={}&surname={}", self.name, self.surname)
|
||||
}
|
||||
}
|
||||
|
||||
/// The query segment is anything that implements https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQuery.html. You can implement that trait for a struct if you want to parse multiple query parameters.
|
||||
impl FromQuery for BlogQuerySegments {
|
||||
/// The query segment is anything that implements <https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQuery.html>. You can implement that trait for a struct if you want to parse multiple query parameters.
|
||||
impl FromQuery for ManualBlogQuerySegments {
|
||||
fn from_query(query: &str) -> Self {
|
||||
let mut name = None;
|
||||
let mut surname = None;
|
||||
@@ -57,13 +63,21 @@ impl FromQuery for BlogQuerySegments {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn BlogPost(cx: Scope, query_params: BlogQuerySegments) -> Element {
|
||||
fn BlogPost(cx: Scope, query_params: ManualBlogQuerySegments) -> Element {
|
||||
render! {
|
||||
div{"This is your blogpost with a query segment:"}
|
||||
div{format!("{:?}", query_params)}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn AutomaticBlogPost(cx: Scope, name: String, surname: String) -> Element {
|
||||
render! {
|
||||
div{"This is your blogpost with a query segment:"}
|
||||
div{format!("name={}&surname={}", name, surname)}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> Element {
|
||||
render! { Router::<Route>{} }
|
||||
|
||||
@@ -53,6 +53,7 @@ fn App(cx: Scope) -> Element {
|
||||
let formatting = "formatting!";
|
||||
let formatting_tuple = ("a", "b");
|
||||
let lazy_fmt = format_args!("lazily formatted text");
|
||||
let asd = 123;
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
// Elements
|
||||
@@ -80,6 +81,10 @@ fn App(cx: Scope) -> Element {
|
||||
// pass simple rust expressions in
|
||||
class: lazy_fmt,
|
||||
id: format_args!("attributes can be passed lazily with std::fmt::Arguments"),
|
||||
class: "asd",
|
||||
class: "{asd}",
|
||||
// if statements can be used to conditionally render attributes
|
||||
class: if formatting.contains("form") { "{asd}" },
|
||||
div {
|
||||
class: {
|
||||
const WORD: &str = "expressions";
|
||||
|
||||
@@ -64,7 +64,7 @@ fn DataEditor(cx: Scope, id: usize) -> Element {
|
||||
fn DataView(cx: Scope, id: usize) -> Element {
|
||||
let cool_data = use_shared_state::<CoolData>(cx).unwrap();
|
||||
|
||||
let oninput = |e: FormEvent| cool_data.write().set(*id, e.value.clone());
|
||||
let oninput = |e: FormEvent| cool_data.write().set(*id, e.value());
|
||||
|
||||
let cool_data = cool_data.read();
|
||||
let my_data = &cool_data.view(id).unwrap();
|
||||
|
||||
+23
-1
@@ -6,11 +6,17 @@ fn main() {
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let running = dioxus_signals::use_signal(cx, || true);
|
||||
let mut count = dioxus_signals::use_signal(cx, || 0);
|
||||
let saved_values = dioxus_signals::use_signal(cx, || vec![0.to_string()]);
|
||||
|
||||
// Signals can be used in async functions without an explicit clone since they're 'static and Copy
|
||||
// Signals are backed by a runtime that is designed to deeply integrate with Dioxus apps
|
||||
use_future!(cx, || async move {
|
||||
loop {
|
||||
count += 1;
|
||||
if running.value() {
|
||||
count += 1;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(400)).await;
|
||||
}
|
||||
});
|
||||
@@ -19,9 +25,25 @@ fn app(cx: Scope) -> Element {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button { onclick: move |_| running.toggle(), "Toggle counter" }
|
||||
button { onclick: move |_| saved_values.push(count.value().to_string()), "Save this value" }
|
||||
button { onclick: move |_| saved_values.write().clear(), "Clear saved values" }
|
||||
|
||||
// We can do boolean operations on the current signal value
|
||||
if count.value() > 5 {
|
||||
rsx!{ h2 { "High five!" } }
|
||||
}
|
||||
|
||||
// We can cleanly map signals with iterators
|
||||
for value in saved_values.read().iter() {
|
||||
h3 { "Saved value: {value}" }
|
||||
}
|
||||
|
||||
// We can also use the signal value as a slice
|
||||
if let [ref first, .., ref last] = saved_values.read().as_slice() {
|
||||
rsx! { li { "First and last: {first}, {last}" } }
|
||||
} else {
|
||||
rsx! { "No saved values" }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let _ = dom.rebuild();
|
||||
let html = dioxus_ssr::render(&dom);
|
||||
|
||||
println!("{}", html);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
render! {
|
||||
Component {
|
||||
width: "10px",
|
||||
extra_data: "hello{1}",
|
||||
extra_data2: "hello{2}",
|
||||
height: "10px",
|
||||
left: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
|
||||
render! {
|
||||
audio { ..cx.props.attributes, "1: {cx.props.extra_data}\n2: {cx.props.extra_data2}" }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct Props<'a> {
|
||||
#[props(extends = GlobalAttributes)]
|
||||
attributes: Vec<Attribute<'a>>,
|
||||
extra_data: &'a str,
|
||||
extra_data2: &'a str,
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_signals::use_signal;
|
||||
use futures_util::{future, stream, Stream, StreamExt};
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let count = use_signal(cx, || 10);
|
||||
|
||||
use_future(cx, (), |_| async move {
|
||||
let mut stream = some_stream();
|
||||
|
||||
while let Some(second) = stream.next().await {
|
||||
count.set(second);
|
||||
}
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
})
|
||||
}
|
||||
|
||||
fn some_stream() -> std::pin::Pin<Box<dyn Stream<Item = i32>>> {
|
||||
Box::pin(
|
||||
stream::once(future::ready(0)).chain(stream::iter(1..).then(|second| async move {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
second
|
||||
})),
|
||||
)
|
||||
}
|
||||
@@ -18,4 +18,4 @@ dioxus = { path = "../../packages/dioxus" }
|
||||
dioxus-desktop = { path = "../../packages/desktop" }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
dioxus-web = { path = "../../packages/web" }
|
||||
dioxus-web = { path = "../../packages/web" }
|
||||
@@ -30,7 +30,7 @@ watch_path = ["src", "public"]
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = ["/tailwind.css"]
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
@@ -7,7 +7,7 @@ This example shows how an app might be styled with TailwindCSS.
|
||||
1. Install the Dioxus CLI:
|
||||
|
||||
```bash
|
||||
cargo install --git https://github.com/DioxusLabs/cli
|
||||
cargo install dioxus-cli
|
||||
```
|
||||
|
||||
2. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{color:inherit;border-top-width:1px;height:0}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (width>=640px){.container{max-width:640px}}@media (width>=768px){.container{max-width:768px}}@media (width>=1024px){.container{max-width:1024px}}@media (width>=1280px){.container{max-width:1280px}}@media (width>=1536px){.container{max-width:1536px}}.mx-auto{margin-left:auto;margin-right:auto}.mb-16{margin-bottom:4rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-5{margin-right:1.25rem}.mt-4{margin-top:1rem}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.w-10{width:2.5rem}.w-4{width:1rem}.w-5\/6{width:83.3333%}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border-0{border-width:0}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.object-cover{-o-object-fit:cover;object-fit:cover}.object-center{-o-object-position:center;object-position:center}.p-2{padding:.5rem}.p-5{padding:1.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-24{padding-top:6rem;padding-bottom:6rem}.text-center{text-align:center}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-medium{font-weight:500}.leading-relaxed{line-heiLine truncated
|
||||
@@ -2,21 +2,23 @@
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
const _STYLE: &str = mg!(file("./public/tailwind.css"));
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
dioxus_desktop::launch_cfg(
|
||||
app,
|
||||
dioxus_desktop::Config::new()
|
||||
.with_custom_head(r#"<link rel="stylesheet" href="public/tailwind.css">"#.to_string()),
|
||||
);
|
||||
dioxus_desktop::launch(app);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
dioxus_web::launch(app);
|
||||
}
|
||||
|
||||
pub fn app(cx: Scope) -> Element {
|
||||
let grey_background = true;
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
header { class: "text-gray-400 bg-gray-900 body-font",
|
||||
header {
|
||||
class: "text-gray-400 body-font",
|
||||
// you can use optional attributes to optionally apply a tailwind class
|
||||
class: if grey_background { "bg-gray-900" },
|
||||
div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center",
|
||||
a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0",
|
||||
StacksIcon {}
|
||||
|
||||
@@ -17,7 +17,7 @@ fn app(cx: Scope) -> Element {
|
||||
rows: "10",
|
||||
cols: "80",
|
||||
value: "{model}",
|
||||
oninput: move |e| model.set(e.value.clone()),
|
||||
oninput: move |e| model.set(e.value().clone()),
|
||||
}
|
||||
})
|
||||
}
|
||||
+133
-79
@@ -7,6 +7,8 @@ fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
const _STYLE: &str = mg!(file("./examples/assets/todomvc.css"));
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum FilterState {
|
||||
All,
|
||||
@@ -24,8 +26,6 @@ pub struct TodoItem {
|
||||
pub fn app(cx: Scope<()>) -> Element {
|
||||
let todos = use_state(cx, im_rc::HashMap::<u32, TodoItem>::default);
|
||||
let filter = use_state(cx, || FilterState::All);
|
||||
let draft = use_state(cx, || "".to_string());
|
||||
let todo_id = use_state(cx, || 0);
|
||||
|
||||
// Filter the todos based on the filter state
|
||||
let mut filtered_todos = todos
|
||||
@@ -47,45 +47,10 @@ pub fn app(cx: Scope<()>) -> Element {
|
||||
|
||||
let show_clear_completed = todos.values().any(|todo| todo.checked);
|
||||
|
||||
let selected = |state| {
|
||||
if *filter == state {
|
||||
"selected"
|
||||
} else {
|
||||
"false"
|
||||
}
|
||||
};
|
||||
|
||||
cx.render(rsx! {
|
||||
section { class: "todoapp",
|
||||
style { include_str!("./assets/todomvc.css") }
|
||||
header { class: "header",
|
||||
h1 {"todos"}
|
||||
input {
|
||||
class: "new-todo",
|
||||
placeholder: "What needs to be done?",
|
||||
value: "{draft}",
|
||||
autofocus: "true",
|
||||
oninput: move |evt| {
|
||||
draft.set(evt.value.clone());
|
||||
},
|
||||
onkeydown: move |evt| {
|
||||
if evt.key() == Key::Enter && !draft.is_empty() {
|
||||
todos.make_mut().insert(
|
||||
**todo_id,
|
||||
TodoItem {
|
||||
id: **todo_id,
|
||||
checked: false,
|
||||
contents: draft.to_string(),
|
||||
},
|
||||
);
|
||||
*todo_id.make_mut() += 1;
|
||||
draft.set("".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
section {
|
||||
class: "main",
|
||||
TodoHeader { todos: todos }
|
||||
section { class: "main",
|
||||
if !todos.is_empty() {
|
||||
rsx! {
|
||||
input {
|
||||
@@ -111,43 +76,58 @@ pub fn app(cx: Scope<()>) -> Element {
|
||||
}))
|
||||
}
|
||||
(!todos.is_empty()).then(|| rsx!(
|
||||
footer { class: "footer",
|
||||
span { class: "todo-count",
|
||||
strong {"{active_todo_count} "}
|
||||
span {"{active_todo_text} left"}
|
||||
}
|
||||
ul { class: "filters",
|
||||
for (state, state_text, url) in [
|
||||
(FilterState::All, "All", "#/"),
|
||||
(FilterState::Active, "Active", "#/active"),
|
||||
(FilterState::Completed, "Completed", "#/completed"),
|
||||
] {
|
||||
li {
|
||||
a {
|
||||
href: url,
|
||||
class: selected(state),
|
||||
onclick: move |_| filter.set(state),
|
||||
prevent_default: "onclick",
|
||||
state_text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
show_clear_completed.then(|| rsx!(
|
||||
button {
|
||||
class: "clear-completed",
|
||||
onclick: move |_| todos.make_mut().retain(|_, todo| !todo.checked),
|
||||
"Clear completed"
|
||||
}
|
||||
))
|
||||
ListFooter {
|
||||
active_todo_count: active_todo_count,
|
||||
active_todo_text: active_todo_text,
|
||||
show_clear_completed: show_clear_completed,
|
||||
todos: todos,
|
||||
filter: filter,
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
footer { class: "info",
|
||||
p { "Double-click to edit a todo" }
|
||||
p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
|
||||
p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
|
||||
PageFooter {}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
pub struct TodoHeaderProps<'a> {
|
||||
todos: &'a UseState<im_rc::HashMap<u32, TodoItem>>,
|
||||
}
|
||||
|
||||
pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
|
||||
let draft = use_state(cx, || "".to_string());
|
||||
let todo_id = use_state(cx, || 0);
|
||||
|
||||
cx.render(rsx! {
|
||||
header { class: "header",
|
||||
h1 { "todos" }
|
||||
input {
|
||||
class: "new-todo",
|
||||
placeholder: "What needs to be done?",
|
||||
value: "{draft}",
|
||||
autofocus: "true",
|
||||
oninput: move |evt| {
|
||||
draft.set(evt.value().clone());
|
||||
},
|
||||
onkeydown: move |evt| {
|
||||
if evt.key() == Key::Enter && !draft.is_empty() {
|
||||
cx.props
|
||||
.todos
|
||||
.make_mut()
|
||||
.insert(
|
||||
**todo_id,
|
||||
TodoItem {
|
||||
id: **todo_id,
|
||||
checked: false,
|
||||
contents: draft.to_string(),
|
||||
},
|
||||
);
|
||||
*todo_id.make_mut() += 1;
|
||||
draft.set("".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -167,8 +147,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
||||
let editing = if **is_editing { "editing" } else { "" };
|
||||
|
||||
cx.render(rsx!{
|
||||
li {
|
||||
class: "{completed} {editing}",
|
||||
li { class: "{completed} {editing}",
|
||||
div { class: "view",
|
||||
input {
|
||||
class: "toggle",
|
||||
@@ -176,26 +155,28 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
||||
id: "cbg-{todo.id}",
|
||||
checked: "{todo.checked}",
|
||||
oninput: move |evt| {
|
||||
cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap();
|
||||
cx.props.todos.make_mut()[&cx.props.id].checked = evt.value().parse().unwrap();
|
||||
}
|
||||
}
|
||||
label {
|
||||
r#for: "cbg-{todo.id}",
|
||||
ondblclick: move |_| is_editing.set(true),
|
||||
ondoubleclick: move |_| is_editing.set(true),
|
||||
prevent_default: "onclick",
|
||||
"{todo.contents}"
|
||||
}
|
||||
button {
|
||||
class: "destroy",
|
||||
onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); },
|
||||
prevent_default: "onclick",
|
||||
onclick: move |_| {
|
||||
cx.props.todos.make_mut().remove(&todo.id);
|
||||
},
|
||||
prevent_default: "onclick"
|
||||
}
|
||||
}
|
||||
is_editing.then(|| rsx!{
|
||||
input {
|
||||
class: "edit",
|
||||
value: "{todo.contents}",
|
||||
oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value.clone(),
|
||||
oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value(),
|
||||
autofocus: "true",
|
||||
onfocusout: move |_| is_editing.set(false),
|
||||
onkeydown: move |evt| {
|
||||
@@ -209,3 +190,76 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
pub struct ListFooterProps<'a> {
|
||||
todos: &'a UseState<im_rc::HashMap<u32, TodoItem>>,
|
||||
active_todo_count: usize,
|
||||
active_todo_text: &'a str,
|
||||
show_clear_completed: bool,
|
||||
filter: &'a UseState<FilterState>,
|
||||
}
|
||||
|
||||
pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
|
||||
let active_todo_count = cx.props.active_todo_count;
|
||||
let active_todo_text = cx.props.active_todo_text;
|
||||
|
||||
let selected = |state| {
|
||||
if *cx.props.filter == state {
|
||||
"selected"
|
||||
} else {
|
||||
"false"
|
||||
}
|
||||
};
|
||||
|
||||
cx.render(rsx! {
|
||||
footer { class: "footer",
|
||||
span { class: "todo-count",
|
||||
strong { "{active_todo_count} " }
|
||||
span { "{active_todo_text} left" }
|
||||
}
|
||||
ul { class: "filters",
|
||||
for (state , state_text , url) in [
|
||||
(FilterState::All, "All", "#/"),
|
||||
(FilterState::Active, "Active", "#/active"),
|
||||
(FilterState::Completed, "Completed", "#/completed"),
|
||||
] {
|
||||
li {
|
||||
a {
|
||||
href: url,
|
||||
class: selected(state),
|
||||
onclick: move |_| cx.props.filter.set(state),
|
||||
prevent_default: "onclick",
|
||||
state_text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cx.props.show_clear_completed {
|
||||
cx.render(rsx! {
|
||||
button {
|
||||
class: "clear-completed",
|
||||
onclick: move |_| cx.props.todos.make_mut().retain(|_, todo| !todo.checked),
|
||||
"Clear completed"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn PageFooter(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
footer { class: "info",
|
||||
p { "Double-click to edit a todo" }
|
||||
p {
|
||||
"Created by "
|
||||
a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }
|
||||
}
|
||||
p {
|
||||
"Part of "
|
||||
a { href: "http://todomvc.com", "TodoMVC" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::wry::http;
|
||||
use dioxus_desktop::wry::http::Response;
|
||||
use dioxus_desktop::{use_asset_handler, AssetRequest};
|
||||
use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
|
||||
use std::{io::SeekFrom, path::PathBuf};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncSeekExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
|
||||
|
||||
fn main() {
|
||||
let video_file = PathBuf::from(VIDEO_PATH);
|
||||
if !video_file.exists() {
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
println!("Downloading video file...");
|
||||
let video_url =
|
||||
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
|
||||
let mut response = reqwest::get(video_url).await.unwrap();
|
||||
let mut file = tokio::fs::File::create(&video_file).await.unwrap();
|
||||
while let Some(chunk) = response.chunk().await.unwrap() {
|
||||
file.write_all(&chunk).await.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
use_asset_handler(cx, "videos", move |request, responder| {
|
||||
// Using dioxus::spawn works, but is slower than a dedicated thread
|
||||
tokio::task::spawn(async move {
|
||||
let video_file = PathBuf::from(VIDEO_PATH);
|
||||
let mut file = tokio::fs::File::open(&video_file).await.unwrap();
|
||||
|
||||
match get_stream_response(&mut file, &request).await {
|
||||
Ok(response) => responder.respond(response),
|
||||
Err(err) => eprintln!("Error: {}", err),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
render! {
|
||||
div {
|
||||
video {
|
||||
src: "/videos/test_video.mp4",
|
||||
autoplay: true,
|
||||
controls: true,
|
||||
width: 640,
|
||||
height: 480
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This was taken from wry's example
|
||||
async fn get_stream_response(
|
||||
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
|
||||
request: &AssetRequest,
|
||||
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
// get stream length
|
||||
let len = {
|
||||
let old_pos = asset.stream_position().await?;
|
||||
let len = asset.seek(SeekFrom::End(0)).await?;
|
||||
asset.seek(SeekFrom::Start(old_pos)).await?;
|
||||
len
|
||||
};
|
||||
|
||||
let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4");
|
||||
|
||||
// if the webview sent a range header, we need to send a 206 in return
|
||||
// Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
|
||||
let http_response = if let Some(range_header) = request.headers().get("range") {
|
||||
let not_satisfiable = || {
|
||||
ResponseBuilder::new()
|
||||
.status(StatusCode::RANGE_NOT_SATISFIABLE)
|
||||
.header(CONTENT_RANGE, format!("bytes */{len}"))
|
||||
.body(vec![])
|
||||
};
|
||||
|
||||
// parse range header
|
||||
let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) {
|
||||
ranges
|
||||
.iter()
|
||||
// map the output back to spec range <start-end>, example: 0-499
|
||||
.map(|r| (r.start, r.start + r.length - 1))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
return Ok(not_satisfiable()?);
|
||||
};
|
||||
|
||||
/// The Maximum bytes we send in one range
|
||||
const MAX_LEN: u64 = 1000 * 1024;
|
||||
|
||||
if ranges.len() == 1 {
|
||||
let &(start, mut end) = ranges.first().unwrap();
|
||||
|
||||
// check if a range is not satisfiable
|
||||
//
|
||||
// this should be already taken care of by HttpRange::parse
|
||||
// but checking here again for extra assurance
|
||||
if start >= len || end >= len || end < start {
|
||||
return Ok(not_satisfiable()?);
|
||||
}
|
||||
|
||||
// adjust end byte for MAX_LEN
|
||||
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
|
||||
|
||||
// calculate number of bytes needed to be read
|
||||
let bytes_to_read = end + 1 - start;
|
||||
|
||||
// allocate a buf with a suitable capacity
|
||||
let mut buf = Vec::with_capacity(bytes_to_read as usize);
|
||||
// seek the file to the starting byte
|
||||
asset.seek(SeekFrom::Start(start)).await?;
|
||||
// read the needed bytes
|
||||
asset.take(bytes_to_read).read_to_end(&mut buf).await?;
|
||||
|
||||
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
|
||||
resp = resp.header(CONTENT_LENGTH, end + 1 - start);
|
||||
resp = resp.status(StatusCode::PARTIAL_CONTENT);
|
||||
resp.body(buf)
|
||||
} else {
|
||||
let mut buf = Vec::new();
|
||||
let ranges = ranges
|
||||
.iter()
|
||||
.filter_map(|&(start, mut end)| {
|
||||
// filter out unsatisfiable ranges
|
||||
//
|
||||
// this should be already taken care of by HttpRange::parse
|
||||
// but checking here again for extra assurance
|
||||
if start >= len || end >= len || end < start {
|
||||
None
|
||||
} else {
|
||||
// adjust end byte for MAX_LEN
|
||||
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
|
||||
Some((start, end))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let boundary = format!("{:x}", rand::random::<u64>());
|
||||
let boundary_sep = format!("\r\n--{boundary}\r\n");
|
||||
let boundary_closer = format!("\r\n--{boundary}\r\n");
|
||||
|
||||
resp = resp.header(
|
||||
CONTENT_TYPE,
|
||||
format!("multipart/byteranges; boundary={boundary}"),
|
||||
);
|
||||
|
||||
for (end, start) in ranges {
|
||||
// a new range is being written, write the range boundary
|
||||
buf.write_all(boundary_sep.as_bytes()).await?;
|
||||
|
||||
// write the needed headers `Content-Type` and `Content-Range`
|
||||
buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())
|
||||
.await?;
|
||||
buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
|
||||
.await?;
|
||||
|
||||
// write the separator to indicate the start of the range body
|
||||
buf.write_all("\r\n".as_bytes()).await?;
|
||||
|
||||
// calculate number of bytes needed to be read
|
||||
let bytes_to_read = end + 1 - start;
|
||||
|
||||
let mut local_buf = vec![0_u8; bytes_to_read as usize];
|
||||
asset.seek(SeekFrom::Start(start)).await?;
|
||||
asset.read_exact(&mut local_buf).await?;
|
||||
buf.extend_from_slice(&local_buf);
|
||||
}
|
||||
// all ranges have been written, write the closing boundary
|
||||
buf.write_all(boundary_closer.as_bytes()).await?;
|
||||
|
||||
resp.body(buf)
|
||||
}
|
||||
} else {
|
||||
resp = resp.header(CONTENT_LENGTH, len);
|
||||
let mut buf = Vec::with_capacity(len as usize);
|
||||
asset.read_to_end(&mut buf).await?;
|
||||
resp.body(buf)
|
||||
};
|
||||
|
||||
http_response.map_err(Into::into)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::tao::event::Event as WryEvent;
|
||||
use dioxus_desktop::tao::event::WindowEvent;
|
||||
use dioxus_desktop::use_wry_event_handler;
|
||||
use dioxus_desktop::wry::application::event::Event as WryEvent;
|
||||
use dioxus_desktop::{Config, WindowCloseBehaviour};
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::use_window;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let window = use_window(cx);
|
||||
let level = use_state(cx, || 1.0);
|
||||
|
||||
cx.render(rsx! {
|
||||
@@ -14,9 +12,9 @@ fn app(cx: Scope) -> Element {
|
||||
r#type: "number",
|
||||
value: "{level}",
|
||||
oninput: |e| {
|
||||
if let Ok(new_zoom) = e.value.parse::<f64>() {
|
||||
if let Ok(new_zoom) = e.value().parse::<f64>() {
|
||||
level.set(new_zoom);
|
||||
window.webview.zoom(new_zoom);
|
||||
dioxus_desktop::window().webview.zoom(new_zoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element {
|
||||
input {
|
||||
value: "{contents}",
|
||||
r#type: "text",
|
||||
oninput: move |e| contents.set(e.value.clone()),
|
||||
oninput: move |e| contents.set(e.value()),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user