133 lines
3.8 KiB
Rust
133 lines
3.8 KiB
Rust
use gloo_net::http::Request;
|
|
use gloo_timers::future::TimeoutFuture;
|
|
use leptos::task::spawn_local;
|
|
use serde::de::DeserializeOwned;
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
use wasm_bindgen::JsCast;
|
|
use wasm_bindgen::prelude::Closure;
|
|
use web_sys::{MessageEvent, WebSocket};
|
|
|
|
thread_local! {
|
|
static SOCKET: RefCell<Option<WebSocket>> = const { RefCell::new(None) };
|
|
}
|
|
|
|
fn endpoint(path: &str) -> String {
|
|
let admin = web_sys::window()
|
|
.and_then(|window| window.location().pathname().ok())
|
|
.is_some_and(|pathname| pathname == "/admin" || pathname.starts_with("/admin/"));
|
|
if admin {
|
|
format!("/admin{path}")
|
|
} else {
|
|
path.to_owned()
|
|
}
|
|
}
|
|
|
|
/// Opens the live socket and calls `on_state` with each pushed snapshot; reconnects after a drop.
|
|
pub fn connect(on_state: impl Fn(String) + 'static) {
|
|
let Some(loc) = web_sys::window().map(|w| w.location()) else {
|
|
return;
|
|
};
|
|
let on_state = Rc::new(on_state);
|
|
let proto = if loc.protocol().is_ok_and(|p| p == "https:") {
|
|
"wss"
|
|
} else {
|
|
"ws"
|
|
};
|
|
let Ok(host) = loc.host() else { return };
|
|
open(
|
|
&format!("{proto}://{host}{}", endpoint("/ws")),
|
|
on_state.clone(),
|
|
);
|
|
|
|
spawn_local(async move {
|
|
loop {
|
|
TimeoutFuture::new(1000).await;
|
|
let connected = SOCKET.with(|socket| {
|
|
socket
|
|
.borrow()
|
|
.as_ref()
|
|
.is_some_and(|socket| socket.ready_state() == WebSocket::OPEN)
|
|
});
|
|
if connected {
|
|
continue;
|
|
}
|
|
if let Some(state) = get_text("/api/state").await {
|
|
on_state(state);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn open(url: &str, on_state: Rc<dyn Fn(String)>) {
|
|
let Ok(ws) = WebSocket::new(url) else { return };
|
|
let cb = on_state.clone();
|
|
let onmessage = Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
|
|
if let Some(text) = e.data().as_string() {
|
|
cb(text);
|
|
}
|
|
});
|
|
ws.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
|
|
onmessage.forget();
|
|
|
|
let url = url.to_string();
|
|
let onclose = Closure::<dyn FnMut()>::new(move || {
|
|
SOCKET.with(|s| *s.borrow_mut() = None);
|
|
let (url, on_state) = (url.clone(), on_state.clone());
|
|
spawn_local(async move {
|
|
TimeoutFuture::new(1000).await;
|
|
open(&url, on_state);
|
|
});
|
|
});
|
|
ws.set_onclose(Some(onclose.as_ref().unchecked_ref()));
|
|
onclose.forget();
|
|
SOCKET.with(|s| *s.borrow_mut() = Some(ws));
|
|
}
|
|
|
|
pub async fn get_json<T: DeserializeOwned>(url: &str) -> Option<T> {
|
|
let url = endpoint(url);
|
|
Request::get(&url).send().await.ok()?.json().await.ok()
|
|
}
|
|
|
|
pub async fn get_text(url: &str) -> Option<String> {
|
|
let url = endpoint(url);
|
|
Request::get(&url).send().await.ok()?.text().await.ok()
|
|
}
|
|
|
|
/// Returns (success, response body).
|
|
pub async fn post(url: &str, body: String) -> Option<(bool, String)> {
|
|
let url = endpoint(url);
|
|
let r = Request::post(&url).body(body).ok()?.send().await.ok()?;
|
|
Some((r.ok(), r.text().await.ok()?))
|
|
}
|
|
|
|
pub fn edit(cmd: String) {
|
|
let sent = SOCKET.with(|s| {
|
|
s.borrow()
|
|
.as_ref()
|
|
.filter(|w| w.ready_state() == WebSocket::OPEN)
|
|
.is_some_and(|w| w.send_with_str(&cmd).is_ok())
|
|
});
|
|
if !sent {
|
|
spawn_local(async move {
|
|
let _ = post("/api/edit", cmd).await;
|
|
});
|
|
}
|
|
}
|
|
|
|
fn storage() -> Option<web_sys::Storage> {
|
|
web_sys::window()?.local_storage().ok().flatten()
|
|
}
|
|
|
|
pub fn vim_pref() -> bool {
|
|
storage()
|
|
.and_then(|s| s.get_item("vim").ok().flatten())
|
|
.is_none_or(|v| v != "0")
|
|
}
|
|
|
|
pub fn set_vim_pref(on: bool) {
|
|
if let Some(s) = storage() {
|
|
let _ = s.set_item("vim", if on { "1" } else { "0" });
|
|
}
|
|
}
|