mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
#830: cap files read in the browser at 512 MiB, and say why a file isn't opened
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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} · ブランチで削除"
|
||||
|
||||
@@ -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} · 브랜치에서 삭제됨"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() })
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user