nightshade_api/web/
files.rs1use wasm_bindgen::JsCast;
8use wasm_bindgen_futures::{JsFuture, spawn_local};
9use web_sys::{Blob, DataTransfer};
10
11pub fn download_file(name: &str, bytes: &[u8]) {
13 let Some(document) = web_sys::window().and_then(|window| window.document()) else {
14 return;
15 };
16 let array = js_sys::Uint8Array::from(bytes);
17 let parts = js_sys::Array::new();
18 parts.push(&array);
19 let options = web_sys::BlobPropertyBag::new();
20 options.set_type("application/octet-stream");
21 let Ok(blob) = Blob::new_with_u8_array_sequence_and_options(&parts, &options) else {
22 return;
23 };
24 let Ok(url) = web_sys::Url::create_object_url_with_blob(&blob) else {
25 return;
26 };
27 if let Ok(element) = document.create_element("a")
28 && let Ok(anchor) = element.dyn_into::<web_sys::HtmlAnchorElement>()
29 {
30 anchor.set_href(&url);
31 anchor.set_download(name);
32 anchor.click();
33 }
34 let _ = web_sys::Url::revoke_object_url(&url);
35}
36
37pub fn apply_pointer_lock(locked: bool) {
40 let Some(document) = web_sys::window().and_then(|window| window.document()) else {
41 return;
42 };
43 if locked {
44 if let Some(canvas) = document.get_element_by_id("canvas") {
45 canvas.request_pointer_lock();
46 }
47 } else {
48 document.exit_pointer_lock();
49 }
50}
51
52pub fn pick_file(accept: &str, on_file: impl FnOnce(String, Vec<u8>) + 'static) {
55 let Some(document) = web_sys::window().and_then(|window| window.document()) else {
56 return;
57 };
58 let Ok(element) = document.create_element("input") else {
59 return;
60 };
61 let Ok(input) = element.dyn_into::<web_sys::HtmlInputElement>() else {
62 return;
63 };
64 input.set_type("file");
65 input.set_accept(accept);
66 let input_for_closure = input.clone();
67 let closure = wasm_bindgen::closure::Closure::once(Box::new(move |_event: web_sys::Event| {
68 let Some(files) = input_for_closure.files() else {
69 return;
70 };
71 let Some(file) = files.get(0) else {
72 return;
73 };
74 read_file(file, on_file);
75 }) as Box<dyn FnOnce(_)>);
76 input.set_onchange(Some(closure.as_ref().unchecked_ref()));
77 closure.forget();
78 input.click();
79}
80
81pub fn read_file(file: web_sys::File, on_file: impl FnOnce(String, Vec<u8>) + 'static) {
83 spawn_local(async move {
84 let name = file.name();
85 let blob: &Blob = file.as_ref();
86 if let Ok(buffer) = JsFuture::from(blob.array_buffer()).await {
87 on_file(name, js_sys::Uint8Array::new(&buffer).to_vec());
88 }
89 });
90}
91
92pub fn read_dropped_files(
96 transfer: DataTransfer,
97 on_files: impl FnOnce(Vec<(String, Vec<u8>)>) + 'static,
98) {
99 let Some(files) = transfer.files() else {
100 return;
101 };
102 if files.length() == 0 {
103 return;
104 }
105 spawn_local(async move {
106 let mut collected: Vec<(String, Vec<u8>)> = Vec::new();
107 for index in 0..files.length() {
108 if let Some(file) = files.item(index) {
109 let name = file.name();
110 let blob: &Blob = file.as_ref();
111 if let Ok(buffer) = JsFuture::from(blob.array_buffer()).await {
112 collected.push((name, js_sys::Uint8Array::new(&buffer).to_vec()));
113 }
114 }
115 }
116 if collected.len() == 1
117 && collected[0].0.to_lowercase().ends_with(".zip")
118 && let Some(unzipped) = unzip(&collected[0].1)
119 {
120 collected = unzipped;
121 }
122 on_files(collected);
123 });
124}
125
126pub enum DroppedFiles {
129 GltfBundle {
130 name: String,
131 gltf: Vec<u8>,
132 resources: Vec<(String, Vec<u8>)>,
133 },
134 Files(Vec<(String, Vec<u8>)>),
135}
136
137pub fn route_dropped(mut files: Vec<(String, Vec<u8>)>) -> DroppedFiles {
141 let gltf_index = files
142 .iter()
143 .position(|(name, _)| name.to_lowercase().ends_with(".gltf"));
144 if let Some(index) = gltf_index
145 && files.len() > 1
146 {
147 let (name, gltf) = files.remove(index);
148 let directory = name
149 .rsplit_once('/')
150 .map(|(directory, _)| format!("{directory}/"))
151 .unwrap_or_default();
152 let resources: Vec<(String, Vec<u8>)> = files
153 .into_iter()
154 .map(|(path, bytes)| {
155 (
156 path.strip_prefix(&directory).unwrap_or(&path).to_string(),
157 bytes,
158 )
159 })
160 .collect();
161 return DroppedFiles::GltfBundle {
162 name,
163 gltf,
164 resources,
165 };
166 }
167 DroppedFiles::Files(files)
168}
169
170fn unzip(bytes: &[u8]) -> Option<Vec<(String, Vec<u8>)>> {
171 use std::io::Read;
172 let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes)).ok()?;
173 let mut out = Vec::new();
174 for index in 0..archive.len() {
175 let Ok(mut entry) = archive.by_index(index) else {
176 continue;
177 };
178 if entry.is_dir() {
179 continue;
180 }
181 let name = entry.name().to_string();
182 let mut data = Vec::new();
183 if entry.read_to_end(&mut data).is_ok() {
184 out.push((name, data));
185 }
186 }
187 Some(out)
188}