nightshade_api/web/
browser.rs1use std::cell::RefCell;
5use std::rc::Rc;
6use std::time::Duration;
7
8use leptos::prelude::*;
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11use wasm_bindgen::JsCast;
12use wasm_bindgen::prelude::Closure;
13
14fn local_storage() -> Option<web_sys::Storage> {
15 web_sys::window().and_then(|window| window.local_storage().ok().flatten())
16}
17
18pub fn download_text(filename: &str, contents: &str) {
21 let Some(document) = web_sys::window().and_then(|window| window.document()) else {
22 return;
23 };
24 let encoded: String = js_sys::encode_uri_component(contents).into();
25 let href = format!("data:text/plain;charset=utf-8,{encoded}");
26 if let Ok(element) = document.create_element("a") {
27 let anchor: web_sys::HtmlAnchorElement = element.unchecked_into();
28 anchor.set_href(&href);
29 anchor.set_download(filename);
30 anchor.click();
31 }
32}
33
34pub fn pick_file_text(on_pick: Callback<String>) {
37 let Some(document) = web_sys::window().and_then(|window| window.document()) else {
38 return;
39 };
40 let Ok(element) = document.create_element("input") else {
41 return;
42 };
43 let input: web_sys::HtmlInputElement = element.unchecked_into();
44 input.set_type("file");
45 let retained = crate::web::use_retained_closures();
46 let on_change = Closure::<dyn Fn(web_sys::Event)>::new(move |event: web_sys::Event| {
47 let Some(target) = event.target() else {
48 return;
49 };
50 let input: web_sys::HtmlInputElement = target.unchecked_into();
51 let Some(file) = input.files().and_then(|files| files.get(0)) else {
52 return;
53 };
54 let Ok(reader) = web_sys::FileReader::new() else {
55 return;
56 };
57 let reader_ref = reader.clone();
58 let on_load = Closure::<dyn Fn()>::new(move || {
59 if let Ok(result) = reader_ref.result()
60 && let Some(text) = result.as_string()
61 {
62 on_pick.run(text);
63 }
64 });
65 reader.set_onload(Some(on_load.as_ref().unchecked_ref()));
66 retained.retain(on_load);
67 let _ = reader.read_as_text(&file);
68 });
69 input.set_onchange(Some(on_change.as_ref().unchecked_ref()));
70 retained.retain(on_change);
71 input.click();
72}
73
74pub fn use_persisted<T>(key: impl Into<String>, default: T) -> RwSignal<T>
77where
78 T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static,
79{
80 let key = key.into();
81 let stored = local_storage()
82 .and_then(|storage| storage.get_item(&key).ok().flatten())
83 .and_then(|raw| serde_json::from_str::<T>(&raw).ok());
84 let signal = RwSignal::new(stored.unwrap_or(default));
85 Effect::new(move |_| {
86 let value = signal.get();
87 if let (Some(storage), Ok(raw)) = (local_storage(), serde_json::to_string(&value)) {
88 let _ = storage.set_item(&key, &raw);
89 }
90 });
91 signal
92}
93
94#[derive(Clone, Copy)]
97pub struct SocketHandle {
98 connected: RwSignal<bool>,
99 socket: StoredValue<Option<web_sys::WebSocket>, LocalStorage>,
100}
101
102impl SocketHandle {
103 pub fn connected(&self) -> Signal<bool> {
105 let connected = self.connected;
106 Signal::derive(move || connected.get())
107 }
108
109 pub fn send(&self, text: &str) {
111 self.socket.with_value(|socket| {
112 if let Some(socket) = socket {
113 let _ = socket.send_with_str(text);
114 }
115 });
116 }
117}
118
119const RECONNECT_MS: u64 = 1000;
120
121type ConnectSlot = Rc<RefCell<Option<Rc<dyn Fn()>>>>;
122type ClosureKeep = StoredValue<Vec<Box<dyn std::any::Any>>, LocalStorage>;
123
124pub fn use_reconnecting_socket(
128 url: impl Into<String>,
129 on_message: Callback<String>,
130) -> SocketHandle {
131 let connected = RwSignal::new(false);
132 let socket = StoredValue::new_local(None::<web_sys::WebSocket>);
133 let keep: ClosureKeep = StoredValue::new_local(Vec::new());
134 let cancelled = StoredValue::new(false);
135 let url = url.into();
136 let connect_slot: ConnectSlot = Rc::new(RefCell::new(None));
137
138 let slot_for_close = connect_slot.clone();
139 let make: Rc<dyn Fn()> = Rc::new(move || {
140 if cancelled.get_value() {
141 return;
142 }
143 let Ok(ws) = web_sys::WebSocket::new(&url) else {
144 return;
145 };
146
147 let on_open = Closure::<dyn Fn()>::new(move || connected.set(true));
148 ws.set_onopen(Some(on_open.as_ref().unchecked_ref()));
149
150 let on_message_closure =
151 Closure::<dyn Fn(web_sys::MessageEvent)>::new(move |event: web_sys::MessageEvent| {
152 if let Some(text) = event.data().as_string() {
153 on_message.run(text);
154 }
155 });
156 ws.set_onmessage(Some(on_message_closure.as_ref().unchecked_ref()));
157
158 let reconnect = slot_for_close.clone();
159 let on_close = Closure::<dyn Fn()>::new(move || {
160 connected.set(false);
161 if !cancelled.get_value()
162 && let Some(again) = reconnect.borrow().clone()
163 {
164 set_timeout(move || again(), Duration::from_millis(RECONNECT_MS));
165 }
166 });
167 ws.set_onclose(Some(on_close.as_ref().unchecked_ref()));
168
169 keep.update_value(|closures| {
170 closures.push(Box::new(on_open));
171 closures.push(Box::new(on_message_closure));
172 closures.push(Box::new(on_close));
173 });
174 socket.set_value(Some(ws));
175 });
176
177 *connect_slot.borrow_mut() = Some(make.clone());
178 make();
179
180 on_cleanup(move || {
181 cancelled.set_value(true);
182 if let Some(ws) = socket.get_value() {
183 ws.set_onopen(None);
184 ws.set_onmessage(None);
185 ws.set_onclose(None);
186 let _ = ws.close();
187 }
188 keep.update_value(Vec::clear);
189 });
190
191 SocketHandle { connected, socket }
192}