1use hadrone_core::LayoutItem;
2use serde::{Deserialize, Serialize};
3use std::io::Result;
4#[cfg(not(target_arch = "wasm32"))]
5use std::time::Duration;
6
7#[cfg(not(target_arch = "wasm32"))]
8use std::path::PathBuf;
9
10#[derive(Serialize, Deserialize, Debug, Clone)]
11pub struct LayoutSnapshot {
12 pub version: u32,
13 pub items: Vec<LayoutItem>,
14 pub cols: i32,
15}
16
17pub trait LayoutStorage: Send + Sync {
18 fn save(&self, key: &str, layout: &LayoutSnapshot) -> Result<()>;
19 fn load(&self, key: &str) -> Result<Option<LayoutSnapshot>>;
20}
21
22#[cfg(not(target_arch = "wasm32"))]
25pub struct FileStorage {
26 base_path: PathBuf,
27}
28
29#[cfg(not(target_arch = "wasm32"))]
30impl FileStorage {
31 pub fn new(base_path: impl Into<PathBuf>) -> Self {
32 let path = base_path.into();
33 std::fs::create_dir_all(&path).ok();
34 Self { base_path: path }
35 }
36}
37
38#[cfg(not(target_arch = "wasm32"))]
39impl LayoutStorage for FileStorage {
40 fn save(&self, key: &str, layout: &LayoutSnapshot) -> Result<()> {
41 let path = self.base_path.join(format!("{}.json", key));
42 let data = serde_json::to_string_pretty(layout)?;
43 std::fs::write(path, data)
44 }
45
46 fn load(&self, key: &str) -> Result<Option<LayoutSnapshot>> {
47 let path = self.base_path.join(format!("{}.json", key));
48 if !path.exists() {
49 return Ok(None);
50 }
51 let data = std::fs::read_to_string(path)?;
52 let layout = serde_json::from_str(&data)
53 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
54 Ok(Some(layout))
55 }
56}
57
58pub struct BrowserStorage {
61 _prefix: String,
62 #[cfg(target_arch = "wasm32")]
63 fallback: std::sync::Mutex<std::collections::HashMap<String, LayoutSnapshot>>,
64}
65
66impl BrowserStorage {
67 pub fn new(prefix: impl Into<String>) -> Self {
68 Self {
69 _prefix: prefix.into(),
70 #[cfg(target_arch = "wasm32")]
71 fallback: std::sync::Mutex::new(std::collections::HashMap::new()),
72 }
73 }
74
75 #[cfg(target_arch = "wasm32")]
76 fn storage(&self) -> Option<web_sys::Storage> {
77 web_sys::window()?.local_storage().ok()?
78 }
79}
80
81impl LayoutStorage for BrowserStorage {
82 fn save(&self, _key: &str, _layout: &LayoutSnapshot) -> Result<()> {
83 #[cfg(target_arch = "wasm32")]
84 {
85 let storage_key = format!("{}:{}", self._prefix, _key);
86 let data = serde_json::to_string(_layout)
87 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
88
89 if let Some(storage) = self.storage() {
90 if storage.set_item(&storage_key, &data).is_ok() {
91 return Ok(());
92 }
93 }
94
95 self.fallback
97 .lock()
98 .unwrap()
99 .insert(_key.to_string(), _layout.clone());
100 }
101 Ok(())
102 }
103
104 fn load(&self, _key: &str) -> Result<Option<LayoutSnapshot>> {
105 #[cfg(target_arch = "wasm32")]
106 {
107 let storage_key = format!("{}:{}", self._prefix, _key);
108
109 if let Some(storage) = self.storage() {
110 if let Ok(Some(data)) = storage.get_item(&storage_key) {
111 return serde_json::from_str(&data)
112 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e));
113 }
114 }
115
116 return Ok(self.fallback.lock().unwrap().get(_key).cloned());
118 }
119
120 #[cfg(not(target_arch = "wasm32"))]
121 Ok(None)
122 }
123}
124
125#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
128pub struct BreakpointConfig {
129 pub name: String,
130 pub cols: i32,
131 pub min_width: i32,
132 pub margin: (i32, i32),
133 pub row_height: f32,
134}
135
136impl Default for BreakpointConfig {
137 fn default() -> Self {
138 Self {
139 name: "lg".into(),
140 cols: 12,
141 min_width: 1200,
142 margin: (10, 10),
143 row_height: 100.0,
144 }
145 }
146}
147
148pub async fn debounce_save(
151 storage: &dyn LayoutStorage,
152 key: &str,
153 layout: Vec<LayoutItem>,
154 cols: i32,
155 ms: u64,
156) {
157 #[cfg(not(target_arch = "wasm32"))]
158 tokio::time::sleep(Duration::from_millis(ms)).await;
159
160 #[cfg(target_arch = "wasm32")]
161 gloo_timers::future::TimeoutFuture::new(ms as u32).await;
162
163 let _ = storage.save(
164 key,
165 &LayoutSnapshot {
166 version: 1,
167 items: layout,
168 cols,
169 },
170 );
171}
172
173#[cfg(feature = "dioxus")]
176pub fn use_responsive_grid(
177 breakpoints: Vec<BreakpointConfig>,
178) -> dioxus::prelude::Signal<BreakpointConfig> {
179 use dioxus::prelude::*;
180
181 let initial_bp = {
182 #[cfg(target_arch = "wasm32")]
183 {
184 if let Some(window) = web_sys::window() {
185 if let Ok(width) = window.inner_width() {
186 if let Some(w) = width.as_f64() {
187 let w = w as i32;
188 let mut best_match = &breakpoints[0];
189 for bp in &breakpoints {
190 if w >= bp.min_width && bp.min_width >= best_match.min_width {
191 best_match = bp;
192 }
193 }
194 best_match.clone()
195 } else {
196 breakpoints[0].clone()
197 }
198 } else {
199 breakpoints[0].clone()
200 }
201 } else {
202 breakpoints[0].clone()
203 }
204 }
205 #[cfg(not(target_arch = "wasm32"))]
206 {
207 breakpoints.last().unwrap_or(&breakpoints[0]).clone()
208 }
209 };
210
211 let current = use_signal(|| initial_bp);
212
213 #[cfg(target_arch = "wasm32")]
214 {
215 use wasm_bindgen::prelude::*;
216 use web_sys::{ResizeObserver, ResizeObserverEntry};
217
218 use_effect(move || {
219 let breakpoints = breakpoints.clone();
220 let mut current = current;
221
222 let closure = Closure::wrap(Box::new(
223 move |entries: js_sys::Array, _observer: ResizeObserver| {
224 if let Some(entry) = entries.get(0).dyn_into::<ResizeObserverEntry>().ok() {
225 let width = entry.content_rect().width() as i32;
226
227 let mut best_match = &breakpoints[0];
229 for bp in &breakpoints {
230 if width >= bp.min_width && bp.min_width >= best_match.min_width {
231 best_match = bp;
232 }
233 }
234 current.set(best_match.clone());
235 }
236 },
237 )
238 as Box<dyn FnMut(js_sys::Array, ResizeObserver)>);
239
240 let observer = ResizeObserver::new(closure.as_ref().unchecked_ref()).unwrap();
241
242 if let Some(window) = web_sys::window() {
243 if let Some(document) = window.document() {
244 if let Some(body) = document.body() {
245 observer.observe(&body);
246 }
247 }
248 }
249
250 closure.forget();
251 });
252 }
253
254 current
255}