1use std::path::PathBuf;
10
11pub mod dialogs {
13 use std::path::PathBuf;
14
15 #[derive(Clone, Debug)]
18 pub enum PickedFile {
19 Path(PathBuf),
20 Bytes { name: String, data: Vec<u8> },
21 }
22
23 #[derive(Clone, Debug)]
27 pub struct SaveOutcome {
28 pub ok: bool,
29 pub path: Option<PathBuf>,
30 }
31
32 pub fn init() {
35 rlobkit_dialogs::init();
36 }
37
38 #[allow(dead_code)] fn open_options(title: &str, extensions: &[&str]) -> rlobkit_dialogs::picker::OpenFileOptions {
41 let exts: Vec<String> = extensions.iter().map(|s| s.to_string()).collect();
42 rlobkit_dialogs::picker::OpenFileOptions {
43 file_type: rlobkit_dialogs::RlobKitType::Custom {
44 extensions: exts,
45 mime_types: vec![],
46 },
47 mode: rlobkit_dialogs::RlobKitMode::Single,
48 title: Some(title.to_string()),
49 initial_directory: None,
50 }
51 }
52
53 #[allow(dead_code)] fn save_options(
56 title: &str,
57 suggested_name: &str,
58 extensions: &[&str],
59 ) -> rlobkit_dialogs::picker::SaveFileOptions {
60 let exts: Vec<String> = extensions.iter().map(|s| s.to_string()).collect();
61 rlobkit_dialogs::picker::SaveFileOptions {
62 suggested_name: Some(suggested_name.to_string()),
63 file_type: Some(rlobkit_dialogs::RlobKitType::Custom {
64 extensions: exts,
65 mime_types: vec![],
66 }),
67 title: Some(title.to_string()),
68 ..Default::default()
69 }
70 }
71
72 pub fn pick_open_file(
77 title: &'static str,
78 extensions: &'static [&'static str],
79 on_done: Box<dyn FnOnce(Option<PickedFile>) + Send + 'static>,
80 ) {
81 #[cfg(not(any(target_os = "android", target_arch = "wasm32")))]
82 {
83 std::thread::spawn(move || {
84 on_done(
85 rlobkit_dialogs::blocking_open_file(title, extensions).map(PickedFile::Path),
86 );
87 });
88 }
89 #[cfg(target_os = "android")]
90 {
91 std::thread::spawn(move || {
92 let opts = open_options(title, extensions);
93 let picked = futures_lite::future::block_on(
94 rlobkit_dialogs::RlobKit::open_file_picker(opts),
95 )
96 .ok()
97 .flatten()
98 .and_then(|mut v| v.pop())
99 .and_then(|f| {
100 let name = f.name().to_string();
101 match f.read_bytes() {
102 Ok(data) => Some(PickedFile::Bytes {
103 name,
104 data: data.to_vec(),
105 }),
106 Err(e) => {
107 log::error!("read picker file failed: {e}");
108 None
109 }
110 }
111 });
112 on_done(picked);
113 });
114 }
115 #[cfg(target_arch = "wasm32")]
116 {
117 let opts = open_options(title, extensions);
118 wasm_bindgen_futures::spawn_local(async move {
119 let picked = rlobkit_dialogs::RlobKit::open_file_picker(opts)
120 .await
121 .ok()
122 .flatten()
123 .and_then(|mut v| v.pop())
124 .and_then(|f| {
125 let name = f.name().to_string();
126 match f
127 .data()
128 .map(|b| b.to_vec())
129 .or_else(|| f.read_bytes().ok().map(|b| b.to_vec()))
130 {
131 Some(data) => Some(PickedFile::Bytes { name, data }),
132 None => None,
133 }
134 });
135 on_done(picked);
136 });
137 }
138 }
139
140 pub fn save_bytes(
143 title: &'static str,
144 suggested_name: String,
145 extensions: &'static [&'static str],
146 data: Vec<u8>,
147 on_done: Box<dyn FnOnce(SaveOutcome) + Send + 'static>,
148 ) {
149 #[cfg(not(any(target_os = "android", target_arch = "wasm32")))]
150 {
151 std::thread::spawn(move || {
152 let outcome = match rlobkit_dialogs::blocking_save_file(
153 title,
154 &suggested_name,
155 &extensions.join(","),
156 ) {
157 Some(path) => SaveOutcome {
158 ok: std::fs::write(&path, &data).is_ok(),
159 path: Some(path),
160 },
161 None => SaveOutcome {
162 ok: false,
163 path: None,
164 },
165 };
166 on_done(outcome);
167 });
168 }
169 #[cfg(target_os = "android")]
170 {
171 std::thread::spawn(move || {
172 let opts = save_options(title, &suggested_name, extensions);
173 let ok = futures_lite::future::block_on(rlobkit_dialogs::RlobKit::save_bytes(
174 opts, &data,
175 ))
176 .ok()
177 .flatten()
178 .is_some();
179 on_done(SaveOutcome { ok, path: None });
180 });
181 }
182 #[cfg(target_arch = "wasm32")]
183 {
184 let opts = save_options(title, &suggested_name, extensions);
185 wasm_bindgen_futures::spawn_local(async move {
186 let result = rlobkit_dialogs::RlobKit::save_bytes(opts, &data).await;
187 let ok = match result {
188 Ok(Some(_)) => true,
189 Ok(None) => {
190 log::warn!("save_bytes returned None (picker dismissed?)");
191 false
192 }
193 Err(e) => {
194 log::error!("save_bytes failed: {e:?}");
195 false
196 }
197 };
198 on_done(SaveOutcome { ok, path: None });
199 });
200 }
201 }
202
203 #[cfg(not(any(target_os = "android", target_arch = "wasm32")))]
206 pub fn export_path(title: &str, suggested_name: &str, extensions: &[&str]) -> Option<PathBuf> {
207 rlobkit_dialogs::blocking_save_file(title, suggested_name, &extensions.join(","))
208 }
209}
210
211#[cfg(not(any(target_os = "android", target_arch = "wasm32")))]
213pub fn autosave_store() -> DirStore {
214 let base = std::env::var_os("RENAMITE_DATA_DIR")
215 .map(PathBuf::from)
216 .or_else(|| std::env::var_os("XDG_DATA_HOME").map(|p| PathBuf::from(p).join("renamite")))
217 .unwrap_or_else(|| PathBuf::from("."));
218 let dir = base.join("autosave");
219 let _ = std::fs::create_dir_all(&dir);
220 DirStore { dir }
221}
222
223pub trait KvStore: Send + Sync {
225 fn get(&self, key: &str) -> Option<Vec<u8>>;
226 fn set(&self, key: &str, value: &[u8]);
227}
228
229pub struct DirStore {
231 pub dir: PathBuf,
232}
233
234impl KvStore for DirStore {
235 fn get(&self, key: &str) -> Option<Vec<u8>> {
236 let path = self.dir.join(sanitize_key(key));
237 std::fs::read(&path).ok()
238 }
239 fn set(&self, key: &str, value: &[u8]) {
240 let path = self.dir.join(sanitize_key(key));
241 let _ = std::fs::write(&path, value);
242 }
243}
244
245fn sanitize_key(key: &str) -> String {
246 key.chars()
247 .map(|c| {
248 if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' {
249 c
250 } else {
251 '_'
252 }
253 })
254 .collect()
255}
256
257pub fn now_ms() -> f64 {
259 std::time::SystemTime::now()
260 .duration_since(std::time::UNIX_EPOCH)
261 .map(|d| d.as_secs_f64() * 1000.0)
262 .unwrap_or(0.0)
263}