#830: cap files read in the browser at 512 MiB, and say why a file isn't opened

This commit is contained in:
bflatastic
2026-10-19 22:39:23 +07:00
parent bafc814712
commit b744ff3e14
22 changed files with 136 additions and 43 deletions
+15 -10
View File
@@ -147,12 +147,10 @@ impl Services for WebServices {
}
}
/// The largest file read from the picker (browsers read at most 4 GiB into one buffer).
const MAX_PICKED_BYTES: f64 = u32::MAX as f64;
/// The file picked in the browser's file picker (`accept`: the extensions it offers), with its
/// name; `None` when the picker is cancelled.
async fn pick_file(accept: &str) -> Result<Option<(String, Vec<u8>)>, wasm_bindgen::JsValue> {
/// name; `None` when the picker is cancelled, `Some(Err)` with what to tell the user when the file
/// can't be read.
async fn pick_file(accept: &str) -> Result<Option<Result<(String, Vec<u8>), String>>, wasm_bindgen::JsValue> {
let window = web_sys::window().ok_or("no window")?;
let document = window.document().ok_or("no document")?;
let body = document.body().ok_or("no body")?;
@@ -169,15 +167,22 @@ async fn pick_file(accept: &str) -> Result<Option<(String, Vec<u8>)>, wasm_bindg
drop(picker);
result?;
let Some(file) = file else { return Ok(None) };
if file.size() > MAX_PICKED_BYTES {
return Err(format!("{} is too large to read in the browser", file.name()).into());
let name = file.name();
if let Err(why) = figcraft_ui_egui::browser_can_read(&name, file.size()) {
return Ok(Some(Err(why)));
}
let buffer = wasm_bindgen_futures::JsFuture::from(file.array_buffer()).await?;
Ok(Some((file.name(), js_sys::Uint8Array::new(&buffer).to_vec())))
Ok(Some(match wasm_bindgen_futures::JsFuture::from(file.array_buffer()).await {
Ok(buffer) => Ok((name, js_sys::Uint8Array::new(&buffer).to_vec())),
Err(e) => {
log::error!("couldn't read {name}: {e:?}");
Err(figcraft_ui_egui::unreadable(&name))
}
}))
}
/// How long after the page gets focus back the file picker counts as closed (milliseconds): a
/// picked file's `change` can come just after the focus, and must win.
/// picked file's `change` can come just after the focus, and must win. A `change` later than this
/// (a slow cloud-backed file in a browser that never sends `cancel`) counts as a cancel.
const PICKER_FOCUS_SETTLE_MS: i32 = 1_000;
/// Settles a file picker's promise once: on `change` (a file was picked), on `cancel` (closed
+1
View File
@@ -1629,6 +1629,7 @@ status = "draft — needs native review"
"{name} is forgotten on this computer; the file stays on {server}" = "Zapomenuto v tomto počítači: {name}; soubor zůstává na serveru {server}"
"{name} is on the server." = "{name} je na serveru."
"{name} is spotlighting" = "{name} sdílí pohled"
"{name} is too large to open in the browser (over 512 MB)." = "Soubor {name} je příliš velký na otevření v prohlížeči (více než 512 MB)."
"{name}\nClick to apply to the selection" = "{name}\nKliknutím použijete na výběr"
"{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it." = "{name}\nPřetáhněte na plátno nebo kliknutím umístěte doprostřed zobrazení.\nPři puštění na instanci podržte Alt (Option) a vyměňte ji."
"{name} (from {library})" = "{name} (z {library})"
+1
View File
@@ -1583,6 +1583,7 @@ status = "draft — needs native review"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} wurde auf diesem Computer vergessen; die Datei bleibt auf {server}"
"{name} is on the server." = "{name} liegt auf dem Server."
"{name} is spotlighting" = "{name} hat das Spotlight"
"{name} is too large to open in the browser (over 512 MB)." = "{name} ist zu groß, um im Browser geöffnet zu werden (über 512 MB)."
"{name} was removed from the server" = "{name} wurde vom Server entfernt"
"{name} · deleted on main" = "{name} · in der Hauptdatei gelöscht"
"{name} · deleted on the branch" = "{name} · im Branch gelöscht"
+1
View File
@@ -1668,6 +1668,7 @@ name = "English"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} is forgotten on this computer; the file stays on {server}"
"{name} is on the server." = "{name} is on the server."
"{name} is spotlighting" = "{name} is spotlighting"
"{name} is too large to open in the browser (over 512 MB)." = "{name} is too large to open in the browser (over 512 MB)."
"{name}\nClick to apply to the selection" = "{name}\nClick to apply to the selection"
"{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it." = "{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it."
"{name} (from {library})" = "{name} (from {library})"
+1
View File
@@ -1669,6 +1669,7 @@ status = "draft — needs native review"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} se olvidó en este ordenador; el archivo se queda en {server}"
"{name} is on the server." = "{name} está en el servidor."
"{name} is spotlighting" = "{name} está en el foco"
"{name} is too large to open in the browser (over 512 MB)." = "{name} es demasiado grande para abrirlo en el navegador (más de 512 MB)."
"{name}\nClick to apply to the selection" = "{name}\nHaz clic para aplicarlo a la selección"
"{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it." = "{name}\nArrastra al lienzo o haz clic para colocarlo en el centro de la vista.\nMantén Alt (Opción) al soltar sobre una instancia para intercambiarla."
"{name} (from {library})" = "{name} (de {library})"
+1
View File
@@ -1676,6 +1676,7 @@ status = "draft — needs native review"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} on unohdettu tällä tietokoneella. Tiedosto jää palvelimelle {server}."
"{name} is on the server." = "{name} on palvelimella."
"{name} is spotlighting" = "{name} nostaa näkymäänsä esiin"
"{name} is too large to open in the browser (over 512 MB)." = "{name} on liian suuri avattavaksi selaimessa (yli 512 Mt)."
"{name} was removed from the server" = "{name} poistettiin palvelimelta"
"{name} · deleted on main" = "{name} · poistettu päähaarassa"
"{name} · deleted on the branch" = "{name} · poistettu haarassa"
+1
View File
@@ -1665,6 +1665,7 @@ status = "draft — needs native review"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} est oublié sur cet ordinateur ; le fichier reste sur {server}"
"{name} is on the server." = "{name} est sur le serveur."
"{name} is spotlighting" = "{name} est sous les projecteurs"
"{name} is too large to open in the browser (over 512 MB)." = "{name} est trop volumineux pour être ouvert dans le navigateur (plus de 512 Mo)."
"{name} was removed from the server" = "{name} a été retiré du serveur"
"{name} · deleted on main" = "{name} · supprimé dans le principal"
"{name} · deleted on the branch" = "{name} · supprimé dans la branche"
+1
View File
@@ -1633,6 +1633,7 @@ status = "draft — needs native review"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} dilupakan di komputer ini; file tetap ada di {server}"
"{name} is on the server." = "{name} ada di server."
"{name} is spotlighting" = "{name} sedang spotlight"
"{name} is too large to open in the browser (over 512 MB)." = "{name} terlalu besar untuk dibuka di browser (lebih dari 512 MB)."
"{name}\nClick to apply to the selection" = "{name}\nKlik untuk menerapkan ke pilihan"
"{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it." = "{name}\nSeret ke kanvas, atau klik untuk menaruh di tengah tampilan.\nTahan Alt (Option) saat melepas di atas instance untuk menukarnya."
"{name} (from {library})" = "{name} (dari {library})"
+1
View File
@@ -1650,6 +1650,7 @@ status = "draft — needs native review"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} の記録をこのコンピューターから消去済み。ファイルは {server} に残る"
"{name} is on the server." = "{name} はサーバーにある。"
"{name} is spotlighting" = "{name} がスポットライト中"
"{name} is too large to open in the browser (over 512 MB)." = "{name} は大きすぎるため、ブラウザーで開けません(512 MB 超)。"
"{name} was removed from the server" = "{name} をサーバーから削除済み"
"{name} · deleted on main" = "{name} · メインで削除"
"{name} · deleted on the branch" = "{name} · ブランチで削除"
+1
View File
@@ -1574,6 +1574,7 @@ status = "draft — needs native review"
"{name} is forgotten on this computer; the file stays on {server}" = "{name}의 기록을 이 컴퓨터에서 지움. 파일은 {server}에 남음"
"{name} is on the server." = "{name}이(가) 서버에 있음."
"{name} is spotlighting" = "{name} 님이 스포트라이트 중"
"{name} is too large to open in the browser (over 512 MB)." = "{name}은(는) 너무 커서 브라우저에서 열 수 없습니다(512MB 초과)."
"{name} was removed from the server" = "{name}을(를) 서버에서 제거함"
"{name} · deleted on main" = "{name} · 메인에서 삭제됨"
"{name} · deleted on the branch" = "{name} · 브랜치에서 삭제됨"
+1
View File
@@ -1673,6 +1673,7 @@ status = "draft — needs native review"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} is op deze computer vergeten; het bestand blijft op {server}"
"{name} is on the server." = "{name} staat op de server."
"{name} is spotlighting" = "{name} staat in de spotlight"
"{name} is too large to open in the browser (over 512 MB)." = "{name} is te groot om in de browser te openen (meer dan 512 MB)."
"{name} was removed from the server" = "{name} is van de server verwijderd"
"{name} · deleted on main" = "{name} · verwijderd op main"
"{name} · deleted on the branch" = "{name} · verwijderd op de branch"
+1
View File
@@ -1573,6 +1573,7 @@ status = "draft — needs native review"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} foi esquecido neste computador; o arquivo continua em {server}"
"{name} is on the server." = "{name} está no servidor."
"{name} is spotlighting" = "{name} está em destaque"
"{name} is too large to open in the browser (over 512 MB)." = "{name} é grande demais para abrir no navegador (mais de 512 MB)."
"{name} was removed from the server" = "{name} foi removido do servidor"
"{name} · deleted on main" = "{name} · excluído no principal"
"{name} · deleted on the branch" = "{name} · excluído na branch"
+1
View File
@@ -1635,6 +1635,7 @@ name = "Русский"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} забыт на этом компьютере; файл остаётся на {server}"
"{name} is on the server." = "{name} находится на сервере."
"{name} is spotlighting" = "{name} показывает экран всем"
"{name} is too large to open in the browser (over 512 MB)." = "Файл {name} слишком велик для открытия в браузере (больше 512 МБ)."
"{name}\nClick to apply to the selection" = "{name}\nНажмите, чтобы применить к выделению"
"{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it." = "{name}\nПеретащите на холст или нажмите, чтобы разместить в центре вида.\nУдерживайте Alt (Option) при переносе на экземпляр, чтобы заменить его."
"{name} (from {library})" = "{name} (из {library})"
+1
View File
@@ -1650,6 +1650,7 @@ name = "Svenska"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} har glömts på den här datorn; filen finns kvar på {server}"
"{name} is on the server." = "{name} finns på servern."
"{name} is spotlighting" = "{name} har spotlight"
"{name} is too large to open in the browser (over 512 MB)." = "{name} är för stor för att öppnas i webbläsaren (över 512 MB)."
"{name}\nClick to apply to the selection" = "{name}\nKlicka för att använda på markeringen"
"{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it." = "{name}\nDra till arbetsytan, eller klicka för att placera i mitten av vyn.\nHåll ned Alt (Option) när du släpper på en instans för att byta ut den."
"{name} (from {library})" = "{name} (från {library})"
+1
View File
@@ -1633,6 +1633,7 @@ name = "Türkçe"
"{name} is forgotten on this computer; the file stays on {server}" = "{name} bu bilgisayarda unutuldu; dosya {server} üzerinde kalır"
"{name} is on the server." = "{name} sunucuda."
"{name} is spotlighting" = "{name} öne çıkarıyor"
"{name} is too large to open in the browser (over 512 MB)." = "{name}, tarayıcıda açılamayacak kadar büyük (512 MB'tan fazla)."
"{name}\nClick to apply to the selection" = "{name}\nSeçime uygulamak için tıklayın"
"{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it." = "{name}\nTuvale sürükleyin veya görünümün ortasına yerleştirmek için tıklayın.\nBir örneği değiştirmek için üzerine bırakırken Alt (Option) tuşunu basılı tutun."
"{name} (from {library})" = "{name} ({library} kitaplığından)"
+1
View File
@@ -1568,6 +1568,7 @@ status = "draft — needs native review"
"{name} is forgotten on this computer; the file stays on {server}" = "已在这台电脑上忘记 {name};文件仍保留在 {server} 上"
"{name} is on the server." = "{name} 已在服务器上。"
"{name} is spotlighting" = "{name} 正在聚焦"
"{name} is too large to open in the browser (over 512 MB)." = "{name} 太大,无法在浏览器中打开(超过 512 MB)。"
"{name}\nClick to apply to the selection" = "{name}\n点击应用于所选项"
"{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it." = "{name}\n拖到画布上,或点击放置在视图中央。\n拖放到实例上时按住 Alt(Option)可替换该实例。"
"{name} (from {library})" = "{name}(来自 {library})"
+31 -1
View File
@@ -87,10 +87,40 @@ impl Services for Contents {
panic!("a platform that picks contents is never asked for a path")
}
fn pick_open_contents(&mut self, _title: &str, _exts: &[&str]) -> Option<ContentsFuture> {
Some(Box::pin(std::future::ready(Some((self.0.to_string(), self.1.clone())))))
Some(Box::pin(std::future::ready(Some(Ok((self.0.to_string(), self.1.clone()))))))
}
}
/// The browser, with a picked file it won't read.
struct Unreadable(String);
impl Services for Unreadable {
fn pick_open_contents(&mut self, _title: &str, _exts: &[&str]) -> Option<ContentsFuture> {
Some(Box::pin(std::future::ready(Some(Err(self.0.clone())))))
}
}
#[test]
fn a_picked_file_that_cannot_be_read_says_why_and_opens_nothing() {
let why = crate::browser_can_read("Huge.fig", 600.0 * 1024.0 * 1024.0).unwrap_err();
let mut app = FigApp::new(Session::empty(), Box::new(Unreadable(why.clone())));
app.open_dialog();
app.poll_dialog(&egui::Context::default());
assert!(app.session.docs.is_empty());
assert_eq!(app.ui.last_error.as_deref(), Some(why.as_str()));
assert!(why.contains("Huge.fig"), "{why}");
}
#[test]
fn the_browser_reads_files_up_to_512_mib() {
let max = (512u64 << 20) as f64;
assert!(crate::browser_can_read("a", 0.0).is_ok());
assert!(crate::browser_can_read("a", max).is_ok());
assert!(crate::browser_can_read("a", max + 1.0).is_err());
assert!(crate::browser_can_read("a", f64::NAN).is_err());
assert!(crate::browser_can_read("a", f64::INFINITY).is_err());
}
#[test]
fn open_dialog_opens_picked_contents_as_a_new_unsaved_document() {
let bytes = figcraft_format::save_bytes(&figcraft_doc::Document::new("Picked"), None).unwrap();
+1 -1
View File
@@ -510,7 +510,7 @@ fn input(app: &mut FigApp, ui: &mut Ui, resp: &egui::Response) -> bool {
let (params, read) = if path.exists() {
(json!({"path": path.to_string_lossy(), "x": pt.x, "y": pt.y}), None)
} else {
(json!({"name": name, "x": pt.x, "y": pt.y}), Some(crate::dropped_bytes(&f)))
(json!({"name": name, "x": pt.x, "y": pt.y}), Some(crate::dropped_bytes(&f, &name)))
};
let Some(target) =
app.session.ds().ok().map(|doc| crate::pending_open::ImportTarget { uid: doc.uid, page: doc.page, name: doc.doc.name.clone() })
+5 -2
View File
@@ -187,8 +187,11 @@ fn dropped(app: &mut FigApp, ctx: &egui::Context) {
}
// `opens` saw an extension, so there is a file name to tell the format by.
let name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
let (params, read) =
if path.exists() { (json!({"path": path.to_string_lossy()}), None) } else { (json!({"name": name}), Some(crate::dropped_bytes(&f))) };
let (params, read) = if path.exists() {
(json!({"path": path.to_string_lossy()}), None)
} else {
(json!({"name": name}), Some(crate::dropped_bytes(&f, &name)))
};
app.ui.pending_opens.push(crate::pending_open::PendingOpen { label: name, params, command: "file.open", target: None, read });
}
}
+7 -4
View File
@@ -573,9 +573,12 @@ fn dropping_a_file_without_one_on_disk_opens_it_as_a_new_unsaved_document() {
assert!(!path.exists(), "nothing is written");
}
/// A drop whose contents arrive later, as every drop in the browser does: `None` until `arrived`
/// What a drop's read gives, once it has arrived.
type Arrival = std::rc::Rc<std::cell::RefCell<Option<Result<Vec<u8>, String>>>>;
/// A drop whose contents arrive later, as every drop in the browser does: pending until `arrived`
/// holds them.
fn later(arrived: &std::rc::Rc<std::cell::RefCell<Option<Option<Vec<u8>>>>>) -> crate::ReadFuture {
fn later(arrived: &Arrival) -> crate::ReadFuture {
let arrived = arrived.clone();
Box::pin(std::future::poll_fn(move |_| match arrived.borrow_mut().take() {
Some(bytes) => std::task::Poll::Ready(bytes),
@@ -603,7 +606,7 @@ fn a_dropped_file_whose_contents_arrive_later_opens_once_they_do() {
assert!(has_text(&s, "Browser drop.svg"), "the card shows while the contents are read");
}
assert!(u.app.session.docs.is_empty(), "nothing opens before the contents arrive");
*arrived.borrow_mut() = Some(Some(svg));
*arrived.borrow_mut() = Some(Ok(svg));
for _ in 0..3 {
u.frame(vec![]);
}
@@ -617,7 +620,7 @@ fn a_dropped_file_whose_contents_arrive_later_opens_once_they_do() {
#[test]
fn a_dropped_file_that_cannot_be_read_says_so_and_opens_nothing() {
let arrived = std::rc::Rc::new(std::cell::RefCell::new(Some(None)));
let arrived = std::rc::Rc::new(std::cell::RefCell::new(Some(Err(crate::unreadable("Unreadable.figcraft")))));
let mut u = first_run();
u.app.ui.home = true;
u.app.ui.pending_opens.push(crate::pending_open::PendingOpen {
+56 -18
View File
@@ -295,32 +295,55 @@ pub trait Services {
}
pub type DialogFuture = Pin<Box<dyn Future<Output = Option<PathBuf>>>>;
/// A picked file's name and contents ([`Services::pick_open_contents`]).
pub type ContentsFuture = Pin<Box<dyn Future<Output = Option<(String, Vec<u8>)>>>>;
/// A dropped file's contents, read once they arrive (`None`: they couldn't be read).
pub type ReadFuture = Pin<Box<dyn Future<Output = Option<Vec<u8>>>>>;
/// A picked file's name and contents ([`Services::pick_open_contents`]); `None` when the picker
/// was cancelled, `Err` with what to tell the user when the file couldn't be read.
pub type ContentsFuture = Pin<Box<dyn Future<Output = Option<Result<(String, Vec<u8>), String>>>>>;
/// A dropped file's contents, read once they arrive (`Err`: what to tell the user when they
/// couldn't be read).
pub type ReadFuture = Pin<Box<dyn Future<Output = Result<Vec<u8>, String>>>>;
type PickFuture = Pin<Box<dyn Future<Output = Option<Picked>>>>;
type DialogDone = Box<dyn FnOnce(&mut FigApp, Picked)>;
/// What a dialog picked: a path, or a file's name and contents where there are no paths.
/// What a dialog picked: a path, or a file's name and contents where there are no paths, or why
/// the picked file couldn't be read.
enum Picked {
Path(PathBuf),
Contents(String, Vec<u8>),
Unreadable(String),
}
impl Picked {
/// The file as `file.open` and `file.import` take it.
fn file_params(self) -> Value {
/// The file as `file.open` and `file.import` take it (`Err`: what to tell the user instead).
fn file_params(self) -> Result<Value, String> {
match self {
Picked::Path(p) => json!({"path": p.to_string_lossy()}),
Picked::Path(p) => Ok(json!({"path": p.to_string_lossy()})),
Picked::Contents(name, bytes) => {
use base64::Engine as _;
json!({"data": base64::engine::general_purpose::STANDARD.encode(&bytes), "name": name})
Ok(json!({"data": base64::engine::general_purpose::STANDARD.encode(&bytes), "name": name}))
}
Picked::Unreadable(why) => Err(why),
}
}
}
/// What the user is told about a file whose contents couldn't be read.
pub fn unreadable(name: &str) -> String {
i18n::trf("Couldn't read {name}.", &[("name", &name)])
}
/// The largest file read in the browser: wasm has at most 4 GiB of memory, and a file's contents
/// are held about three times on their way to `file.open` (the bytes, base64, decoded again).
/// As large as one member of a `.figcraft` file may be.
const MAX_BROWSER_FILE: f64 = (512u64 << 20) as f64;
/// Checks a file of `size` bytes before the browser reads it (`Err`: what to tell the user).
pub fn browser_can_read(name: &str, size: f64) -> Result<(), String> {
if size.is_nan() || size > MAX_BROWSER_FILE {
return Err(i18n::trf("{name} is too large to open in the browser (over 512 MB).", &[("name", &name)]));
}
Ok(())
}
/// A native dialog in flight. The app keeps working while it is open, so its result goes to the
/// file and window it was opened for, not to whichever is in front when it closes.
struct PendingDialog {
@@ -809,6 +832,11 @@ impl FigApp {
fn failed(&mut self, id: &str, e: &figcraft_engine::EngineError) {
let msg = e.to_string();
log::warn!("{id}: {msg}");
self.show_error(msg);
}
/// Show an error as a toast, kept as the last error.
pub(crate) fn show_error(&mut self, msg: String) {
self.toast(msg.clone());
self.ui.last_error = Some(msg);
}
@@ -1154,7 +1182,9 @@ impl FigApp {
self.start_pick(
|services| -> PickFuture {
match services.pick_open_contents(title, exts) {
Some(read) => Box::pin(async move { read.await.map(|(name, bytes)| Picked::Contents(name, bytes)) }),
Some(read) => {
Box::pin(async move { read.await.map(|r| r.map_or_else(Picked::Unreadable, |(name, bytes)| Picked::Contents(name, bytes))) })
}
None => {
let pick = services.pick_open(title, exts);
Box::pin(async move { pick.await.map(Picked::Path) })
@@ -1162,7 +1192,10 @@ impl FigApp {
}
},
pin,
move |app, picked| done(app, picked.file_params()),
move |app, picked| match picked.file_params() {
Ok(file) => done(app, file),
Err(why) => app.show_error(why),
},
);
}
@@ -1751,17 +1784,22 @@ pub(crate) fn opens(path: &Path) -> bool {
path.extension().and_then(|e| e.to_str()).is_some_and(|e| OPEN_EXTENSIONS.iter().any(|x| x.eq_ignore_ascii_case(e)))
}
/// Reads the contents of a dropped file that has no file on disk to read by path.
/// Reads the contents of dropped file `name` that has no file on disk to read by path.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn dropped_bytes(f: &egui::DroppedFileHandle) -> ReadFuture {
Box::pin(std::future::ready(f.bytes().ok()))
pub(crate) fn dropped_bytes(f: &egui::DroppedFileHandle, name: &str) -> ReadFuture {
Box::pin(std::future::ready(f.bytes().map_err(|_| unreadable(name))))
}
/// Reads the contents of a dropped file; the browser hands them over asynchronously.
/// Reads the contents of dropped file `name`; the browser hands them over asynchronously.
#[cfg(target_arch = "wasm32")]
pub(crate) fn dropped_bytes(f: &egui::DroppedFileHandle) -> ReadFuture {
let f = f.clone();
Box::pin(async move { f.bytes_async().await.ok() })
pub(crate) fn dropped_bytes(f: &egui::DroppedFileHandle, name: &str) -> ReadFuture {
let (f, name) = (f.clone(), name.to_string());
Box::pin(async move {
if let Some(file) = f.web_file() {
browser_can_read(&name, file.size())?;
}
f.bytes_async().await.map_err(|_| unreadable(&name))
})
}
/// While files are dragged over the window: a tinted, outlined `rect` with `label` in its middle,
+6 -7
View File
@@ -72,8 +72,7 @@ fn run_pending(app: &mut FigApp, pending: PendingOpen) -> Option<Value> {
/// An import that has no destination left: nothing is imported, and the user is told why.
fn cancel(app: &mut FigApp, message: String) {
app.ui.last_error = Some(message.clone());
app.toast(message);
app.show_error(message);
}
/// Frames the card is drawn before the files open: egui lays a new card out invisibly on its first
@@ -94,20 +93,20 @@ fn read_contents(app: &mut FigApp, ctx: &egui::Context) -> bool {
reading = true;
true
}
Poll::Ready(Some(bytes)) => {
Poll::Ready(Ok(bytes)) => {
use base64::Engine as _;
pending.params["data"] = json!(base64::engine::general_purpose::STANDARD.encode(&bytes));
pending.read = None;
true
}
Poll::Ready(None) => {
unread.push(pending.label.clone());
Poll::Ready(Err(why)) => {
unread.push(why);
false
}
}
});
for name in unread {
cancel(app, trf("Couldn't read {name}.", &[("name", &name)]));
for why in unread {
app.show_error(why);
}
reading
}