mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
Merge #1010: Web build: Save, Save as and Export arrive as downloads (@Ridzzz0Alam)
This commit is contained in:
@@ -63,10 +63,17 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"file.saveAs",
|
||||
"Save as",
|
||||
"{path} (no extension adds .figcraft); a .fig path writes an experimental export copy of the shown document with `warnings`, keeping the native path, unsaved changes and version preview",
|
||||
"{path | data: true (no path: returns the file as base64 `data` with its `name`, for a download)} (no extension adds .figcraft); a .fig path writes an experimental export copy of the shown document with `warnings`, keeping the native path, unsaved changes and version preview",
|
||||
false,
|
||||
save_as
|
||||
),
|
||||
spec!(
|
||||
"file.markSaved",
|
||||
"Mark saved",
|
||||
"{} clears the unsaved changes of the active tab's file and its branch tabs: run it once the file from `file.saveAs {data: true}` was delivered (a refused download keeps them)",
|
||||
false,
|
||||
mark_saved
|
||||
),
|
||||
spec!(
|
||||
"file.export",
|
||||
"Export",
|
||||
@@ -77,7 +84,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"file.exportLayers",
|
||||
"Export layers",
|
||||
"{ids? (default the selection, else every layer on the page with export settings), assets? (true: the Inspect tab's assets in and under them instead: layers with export settings, icons as SVG, images as PNG), dir? (folder to write into; else each file comes back as base64 `data`), bleed? (PDF: points, or with a unit: \"3mm\", \"0.125in\", \"9pt\"), cropMarks?, bleedMarks?, cmyk? (PDF print options): for every PDF instead of each setting's own} every layer once per export setting, named `<layer><suffix>.<format>` (\"@2x\" when a scaled export has no suffix; repeats get \" 2\"; slashes in a layer name make folders). Returns {files: [{name, id, bytes, path | data}], skipped (layers without settings)}",
|
||||
"{ids? (default the selection, else every layer on the page with export settings), assets? (true: the Inspect tab's assets in and under them instead: layers with export settings, icons as SVG, images as PNG), dir? (folder to write into; else each file comes back as base64 `data`), zip? (no dir: the files come back together as one base64 zip `data` instead), bleed? (PDF: points, or with a unit: \"3mm\", \"0.125in\", \"9pt\"), cropMarks?, bleedMarks?, cmyk? (PDF print options): for every PDF instead of each setting's own} every layer once per export setting, named `<layer><suffix>.<format>` (\"@2x\" when a scaled export has no suffix; repeats get \" 2\"; slashes in a layer name make folders). Returns {files: [{name, id, bytes, path | data}], skipped (layers without settings), data? (zip)}",
|
||||
false,
|
||||
export_layers
|
||||
),
|
||||
@@ -855,37 +862,59 @@ fn color_profile(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
pub(crate) fn save_ends_preview(s: &Session, cmd: &str, p: &Value) -> bool {
|
||||
let path =
|
||||
string(p, "path").or_else(|| if cmd == "file.save" { s.docs.get(crate::branches::file_tab(s)).and_then(|ds| ds.path.clone()) } else { None });
|
||||
path.is_some_and(|path| ext(&path) != "fig")
|
||||
path.is_some_and(|path| ext(&path) != "fig") || (cmd == "file.saveAs" && wants_data(p))
|
||||
}
|
||||
|
||||
/// `file.saveAs` asked for the file's bytes instead of a path to write (the browser's download).
|
||||
fn wants_data(p: &Value) -> bool {
|
||||
p.get("data").and_then(Value::as_bool) == Some(true)
|
||||
}
|
||||
|
||||
/// Save the file of the active tab (the main file for a branch tab, with its branches), or export
|
||||
/// the active document as an experimental `.fig` copy without changing its native save state.
|
||||
fn save_to(s: &mut Session, path: String) -> Result<Value> {
|
||||
if ext(&path) == "fig" {
|
||||
/// Without a path the file comes back as base64 `data` for the caller to deliver (#969).
|
||||
fn save_to(s: &mut Session, path: Option<String>) -> Result<Value> {
|
||||
if let Some(path) = path.as_deref().filter(|p| ext(p) == "fig") {
|
||||
return export_fig(s, &json!({"path": path}));
|
||||
}
|
||||
let i = crate::branches::file_tab(s);
|
||||
let ds = s.docs.get_mut(i).ok_or(EngineError::NoDocument)?;
|
||||
let mut snapshot = FileSnapshot::of(ds);
|
||||
let follows_path = ds.path.as_deref().map_or(ds.doc.name == "Untitled", |old| ds.doc.name == file_stem(old));
|
||||
if follows_path && ds.path.as_deref() != Some(path.as_str()) {
|
||||
Arc::make_mut(&mut snapshot.doc).name = file_stem(&path);
|
||||
if let Some(path) = path.as_deref()
|
||||
&& follows_path
|
||||
&& ds.path.as_deref() != Some(path)
|
||||
{
|
||||
Arc::make_mut(&mut snapshot.doc).name = file_stem(path);
|
||||
}
|
||||
let bytes = snapshot.encode()?;
|
||||
let Some(path) = path else {
|
||||
// The download can still be refused: the caller marks the file saved once it was delivered.
|
||||
let name = format!("{}.figcraft", ds.doc.name);
|
||||
return Ok(json!({"name": name, "bytes": bytes.len(), "data": base64::engine::general_purpose::STANDARD.encode(&bytes)}));
|
||||
};
|
||||
write(&path, &bytes)?;
|
||||
ds.doc = snapshot.doc;
|
||||
ds.path = Some(path.clone());
|
||||
ds.mark_saved();
|
||||
ds.revision += 1;
|
||||
let uid = ds.uid;
|
||||
branch_tabs_saved(s, uid);
|
||||
mark_saved(s, &json!({}))?;
|
||||
remember(s, &path);
|
||||
Ok(json!({"path": path, "bytes": bytes.len()}))
|
||||
}
|
||||
|
||||
/// The file of the active tab (with its branch tabs) now matches what was written or delivered.
|
||||
fn mark_saved(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let i = crate::branches::file_tab(s);
|
||||
let ds = s.docs.get_mut(i).ok_or(EngineError::NoDocument)?;
|
||||
ds.mark_saved();
|
||||
ds.revision += 1;
|
||||
let uid = ds.uid;
|
||||
branch_tabs_saved(s, uid);
|
||||
Ok(json!({"saved": true}))
|
||||
}
|
||||
|
||||
fn save(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(path) = string(p, "path") {
|
||||
return save_to(s, path);
|
||||
return save_to(s, Some(path));
|
||||
}
|
||||
match s.docs.get(crate::branches::file_tab(s)).ok_or(EngineError::NoDocument)?.path.clone() {
|
||||
// Earlier builds wrote native data under a .fig name: never overwrite that file with an export.
|
||||
@@ -893,7 +922,7 @@ fn save(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.request_ui("dialog.saveAs", json!({}));
|
||||
Err(EngineError::Disabled("file.save".into(), "the saved path ends in .fig: choose Save as or pass `path`".into()))
|
||||
}
|
||||
Some(path) => save_to(s, path),
|
||||
Some(path) => save_to(s, Some(path)),
|
||||
None => {
|
||||
s.request_ui("dialog.saveAs", json!({}));
|
||||
Err(EngineError::Disabled("file.save".into(), "the file has no path yet: pass `path`".into()))
|
||||
@@ -907,8 +936,9 @@ fn save_as(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if ext(&path).is_empty() {
|
||||
path.push_str(".figcraft");
|
||||
}
|
||||
save_to(s, path)
|
||||
save_to(s, Some(path))
|
||||
}
|
||||
None if wants_data(p) => save_to(s, None),
|
||||
None => {
|
||||
s.request_ui("dialog.saveAs", json!({}));
|
||||
Err(EngineError::Disabled("file.saveAs".into(), "pass `path`".into()))
|
||||
@@ -1207,6 +1237,9 @@ fn export_layers(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(EngineError::Disabled(cmd.into(), "no export settings (add one with export.add)".into()));
|
||||
}
|
||||
let dir = string(p, "dir");
|
||||
// The browser saves one download at a time: several files go as one zip (#969).
|
||||
let zip = dir.is_none() && boolean(p, "zip").unwrap_or(false);
|
||||
let mut members = Vec::new();
|
||||
// Print options passed here stand in for every PDF setting's own.
|
||||
let print = has_print_param(p).then(|| print_param(cmd, p, PrintSettings::default())).transpose()?;
|
||||
let mut files = Vec::with_capacity(jobs.len());
|
||||
@@ -1226,12 +1259,18 @@ fn export_layers(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
write(&path, &bytes)?;
|
||||
f["path"] = json!(path);
|
||||
}
|
||||
None if zip => members.push((job.file.as_str(), bytes)),
|
||||
None => f["data"] = json!(base64::engine::general_purpose::STANDARD.encode(&bytes)),
|
||||
}
|
||||
files.push(f);
|
||||
}
|
||||
let with_settings: std::collections::BTreeSet<NodeId> = jobs.iter().map(|j| j.id).collect();
|
||||
Ok(json!({"files": files, "skipped": ids.len().saturating_sub(with_settings.len())}))
|
||||
let mut r = json!({"files": files, "skipped": ids.len().saturating_sub(with_settings.len())});
|
||||
if zip {
|
||||
let archive = figcraft_format::zip_files(members.iter().map(|(n, b)| (*n, b.as_slice()))).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
r["data"] = json!(base64::engine::general_purpose::STANDARD.encode(&archive));
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// A PDF with one page per group of layers (an sRGB document), titled after the file; `area`
|
||||
@@ -1578,6 +1617,25 @@ mod rest_import_tests {
|
||||
assert_eq!(s.docs.len(), open, "a refused open adds nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_data_hands_back_the_file_and_marks_it_saved_once_delivered() {
|
||||
// The browser has no paths: Save as returns the file for a download (#969).
|
||||
let mut s = Session::new();
|
||||
s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
assert!(s.execute("file.saveAs", &json!({})).is_err(), "no path and no data: still asks for a path");
|
||||
let recent = s.prefs.recent_files.clone();
|
||||
let r = s.execute("file.saveAs", &json!({"data": true})).unwrap();
|
||||
assert_eq!(r["name"], "Untitled.figcraft");
|
||||
assert!(s.ds().unwrap().is_dirty(), "not delivered yet: a refused download must not lose the edits");
|
||||
s.execute("file.markSaved", &json!({})).unwrap();
|
||||
assert!(!s.ds().unwrap().is_dirty());
|
||||
assert_eq!(s.ds().unwrap().path, None, "still no path to save over");
|
||||
assert_eq!(s.prefs.recent_files, recent, "nothing on disk to remember");
|
||||
let mut t = Session::new();
|
||||
t.execute("file.open", &json!({"data": r["data"], "name": r["name"]})).unwrap();
|
||||
assert!(t.doc().unwrap().nodes().any(|n| matches!(n.kind, NodeKind::Rectangle)), "the bytes reopen");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_and_import_rest_json() {
|
||||
let dir = std::env::temp_dir().join(format!("figcraft-rest-{}", std::process::id()));
|
||||
|
||||
@@ -368,7 +368,8 @@ pub const PREVIEW_READ_ONLY: &str = "you're viewing an earlier version: restore
|
||||
|
||||
/// Commands that first end a version preview: they act on the document itself. Saving does so
|
||||
/// only with a native destination; export copies and pending format choices keep the preview.
|
||||
const ENDS_PREVIEW: &[&str] = &["file.save", "file.saveAs", "file.close", "file.revert", "file.saveVersion", "file.duplicate", "version.restore"];
|
||||
const ENDS_PREVIEW: &[&str] =
|
||||
&["file.save", "file.saveAs", "file.markSaved", "file.close", "file.revert", "file.saveVersion", "file.duplicate", "version.restore"];
|
||||
|
||||
static NEXT_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
||||
|
||||
@@ -343,6 +343,20 @@ pub(crate) fn put<W: Write + Seek>(
|
||||
z.write_all(bytes).map_err(io_err)
|
||||
}
|
||||
|
||||
/// `files` (name, bytes; a `/` in a name makes a folder) as one zip archive: several exports
|
||||
/// delivered as a single download (#969).
|
||||
pub fn zip_files<'a>(files: impl IntoIterator<Item = (&'a str, &'a [u8])>) -> Result<Vec<u8>, FormatError> {
|
||||
let mut buf = std::io::Cursor::new(Vec::new());
|
||||
{
|
||||
let mut z = zip::ZipWriter::new(&mut buf);
|
||||
for (name, bytes) in files {
|
||||
put(&mut z, name, zip::write::SimpleFileOptions::default(), bytes)?;
|
||||
}
|
||||
z.finish().map_err(io_err)?;
|
||||
}
|
||||
Ok(buf.into_inner())
|
||||
}
|
||||
|
||||
/// Add `doc` as a `document.json`-style member, serialized straight into the compressor.
|
||||
fn put_json<W: Write + Seek>(z: &mut zip::ZipWriter<W>, name: &str, doc: &Document) -> Result<(), FormatError> {
|
||||
z.start_file(name, json_options()).map_err(io_err)?;
|
||||
|
||||
@@ -323,6 +323,7 @@ name = "English"
|
||||
"Corner radius" = "Corner radius"
|
||||
"Corner smoothing" = "Corner smoothing"
|
||||
"Couldn't download the variables" = "Couldn't download the variables"
|
||||
"Couldn't download {name}" = "Couldn't download {name}"
|
||||
"Couldn't reach {server}: showing what this computer remembers." = "Couldn't reach {server}: showing what this computer remembers."
|
||||
"Couldn't read {name}." = "Couldn't read {name}."
|
||||
"Couldn't save automatically: {error}" = "Couldn't save automatically: {error}"
|
||||
|
||||
@@ -14,6 +14,7 @@ status = "draft — needs native review"
|
||||
"Compatibility notes" = "Notas de compatibilidad"
|
||||
"Connector" = "Conector"
|
||||
"Couldn't download the variables" = "No se pudieron descargar las variables"
|
||||
"Couldn't download {name}" = "No se pudo descargar {name}"
|
||||
|
||||
"% of all commits" = "% del total de commits"
|
||||
"(no longer published)" = "(ya no publicado)"
|
||||
@@ -1965,6 +1966,7 @@ status = "draft — needs native review"
|
||||
"file.revert" = "Volver a lo guardado"
|
||||
"file.save" = "Guardar"
|
||||
"file.saveAs" = "Guardar como"
|
||||
"file.markSaved" = "Marcar como guardado"
|
||||
"file.saveVersion" = "Guardar en el historial de versiones"
|
||||
"file.versionHistory" = "Mostrar historial de versiones"
|
||||
"fill.add" = "Añadir relleno"
|
||||
|
||||
@@ -695,7 +695,11 @@ pub(crate) fn file_saved(app: &mut FigApp, result: &Value) {
|
||||
}
|
||||
|
||||
fn result_file_name(result: &Value) -> String {
|
||||
result["path"].as_str().and_then(|p| Path::new(p).file_name()).map(|f| f.to_string_lossy().into_owned()).unwrap_or_default()
|
||||
match result["path"].as_str() {
|
||||
Some(p) => Path::new(p).file_name().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default(),
|
||||
// A download (#969).
|
||||
None => result["name"].as_str().unwrap_or_default().to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The `.fig` result stays an export: native save state is unchanged, and omissions are reported.
|
||||
@@ -711,6 +715,15 @@ pub(crate) fn fig_exported(app: &mut FigApp, result: &Value) {
|
||||
/// Save the whole file as `.fig`; the toast says how much was left out.
|
||||
fn save_fig(app: &mut FigApp, name: &str) {
|
||||
let filename = format!("{name}.fig");
|
||||
if app.services.downloads() {
|
||||
if let Some(mut r) = app.run("file.export", json!({"format": "fig"}))
|
||||
&& app.download(&filename, &r)
|
||||
{
|
||||
r["name"] = json!(filename);
|
||||
fig_exported(app, &r);
|
||||
}
|
||||
return;
|
||||
}
|
||||
app.pick_save(tr("Export"), &filename, &["fig"], |app, p| {
|
||||
if let Some(r) = app.run("file.export", json!({"format": "fig", "path": p.to_string_lossy()})) {
|
||||
fig_exported(app, &r);
|
||||
|
||||
@@ -320,6 +320,16 @@ pub(crate) fn export_layers(app: &mut FigApp, ids: &[NodeId], assets: bool) {
|
||||
[] => {}
|
||||
// The suggested name keeps only the file's own name (a layer's folders apply to folders).
|
||||
[one] => save_one(app, &[one.id], &one.setting, one.file.rsplit('/').next().unwrap_or(&one.file)),
|
||||
// No folders in the browser, and it lets a page start one download at a time: the files
|
||||
// arrive together as one zip (#969).
|
||||
_ if app.services.downloads() => {
|
||||
let name = format!("{}.zip", app.session.doc().map_or("Export", |d| d.name.as_str()));
|
||||
if let Some(r) = app.run("file.exportLayers", json!({"ids": ids, "assets": assets, "zip": true}))
|
||||
&& app.download(&name, &r)
|
||||
{
|
||||
app.toast(trn("Exported {n} files", r["files"].as_array().map_or(0, Vec::len), &[]));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let ids = ids.to_vec();
|
||||
app.pick_folder(tr("Export"), move |app, dir| {
|
||||
@@ -337,6 +347,14 @@ pub(crate) fn save_one(app: &mut FigApp, ids: &[NodeId], setting: &ExportSetting
|
||||
let ext = setting.format.ext();
|
||||
let mut params = setting_params(setting);
|
||||
params["ids"] = json!(ids);
|
||||
if app.services.downloads() {
|
||||
if let Some(r) = app.run("file.export", params)
|
||||
&& app.download(file, &r)
|
||||
{
|
||||
app.toast(trf("Exported {name}", &[("name", &file)]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
app.pick_save(tr("Export"), file, &[ext], move |app, p| {
|
||||
params["path"] = json!(p.to_string_lossy());
|
||||
if app.run("file.export", params).is_some() {
|
||||
|
||||
@@ -236,6 +236,8 @@ mod vector_tail_ui_tests;
|
||||
#[cfg(test)]
|
||||
mod web_clipboard_tests;
|
||||
#[cfg(test)]
|
||||
mod web_save_tests;
|
||||
#[cfg(test)]
|
||||
mod windows_qa_tests;
|
||||
#[cfg(test)]
|
||||
mod windows_tests;
|
||||
@@ -1185,6 +1187,15 @@ impl FigApp {
|
||||
}
|
||||
|
||||
pub fn save_as_dialog(&mut self) {
|
||||
if self.services.downloads() {
|
||||
if let Some(r) = self.run("file.saveAs", json!({"data": true}))
|
||||
&& self.download(r["name"].as_str().unwrap_or_default(), &r)
|
||||
{
|
||||
self.run("file.markSaved", json!({}));
|
||||
dialogs::file_saved(self, &r);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Ok(name) = self.session.doc().map(|d| d.name.clone()) else { return };
|
||||
self.pick_save(i18n::tr("Save as"), &format!("{name}.figcraft"), &["figcraft", "fig"], |app, mut p| {
|
||||
if p.extension().is_none_or(|e| e.is_empty()) {
|
||||
@@ -1289,6 +1300,18 @@ impl FigApp {
|
||||
self.start_dialog(|services| services.pick_save(title, name, exts), true, done);
|
||||
}
|
||||
|
||||
/// Hand a command result's base64 `data` over as a download named `name`, where the platform
|
||||
/// saves by downloads rather than save dialogs (the browser, #969). Says so when nothing arrived.
|
||||
pub(crate) fn download(&mut self, name: &str, result: &Value) -> bool {
|
||||
use base64::Engine as _;
|
||||
let bytes = result["data"].as_str().and_then(|d| base64::engine::general_purpose::STANDARD.decode(d).ok());
|
||||
if bytes.is_some_and(|b| self.services.download(name, &b)) {
|
||||
return true;
|
||||
}
|
||||
self.toast(i18n::trf("Couldn't download {name}", &[("name", &name)]));
|
||||
false
|
||||
}
|
||||
|
||||
/// Pick a folder; `done` runs as [`FigApp::pick_open`]'s does.
|
||||
pub fn pick_folder(&mut self, title: &str, done: impl FnOnce(&mut FigApp, PathBuf) + 'static) {
|
||||
self.start_dialog(|services| services.pick_folder(title), true, done);
|
||||
|
||||
@@ -282,6 +282,15 @@ pub fn invoke(app: &mut FigApp, id: &str) {
|
||||
/// Every visible top-level frame of the page as one PDF page each, through a save dialog.
|
||||
fn export_frames_pdf(app: &mut FigApp) {
|
||||
let name = app.session.doc().map(|d| d.name.clone()).unwrap_or_else(|_| "Untitled".into());
|
||||
if app.services.downloads() {
|
||||
let file = format!("{name}.pdf");
|
||||
if let Some(r) = app.run("file.exportFramesPdf", json!({}))
|
||||
&& app.download(&file, &r)
|
||||
{
|
||||
app.toast(trf("Exported {name}", &[("name", &file)]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
app.pick_save(tr("Export"), &format!("{name}.pdf"), &["pdf"], |app, p| {
|
||||
if app.run("file.exportFramesPdf", json!({"path": p.to_string_lossy()})).is_some() {
|
||||
app.toast(trf("Exported {name}", &[("name", &p.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default())]));
|
||||
|
||||
@@ -9,12 +9,12 @@ use serde_json::{Value, json};
|
||||
use crate::variables_panel::export_dialog;
|
||||
use crate::{FigApp, NoServices, Services};
|
||||
|
||||
type Downloads = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
|
||||
pub(crate) type Downloads = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
|
||||
|
||||
/// Services that save by downloads and keep them (`deliver` false: the browser refused).
|
||||
struct Browser {
|
||||
got: Downloads,
|
||||
deliver: bool,
|
||||
pub(crate) struct Browser {
|
||||
pub(crate) got: Downloads,
|
||||
pub(crate) deliver: bool,
|
||||
}
|
||||
|
||||
impl Services for Browser {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//! Save, Save as and Export where the platform saves by downloads (the browser, which has no save
|
||||
//! dialogs or paths): the file arrives as a download instead of nothing happening (#969).
|
||||
|
||||
use base64::Engine as _;
|
||||
use figcraft_doc::{ExportSetting, NodeId};
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::FigApp;
|
||||
use crate::variables_export_ui_tests::{Browser, Downloads};
|
||||
|
||||
fn browser(deliver: bool) -> (FigApp, Downloads, NodeId) {
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
let id = serde_json::from_value(r["id"].clone()).unwrap();
|
||||
let got = Downloads::default();
|
||||
(FigApp::new(s, Box::new(Browser { got: got.clone(), deliver })), got, id)
|
||||
}
|
||||
|
||||
fn toast(app: &FigApp) -> String {
|
||||
app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_downloads_the_figcraft_file() {
|
||||
let (mut app, got, _) = browser(true);
|
||||
app.save();
|
||||
let got = got.lock().unwrap();
|
||||
let [(name, bytes)] = got.as_slice() else { panic!("one download, got {}", got.len()) };
|
||||
assert_eq!(name, "Untitled.figcraft");
|
||||
let mut reopened = Session::new();
|
||||
let data = base64::engine::general_purpose::STANDARD.encode(bytes);
|
||||
reopened.execute("file.open", &json!({"data": data, "name": name})).unwrap();
|
||||
assert!(!app.session.ds().unwrap().is_dirty());
|
||||
assert_eq!(toast(&app), "Saved Untitled.figcraft");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_downloads_the_exported_file() {
|
||||
let (mut app, got, id) = browser(true);
|
||||
crate::export_panel::save_one(&mut app, &[id], &ExportSetting::default(), "Rectangle.png");
|
||||
let got = got.lock().unwrap();
|
||||
let [(name, bytes)] = got.as_slice() else { panic!("one download, got {}", got.len()) };
|
||||
assert_eq!(name, "Rectangle.png");
|
||||
assert!(bytes.starts_with(b"\x89PNG"));
|
||||
assert_eq!(toast(&app), "Exported Rectangle.png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_refused_download_says_so() {
|
||||
let (mut app, got, _) = browser(false);
|
||||
app.save_as_dialog();
|
||||
assert_eq!(got.lock().unwrap().len(), 1);
|
||||
assert_eq!(toast(&app), "Couldn't download Untitled.figcraft");
|
||||
assert!(app.session.ds().unwrap().is_dirty(), "no file arrived: closing must still warn about the edits");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn several_layers_download_as_one_zip() {
|
||||
// The browser lets a page start one download at a time: two downloads lost the second.
|
||||
let (mut app, got, a) = browser(true);
|
||||
let r = app.session.execute("shape.rectangle", &json!({"x": 20, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
let b: NodeId = serde_json::from_value(r["id"].clone()).unwrap();
|
||||
app.session.execute("export.add", &json!({"ids": [a, b]})).unwrap();
|
||||
crate::export_panel::export_layers(&mut app, &[a, b], false);
|
||||
let got = got.lock().unwrap();
|
||||
let [(name, bytes)] = got.as_slice() else { panic!("one download, got {}", got.len()) };
|
||||
assert_eq!(name, "Untitled.zip");
|
||||
assert!(bytes.starts_with(b"PK\x03\x04"));
|
||||
assert!(bytes.windows(15).any(|w| w == b"Rectangle 2.png"), "both layers are in the zip");
|
||||
assert_eq!(toast(&app), "Exported 2 files");
|
||||
}
|
||||
Reference in New Issue
Block a user