Skip to content

Commit 8a19533

Browse files
committed
Editor: Generalize external files code
1 parent 65891c7 commit 8a19533

3 files changed

Lines changed: 203 additions & 159 deletions

File tree

crates/opensi-editor/src/app/file_dialogs.rs

Lines changed: 0 additions & 103 deletions
This file was deleted.
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use std::path::{Path, PathBuf};
2+
3+
use log::{error, warn};
4+
#[cfg(not(target_arch = "wasm32"))]
5+
use tokio;
6+
#[cfg(target_arch = "wasm32")]
7+
use tokio_with_wasm::alias as tokio;
8+
9+
use tokio::sync::oneshot;
10+
11+
use crate::EditorApp;
12+
13+
/// Result for in-progress loading.
14+
pub type LoadingResult<T> = Result<T, FileError>;
15+
16+
/// Receiver of a singular [`LoadingResult`] with file.
17+
pub type LoadingFileReceiver = oneshot::Receiver<LoadingResult<(Vec<u8>, PathBuf)>>;
18+
/// Result for loading files.
19+
pub type LoadingFileResult = Result<(Vec<u8>, PathBuf), FileError>;
20+
21+
/// Error for loading files.
22+
#[derive(thiserror::Error, Debug)]
23+
pub enum FileError {
24+
#[error("No file was selected")]
25+
NoFileSelected,
26+
#[error("Archive error: {0}")]
27+
ArchiveError(std::io::Error),
28+
}
29+
30+
/// Async file loader that can mutate [`EditorApp`] upon loading.
31+
pub struct FileLoader {
32+
receiver: LoadingFileReceiver,
33+
op: Option<Box<dyn FnOnce(Vec<u8>, &Path, &mut EditorApp) -> LoadingResult<()>>>,
34+
}
35+
36+
impl FileLoader {
37+
pub fn update(&mut self, app: &mut EditorApp) -> bool {
38+
match self.receiver.try_recv() {
39+
Ok(Ok((data, file))) => {
40+
if let Some(op) = self.op.take() {
41+
let _ = op(data, file.as_path(), app).inspect_err(|err| {
42+
error!("Error running a loader: {err}");
43+
});
44+
}
45+
return true;
46+
},
47+
Ok(Err(err)) => {
48+
error!("Error loading file: {err}");
49+
},
50+
Err(_) => {},
51+
}
52+
false
53+
}
54+
}
55+
56+
/// Read a file directly from a file on systems that support
57+
/// direct file systems and return a [`FileLoader`]: it will
58+
/// run `op` once the file is loaded.
59+
#[cfg(not(target_arch = "wasm32"))]
60+
#[must_use = "Use loader to properly load a file"]
61+
pub fn load_file(
62+
path: impl AsRef<Path>,
63+
loader: impl FnOnce(Vec<u8>, &Path, &mut EditorApp) -> LoadingResult<()> + 'static,
64+
) -> FileLoader {
65+
fn read_file(file: impl AsRef<Path>) -> LoadingFileResult {
66+
let file = file.as_ref();
67+
let buffer = std::fs::read(file).map_err(FileError::ArchiveError)?;
68+
Ok((buffer, file.to_owned()))
69+
}
70+
71+
let (sender, receiver) = tokio::sync::oneshot::channel();
72+
match sender.send(read_file(path)) {
73+
Ok(_) => {},
74+
Err(_) => error!("Error sending imported package !"),
75+
};
76+
77+
FileLoader { receiver, op: Some(Box::new(loader)) }
78+
}
79+
80+
/// Show a file picker and return a [`FileLoader`] with this file:
81+
/// it will run `op` once the file is loaded.
82+
#[must_use = "Use loader to properly load a file"]
83+
pub fn pick_file(
84+
title: impl ToString,
85+
file_filter: (impl ToString, impl IntoIterator<Item = &'static str>),
86+
loader: impl FnOnce(Vec<u8>, &Path, &mut EditorApp) -> LoadingResult<()> + 'static,
87+
) -> FileLoader {
88+
async fn show_file_picker(
89+
title: &String,
90+
file_filter: &(String, Vec<&'static str>),
91+
) -> LoadingFileResult {
92+
let file = rfd::AsyncFileDialog::new()
93+
.set_title(title)
94+
.add_filter(&file_filter.0, &file_filter.1)
95+
.set_directory(default_directory())
96+
.set_can_create_directories(false)
97+
.pick_file()
98+
.await
99+
.ok_or(FileError::NoFileSelected)?;
100+
101+
let buffer = file.read().await;
102+
103+
#[cfg(not(target_arch = "wasm32"))]
104+
let path = file.path().to_owned();
105+
#[cfg(target_arch = "wasm32")]
106+
let path = file.file_name().into();
107+
108+
Ok((buffer, path))
109+
}
110+
111+
let title = title.to_string();
112+
let file_filter = (file_filter.0.to_string(), file_filter.1.into_iter().collect::<Vec<_>>());
113+
114+
let (sender, receiver) = tokio::sync::oneshot::channel();
115+
let _handle = tokio::spawn(async move {
116+
let result = show_file_picker(&title, &file_filter).await;
117+
match sender.send(result) {
118+
Ok(_) => {},
119+
Err(_) => error!("Error sending picked file"),
120+
};
121+
});
122+
FileLoader { receiver, op: Some(Box::new(loader)) }
123+
}
124+
125+
/// Show a dialog to save file.
126+
pub fn save_to(
127+
title: impl ToString,
128+
file_name: impl ToString,
129+
generate_data: impl FnOnce() -> Option<Vec<u8>> + Send + Sync + 'static,
130+
) {
131+
let title = title.to_string();
132+
let file_name = file_name.to_string();
133+
134+
let _handle = tokio::spawn(async move {
135+
let file = rfd::AsyncFileDialog::new()
136+
.set_title(title)
137+
.set_directory(default_directory())
138+
.set_file_name(file_name)
139+
.save_file()
140+
.await?;
141+
142+
if let Some(bytes) = generate_data() {
143+
file.write(&bytes).await.ok()
144+
} else {
145+
warn!("File saving interrupted: no bytes");
146+
Some(())
147+
}
148+
});
149+
}
150+
151+
/// Get default directory for file pickers.
152+
fn default_directory() -> impl AsRef<Path> {
153+
#[cfg(not(target_arch = "wasm32"))]
154+
return dirs::home_dir().unwrap_or_default();
155+
#[cfg(target_arch = "wasm32")]
156+
return "/";
157+
}

0 commit comments

Comments
 (0)