util.rs
⎇
Raw
1//! Formatting + small browser helpers.
2
3use leptos::prelude::*;
4use wasm_bindgen::JsCast;
5use wasm_bindgen::JsValue;
6use wasm_bindgen::closure::Closure;
7
8/// A window event listener that is removed when the current owner is disposed.
9///
10/// Leptos' `window_event_listener` returns a `WindowListenerHandle` with no
11/// `Drop` impl — if the owning component is unmounted without an explicit
12/// `remove()`, the listener keeps firing on `window` and panics by reading
13/// the component's now-disposed signals.
14pub fn owned_window_listener<E: leptos::ev::EventDescriptor + 'static>(
15 event: E,
16 cb: impl Fn(E::EventType) + 'static,
17) where
18 E::EventType: wasm_bindgen::JsCast,
19{
20 let handle = window_event_listener(event, cb);
21 on_cleanup(move || handle.remove());
22}
23
24/// Same as [`owned_window_listener`], but registered in the capture phase,
25/// so it runs before the bubble-phase window listeners (registration order
26/// otherwise decides, and that depends on component creation order).
27pub fn owned_window_listener_capture<E: leptos::ev::EventDescriptor + 'static>(
28 event: E,
29 cb: impl Fn(E::EventType) + 'static,
30) where
31 E::EventType: wasm_bindgen::JsCast,
32{
33 let closure = Closure::<dyn FnMut(web_sys::Event)>::new({
34 move |e: web_sys::Event| cb(e.unchecked_into::<E::EventType>())
35 });
36 let add_options = web_sys::AddEventListenerOptions::new();
37 add_options.set_capture(true);
38 let remove_options = web_sys::EventListenerOptions::new();
39 remove_options.set_capture(true);
40 let name = event.name();
41 web_sys::window()
42 .expect("window")
43 .add_event_listener_with_callback_and_add_event_listener_options(
44 &name,
45 closure.as_ref().unchecked_ref(),
46 &add_options,
47 )
48 .expect("failed to add window event listener");
49 let closure_js = closure.into_js_value();
50 on_cleanup(move || {
51 let closure: js_sys::Function = closure_js.unchecked_into();
52 let _ = web_sys::window()
53 .expect("window")
54 .remove_event_listener_with_callback_and_event_listener_options(
55 &name,
56 &closure,
57 &remove_options,
58 );
59 });
60}
61
62pub fn format_size(bytes: u64) -> String {
63 const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
64 let mut v = bytes as f64;
65 let mut i = 0;
66 while v >= 1024.0 && i < UNITS.len() - 1 {
67 v /= 1024.0;
68 i += 1;
69 }
70 if i == 0 {
71 format!("{bytes} B")
72 } else {
73 format!("{v:.1} {}", UNITS[i])
74 }
75}
76
77/// Format an RFC 3339 timestamp as "YYYY-MM-DD HH:MM" (UTC) for display.
78/// Falls back to the raw string when it cannot be parsed.
79pub fn format_date(rfc3339: &str) -> String {
80 let d = js_sys::Date::new(&JsValue::from_str(rfc3339));
81 if d.get_time().is_nan() {
82 return rfc3339.to_string();
83 }
84 fn pad(n: u32) -> String {
85 if n < 10 {
86 format!("0{n}")
87 } else {
88 n.to_string()
89 }
90 }
91 format!(
92 "{:04}-{}-{} {}:{}",
93 d.get_utc_full_year(),
94 pad(d.get_utc_month() + 1),
95 pad(d.get_utc_date()),
96 pad(d.get_utc_hours()),
97 pad(d.get_utc_minutes()),
98 )
99}
100
101pub fn storage_get(key: &str) -> Option<String> {
102 let w = web_sys::window()?;
103 let storage_opt = w.local_storage().ok()?;
104 let storage = storage_opt.as_ref()?;
105 storage.get_item(key).ok().flatten()
106}
107
108pub fn storage_set(key: &str, value: &str) {
109 if let Some(s) = web_sys::window().and_then(|w| w.local_storage().ok().flatten()) {
110 let _ = s.set_item(key, value);
111 }
112}
113
114#[derive(Clone, Copy, PartialEq, Debug)]
115pub enum ViewMode {
116 Grid,
117 List,
118}
119
120impl ViewMode {
121 const KEY: &'static str = "fbng.view";
122
123 pub fn load() -> Self {
124 match storage_get(Self::KEY).as_deref() {
125 Some("list") => ViewMode::List,
126 _ => ViewMode::Grid,
127 }
128 }
129
130 pub fn save(self) {
131 storage_set(
132 Self::KEY,
133 if self == ViewMode::List {
134 "list"
135 } else {
136 "grid"
137 },
138 );
139 }
140}
141