Implement LiveView Router Integration (#1505)
* Fix GoForwardButton calling can_go_back instead of can_go_forward * Add first draft of LiveviewHistory * Add external URL redirect * Lock evaluator channel outside loop * Add liveview to router examples * fixup! Add liveview to router examples * Communicate with liveview server on page load * Add PopState event to Liveview routing * Call updater callback from liveview history * Add rudimentary PopState functionality to liveview router. * Fix linter errors * Refactor * Fix navigator external redirection not working. * Add go back and go forward buttons to router examples * Finish functionality for timeline stack in liveview router * Add docs to LiveviewHistory * Replace Liveview history context attachment with constructor that takes context * Fix go forward/backward history/future shuffle * Support history across entire liveview session, if contiguous page jumps. * Remove unnecessary bounds * Add query and hash to location string * Run rustfmt * fix: Update server function docs link (#1489) * liveview: Add `interpreter_glue_relative_uri (#1481) * liveview: Add `interpreter_glue_relative_uri` By utilizing `window.location.host` in the client-side JavaScript, we can easily derive the WebSocket URI from a relative path URI. This approach obviates the need for host address retrieval on the server side, unlike the method of serving glue code in liveview using `interpreter_glue`. * liveview: Merge `.._relative_url` functionality - Merged `.._relative_url` to current API `interpreter_glue`. - Edit axum example to work with new feature. * liveview: Fix clippy warning * Rename modules to use snake_case (#1498) * Change Scope into &ScopeState * Move synchronization of state into router and make it opt-in --------- Co-authored-by: Marc Espín <mespinsanz@gmail.com> Co-authored-by: Seungwoo Kang <ki6080@gmail.com> Co-authored-by: Leonard <tigerros.gh@gmail.com> Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
This commit is contained in:
6 files changed
+517
-10
No files matched your search
@@ -19,6 +19,7 @@ thiserror = { workspace = true }
|
|||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
serde_json = { version = "1.0.91", optional = true }
|
||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
wasm-bindgen = { workspace = true, optional = true }
|
wasm-bindgen = { workspace = true, optional = true }
|
||||||
web-sys = { version = "0.3.60", optional = true, features = [
|
web-sys = { version = "0.3.60", optional = true, features = [
|
||||||
@@ -26,18 +27,22 @@ web-sys = { version = "0.3.60", optional = true, features = [
|
|||||||
] }
|
] }
|
||||||
js-sys = { version = "0.3.63", optional = true }
|
js-sys = { version = "0.3.63", optional = true }
|
||||||
gloo-utils = { version = "0.1.6", optional = true }
|
gloo-utils = { version = "0.1.6", optional = true }
|
||||||
|
dioxus-liveview = { workspace = true, optional = true }
|
||||||
dioxus-ssr = { workspace = true, optional = true }
|
dioxus-ssr = { workspace = true, optional = true }
|
||||||
tokio = { workspace = true, features = ["full"], optional = true }
|
tokio = { workspace = true, features = ["full"], optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["web"]
|
default = ["web"]
|
||||||
ssr = ["dioxus-ssr", "tokio"]
|
ssr = ["dioxus-ssr", "tokio"]
|
||||||
|
liveview = ["dioxus-liveview", "tokio", "dep:serde", "serde_json"]
|
||||||
wasm_test = []
|
wasm_test = []
|
||||||
serde = ["dep:serde", "gloo-utils/serde"]
|
serde = ["dep:serde", "gloo-utils/serde"]
|
||||||
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
|
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
axum = { version = "0.6.1", features = ["ws"] }
|
||||||
dioxus = { path = "../dioxus" }
|
dioxus = { path = "../dioxus" }
|
||||||
|
dioxus-liveview = { workspace = true, features = ["axum"] }
|
||||||
dioxus-ssr = { path = "../ssr" }
|
dioxus-ssr = { path = "../ssr" }
|
||||||
criterion = { version = "0.5", features = ["async_tokio", "html_reports"] }
|
criterion = { version = "0.5", features = ["async_tokio", "html_reports"] }
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,47 @@ use dioxus::prelude::*;
|
|||||||
use dioxus_router::prelude::*;
|
use dioxus_router::prelude::*;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[cfg(feature = "liveview")]
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
|
||||||
|
|
||||||
|
let listen_address: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
|
||||||
|
let view = dioxus_liveview::LiveViewPool::new();
|
||||||
|
let app = Router::new()
|
||||||
|
.fallback(get(move || async move {
|
||||||
|
Html(format!(
|
||||||
|
r#"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body><div id="main"></div></body>
|
||||||
|
{glue}
|
||||||
|
</html>
|
||||||
|
"#,
|
||||||
|
glue = dioxus_liveview::interpreter_glue(&format!("ws://{listen_address}/ws"))
|
||||||
|
))
|
||||||
|
}))
|
||||||
|
.route(
|
||||||
|
"/ws",
|
||||||
|
get(move |ws: WebSocketUpgrade| async move {
|
||||||
|
ws.on_upgrade(move |socket| async move {
|
||||||
|
_ = view
|
||||||
|
.launch(dioxus_liveview::axum_socket(socket), Root)
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("Listening on http://{listen_address}");
|
||||||
|
|
||||||
|
axum::Server::bind(&listen_address.to_string().parse().unwrap())
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "liveview"))]
|
||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
dioxus_desktop::launch(Root);
|
dioxus_desktop::launch(Root);
|
||||||
@@ -10,21 +51,26 @@ fn main() {
|
|||||||
dioxus_web::launch(root);
|
dioxus_web::launch(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "liveview")]
|
||||||
#[component]
|
#[component]
|
||||||
fn Root(cx: Scope) -> Element {
|
fn Root(cx: Scope) -> Element {
|
||||||
render! {
|
let history = LiveviewHistory::new(cx);
|
||||||
Router::<Route> {}
|
render! { Router::<Route> {
|
||||||
}
|
config: || RouterConfig::default().history(history),
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "liveview"))]
|
||||||
|
#[component]
|
||||||
|
fn Root(cx: Scope) -> Element {
|
||||||
|
render! { Router::<Route> {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn UserFrame(cx: Scope, user_id: usize) -> Element {
|
fn UserFrame(cx: Scope, user_id: usize) -> Element {
|
||||||
render! {
|
render! {
|
||||||
pre {
|
pre { "UserFrame{{\n\tuser_id:{user_id}\n}}" }
|
||||||
"UserFrame{{\n\tuser_id:{user_id}\n}}"
|
div { background_color: "rgba(0,0,0,50%)",
|
||||||
}
|
|
||||||
div {
|
|
||||||
background_color: "rgba(0,0,0,50%)",
|
|
||||||
"children:"
|
"children:"
|
||||||
Outlet::<Route> {}
|
Outlet::<Route> {}
|
||||||
}
|
}
|
||||||
@@ -88,6 +134,16 @@ fn Route3(cx: Scope, dynamic: String) -> Element {
|
|||||||
to: Route::Route2 { user_id: 8888 },
|
to: Route::Route2 { user_id: 8888 },
|
||||||
"hello world link"
|
"hello world link"
|
||||||
}
|
}
|
||||||
|
button {
|
||||||
|
disabled: !navigator.can_go_back(),
|
||||||
|
onclick: move |_| { navigator.go_back(); },
|
||||||
|
"go back"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
disabled: !navigator.can_go_forward(),
|
||||||
|
onclick: move |_| { navigator.go_forward(); },
|
||||||
|
"go forward"
|
||||||
|
}
|
||||||
button {
|
button {
|
||||||
onclick: move |_| { navigator.push("https://www.google.com"); },
|
onclick: move |_| { navigator.push("https://www.google.com"); },
|
||||||
"google link"
|
"google link"
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let disabled = !router.can_go_back();
|
let disabled = !router.can_go_forward();
|
||||||
|
|
||||||
render! {
|
render! {
|
||||||
button {
|
button {
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ impl RouterContext {
|
|||||||
IntoRoutable::FromStr(url) => {
|
IntoRoutable::FromStr(url) => {
|
||||||
let parsed_route: NavigationTarget<Rc<dyn Any>> = match self.route_from_str(&url) {
|
let parsed_route: NavigationTarget<Rc<dyn Any>> = match self.route_from_str(&url) {
|
||||||
Ok(route) => NavigationTarget::Internal(route),
|
Ok(route) => NavigationTarget::Internal(route),
|
||||||
Err(err) => NavigationTarget::External(err),
|
Err(_) => NavigationTarget::External(url),
|
||||||
};
|
};
|
||||||
parsed_route
|
parsed_route
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,441 @@
|
|||||||
|
use super::HistoryProvider;
|
||||||
|
use crate::routable::Routable;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::{Mutex, RwLock};
|
||||||
|
use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
/// A [`HistoryProvider`] that evaluates history through JS.
|
||||||
|
pub struct LiveviewHistory<R: Routable>
|
||||||
|
where
|
||||||
|
<R as FromStr>::Err: std::fmt::Display,
|
||||||
|
{
|
||||||
|
action_tx: tokio::sync::mpsc::UnboundedSender<Action<R>>,
|
||||||
|
timeline: Arc<Mutex<Timeline<R>>>,
|
||||||
|
updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Timeline<R: Routable>
|
||||||
|
where
|
||||||
|
<R as FromStr>::Err: std::fmt::Display,
|
||||||
|
{
|
||||||
|
current_index: usize,
|
||||||
|
routes: BTreeMap<usize, R>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct State {
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct Session<R: Routable>
|
||||||
|
where
|
||||||
|
<R as FromStr>::Err: std::fmt::Display,
|
||||||
|
{
|
||||||
|
#[serde(with = "routes")]
|
||||||
|
routes: BTreeMap<usize, R>,
|
||||||
|
last_visited: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct SessionStorage {
|
||||||
|
liveview: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Action<R: Routable> {
|
||||||
|
GoBack,
|
||||||
|
GoForward,
|
||||||
|
Push(R),
|
||||||
|
Replace(R),
|
||||||
|
External(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> Timeline<R>
|
||||||
|
where
|
||||||
|
<R as FromStr>::Err: std::fmt::Display,
|
||||||
|
{
|
||||||
|
fn new(initial_path: R) -> Self {
|
||||||
|
Self {
|
||||||
|
current_index: 0,
|
||||||
|
routes: BTreeMap::from([(0, initial_path)]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
&mut self,
|
||||||
|
route: R,
|
||||||
|
state: Option<State>,
|
||||||
|
session: Option<Session<R>>,
|
||||||
|
depth: usize,
|
||||||
|
) -> State {
|
||||||
|
if let Some(session) = session {
|
||||||
|
self.routes = session.routes;
|
||||||
|
if state.is_none() {
|
||||||
|
// top of stack
|
||||||
|
let last_visited = session.last_visited;
|
||||||
|
self.routes.retain(|&lhs, _| lhs <= last_visited);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let state = match state {
|
||||||
|
Some(state) => {
|
||||||
|
self.current_index = state.index;
|
||||||
|
state
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let index = depth - 1;
|
||||||
|
self.current_index = index;
|
||||||
|
State { index }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.routes.insert(state.index, route);
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, route: R, state: Option<State>) -> State {
|
||||||
|
if let Some(state) = state {
|
||||||
|
self.current_index = state.index;
|
||||||
|
self.routes.insert(self.current_index, route);
|
||||||
|
state
|
||||||
|
} else {
|
||||||
|
self.push(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, route: R) -> State {
|
||||||
|
// top of stack
|
||||||
|
let index = self.current_index + 1;
|
||||||
|
self.current_index = index;
|
||||||
|
self.routes.insert(index, route);
|
||||||
|
self.routes.retain(|&rhs, _| index >= rhs);
|
||||||
|
State {
|
||||||
|
index: self.current_index,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace(&mut self, route: R) -> State {
|
||||||
|
self.routes.insert(self.current_index, route);
|
||||||
|
State {
|
||||||
|
index: self.current_index,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_route(&self) -> &R {
|
||||||
|
&self.routes[&self.current_index]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session(&self) -> Session<R> {
|
||||||
|
Session {
|
||||||
|
routes: self.routes.clone(),
|
||||||
|
last_visited: self.current_index,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> LiveviewHistory<R>
|
||||||
|
where
|
||||||
|
<R as FromStr>::Err: std::fmt::Display,
|
||||||
|
{
|
||||||
|
/// Create a [`LiveviewHistory`] in the given scope.
|
||||||
|
/// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if not in a Liveview context.
|
||||||
|
pub fn new(cx: &ScopeState) -> Self {
|
||||||
|
Self::new_with_initial_path(
|
||||||
|
cx,
|
||||||
|
"/".parse().unwrap_or_else(|err| {
|
||||||
|
panic!("index route does not exist:\n{}\n use LiveviewHistory::new_with_initial_path to set a custom path", err)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a [`LiveviewHistory`] in the given scope, starting at `initial_path`.
|
||||||
|
/// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if not in a Liveview context.
|
||||||
|
pub fn new_with_initial_path(cx: &ScopeState, initial_path: R) -> Self {
|
||||||
|
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel::<Action<R>>();
|
||||||
|
let action_rx = Arc::new(Mutex::new(action_rx));
|
||||||
|
let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
|
||||||
|
let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
|
||||||
|
Arc::new(RwLock::new(Arc::new(|| {})));
|
||||||
|
|
||||||
|
let eval_provider = cx
|
||||||
|
.consume_context::<Rc<dyn EvalProvider>>()
|
||||||
|
.expect("evaluator not provided");
|
||||||
|
|
||||||
|
let create_eval = Rc::new(move |script: &str| {
|
||||||
|
eval_provider
|
||||||
|
.new_evaluator(script.to_string())
|
||||||
|
.map(UseEval::new)
|
||||||
|
}) as Rc<dyn Fn(&str) -> Result<UseEval, EvalError>>;
|
||||||
|
|
||||||
|
// Listen to server actions
|
||||||
|
cx.push_future({
|
||||||
|
let timeline = timeline.clone();
|
||||||
|
let action_rx = action_rx.clone();
|
||||||
|
let create_eval = create_eval.clone();
|
||||||
|
async move {
|
||||||
|
let mut action_rx = action_rx.lock().expect("unpoisoned mutex");
|
||||||
|
loop {
|
||||||
|
let eval = action_rx.recv().await.expect("sender to exist");
|
||||||
|
let _ = match eval {
|
||||||
|
Action::GoBack => create_eval(
|
||||||
|
r#"
|
||||||
|
// this triggers a PopState event
|
||||||
|
history.back();
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
Action::GoForward => create_eval(
|
||||||
|
r#"
|
||||||
|
// this triggers a PopState event
|
||||||
|
history.forward();
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
Action::Push(route) => {
|
||||||
|
let mut timeline = timeline.lock().expect("unpoisoned mutex");
|
||||||
|
let state = timeline.push(route.clone());
|
||||||
|
let state = serde_json::to_string(&state).expect("serializable state");
|
||||||
|
let session = serde_json::to_string(&timeline.session())
|
||||||
|
.expect("serializable session");
|
||||||
|
create_eval(&format!(
|
||||||
|
r#"
|
||||||
|
// this does not trigger a PopState event
|
||||||
|
history.pushState({state}, "", "{route}");
|
||||||
|
sessionStorage.setItem("liveview", '{session}');
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Action::Replace(route) => {
|
||||||
|
let mut timeline = timeline.lock().expect("unpoisoned mutex");
|
||||||
|
let state = timeline.replace(route.clone());
|
||||||
|
let state = serde_json::to_string(&state).expect("serializable state");
|
||||||
|
let session = serde_json::to_string(&timeline.session())
|
||||||
|
.expect("serializable session");
|
||||||
|
create_eval(&format!(
|
||||||
|
r#"
|
||||||
|
// this does not trigger a PopState event
|
||||||
|
history.replaceState({state}, "", "{route}");
|
||||||
|
sessionStorage.setItem("liveview", '{session}');
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Action::External(url) => create_eval(&format!(
|
||||||
|
r#"
|
||||||
|
location.href = "{url}";
|
||||||
|
"#
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen to browser actions
|
||||||
|
cx.push_future({
|
||||||
|
let updater = updater_callback.clone();
|
||||||
|
let timeline = timeline.clone();
|
||||||
|
let create_eval = create_eval.clone();
|
||||||
|
async move {
|
||||||
|
let popstate_eval = {
|
||||||
|
let init_eval = create_eval(
|
||||||
|
r#"
|
||||||
|
return [
|
||||||
|
document.location.pathname + "?" + document.location.search + "\#" + document.location.hash,
|
||||||
|
history.state,
|
||||||
|
JSON.parse(sessionStorage.getItem("liveview")),
|
||||||
|
history.length,
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
).expect("failed to load state").await.expect("serializable state");
|
||||||
|
let (route, state, session, depth) = serde_json::from_value::<(
|
||||||
|
String,
|
||||||
|
Option<State>,
|
||||||
|
Option<Session<R>>,
|
||||||
|
usize,
|
||||||
|
)>(init_eval).expect("serializable state");
|
||||||
|
let Ok(route) = R::from_str(&route.to_string()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut timeline = timeline.lock().expect("unpoisoned mutex");
|
||||||
|
let state = timeline.init(route.clone(), state, session, depth);
|
||||||
|
let state = serde_json::to_string(&state).expect("serializable state");
|
||||||
|
let session = serde_json::to_string(&timeline.session())
|
||||||
|
.expect("serializable session");
|
||||||
|
|
||||||
|
// Call the updater callback
|
||||||
|
(updater.read().unwrap())();
|
||||||
|
|
||||||
|
create_eval(&format!(r#"
|
||||||
|
// this does not trigger a PopState event
|
||||||
|
history.replaceState({state}, "", "{route}");
|
||||||
|
sessionStorage.setItem("liveview", '{session}');
|
||||||
|
|
||||||
|
window.addEventListener("popstate", (event) => {{
|
||||||
|
dioxus.send([
|
||||||
|
document.location.pathname + "?" + document.location.search + "\#" + document.location.hash,
|
||||||
|
event.state,
|
||||||
|
]);
|
||||||
|
}});
|
||||||
|
"#)).expect("failed to initialize popstate")
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let event = match popstate_eval.recv().await {
|
||||||
|
Ok(event) => event,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let (route, state) = serde_json::from_value::<(String, Option<State>)>(event).expect("serializable state");
|
||||||
|
let Ok(route) = R::from_str(&route.to_string()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut timeline = timeline.lock().expect("unpoisoned mutex");
|
||||||
|
let state = timeline.update(route.clone(), state);
|
||||||
|
let state = serde_json::to_string(&state).expect("serializable state");
|
||||||
|
let session = serde_json::to_string(&timeline.session())
|
||||||
|
.expect("serializable session");
|
||||||
|
|
||||||
|
let _ = create_eval(&format!(
|
||||||
|
r#"
|
||||||
|
// this does not trigger a PopState event
|
||||||
|
history.replaceState({state}, "", "{route}");
|
||||||
|
sessionStorage.setItem("liveview", '{session}');
|
||||||
|
"#));
|
||||||
|
|
||||||
|
// Call the updater callback
|
||||||
|
(updater.read().unwrap())();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
action_tx,
|
||||||
|
timeline,
|
||||||
|
updater_callback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> HistoryProvider<R> for LiveviewHistory<R>
|
||||||
|
where
|
||||||
|
<R as FromStr>::Err: std::fmt::Display,
|
||||||
|
{
|
||||||
|
fn go_back(&mut self) {
|
||||||
|
let _ = self.action_tx.send(Action::GoBack);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn go_forward(&mut self) {
|
||||||
|
let _ = self.action_tx.send(Action::GoForward);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, route: R) {
|
||||||
|
let _ = self.action_tx.send(Action::Push(route));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace(&mut self, route: R) {
|
||||||
|
let _ = self.action_tx.send(Action::Replace(route));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn external(&mut self, url: String) -> bool {
|
||||||
|
let _ = self.action_tx.send(Action::External(url));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_route(&self) -> R {
|
||||||
|
let timeline = self.timeline.lock().expect("unpoisoned mutex");
|
||||||
|
timeline.current_route().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_go_back(&self) -> bool {
|
||||||
|
let timeline = self.timeline.lock().expect("unpoisoned mutex");
|
||||||
|
// Check if the one before is contiguous (i.e., not an external page)
|
||||||
|
let visited_indices: Vec<usize> = timeline.routes.keys().cloned().collect();
|
||||||
|
visited_indices
|
||||||
|
.iter()
|
||||||
|
.position(|&rhs| timeline.current_index == rhs)
|
||||||
|
.map_or(false, |index| {
|
||||||
|
index > 0 && visited_indices[index - 1] == timeline.current_index - 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_go_forward(&self) -> bool {
|
||||||
|
let timeline = self.timeline.lock().expect("unpoisoned mutex");
|
||||||
|
// Check if the one after is contiguous (i.e., not an external page)
|
||||||
|
let visited_indices: Vec<usize> = timeline.routes.keys().cloned().collect();
|
||||||
|
visited_indices
|
||||||
|
.iter()
|
||||||
|
.rposition(|&rhs| timeline.current_index == rhs)
|
||||||
|
.map_or(false, |index| {
|
||||||
|
index < visited_indices.len() - 1
|
||||||
|
&& visited_indices[index + 1] == timeline.current_index + 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
|
||||||
|
let mut updater_callback = self.updater_callback.write().unwrap();
|
||||||
|
*updater_callback = callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod routes {
|
||||||
|
use crate::prelude::Routable;
|
||||||
|
use core::str::FromStr;
|
||||||
|
use serde::de::{MapAccess, Visitor};
|
||||||
|
use serde::{ser::SerializeMap, Deserializer, Serializer};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
pub fn serialize<S, R>(routes: &BTreeMap<usize, R>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
R: Routable,
|
||||||
|
{
|
||||||
|
let mut map = serializer.serialize_map(Some(routes.len()))?;
|
||||||
|
for (index, route) in routes.iter() {
|
||||||
|
map.serialize_entry(&index.to_string(), &route.to_string())?;
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D, R>(deserializer: D) -> Result<BTreeMap<usize, R>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
R: Routable,
|
||||||
|
<R as FromStr>::Err: std::fmt::Display,
|
||||||
|
{
|
||||||
|
struct BTreeMapVisitor<R> {
|
||||||
|
marker: std::marker::PhantomData<R>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de, R> Visitor<'de> for BTreeMapVisitor<R>
|
||||||
|
where
|
||||||
|
R: Routable,
|
||||||
|
<R as FromStr>::Err: std::fmt::Display,
|
||||||
|
{
|
||||||
|
type Value = BTreeMap<usize, R>;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a map with indices and routable values")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||||
|
where
|
||||||
|
M: MapAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut routes = BTreeMap::new();
|
||||||
|
while let Some((index, route)) = map.next_entry::<String, String>()? {
|
||||||
|
let index = index.parse::<usize>().map_err(serde::de::Error::custom)?;
|
||||||
|
let route = R::from_str(&route).map_err(serde::de::Error::custom)?;
|
||||||
|
routes.insert(index, route);
|
||||||
|
}
|
||||||
|
Ok(routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_map(BTreeMapVisitor {
|
||||||
|
marker: std::marker::PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,11 @@ pub use web::*;
|
|||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
pub(crate) mod web_history;
|
pub(crate) mod web_history;
|
||||||
|
|
||||||
|
#[cfg(feature = "liveview")]
|
||||||
|
mod liveview;
|
||||||
|
#[cfg(feature = "liveview")]
|
||||||
|
pub use liveview::*;
|
||||||
|
|
||||||
// #[cfg(feature = "web")]
|
// #[cfg(feature = "web")]
|
||||||
// mod web_hash;
|
// mod web_hash;
|
||||||
// #[cfg(feature = "web")]
|
// #[cfg(feature = "web")]
|
||||||
|
|||||||
Reference in new issue
Block a user