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 ok = rlobkit_dialogs::RlobKit::save_bytes(opts, &data)
187 .await
188 .ok()
189 .flatten()
190 .is_some();
191 on_done(SaveOutcome { ok, path: None });
192 });
193 }
194 }
195
196 #[cfg(not(any(target_os = "android", target_arch = "wasm32")))]
199 pub fn export_path(title: &str, suggested_name: &str, extensions: &[&str]) -> Option<PathBuf> {
200 rlobkit_dialogs::blocking_save_file(title, suggested_name, &extensions.join(","))
201 }
202}
203
204#[cfg(not(any(target_os = "android", target_arch = "wasm32")))]
206pub fn autosave_store() -> DirStore {
207 let base = std::env::var_os("RENAMITE_DATA_DIR")
208 .map(PathBuf::from)
209 .or_else(|| std::env::var_os("XDG_DATA_HOME").map(|p| PathBuf::from(p).join("renamite")))
210 .unwrap_or_else(|| PathBuf::from("."));
211 let dir = base.join("autosave");
212 let _ = std::fs::create_dir_all(&dir);
213 DirStore { dir }
214}
215
216pub trait KvStore: Send + Sync {
218 fn get(&self, key: &str) -> Option<Vec<u8>>;
219 fn set(&self, key: &str, value: &[u8]);
220}
221
222pub struct DirStore {
224 pub dir: PathBuf,
225}
226
227impl KvStore for DirStore {
228 fn get(&self, key: &str) -> Option<Vec<u8>> {
229 let path = self.dir.join(sanitize_key(key));
230 std::fs::read(&path).ok()
231 }
232 fn set(&self, key: &str, value: &[u8]) {
233 let path = self.dir.join(sanitize_key(key));
234 let _ = std::fs::write(&path, value);
235 }
236}
237
238fn sanitize_key(key: &str) -> String {
239 key.chars()
240 .map(|c| {
241 if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' {
242 c
243 } else {
244 '_'
245 }
246 })
247 .collect()
248}
249
250pub fn now_ms() -> f64 {
252 std::time::SystemTime::now()
253 .duration_since(std::time::UNIX_EPOCH)
254 .map(|d| d.as_secs_f64() * 1000.0)
255 .unwrap_or(0.0)
256}