mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 13:26:54 -06:00
M1: show an import report after opening a file
Opening or importing a .fig or REST JSON file, or an SVG that has notes, from the app (File > Open or Import, a recent file, a drop, a paste, the system's open event) now shows an Import report window with the document's name, its page, layer and image counts and the compatibility notes the importer collected, instead of leaving them only in the command result. Commands sent over the control channel or MCP never open it. file.open and file.import return the counts as `summary`; ui.inspect reports the open report as `importReport`. The fixed notes the importers emit are translated (German and Spanish) and shown with a capital first letter; other notes, such as SVG's, show as written. The report goes away with its document. Escape or Close dismisses it.
This commit is contained in:
@@ -50,7 +50,7 @@ pub fn poll(inbox: &Inbox, fig: &mut FigApp, ctx: &egui::Context) {
|
||||
/// Open `paths` as File › Open does; a file that fails shows its error and the others still open.
|
||||
pub fn open_documents(fig: &mut FigApp, paths: Vec<PathBuf>) {
|
||||
for p in paths {
|
||||
if fig.run("file.open", json!({"path": p.to_string_lossy()})).is_some() {
|
||||
if fig.run_user("file.open", json!({"path": p.to_string_lossy()})).is_some() {
|
||||
fig.ui.home = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"file.open",
|
||||
"Open",
|
||||
"{path, variables? (REST: the local variables JSON as an object, JSON text or a path; default `<name>.variables.json` or `variables.json` next to the file)} (.figcraft, .json: FigCraft or REST API file JSON, .fig (best effort), .svg, .png, .jpg …); REST and .fig imports return `warnings`",
|
||||
"{path, variables? (REST: the local variables JSON as an object, JSON text or a path; default `<name>.variables.json` or `variables.json` next to the file)} (.figcraft, .json: FigCraft or REST API file JSON, .fig (best effort), .svg, .png, .jpg …); REST and .fig imports return `warnings`; a file opened anew returns `summary` {name, pages, layers, images} (one that was already open just returns its `index`)",
|
||||
false,
|
||||
open
|
||||
),
|
||||
@@ -74,7 +74,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"file.import",
|
||||
"Import",
|
||||
"{path | data (base64) + name, x?, y?, center? (SVG: centre it on x, y), variables? (REST: the local variables JSON as an object, JSON text or a path)} SVG becomes an editable frame (shapes, text, gradients, masks, effects; with `warnings` for what it left out), images become image layers, .figcraft, REST API file JSON and .fig pages are appended with their variables and flows (REST and .fig: with `warnings`)",
|
||||
"{path | data (base64) + name, x?, y?, center? (SVG: centre it on x, y), variables? (REST: the local variables JSON as an object, JSON text or a path)} SVG becomes an editable frame (shapes, text, gradients, masks, effects; with `warnings` for what it left out), images become image layers, .figcraft, REST API file JSON and .fig pages are appended with their variables and flows (REST and .fig: with `warnings`; those return `summary` {name, pages, layers, images} of the file read)",
|
||||
true,
|
||||
import
|
||||
),
|
||||
@@ -461,6 +461,11 @@ fn file_stem(path: &str) -> String {
|
||||
std::path::Path::new(path).file_stem().and_then(|e| e.to_str()).unwrap_or("Untitled").to_string()
|
||||
}
|
||||
|
||||
/// The counts the import report shows for a freshly loaded document.
|
||||
fn import_summary(doc: &Document) -> Value {
|
||||
json!({"name": doc.name, "pages": doc.pages.len(), "layers": doc.nodes().filter(|n| !n.is_page()).count(), "images": doc.images.len()})
|
||||
}
|
||||
|
||||
/// Load a document from any supported file (REST API file JSON images come from an `images`
|
||||
/// folder next to the file).
|
||||
pub fn load(path: &str) -> Result<(Document, Vec<String>)> {
|
||||
@@ -672,9 +677,10 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
more => format!("{}: {} problems were repaired or left out while opening it, e.g. {}", file_stem(&path), more.len(), more[0]),
|
||||
});
|
||||
}
|
||||
let summary = import_summary(&loaded.doc);
|
||||
s.docs.push(DocState::new(loaded.doc, native.then_some(path)).with_versions(loaded.versions).with_branches(loaded.branches));
|
||||
s.active = s.docs.len() - 1;
|
||||
let mut out = json!({"index": s.active});
|
||||
let mut out = json!({"index": s.active, "summary": summary});
|
||||
out[if warnings { "warnings" } else { "repairs" }] = json!(loaded.issues);
|
||||
Ok(out)
|
||||
}
|
||||
@@ -1215,8 +1221,9 @@ fn import(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let sibling = if variables.is_none() { path.as_deref().and_then(sibling_variables) } else { None };
|
||||
let (other, rest) = load_with(&bytes, &name, variables.as_ref().or(sibling.as_ref()), images)?;
|
||||
let summary = import_summary(&other.doc);
|
||||
let pages = append_pages(s.doc_mut()?, &other.doc)?;
|
||||
let mut out = json!({"pages": pages});
|
||||
let mut out = json!({"pages": pages, "summary": summary});
|
||||
if rest {
|
||||
out["warnings"] = json!(other.issues);
|
||||
}
|
||||
|
||||
@@ -261,6 +261,7 @@ status = "draft — needs native review"
|
||||
"Commits" = "Commits"
|
||||
"Company" = "Unternehmen"
|
||||
"Compare changes" = "Änderungen vergleichen"
|
||||
"Compatibility notes" = "Hinweise zur Kompatibilität"
|
||||
"Completed" = "Abgeschlossen"
|
||||
"Component" = "Komponente"
|
||||
"Component name" = "Komponentenname"
|
||||
@@ -464,6 +465,7 @@ status = "draft — needs native review"
|
||||
"False" = "Falsch"
|
||||
"Fetching from the server…" = "Wird vom Server abgerufen…"
|
||||
"FigCraft couldn't open its window: {error}" = "FigCraft konnte sein Fenster nicht öffnen: {error}"
|
||||
"FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged." = "FigCraft unterstützt noch nicht alle Funktionen dieser Dateien. Die Hinweise nennen, was in der importierten Kopie weggelassen oder geändert wurde; die Originaldatei bleibt unverändert."
|
||||
"File" = "Datei"
|
||||
"File name" = "Dateiname"
|
||||
"File published" = "Datei veröffentlicht"
|
||||
@@ -580,11 +582,14 @@ status = "draft — needs native review"
|
||||
"If {condition}" = "Wenn {condition}"
|
||||
"Ignore auto layout (absolute position)" = "Auto-Layout ignorieren (absolute Position)"
|
||||
"Image" = "Bild"
|
||||
"Images" = "Bilder"
|
||||
"Import" = "Importieren"
|
||||
"Import path" = "Importpfad"
|
||||
"Import plugin from manifest" = "Plugin aus Manifest importieren"
|
||||
"Import plugin from manifest…" = "Plugin aus Manifest importieren…"
|
||||
"Import report" = "Importbericht"
|
||||
"Import variables" = "Variablen importieren"
|
||||
"Imported document" = "Importiertes Dokument"
|
||||
"Imported {name}" = "{name} importiert"
|
||||
"Imported {n} new variables, updated {updated}" = { one = "{n} neue Variable importiert, {updated} aktualisiert", other = "{n} neue Variablen importiert, {updated} aktualisiert" }
|
||||
"Import…" = "Importieren…"
|
||||
@@ -774,6 +779,7 @@ status = "draft — needs native review"
|
||||
"No colour variables fit here." = "Hier passen keine Farbvariablen."
|
||||
"No colour variables yet. Create them in Local variables." = "Noch keine Farbvariablen. Erstellen Sie sie unter Lokale Variablen."
|
||||
"No comments yet" = "Noch keine Kommentare"
|
||||
"No compatibility warnings reported" = "Keine Kompatibilitätswarnungen gemeldet"
|
||||
"No components yet. Select layers and press {keys}." = "Noch keine Komponenten. Wählen Sie Ebenen aus und drücken Sie {keys}."
|
||||
"No conflicts." = "Keine Konflikte."
|
||||
"No contributor data was built into this copy." = "Diese Kopie enthält keine Daten zu Mitwirkenden."
|
||||
@@ -860,6 +866,7 @@ status = "draft — needs native review"
|
||||
"Open overlay" = "Overlay öffnen"
|
||||
"Open prototype preview" = "Prototyp-Vorschau öffnen"
|
||||
"Open variables ({count})" = "Variablen öffnen ({count})"
|
||||
"Opened as a new document" = "Als neues Dokument geöffnet"
|
||||
"Opened “{name}” in a new tab" = "„{name}“ in neuem Tab geöffnet"
|
||||
"Opening the file…" = "Datei wird geöffnet…"
|
||||
"Opening {n} files as new files" = { one = "Wird als neue Datei geöffnet", other = "{n} Dateien werden als neue Dateien geöffnet" }
|
||||
@@ -885,6 +892,8 @@ status = "draft — needs native review"
|
||||
"Padding" = "Innenabstand"
|
||||
"Page" = "Seite"
|
||||
"Pages" = "Seiten"
|
||||
"Pages added to the document" = "Seiten zum Dokument hinzugefügt"
|
||||
"Placed on the current page" = "Auf der aktuellen Seite platziert"
|
||||
"Pages: {pages}" = "Seiten: {pages}"
|
||||
"Paint bucket" = "Farbeimer"
|
||||
"Pan the canvas" = "Arbeitsfläche verschieben"
|
||||
@@ -1435,25 +1444,87 @@ status = "draft — needs native review"
|
||||
"call any command its access allows" = "jeden Befehl aufrufen, den sein Zugriff erlaubt"
|
||||
"call only: {commands}" = "nur aufrufen: {commands}"
|
||||
"can edit the document" = "kann das Dokument bearbeiten"
|
||||
"conditional prototype actions nested too deeply were skipped" = "zu tief verschachtelte bedingte Prototyp-Aktionen wurden übersprungen"
|
||||
"conditional prototype actions with conditions that can't be evaluated were skipped" = "bedingte Prototyp-Aktionen mit nicht auswertbaren Bedingungen wurden übersprungen"
|
||||
"connector labels were skipped" = "Verbinderbeschriftungen wurden übersprungen"
|
||||
"connectors whose ends can't be placed are empty" = "Verbinder, deren Enden sich nicht platzieren lassen, sind leer"
|
||||
"crop marks" = "Schnittmarken"
|
||||
"damaged vector geometry was skipped" = "beschädigte Vektorgeometrie wurde übersprungen"
|
||||
"damaged vector networks import as outlines" = "beschädigte Vektornetze werden als Umrisse importiert"
|
||||
"development: {path}" = "Entwicklung: {path}"
|
||||
"document children other than pages were skipped" = "Dokumentinhalte, die keine Seiten sind, wurden übersprungen"
|
||||
"e.g. count + 1" = "z. B. count + 1"
|
||||
"e.g. count >= 3 and not done" = "z. B. count >= 3 and not done"
|
||||
"elbowed and curved connectors without a saved path import as straight lines" = "gewinkelte und gebogene Verbinder ohne gespeicherten Pfad werden als gerade Linien importiert"
|
||||
"entries without a node type were skipped" = "Einträge ohne Knotentyp wurden übersprungen"
|
||||
"figcraft:// links from a browser, chat or email open this app and join the session." = "figcraft://-Links aus Browser, Chat oder E-Mail öffnen diese App und treten der Sitzung bei."
|
||||
"filled areas of a vector with their own fill use the layer's fill" = "gefüllte Bereiche eines Vektors mit eigener Füllung verwenden die Füllung der Ebene"
|
||||
"from {server}" = "von {server}"
|
||||
"gamepad triggers without exactly one known controller button were skipped" = "Gamepad-Auslöser ohne genau eine bekannte Controllertaste wurden übersprungen"
|
||||
"grid tracks of fixed or content size import as equal tracks" = "Rasterspuren mit fester oder inhaltsabhängiger Größe werden als gleich große Spuren importiert"
|
||||
"hidden" = "ausgeblendet"
|
||||
"iOS" = "iOS"
|
||||
"instance swap properties whose default component isn't in the file were skipped" = "Instanztausch-Eigenschaften, deren Standardkomponente nicht in der Datei ist, wurden übersprungen"
|
||||
"instances inside their own main component were imported as frames" = "Instanzen innerhalb ihrer eigenen Hauptkomponente wurden als Rahmen importiert"
|
||||
"instances of components that aren't in the file show no layers" = "Instanzen von Komponenten, die nicht in der Datei sind, zeigen keine Ebenen"
|
||||
"instances of components that aren't in the file were imported as frames" = "Instanzen von Komponenten, die nicht in der Datei sind, wurden als Rahmen importiert"
|
||||
"instances that would nest a component inside itself were imported as frames" = "Instanzen, die eine Komponente in sich selbst verschachteln würden, wurden als Rahmen importiert"
|
||||
"instances without a main component were imported as empty frames" = "Instanzen ohne Hauptkomponente wurden als leere Rahmen importiert"
|
||||
"invalid or oversized vector networks import as outlines" = "ungültige oder zu große Vektornetze werden als Umrisse importiert"
|
||||
"invalid or oversized vector path data was skipped" = "ungültige oder zu große Vektorpfaddaten wurden übersprungen"
|
||||
"key triggers without a supported key were skipped" = "Tastenauslöser ohne unterstützte Taste wurden übersprungen"
|
||||
"key up triggers import as key presses" = "Auslöser beim Loslassen einer Taste werden als Tastendruck importiert"
|
||||
"layers without a position were placed at their parent's origin" = "Ebenen ohne Position wurden am Ursprung ihres übergeordneten Elements platziert"
|
||||
"layers without a type were skipped" = "Ebenen ohne Typ wurden übersprungen"
|
||||
"locked" = "gesperrt"
|
||||
"no bleed" = "kein Anschnitt"
|
||||
"nobody else here yet" = "noch niemand sonst hier"
|
||||
"now" = "jetzt"
|
||||
"or" = "oder"
|
||||
"points with their own stroke join use the layer's" = "Punkte mit eigener Konturecke verwenden die der Ebene"
|
||||
"prototype actions of unknown types were skipped" = "Prototyp-Aktionen unbekannter Art wurden übersprungen"
|
||||
"prototype actions without a destination in the file were skipped" = "Prototyp-Aktionen ohne Ziel in der Datei wurden übersprungen"
|
||||
"prototype expressions on variables that aren't in the variables JSON were skipped" = "Prototyp-Ausdrücke mit Variablen, die nicht im Variablen-JSON sind, wurden übersprungen"
|
||||
"prototype expressions with unsupported functions were skipped" = "Prototyp-Ausdrücke mit nicht unterstützten Funktionen wurden übersprungen"
|
||||
"prototype interactions without supported actions were skipped" = "Prototyp-Interaktionen ohne unterstützte Aktionen wurden übersprungen"
|
||||
"prototype navigations of unknown types were skipped" = "Prototyp-Navigationen unbekannter Art wurden übersprungen"
|
||||
"prototype transitions of unknown types play instantly" = "Prototyp-Übergänge unbekannter Art werden sofort ausgeführt"
|
||||
"prototype triggers of unknown types were skipped" = "Prototyp-Auslöser unbekannter Art wurden übersprungen"
|
||||
"put a widget on the canvas" = "ein Widget auf die Arbeitsfläche setzen"
|
||||
"read and edit the document" = "das Dokument lesen und bearbeiten"
|
||||
"read the document" = "das Dokument lesen"
|
||||
"reads the document" = "liest das Dokument"
|
||||
"set-variable actions on variables that aren't in the variables JSON were skipped" = "Aktionen „Variable festlegen“ für Variablen, die nicht im Variablen-JSON sind, wurden übersprungen"
|
||||
"set-variable actions with values that can't be evaluated were skipped" = "Aktionen „Variable festlegen“ mit nicht auswertbaren Werten wurden übersprungen"
|
||||
"set-variable actions with values that don't fit the variable were skipped" = "Aktionen „Variable festlegen“ mit Werten, die nicht zur Variable passen, wurden übersprungen"
|
||||
"set-variable actions without a value were skipped" = "Aktionen „Variable festlegen“ ohne Wert wurden übersprungen"
|
||||
"set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "Aktionen „Variablenmodus festlegen“ für Sammlungen oder Modi, die nicht im Variablen-JSON sind, wurden übersprungen"
|
||||
"stars and polygons without point counts or geometry use the default point count" = "Sterne und Polygone ohne Punktanzahl oder Geometrie verwenden die Standard-Punktanzahl"
|
||||
"text style overrides refer to missing table entries" = "Textstil-Überschreibungen verweisen auf fehlende Tabelleneinträge"
|
||||
"the file has no pages" = "die Datei hat keine Seiten"
|
||||
"the plugin" = "das Plugin"
|
||||
"the variables JSON has no `variableCollections` (expected the local variables response) and was ignored" = "das Variablen-JSON enthält keine `variableCollections` (erwartet wurde die Antwort mit den lokalen Variablen) und wurde ignoriert"
|
||||
"this session only" = "nur diese Sitzung"
|
||||
"unknown component property types were skipped" = "unbekannte Komponenteneigenschaftstypen wurden übersprungen"
|
||||
"variable aliases to variables that aren't in the variables JSON (or of another type) were skipped" = "Variablen-Aliase auf Variablen, die nicht im Variablen-JSON sind (oder einen anderen Typ haben), wurden übersprungen"
|
||||
"variable bindings to properties the layer doesn't have were skipped" = "Variablenbindungen an Eigenschaften, die die Ebene nicht hat, wurden übersprungen"
|
||||
"variable bindings to variables that aren't in the variables JSON (library variables) were skipped" = "Variablenbindungen an Variablen, die nicht im Variablen-JSON sind (Bibliotheksvariablen), wurden übersprungen"
|
||||
"variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)" = "Variablenbindungen wurden übersprungen: Das Datei-JSON enthält keine Variablendefinitionen (übergeben Sie das lokale Variablen-JSON als `variables` oder betten Sie dessen `meta` neben dem Datei-JSON ein)"
|
||||
"variable bindings whose variable type doesn't fit the property were skipped" = "Variablenbindungen, deren Variablentyp nicht zur Eigenschaft passt, wurden übersprungen"
|
||||
"variable collections without modes were skipped" = "Variablensammlungen ohne Modi wurden übersprungen"
|
||||
"variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped" = "Variablenmodus-Abfragen in Prototyp-Ausdrücken, die nicht aus einer Variable und einem ihrer Modi bestehen, wurden übersprungen"
|
||||
"variable modes of collections that aren't in the file were skipped" = "Variablenmodi von Sammlungen, die nicht in der Datei sind, wurden übersprungen"
|
||||
"variable modes of collections that aren't in the variables JSON were skipped" = "Variablenmodi von Sammlungen, die nicht im Variablen-JSON sind, wurden übersprungen"
|
||||
"variable values that don't match their type (or composed colours) were skipped" = "Variablenwerte, die nicht zu ihrem Typ passen (oder zusammengesetzte Farben), wurden übersprungen"
|
||||
"variables of unknown types were skipped" = "Variablen unbekannter Typen wurden übersprungen"
|
||||
"variables whose collection isn't in the variables JSON were skipped" = "Variablen, deren Sammlung nicht im Variablen-JSON ist, wurden übersprungen"
|
||||
"variables without readable values were skipped" = "Variablen ohne lesbare Werte wurden übersprungen"
|
||||
"vectors without geometry are empty" = "Vektoren ohne Geometrie sind leer"
|
||||
"vectors without geometry are empty (export the file JSON with geometry=paths)" = "Vektoren ohne Geometrie sind leer (exportieren Sie das Datei-JSON mit geometry=paths)"
|
||||
"very long text was cut to 1,048,576 characters" = "sehr langer Text wurde auf 1.048.576 Zeichen gekürzt"
|
||||
"very long vector paths were skipped" = "sehr lange Vektorpfade wurden übersprungen"
|
||||
"video actions are not supported and were skipped" = "Videoaktionen werden nicht unterstützt und wurden übersprungen"
|
||||
"video triggers are not supported and were skipped" = "Videoauslöser werden nicht unterstützt und wurden übersprungen"
|
||||
"{instances} in {files}" = "{instances} in {files}"
|
||||
"{language}: draft translation, waiting for a native speaker's review." = "{language}: Übersetzungsentwurf, wartet auf die Prüfung durch Muttersprachler."
|
||||
"{length} bleed" = "{length} Anschnitt"
|
||||
@@ -1496,6 +1567,7 @@ status = "draft — needs native review"
|
||||
"{n} matches" = { one = "{n} Treffer", other = "{n} Treffer" }
|
||||
"{n} minutes ago" = { one = "vor {n} Minute", other = "vor {n} Minuten" }
|
||||
"{n} months ago" = { one = "vor {n} Monat", other = "vor {n} Monaten" }
|
||||
"{n} more notes omitted from this report" = { one = "{n} weitere Notiz in diesem Bericht nicht aufgeführt", other = "{n} weitere Notizen in diesem Bericht nicht aufgeführt" }
|
||||
"{n} of {total}" = "{n} von {total}"
|
||||
"{n} people joined" = { one = "{n} Person ist beigetreten", other = "{n} Personen sind beigetreten" }
|
||||
"{n} preferred components" = { one = "{n} bevorzugte Komponente", other = "{n} bevorzugte Komponenten" }
|
||||
|
||||
@@ -270,6 +270,7 @@ name = "English"
|
||||
"Commits" = "Commits"
|
||||
"Company" = "Company"
|
||||
"Compare changes" = "Compare changes"
|
||||
"Compatibility notes" = "Compatibility notes"
|
||||
"Completed" = "Completed"
|
||||
"Component" = "Component"
|
||||
"Component name" = "Component name"
|
||||
@@ -482,6 +483,7 @@ name = "English"
|
||||
"Feb" = "Feb"
|
||||
"Fetching from the server…" = "Fetching from the server…"
|
||||
"FigCraft couldn't open its window: {error}" = "FigCraft couldn't open its window: {error}"
|
||||
"FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged." = "FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged."
|
||||
"File" = "File"
|
||||
"File name" = "File name"
|
||||
"File not found" = "File not found"
|
||||
@@ -606,11 +608,14 @@ name = "English"
|
||||
"If {condition}" = "If {condition}"
|
||||
"Ignore auto layout (absolute position)" = "Ignore auto layout (absolute position)"
|
||||
"Image" = "Image"
|
||||
"Images" = "Images"
|
||||
"Import" = "Import"
|
||||
"Import path" = "Import path"
|
||||
"Import plugin from manifest" = "Import plugin from manifest"
|
||||
"Import plugin from manifest…" = "Import plugin from manifest…"
|
||||
"Import report" = "Import report"
|
||||
"Import variables" = "Import variables"
|
||||
"Imported document" = "Imported document"
|
||||
"Imported {name}" = "Imported {name}"
|
||||
"Imported {n} new variables, updated {updated}" = { one = "Imported {n} new variable, updated {updated}", other = "Imported {n} new variables, updated {updated}" }
|
||||
"Import…" = "Import…"
|
||||
@@ -812,6 +817,7 @@ name = "English"
|
||||
"No colour variables fit here." = "No colour variables fit here."
|
||||
"No colour variables yet. Create them in Local variables." = "No colour variables yet. Create them in Local variables."
|
||||
"No comments yet" = "No comments yet"
|
||||
"No compatibility warnings reported" = "No compatibility warnings reported"
|
||||
"No components yet. Select layers and press {keys}." = "No components yet. Select layers and press {keys}."
|
||||
"No conflicts." = "No conflicts."
|
||||
"No contributor data was built into this copy." = "No contributor data was built into this copy."
|
||||
@@ -903,6 +909,7 @@ name = "English"
|
||||
"Open overlay" = "Open overlay"
|
||||
"Open prototype preview" = "Open prototype preview"
|
||||
"Open variables ({count})" = "Open variables ({count})"
|
||||
"Opened as a new document" = "Opened as a new document"
|
||||
"Opened “{name}” in a new tab" = "Opened “{name}” in a new tab"
|
||||
"Opening the file…" = "Opening the file…"
|
||||
"Opening {n} files as new files" = { one = "Opening as a new file", other = "Opening {n} files as new files" }
|
||||
@@ -929,6 +936,8 @@ name = "English"
|
||||
"Padding" = "Padding"
|
||||
"Page" = "Page"
|
||||
"Pages" = "Pages"
|
||||
"Pages added to the document" = "Pages added to the document"
|
||||
"Placed on the current page" = "Placed on the current page"
|
||||
"Pages: {pages}" = "Pages: {pages}"
|
||||
"Paint bucket" = "Paint bucket"
|
||||
"Pan the canvas" = "Pan the canvas"
|
||||
@@ -1506,26 +1515,88 @@ name = "English"
|
||||
"call any command its access allows" = "call any command its access allows"
|
||||
"call only: {commands}" = "call only: {commands}"
|
||||
"can edit the document" = "can edit the document"
|
||||
"conditional prototype actions nested too deeply were skipped" = "conditional prototype actions nested too deeply were skipped"
|
||||
"conditional prototype actions with conditions that can't be evaluated were skipped" = "conditional prototype actions with conditions that can't be evaluated were skipped"
|
||||
"connector labels were skipped" = "connector labels were skipped"
|
||||
"connectors whose ends can't be placed are empty" = "connectors whose ends can't be placed are empty"
|
||||
"crop marks" = "crop marks"
|
||||
"damaged vector geometry was skipped" = "damaged vector geometry was skipped"
|
||||
"damaged vector networks import as outlines" = "damaged vector networks import as outlines"
|
||||
"development: {path}" = "development: {path}"
|
||||
"document children other than pages were skipped" = "document children other than pages were skipped"
|
||||
"e.g. count + 1" = "e.g. count + 1"
|
||||
"e.g. count >= 3 and not done" = "e.g. count >= 3 and not done"
|
||||
"elbowed and curved connectors without a saved path import as straight lines" = "elbowed and curved connectors without a saved path import as straight lines"
|
||||
"entries without a node type were skipped" = "entries without a node type were skipped"
|
||||
"figcraft:// links from a browser, chat or email open this app and join the session." = "figcraft:// links from a browser, chat or email open this app and join the session."
|
||||
"filled areas of a vector with their own fill use the layer's fill" = "filled areas of a vector with their own fill use the layer's fill"
|
||||
"from {server}" = "from {server}"
|
||||
"gamepad triggers without exactly one known controller button were skipped" = "gamepad triggers without exactly one known controller button were skipped"
|
||||
"grid tracks of fixed or content size import as equal tracks" = "grid tracks of fixed or content size import as equal tracks"
|
||||
"hidden" = "hidden"
|
||||
"iOS" = "iOS"
|
||||
"instance swap properties whose default component isn't in the file were skipped" = "instance swap properties whose default component isn't in the file were skipped"
|
||||
"instances inside their own main component were imported as frames" = "instances inside their own main component were imported as frames"
|
||||
"instances of components that aren't in the file show no layers" = "instances of components that aren't in the file show no layers"
|
||||
"instances of components that aren't in the file were imported as frames" = "instances of components that aren't in the file were imported as frames"
|
||||
"instances that would nest a component inside itself were imported as frames" = "instances that would nest a component inside itself were imported as frames"
|
||||
"instances without a main component were imported as empty frames" = "instances without a main component were imported as empty frames"
|
||||
"invalid or oversized vector networks import as outlines" = "invalid or oversized vector networks import as outlines"
|
||||
"invalid or oversized vector path data was skipped" = "invalid or oversized vector path data was skipped"
|
||||
"just now" = "just now"
|
||||
"key triggers without a supported key were skipped" = "key triggers without a supported key were skipped"
|
||||
"key up triggers import as key presses" = "key up triggers import as key presses"
|
||||
"layers without a position were placed at their parent's origin" = "layers without a position were placed at their parent's origin"
|
||||
"layers without a type were skipped" = "layers without a type were skipped"
|
||||
"locked" = "locked"
|
||||
"no bleed" = "no bleed"
|
||||
"nobody else here yet" = "nobody else here yet"
|
||||
"now" = "now"
|
||||
"or" = "or"
|
||||
"points with their own stroke join use the layer's" = "points with their own stroke join use the layer's"
|
||||
"prototype actions of unknown types were skipped" = "prototype actions of unknown types were skipped"
|
||||
"prototype actions without a destination in the file were skipped" = "prototype actions without a destination in the file were skipped"
|
||||
"prototype expressions on variables that aren't in the variables JSON were skipped" = "prototype expressions on variables that aren't in the variables JSON were skipped"
|
||||
"prototype expressions with unsupported functions were skipped" = "prototype expressions with unsupported functions were skipped"
|
||||
"prototype interactions without supported actions were skipped" = "prototype interactions without supported actions were skipped"
|
||||
"prototype navigations of unknown types were skipped" = "prototype navigations of unknown types were skipped"
|
||||
"prototype transitions of unknown types play instantly" = "prototype transitions of unknown types play instantly"
|
||||
"prototype triggers of unknown types were skipped" = "prototype triggers of unknown types were skipped"
|
||||
"put a widget on the canvas" = "put a widget on the canvas"
|
||||
"read and edit the document" = "read and edit the document"
|
||||
"read the document" = "read the document"
|
||||
"reads the document" = "reads the document"
|
||||
"set-variable actions on variables that aren't in the variables JSON were skipped" = "set-variable actions on variables that aren't in the variables JSON were skipped"
|
||||
"set-variable actions with values that can't be evaluated were skipped" = "set-variable actions with values that can't be evaluated were skipped"
|
||||
"set-variable actions with values that don't fit the variable were skipped" = "set-variable actions with values that don't fit the variable were skipped"
|
||||
"set-variable actions without a value were skipped" = "set-variable actions without a value were skipped"
|
||||
"set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped"
|
||||
"stars and polygons without point counts or geometry use the default point count" = "stars and polygons without point counts or geometry use the default point count"
|
||||
"text style overrides refer to missing table entries" = "text style overrides refer to missing table entries"
|
||||
"the file has no pages" = "the file has no pages"
|
||||
"the plugin" = "the plugin"
|
||||
"the variables JSON has no `variableCollections` (expected the local variables response) and was ignored" = "the variables JSON has no `variableCollections` (expected the local variables response) and was ignored"
|
||||
"this session only" = "this session only"
|
||||
"unknown component property types were skipped" = "unknown component property types were skipped"
|
||||
"variable aliases to variables that aren't in the variables JSON (or of another type) were skipped" = "variable aliases to variables that aren't in the variables JSON (or of another type) were skipped"
|
||||
"variable bindings to properties the layer doesn't have were skipped" = "variable bindings to properties the layer doesn't have were skipped"
|
||||
"variable bindings to variables that aren't in the variables JSON (library variables) were skipped" = "variable bindings to variables that aren't in the variables JSON (library variables) were skipped"
|
||||
"variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)" = "variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)"
|
||||
"variable bindings whose variable type doesn't fit the property were skipped" = "variable bindings whose variable type doesn't fit the property were skipped"
|
||||
"variable collections without modes were skipped" = "variable collections without modes were skipped"
|
||||
"variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped" = "variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped"
|
||||
"variable modes of collections that aren't in the file were skipped" = "variable modes of collections that aren't in the file were skipped"
|
||||
"variable modes of collections that aren't in the variables JSON were skipped" = "variable modes of collections that aren't in the variables JSON were skipped"
|
||||
"variable values that don't match their type (or composed colours) were skipped" = "variable values that don't match their type (or composed colours) were skipped"
|
||||
"variables of unknown types were skipped" = "variables of unknown types were skipped"
|
||||
"variables whose collection isn't in the variables JSON were skipped" = "variables whose collection isn't in the variables JSON were skipped"
|
||||
"variables without readable values were skipped" = "variables without readable values were skipped"
|
||||
"vectors without geometry are empty" = "vectors without geometry are empty"
|
||||
"vectors without geometry are empty (export the file JSON with geometry=paths)" = "vectors without geometry are empty (export the file JSON with geometry=paths)"
|
||||
"very long text was cut to 1,048,576 characters" = "very long text was cut to 1,048,576 characters"
|
||||
"very long vector paths were skipped" = "very long vector paths were skipped"
|
||||
"video actions are not supported and were skipped" = "video actions are not supported and were skipped"
|
||||
"video triggers are not supported and were skipped" = "video triggers are not supported and were skipped"
|
||||
"yesterday" = "yesterday"
|
||||
"{instances} in {files}" = "{instances} in {files}"
|
||||
"{language}: draft translation, waiting for a native speaker's review." = "{language}: draft translation, waiting for a native speaker's review."
|
||||
@@ -1570,6 +1641,7 @@ name = "English"
|
||||
"{n} matches" = { one = "{n} match", other = "{n} matches" }
|
||||
"{n} minutes ago" = { one = "{n} minute ago", other = "{n} minutes ago" }
|
||||
"{n} months ago" = { one = "{n} month ago", other = "{n} months ago" }
|
||||
"{n} more notes omitted from this report" = { one = "{n} more note omitted from this report", other = "{n} more notes omitted from this report" }
|
||||
"{n} of {total}" = "{n} of {total}"
|
||||
"{n} people joined" = { one = "{n} person joined", other = "{n} people joined" }
|
||||
"{n} preferred components" = { one = "{n} preferred component", other = "{n} preferred components" }
|
||||
|
||||
@@ -8,6 +8,7 @@ name = "Español"
|
||||
status = "draft — needs native review"
|
||||
|
||||
[ui]
|
||||
"Compatibility notes" = "Notas de compatibilidad"
|
||||
"Couldn't download the variables" = "No se pudieron descargar las variables"
|
||||
|
||||
"% of all commits" = "% del total de commits"
|
||||
@@ -483,6 +484,7 @@ status = "draft — needs native review"
|
||||
"Feb" = "feb"
|
||||
"Fetching from the server…" = "Obteniendo del servidor…"
|
||||
"FigCraft couldn't open its window: {error}" = "FigCraft no pudo abrir su ventana: {error}"
|
||||
"FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged." = "FigCraft aún no admite todas las funciones de estos archivos. Las notas indican qué se omitió o cambió en la copia importada; el archivo original no se modifica."
|
||||
"File" = "Archivo"
|
||||
"File name" = "Nombre del archivo"
|
||||
"File not found" = "No se encuentra el archivo"
|
||||
@@ -607,11 +609,14 @@ status = "draft — needs native review"
|
||||
"If {condition}" = "Si {condition}"
|
||||
"Ignore auto layout (absolute position)" = "Ignorar diseño automático (posición absoluta)"
|
||||
"Image" = "Imagen"
|
||||
"Images" = "Imágenes"
|
||||
"Import" = "Importar"
|
||||
"Import path" = "Ruta de importación"
|
||||
"Import plugin from manifest" = "Importar plugin desde manifiesto"
|
||||
"Import plugin from manifest…" = "Importar plugin desde manifiesto…"
|
||||
"Import report" = "Informe de importación"
|
||||
"Import variables" = "Importar variables"
|
||||
"Imported document" = "Documento importado"
|
||||
"Imported {name}" = "{name} importado"
|
||||
"Imported {n} new variables, updated {updated}" = { one = "Variables importadas: {n} nueva; actualizadas: {updated}", other = "Variables importadas: {n} nuevas; actualizadas: {updated}" }
|
||||
"Import…" = "Importar…"
|
||||
@@ -813,6 +818,7 @@ status = "draft — needs native review"
|
||||
"No colour variables fit here." = "Ninguna variable de color encaja aquí."
|
||||
"No colour variables yet. Create them in Local variables." = "Aún no hay variables de color. Créalas en Variables locales."
|
||||
"No comments yet" = "Aún no hay comentarios"
|
||||
"No compatibility warnings reported" = "No se han registrado advertencias de compatibilidad"
|
||||
"No components yet. Select layers and press {keys}." = "Aún no hay componentes. Selecciona capas y pulsa {keys}."
|
||||
"No conflicts." = "Sin conflictos."
|
||||
"No contributor data was built into this copy." = "Esta copia no incluye datos de colaboradores."
|
||||
@@ -904,6 +910,7 @@ status = "draft — needs native review"
|
||||
"Open overlay" = "Abrir superposición"
|
||||
"Open prototype preview" = "Abrir vista previa del prototipo"
|
||||
"Open variables ({count})" = "Abrir variables ({count})"
|
||||
"Opened as a new document" = "Abierto como un documento nuevo"
|
||||
"Opened “{name}” in a new tab" = "“{name}” abierta en una pestaña nueva"
|
||||
"Opening the file…" = "Abriendo el archivo…"
|
||||
"Opening {n} files as new files" = { one = "Abriendo como archivo nuevo", other = "Abriendo {n} archivos como archivos nuevos" }
|
||||
@@ -930,6 +937,8 @@ status = "draft — needs native review"
|
||||
"Padding" = "Margen interno"
|
||||
"Page" = "Página"
|
||||
"Pages" = "Páginas"
|
||||
"Pages added to the document" = "Páginas añadidas al documento"
|
||||
"Placed on the current page" = "Colocado en la página actual"
|
||||
"Pages: {pages}" = "Páginas: {pages}"
|
||||
"Paint bucket" = "Bote de pintura"
|
||||
"Pan the canvas" = "Desplazar el lienzo"
|
||||
@@ -1507,26 +1516,88 @@ status = "draft — needs native review"
|
||||
"call any command its access allows" = "llamar a cualquier comando que su acceso permita"
|
||||
"call only: {commands}" = "llamar solo a: {commands}"
|
||||
"can edit the document" = "puede editar el documento"
|
||||
"conditional prototype actions nested too deeply were skipped" = "se omitieron las acciones condicionales de prototipo anidadas a demasiada profundidad"
|
||||
"conditional prototype actions with conditions that can't be evaluated were skipped" = "se omitieron las acciones condicionales de prototipo con condiciones que no se pueden evaluar"
|
||||
"connector labels were skipped" = "se omitieron las etiquetas de los conectores"
|
||||
"connectors whose ends can't be placed are empty" = "los conectores cuyos extremos no se pueden colocar están vacíos"
|
||||
"crop marks" = "marcas de corte"
|
||||
"damaged vector geometry was skipped" = "se omitió la geometría vectorial dañada"
|
||||
"damaged vector networks import as outlines" = "las redes vectoriales dañadas se importan como contornos"
|
||||
"development: {path}" = "desarrollo: {path}"
|
||||
"document children other than pages were skipped" = "se omitieron los elementos del documento que no son páginas"
|
||||
"e.g. count + 1" = "p. ej. count + 1"
|
||||
"e.g. count >= 3 and not done" = "p. ej. count >= 3 and not done"
|
||||
"elbowed and curved connectors without a saved path import as straight lines" = "los conectores angulares y curvos sin trazado guardado se importan como líneas rectas"
|
||||
"entries without a node type were skipped" = "se omitieron las entradas sin tipo de nodo"
|
||||
"figcraft:// links from a browser, chat or email open this app and join the session." = "Los enlaces figcraft:// de un navegador, chat o correo abren esta app y se unen a la sesión."
|
||||
"filled areas of a vector with their own fill use the layer's fill" = "las áreas rellenas de un vector con relleno propio usan el relleno de la capa"
|
||||
"from {server}" = "de {server}"
|
||||
"gamepad triggers without exactly one known controller button were skipped" = "se omitieron los desencadenantes de mando sin exactamente un botón de mando conocido"
|
||||
"grid tracks of fixed or content size import as equal tracks" = "las pistas de cuadrícula de tamaño fijo o según el contenido se importan como pistas iguales"
|
||||
"hidden" = "oculta"
|
||||
"iOS" = "iOS"
|
||||
"instance swap properties whose default component isn't in the file were skipped" = "se omitieron las propiedades de intercambio de instancia cuyo componente predeterminado no está en el archivo"
|
||||
"instances inside their own main component were imported as frames" = "las instancias dentro de su propio componente principal se importaron como marcos"
|
||||
"instances of components that aren't in the file show no layers" = "las instancias de componentes que no están en el archivo no muestran capas"
|
||||
"instances of components that aren't in the file were imported as frames" = "las instancias de componentes que no están en el archivo se importaron como marcos"
|
||||
"instances that would nest a component inside itself were imported as frames" = "las instancias que anidarían un componente dentro de sí mismo se importaron como marcos"
|
||||
"instances without a main component were imported as empty frames" = "las instancias sin componente principal se importaron como marcos vacíos"
|
||||
"invalid or oversized vector networks import as outlines" = "las redes vectoriales no válidas o demasiado grandes se importan como contornos"
|
||||
"invalid or oversized vector path data was skipped" = "se omitieron los datos de trazados vectoriales no válidos o demasiado grandes"
|
||||
"just now" = "justo ahora"
|
||||
"key triggers without a supported key were skipped" = "se omitieron los desencadenantes de tecla sin una tecla compatible"
|
||||
"key up triggers import as key presses" = "los desencadenantes al soltar una tecla se importan como pulsaciones de tecla"
|
||||
"layers without a position were placed at their parent's origin" = "las capas sin posición se colocaron en el origen de su elemento padre"
|
||||
"layers without a type were skipped" = "se omitieron las capas sin tipo"
|
||||
"locked" = "bloqueada"
|
||||
"no bleed" = "sin sangrado"
|
||||
"nobody else here yet" = "aún no hay nadie más"
|
||||
"now" = "ahora"
|
||||
"or" = "o"
|
||||
"points with their own stroke join use the layer's" = "los puntos con su propia unión de trazo usan la de la capa"
|
||||
"prototype actions of unknown types were skipped" = "se omitieron las acciones de prototipo de tipos desconocidos"
|
||||
"prototype actions without a destination in the file were skipped" = "se omitieron las acciones de prototipo sin destino en el archivo"
|
||||
"prototype expressions on variables that aren't in the variables JSON were skipped" = "se omitieron las expresiones de prototipo con variables que no están en el JSON de variables"
|
||||
"prototype expressions with unsupported functions were skipped" = "se omitieron las expresiones de prototipo con funciones no compatibles"
|
||||
"prototype interactions without supported actions were skipped" = "se omitieron las interacciones de prototipo sin acciones compatibles"
|
||||
"prototype navigations of unknown types were skipped" = "se omitieron las navegaciones de prototipo de tipos desconocidos"
|
||||
"prototype transitions of unknown types play instantly" = "las transiciones de prototipo de tipos desconocidos se reproducen al instante"
|
||||
"prototype triggers of unknown types were skipped" = "se omitieron los desencadenantes de prototipo de tipos desconocidos"
|
||||
"put a widget on the canvas" = "poner un widget en el lienzo"
|
||||
"read and edit the document" = "leer y editar el documento"
|
||||
"read the document" = "leer el documento"
|
||||
"reads the document" = "lee el documento"
|
||||
"set-variable actions on variables that aren't in the variables JSON were skipped" = "se omitieron las acciones «Establecer variable» sobre variables que no están en el JSON de variables"
|
||||
"set-variable actions with values that can't be evaluated were skipped" = "se omitieron las acciones «Establecer variable» con valores que no se pueden evaluar"
|
||||
"set-variable actions with values that don't fit the variable were skipped" = "se omitieron las acciones «Establecer variable» con valores que no encajan con la variable"
|
||||
"set-variable actions without a value were skipped" = "se omitieron las acciones «Establecer variable» sin valor"
|
||||
"set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "se omitieron las acciones «Establecer modo de variable» sobre colecciones o modos que no están en el JSON de variables"
|
||||
"stars and polygons without point counts or geometry use the default point count" = "las estrellas y los polígonos sin número de puntos ni geometría usan el número de puntos predeterminado"
|
||||
"text style overrides refer to missing table entries" = "las anulaciones de estilo de texto hacen referencia a entradas de tabla que faltan"
|
||||
"the file has no pages" = "el archivo no tiene páginas"
|
||||
"the plugin" = "el plugin"
|
||||
"the variables JSON has no `variableCollections` (expected the local variables response) and was ignored" = "el JSON de variables no tiene `variableCollections` (se esperaba la respuesta de variables locales) y se ignoró"
|
||||
"this session only" = "solo esta sesión"
|
||||
"unknown component property types were skipped" = "se omitieron los tipos de propiedad de componente desconocidos"
|
||||
"variable aliases to variables that aren't in the variables JSON (or of another type) were skipped" = "se omitieron los alias a variables que no están en el JSON de variables (o son de otro tipo)"
|
||||
"variable bindings to properties the layer doesn't have were skipped" = "se omitieron las vinculaciones de variables a propiedades que la capa no tiene"
|
||||
"variable bindings to variables that aren't in the variables JSON (library variables) were skipped" = "se omitieron las vinculaciones a variables que no están en el JSON de variables (variables de biblioteca)"
|
||||
"variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)" = "se omitieron las vinculaciones de variables: el JSON del archivo no tiene definiciones de variables (pasa el JSON de variables locales como `variables` o incrusta su `meta` junto al JSON del archivo)"
|
||||
"variable bindings whose variable type doesn't fit the property were skipped" = "se omitieron las vinculaciones de variables cuyo tipo no encaja con la propiedad"
|
||||
"variable collections without modes were skipped" = "se omitieron las colecciones de variables sin modos"
|
||||
"variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped" = "se omitieron las consultas de modo de variable en expresiones de prototipo que no son una variable y uno de sus modos"
|
||||
"variable modes of collections that aren't in the file were skipped" = "se omitieron los modos de variable de colecciones que no están en el archivo"
|
||||
"variable modes of collections that aren't in the variables JSON were skipped" = "se omitieron los modos de variable de colecciones que no están en el JSON de variables"
|
||||
"variable values that don't match their type (or composed colours) were skipped" = "se omitieron los valores de variable que no coinciden con su tipo (o colores compuestos)"
|
||||
"variables of unknown types were skipped" = "se omitieron las variables de tipos desconocidos"
|
||||
"variables whose collection isn't in the variables JSON were skipped" = "se omitieron las variables cuya colección no está en el JSON de variables"
|
||||
"variables without readable values were skipped" = "se omitieron las variables sin valores legibles"
|
||||
"vectors without geometry are empty" = "los vectores sin geometría están vacíos"
|
||||
"vectors without geometry are empty (export the file JSON with geometry=paths)" = "los vectores sin geometría están vacíos (exporta el JSON del archivo con geometry=paths)"
|
||||
"very long text was cut to 1,048,576 characters" = "el texto muy largo se recortó a 1.048.576 caracteres"
|
||||
"very long vector paths were skipped" = "se omitieron los trazados vectoriales muy largos"
|
||||
"video actions are not supported and were skipped" = "las acciones de vídeo no son compatibles y se omitieron"
|
||||
"video triggers are not supported and were skipped" = "los desencadenantes de vídeo no son compatibles y se omitieron"
|
||||
"yesterday" = "ayer"
|
||||
"{instances} in {files}" = "{instances} en {files}"
|
||||
"{language}: draft translation, waiting for a native speaker's review." = "{language}: traducción en borrador, pendiente de revisión por un hablante nativo."
|
||||
@@ -1571,6 +1642,7 @@ status = "draft — needs native review"
|
||||
"{n} matches" = { one = "{n} coincidencia", other = "{n} coincidencias" }
|
||||
"{n} minutes ago" = { one = "hace {n} minuto", other = "hace {n} minutos" }
|
||||
"{n} months ago" = { one = "hace {n} mes", other = "hace {n} meses" }
|
||||
"{n} more notes omitted from this report" = { one = "{n} nota más omitida de este informe", other = "{n} notas más omitidas de este informe" }
|
||||
"{n} of {total}" = "{n} de {total}"
|
||||
"{n} people joined" = { one = "{n} persona se ha unido", other = "{n} personas se han unido" }
|
||||
"{n} preferred components" = { one = "{n} componente preferido", other = "{n} componentes preferidos" }
|
||||
|
||||
@@ -139,6 +139,7 @@ pub fn keys(app: &mut FigApp, ctx: &Context) {
|
||||
let open = u.color_profile.is_some()
|
||||
|| u.find.is_some()
|
||||
|| u.batch_rename.is_some()
|
||||
|| u.import_report.is_some()
|
||||
|| u.confirm_close
|
||||
|| u.export_preview
|
||||
|| u.shortcuts_panel
|
||||
@@ -147,7 +148,7 @@ pub fn keys(app: &mut FigApp, ctx: &Context) {
|
||||
if !open || !ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) {
|
||||
return;
|
||||
}
|
||||
if u.color_profile.take().is_some() || u.find.take().is_some() || u.batch_rename.take().is_some() {
|
||||
if u.color_profile.take().is_some() || u.find.take().is_some() || u.batch_rename.take().is_some() || u.import_report.take().is_some() {
|
||||
return;
|
||||
}
|
||||
for flag in [&mut u.confirm_close, &mut u.export_preview, &mut u.shortcuts_panel, &mut u.preferences, &mut u.about] {
|
||||
@@ -160,6 +161,7 @@ pub fn keys(app: &mut FigApp, ctx: &Context) {
|
||||
pub fn show(app: &mut FigApp, ctx: &Context) {
|
||||
// Dropped files waiting to open: their card, then (once it has shown) the files.
|
||||
crate::pending_open::pending_opens(app, ctx);
|
||||
crate::import_report::show(app, ctx);
|
||||
let t = Tokens::get(ctx);
|
||||
if app.ui.about {
|
||||
let mut open = true;
|
||||
|
||||
@@ -568,7 +568,7 @@ pub fn section(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
action = action.or(chosen);
|
||||
match action {
|
||||
Some(Action::Open(path)) => {
|
||||
if app.run("file.open", json!({"path": path})).is_some() {
|
||||
if app.run_user("file.open", json!({"path": path})).is_some() {
|
||||
app.ui.home = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
//! The import report: what a successful `.fig`, REST JSON or SVG import left out or changed, shown
|
||||
//! in a dialog window after the file opens, so the notes don't vanish with a toast.
|
||||
|
||||
use egui::RichText;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::FigApp;
|
||||
use crate::i18n::{tr, tr_noop, trn};
|
||||
use crate::theme::{Tokens, regular};
|
||||
|
||||
/// A report stays small even when a damaged input produces many or very long notes.
|
||||
pub const MAX_NOTES: usize = 500;
|
||||
pub const MAX_NOTE_CHARS: usize = 2_000;
|
||||
|
||||
/// The fixed notes of the `.fig` and REST importers, translated in the report. `.fig` imports name
|
||||
/// "the file" where REST imports name the JSON they read. Other notes (SVG's, say) are shown as they are.
|
||||
const IMPORT_NOTES: &[&str] = &[
|
||||
tr_noop("conditional prototype actions nested too deeply were skipped"),
|
||||
tr_noop("conditional prototype actions with conditions that can't be evaluated were skipped"),
|
||||
tr_noop("connector labels were skipped"),
|
||||
tr_noop("connectors whose ends can't be placed are empty"),
|
||||
tr_noop("damaged vector geometry was skipped"),
|
||||
tr_noop("damaged vector networks import as outlines"),
|
||||
tr_noop("document children other than pages were skipped"),
|
||||
tr_noop("elbowed and curved connectors without a saved path import as straight lines"),
|
||||
tr_noop("entries without a node type were skipped"),
|
||||
tr_noop("filled areas of a vector with their own fill use the layer's fill"),
|
||||
tr_noop("gamepad triggers without exactly one known controller button were skipped"),
|
||||
tr_noop("grid tracks of fixed or content size import as equal tracks"),
|
||||
tr_noop("instance swap properties whose default component isn't in the file were skipped"),
|
||||
tr_noop("instances inside their own main component were imported as frames"),
|
||||
tr_noop("instances of components that aren't in the file show no layers"),
|
||||
tr_noop("instances of components that aren't in the file were imported as frames"),
|
||||
tr_noop("instances that would nest a component inside itself were imported as frames"),
|
||||
tr_noop("instances without a main component were imported as empty frames"),
|
||||
tr_noop("invalid or oversized vector networks import as outlines"),
|
||||
tr_noop("invalid or oversized vector path data was skipped"),
|
||||
tr_noop("key triggers without a supported key were skipped"),
|
||||
tr_noop("key up triggers import as key presses"),
|
||||
tr_noop("layers without a position were placed at their parent's origin"),
|
||||
tr_noop("layers without a type were skipped"),
|
||||
tr_noop("points with their own stroke join use the layer's"),
|
||||
tr_noop("prototype actions of unknown types were skipped"),
|
||||
tr_noop("prototype actions without a destination in the file were skipped"),
|
||||
tr_noop("prototype expressions on variables that aren't in the variables JSON were skipped"),
|
||||
tr_noop("prototype expressions with unsupported functions were skipped"),
|
||||
tr_noop("prototype interactions without supported actions were skipped"),
|
||||
tr_noop("prototype navigations of unknown types were skipped"),
|
||||
tr_noop("prototype transitions of unknown types play instantly"),
|
||||
tr_noop("prototype triggers of unknown types were skipped"),
|
||||
tr_noop("set-variable actions on variables that aren't in the variables JSON were skipped"),
|
||||
tr_noop("set-variable actions with values that can't be evaluated were skipped"),
|
||||
tr_noop("set-variable actions with values that don't fit the variable were skipped"),
|
||||
tr_noop("set-variable actions without a value were skipped"),
|
||||
tr_noop("set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped"),
|
||||
tr_noop("stars and polygons without point counts or geometry use the default point count"),
|
||||
tr_noop("text style overrides refer to missing table entries"),
|
||||
tr_noop("the file has no pages"),
|
||||
tr_noop("the variables JSON has no `variableCollections` (expected the local variables response) and was ignored"),
|
||||
tr_noop("unknown component property types were skipped"),
|
||||
tr_noop("variable aliases to variables that aren't in the variables JSON (or of another type) were skipped"),
|
||||
tr_noop("variable bindings to properties the layer doesn't have were skipped"),
|
||||
tr_noop("variable bindings to variables that aren't in the variables JSON (library variables) were skipped"),
|
||||
tr_noop(
|
||||
"variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)",
|
||||
),
|
||||
tr_noop("variable bindings whose variable type doesn't fit the property were skipped"),
|
||||
tr_noop("variable collections without modes were skipped"),
|
||||
tr_noop("variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped"),
|
||||
tr_noop("variable modes of collections that aren't in the file were skipped"),
|
||||
tr_noop("variable modes of collections that aren't in the variables JSON were skipped"),
|
||||
tr_noop("variable values that don't match their type (or composed colours) were skipped"),
|
||||
tr_noop("variables of unknown types were skipped"),
|
||||
tr_noop("variables whose collection isn't in the variables JSON were skipped"),
|
||||
tr_noop("variables without readable values were skipped"),
|
||||
tr_noop("vectors without geometry are empty"),
|
||||
tr_noop("vectors without geometry are empty (export the file JSON with geometry=paths)"),
|
||||
tr_noop("very long text was cut to 1,048,576 characters"),
|
||||
tr_noop("very long vector paths were skipped"),
|
||||
tr_noop("video actions are not supported and were skipped"),
|
||||
tr_noop("video triggers are not supported and were skipped"),
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Report {
|
||||
pub name: String,
|
||||
pub pages: Option<u64>,
|
||||
pub layers: Option<u64>,
|
||||
pub images: Option<u64>,
|
||||
/// The pages of an imported file were added to the open document.
|
||||
pub appended: bool,
|
||||
/// An SVG was placed on the current page of the open document.
|
||||
pub placed: bool,
|
||||
/// The document the report belongs to: it goes when that document closes.
|
||||
pub uid: u64,
|
||||
pub notes: Vec<String>,
|
||||
pub note_count: usize,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
/// Only successful `file.open` and `file.import` results with import `warnings` create a report;
|
||||
/// a native file's `repairs` are shown by the engine's own message, and an SVG opened without
|
||||
/// notes has nothing to report. `params` are the command's, for the file's name.
|
||||
pub fn from_result(command: &str, params: &Value, result: &Value) -> Option<Self> {
|
||||
if !matches!(command, "file.open" | "file.openRecent" | "file.import") {
|
||||
return None;
|
||||
}
|
||||
let warnings = result.get("warnings").and_then(Value::as_array)?;
|
||||
let file_name = params.get("path").and_then(Value::as_str).and_then(|p| std::path::Path::new(p).file_name()).and_then(|n| n.to_str());
|
||||
if command != "file.import" && warnings.is_empty() && file_name.is_some_and(|n| n.to_ascii_lowercase().ends_with(".svg")) {
|
||||
return None;
|
||||
}
|
||||
// `file.import` of an SVG places a frame on the current page (no `pages`): its notes
|
||||
// are reported, but no pages were appended.
|
||||
let appended = command == "file.import" && result.get("pages").is_some();
|
||||
if command == "file.import" && !appended && warnings.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let summary = result.get("summary");
|
||||
let count = |key: &str| summary.and_then(|s| s.get(key)).and_then(Value::as_u64);
|
||||
let name = summary
|
||||
.and_then(|s| s.get("name"))
|
||||
.and_then(Value::as_str)
|
||||
.or(file_name)
|
||||
.or_else(|| params.get("name").and_then(Value::as_str))
|
||||
.unwrap_or(tr("Imported document"));
|
||||
let mut report = Self {
|
||||
name: name.chars().take(512).collect(),
|
||||
pages: count("pages"),
|
||||
layers: count("layers"),
|
||||
images: count("images"),
|
||||
appended,
|
||||
placed: command == "file.import" && !appended,
|
||||
uid: 0,
|
||||
notes: Vec::new(),
|
||||
note_count: 0,
|
||||
};
|
||||
for note in warnings.iter().filter_map(Value::as_str) {
|
||||
report.add_note(note);
|
||||
}
|
||||
Some(report)
|
||||
}
|
||||
|
||||
fn add_note(&mut self, note: &str) {
|
||||
self.note_count = self.note_count.saturating_add(1);
|
||||
if self.notes.len() < MAX_NOTES {
|
||||
let mut text: String = note.chars().take(MAX_NOTE_CHARS).collect();
|
||||
if note.chars().nth(MAX_NOTE_CHARS).is_some() {
|
||||
text.push('…');
|
||||
}
|
||||
self.notes.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inspect(&self) -> Value {
|
||||
json!({"name": self.name, "pages": self.pages, "layers": self.layers, "images": self.images,
|
||||
"appended": self.appended, "placed": self.placed, "warnings": self.notes, "warningCount": self.note_count})
|
||||
}
|
||||
}
|
||||
|
||||
/// `note` in the UI language when it is a known import note; a repeat count (` (×N)`) is kept. The
|
||||
/// stored notes are lower-case fragments; a known note is shown as a sentence, so its first letter
|
||||
/// is capitalised here (unknown notes are left as they are).
|
||||
pub(crate) fn note_text(note: &str) -> String {
|
||||
let translated = |base: &str| {
|
||||
if !IMPORT_NOTES.contains(&base) {
|
||||
return base.to_owned();
|
||||
}
|
||||
let text = tr(base);
|
||||
let mut chars = text.chars();
|
||||
chars.next().map_or_else(String::new, |first| first.to_uppercase().chain(chars).collect())
|
||||
};
|
||||
match note.rsplit_once(" (×") {
|
||||
Some((base, count)) if count.strip_suffix(')').is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())) => {
|
||||
format!("{} (×{count}", translated(base))
|
||||
}
|
||||
_ => translated(note),
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the report for a file the user opened or imported (menus, drops, pastes, recents, the
|
||||
/// system's open event); commands from the control channel, MCP or scripts never call this.
|
||||
pub fn capture(app: &mut FigApp, command: &str, params: &Value, result: &Value) {
|
||||
if let Some(mut report) = Report::from_result(command, params, result) {
|
||||
report.uid = app.session.ds().map_or(0, |d| d.uid);
|
||||
app.ui.import_report = Some(report);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut FigApp, ctx: &egui::Context) {
|
||||
// The report goes with its document.
|
||||
if app.ui.import_report.as_ref().is_some_and(|r| !app.session.docs.iter().any(|d| d.uid == r.uid)) {
|
||||
app.ui.import_report = None;
|
||||
}
|
||||
let Some(report) = &app.ui.import_report else { return };
|
||||
let t = Tokens::get(ctx);
|
||||
let mut open = true;
|
||||
let mut close_clicked = false;
|
||||
crate::dialogs::window(tr("Import report"), ctx).open(&mut open).show(ctx, |ui| {
|
||||
ui.set_width(420.0);
|
||||
ui.label(RichText::new(&report.name).strong());
|
||||
ui.label(if report.appended {
|
||||
tr("Pages added to the document")
|
||||
} else if report.placed {
|
||||
tr("Placed on the current page")
|
||||
} else {
|
||||
tr("Opened as a new document")
|
||||
});
|
||||
let counts: Vec<String> = [(tr("Pages"), report.pages), (tr("Layers"), report.layers), (tr("Images"), report.images)]
|
||||
.into_iter()
|
||||
.filter_map(|(label, count)| count.map(|n| format!("{label}: {n}")))
|
||||
.collect();
|
||||
if !counts.is_empty() {
|
||||
ui.label(counts.join(" · "));
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
if report.notes.is_empty() {
|
||||
ui.label(tr("No compatibility warnings reported"));
|
||||
} else {
|
||||
ui.label(RichText::new(tr("Compatibility notes")).strong());
|
||||
ui.label(
|
||||
RichText::new(tr(
|
||||
"FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged.",
|
||||
))
|
||||
.color(t.text_secondary)
|
||||
.font(regular(11.0)),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
egui::ScrollArea::vertical().id_salt("import_report_notes").max_height(260.0).show(ui, |ui| {
|
||||
for note in &report.notes {
|
||||
ui.label(note_text(note));
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
let omitted = report.note_count.saturating_sub(report.notes.len());
|
||||
if omitted > 0 {
|
||||
ui.label(trn("{n} more notes omitted from this report", omitted, &[]));
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
close_clicked = crate::widgets::button(ui, tr("Close"), true, true).clicked();
|
||||
});
|
||||
if !open || close_clicked {
|
||||
app.ui.import_report = None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::import_report::{MAX_NOTE_CHARS, MAX_NOTES, Report, note_text};
|
||||
use crate::{FigApp, NoServices};
|
||||
|
||||
#[test]
|
||||
fn import_results_with_warnings_become_reports_with_counts() {
|
||||
let result =
|
||||
json!({"warnings": ["layers without a type were skipped"], "summary": {"name": "Synthetic design", "pages": 2, "layers": 12, "images": 3}});
|
||||
let report = Report::from_result("file.import", &json!({}), &result).unwrap();
|
||||
assert_eq!(report.name, "Synthetic design");
|
||||
assert_eq!((report.pages, report.layers, report.images), (Some(2), Some(12), Some(3)));
|
||||
assert_eq!(report.notes, ["layers without a type were skipped"]);
|
||||
assert_eq!(report.note_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_import_commands_with_warnings_create_reports() {
|
||||
// A warning-free `.fig` or REST open still has a summary to show.
|
||||
let report = Report::from_result("file.open", &json!({}), &json!({"warnings": [], "summary": {"name": "Local", "pages": 1}})).unwrap();
|
||||
assert!(!report.appended && report.notes.is_empty());
|
||||
// A native file's repairs have their own message; an SVG without notes has no `warnings`.
|
||||
assert!(Report::from_result("file.open", &json!({}), &json!({"repairs": ["Repaired parent"]})).is_none());
|
||||
assert!(Report::from_result("file.open", &json!({}), &json!({"index": 0})).is_none());
|
||||
assert!(Report::from_result("file.export", &json!({}), &json!({"warnings": ["Export warning"]})).is_none());
|
||||
assert!(Report::from_result("shape.rectangle", &json!({}), &json!({})).is_none());
|
||||
// File ▸ Open recent opens by path and reports like File ▸ Open.
|
||||
let recent = json!({"warnings": ["layers without a type were skipped"], "summary": {"name": "Recent", "pages": 1}});
|
||||
assert!(Report::from_result("file.openRecent", &json!({"path": "Recent.json"}), &recent).is_some());
|
||||
assert!(Report::from_result("file.openRecent", &json!({"path": "Clean.svg"}), &json!({"warnings": []})).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_placements_are_not_reported_as_appended_pages() {
|
||||
assert!(Report::from_result("file.import", &json!({}), &json!({"id": 7})).is_none());
|
||||
let report = Report::from_result("file.import", &json!({}), &json!({"id": 7, "warnings": ["Filter skipped"]})).unwrap();
|
||||
assert!(!report.appended);
|
||||
assert!(Report::from_result("file.import", &json!({}), &json!({"pages": [1], "summary": {"pages": 1}, "warnings": []})).unwrap().appended);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_retention_is_bounded_and_truncates_only_at_character_boundaries() {
|
||||
let long_note = "🦎".repeat(MAX_NOTE_CHARS + 20);
|
||||
let notes: Vec<_> = std::iter::repeat_n(long_note, MAX_NOTES + 37).collect();
|
||||
let report = Report::from_result("file.open", &json!({}), &json!({"warnings": notes})).unwrap();
|
||||
assert_eq!(report.note_count, MAX_NOTES + 37);
|
||||
assert_eq!(report.notes.len(), MAX_NOTES);
|
||||
assert!(report.notes.iter().all(|note| note.chars().count() == MAX_NOTE_CHARS + 1 && note.ends_with('…')));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_text_keeps_the_repeat_suffix_and_unknown_notes() {
|
||||
let base = "instances of components that aren't in the file were imported as frames";
|
||||
let repeated = note_text(&format!("{base} (×122)"));
|
||||
assert_eq!(repeated, "Instances of components that aren't in the file were imported as frames (×122)");
|
||||
assert_eq!(note_text("some unknown note"), "some unknown note");
|
||||
assert_eq!(note_text("weird (×x)"), "weird (×x)");
|
||||
assert_eq!(note_text("weird (×12) tail"), "weird (×12) tail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_a_rest_json_file_reports_what_was_skipped() {
|
||||
let path = std::env::temp_dir().join(format!("figcraft-import-report-{}.json", std::process::id()));
|
||||
let file = json!({"document": {"id": "0:0", "name": "Synthetic", "type": "DOCUMENT", "children": [
|
||||
{"id": "0:1", "name": "Page 1", "type": "CANVAS", "children": [
|
||||
{"id": "1:1", "name": "Card", "type": "FRAME", "size": {"x": 200, "y": 100}, "relativeTransform": [[1, 0, 10], [0, 1, 20]],
|
||||
"children": [{"id": "1:2", "name": "No type"}]}]}]}});
|
||||
std::fs::write(&path, file.to_string()).unwrap();
|
||||
let mut app = FigApp::new(Session::new(), Box::new(NoServices));
|
||||
// A programmatic open (control channel, MCP) never opens the window.
|
||||
assert!(app.run("file.open", json!({"path": path.to_string_lossy()})).is_some());
|
||||
assert!(app.ui.import_report.is_none());
|
||||
let opened = app.run_user("file.open", json!({"path": path.to_string_lossy()}));
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
assert!(opened.is_some());
|
||||
let report = app.ui.import_report.as_ref().expect("a report for the REST import");
|
||||
assert!(!report.appended);
|
||||
assert_eq!(report.pages, Some(1));
|
||||
assert!(report.layers.is_some_and(|n| n >= 1));
|
||||
assert!(report.notes.iter().any(|n| n == "layers without a type were skipped"), "{:?}", report.notes);
|
||||
assert_eq!(app.inspect()["importReport"]["warningCount"], report.note_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_closes_the_import_report_like_other_simple_dialogs() {
|
||||
let mut u = crate::layers_drag_tests::Ui::new(Session::new());
|
||||
let mut report = Report::from_result("file.open", &json!({}), &json!({"warnings": ["Synthetic note"], "summary": {"name": "Synthetic design"}}));
|
||||
report.as_mut().unwrap().uid = u.app.session.ds().unwrap().uid;
|
||||
u.app.ui.import_report = report;
|
||||
u.frame(vec![]);
|
||||
let escape =
|
||||
vec![egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }];
|
||||
u.frame(escape);
|
||||
assert!(u.app.ui.import_report.is_none(), "Escape closes the report");
|
||||
}
|
||||
|
||||
fn shown_texts(shapes: Vec<egui::epaint::ClippedShape>) -> Vec<String> {
|
||||
fn walk(shape: &egui::Shape, out: &mut Vec<String>) {
|
||||
match shape {
|
||||
egui::Shape::Text(t) => out.push(t.galley.text().to_string()),
|
||||
egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
shapes.iter().for_each(|c| walk(&c.shape, &mut out));
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clean_svg_open_has_no_report_and_a_placed_svg_says_where_it_went() {
|
||||
let svg_open = Report::from_result("file.open", &json!({"path": "icons/Logo.SVG"}), &json!({"warnings": []}));
|
||||
assert!(svg_open.is_none(), "a clean SVG opened with file.open has nothing to report");
|
||||
let noted = Report::from_result("file.open", &json!({"path": "Logo.svg"}), &json!({"warnings": ["Filter skipped"]})).unwrap();
|
||||
assert_eq!(noted.name, "Logo.svg");
|
||||
|
||||
let placed = Report::from_result("file.import", &json!({"path": "dir/Logo.svg"}), &json!({"id": 7, "warnings": ["Filter skipped"]})).unwrap();
|
||||
assert!(placed.placed && !placed.appended);
|
||||
assert_eq!(placed.name, "Logo.svg");
|
||||
let mut u = crate::layers_drag_tests::Ui::new(Session::new());
|
||||
let mut placed = placed;
|
||||
placed.uid = u.app.session.ds().unwrap().uid;
|
||||
u.app.ui.import_report = Some(placed);
|
||||
let texts = shown_texts({
|
||||
u.frame(vec![]);
|
||||
u.frame_output(vec![]).shapes
|
||||
});
|
||||
assert!(texts.iter().any(|t| t == "Placed on the current page"), "{texts:?}");
|
||||
assert!(!texts.iter().any(|t| t == "Opened as a new document"), "{texts:?}");
|
||||
assert!(texts.iter().any(|t| t == "Logo.svg"), "{texts:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_report_goes_when_its_document_closes_and_counts_omitted_notes_in_the_singular() {
|
||||
let mut u = crate::layers_drag_tests::Ui::new(Session::new());
|
||||
let mut report = Report::from_result("file.open", &json!({}), &json!({"warnings": ["One note"], "summary": {"name": "Synthetic"}})).unwrap();
|
||||
report.uid = u.app.session.ds().unwrap().uid;
|
||||
report.note_count = 2;
|
||||
u.app.ui.import_report = Some(report);
|
||||
let texts = shown_texts({
|
||||
u.frame(vec![]);
|
||||
u.frame_output(vec![]).shapes
|
||||
});
|
||||
assert!(texts.iter().any(|t| t == "1 more note omitted from this report"), "{texts:?}");
|
||||
|
||||
u.app.session.execute("file.new", &json!({"name": "Other"})).unwrap();
|
||||
u.frame(vec![]);
|
||||
assert!(u.app.ui.import_report.is_some(), "another tab was added; the report's document is still open");
|
||||
u.app.session.execute("file.close", &json!({"index": 0, "force": true})).unwrap();
|
||||
u.frame(vec![]);
|
||||
assert!(u.app.ui.import_report.is_none(), "the report left with its document");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dropped_file_shows_its_report_once_it_opens() {
|
||||
let path = std::env::temp_dir().join(format!("figcraft-import-report-drop-{}.json", std::process::id()));
|
||||
let file = json!({"document": {"id": "0:0", "name": "Dropped", "type": "DOCUMENT", "children": [
|
||||
{"id": "0:1", "name": "Page 1", "type": "CANVAS", "children": [{"id": "1:1", "name": "No type"}]}]}});
|
||||
std::fs::write(&path, file.to_string()).unwrap();
|
||||
let mut u = crate::layers_drag_tests::Ui::new(Session::new());
|
||||
u.app.ui.pending_opens.push(crate::pending_open::PendingOpen {
|
||||
label: "Dropped.json".into(),
|
||||
params: json!({"path": path.to_string_lossy()}),
|
||||
command: "file.open",
|
||||
target: None,
|
||||
});
|
||||
for _ in 0..6 {
|
||||
u.frame(vec![]);
|
||||
}
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
let report = u.app.ui.import_report.as_ref().expect("the dropped file's report");
|
||||
assert_eq!(report.uid, u.app.session.ds().unwrap().uid);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ pub mod home;
|
||||
pub mod home_recents;
|
||||
pub mod i18n;
|
||||
pub mod icons;
|
||||
pub mod import_report;
|
||||
pub mod layers;
|
||||
pub mod layout_panel;
|
||||
pub mod list_drag;
|
||||
@@ -108,6 +109,8 @@ mod home_tests;
|
||||
#[cfg(test)]
|
||||
mod idle_tests;
|
||||
#[cfg(test)]
|
||||
mod import_report_tests;
|
||||
#[cfg(test)]
|
||||
mod layers_behaviour_tests;
|
||||
#[cfg(test)]
|
||||
mod layers_drag_tests;
|
||||
@@ -456,6 +459,8 @@ pub struct UiState {
|
||||
/// ([`pending_open::pending_opens`]), and the frames it has been drawn.
|
||||
pub pending_opens: Vec<pending_open::PendingOpen>,
|
||||
pub pending_open_frames: u8,
|
||||
/// The latest successful file import's summary and compatibility notes.
|
||||
pub import_report: Option<import_report::Report>,
|
||||
pub screenshot_requests: Vec<(u64, String)>,
|
||||
pub frame_ms: f64,
|
||||
pub render_ms: f64,
|
||||
@@ -564,6 +569,7 @@ impl Default for UiState {
|
||||
last_error: None,
|
||||
pending_opens: Vec::new(),
|
||||
pending_open_frames: 0,
|
||||
import_report: None,
|
||||
screenshot_requests: Vec::new(),
|
||||
frame_ms: 0.0,
|
||||
render_ms: 0.0,
|
||||
@@ -676,6 +682,16 @@ impl FigApp {
|
||||
r.map_err(|e| self.failed(id, &e)).ok()
|
||||
}
|
||||
|
||||
/// [`run`](Self::run) for something the user did (a dialog, a drop, a paste, a recent file):
|
||||
/// a file it imports shows its import report. Programmatic calls use `run` and never open one.
|
||||
pub fn run_user(&mut self, id: &str, params: Value) -> Option<Value> {
|
||||
let result = self.run(id, params.clone());
|
||||
if let Some(result) = &result {
|
||||
import_report::capture(self, id, ¶ms, result);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Show command `id`'s error.
|
||||
fn failed(&mut self, id: &str, e: &figcraft_engine::EngineError) {
|
||||
let msg = e.to_string();
|
||||
@@ -966,7 +982,7 @@ impl FigApp {
|
||||
false,
|
||||
|app, p| {
|
||||
let ps = p.to_string_lossy().to_string();
|
||||
if app.run("file.open", json!({"path": ps})).is_some() {
|
||||
if app.run_user("file.open", json!({"path": ps})).is_some() {
|
||||
app.ui.home = false;
|
||||
}
|
||||
},
|
||||
@@ -982,7 +998,7 @@ impl FigApp {
|
||||
let title = if place { i18n::tr("Place image") } else { i18n::tr("Import") };
|
||||
self.pick_open(title, exts, move |app, p| {
|
||||
let ps = p.to_string_lossy().to_string();
|
||||
app.run("file.import", json!({"path": ps}));
|
||||
app.run_user("file.import", json!({"path": ps}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1527,6 +1543,7 @@ impl FigApp {
|
||||
"panels": {"left": self.ui.left_width, "right": self.ui.right_width, "leftCollapsed": self.ui.left_collapsed},
|
||||
"status": self.session.status(),
|
||||
"lastError": self.ui.last_error,
|
||||
"importReport": self.ui.import_report.as_ref().map(import_report::Report::inspect),
|
||||
});
|
||||
state["pendingOpens"] = json!(self.ui.pending_opens.len());
|
||||
state
|
||||
|
||||
@@ -54,7 +54,7 @@ pub fn paste(app: &mut FigApp, event_text: Option<&str>) {
|
||||
} else if figcraft_engine::looks_like_svg(&text) {
|
||||
use base64::Engine as _;
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(text.as_bytes());
|
||||
app.run("file.import", json!({"data": b64, "name": "Pasted SVG.svg", "x": cx, "y": cy, "center": true}));
|
||||
app.run_user("file.import", json!({"data": b64, "name": "Pasted SVG.svg", "x": cx, "y": cy, "center": true}));
|
||||
} else {
|
||||
app.run("text.create", json!({"text": text, "x": cx, "y": cy}));
|
||||
}
|
||||
@@ -234,24 +234,25 @@ fn recent_menu(app: &mut FigApp, ui: &mut Ui, prefix: &str, key: &str, label: &s
|
||||
let picked = submenu(ui, prefix, &format!("{prefix}/{key}"), label, t, |ui| {
|
||||
let recents = &app.session.prefs.recent_files;
|
||||
let mut picked = None;
|
||||
for (i, p) in recents.iter().enumerate() {
|
||||
for p in recents.iter() {
|
||||
let path = std::path::Path::new(p);
|
||||
let name = path.file_name().map_or_else(|| p.clone(), |f| f.to_string_lossy().into_owned());
|
||||
let exists = path.exists();
|
||||
if item(ui, &name, "", exists, false, t) && exists {
|
||||
picked = Some(json!({"index": i}));
|
||||
picked = Some(("file.openRecent", json!({"path": p})));
|
||||
}
|
||||
}
|
||||
if !recents.is_empty() {
|
||||
separator(ui);
|
||||
}
|
||||
if item(ui, tr("Clear recent"), "", !recents.is_empty(), false, t) && !recents.is_empty() {
|
||||
picked = Some(json!({"clear": true}));
|
||||
picked = Some(("file.openRecent", json!({"clear": true})));
|
||||
}
|
||||
picked
|
||||
});
|
||||
let Some(params) = picked else { return false };
|
||||
if app.run("file.openRecent", params).is_some_and(|r| r.get("index").is_some()) {
|
||||
let Some((command, params)) = picked else { return false };
|
||||
// Through the user path, so an import report shows; `file.openRecent` also drops a file that is gone.
|
||||
if app.run_user(command, params).is_some_and(|r| r.get("index").is_some()) {
|
||||
app.ui.home = false;
|
||||
}
|
||||
true
|
||||
|
||||
@@ -29,7 +29,7 @@ pub struct ImportTarget {
|
||||
fn run_pending(app: &mut FigApp, pending: PendingOpen) -> Option<Value> {
|
||||
let target = match pending.target {
|
||||
Some(target) => target,
|
||||
None if pending.command != "file.import" => return app.run(pending.command, pending.params),
|
||||
None if pending.command != "file.import" => return app.run_user(pending.command, pending.params),
|
||||
None => {
|
||||
cancel(app, tr("The destination document is unavailable. Nothing was imported.").to_string());
|
||||
return None;
|
||||
@@ -49,7 +49,7 @@ fn run_pending(app: &mut FigApp, pending: PendingOpen) -> Option<Value> {
|
||||
let previous_selection = app.session.docs[index].selection.clone();
|
||||
app.session.active = index;
|
||||
app.session.docs[index].page = target.page;
|
||||
let result = app.run(pending.command, pending.params);
|
||||
let result = app.run_user(pending.command, pending.params);
|
||||
if previous_page != target.page
|
||||
&& let Some(doc) = app.session.docs.iter_mut().find(|doc| doc.uid == target.uid)
|
||||
&& doc.doc.pages.contains(&previous_page)
|
||||
|
||||
@@ -117,7 +117,13 @@ zoom preview or the refined image (`canvas.quality`: `preview` or `refined`) and
|
||||
asking for a render to its preview and its refined image being handed to egui as the canvas
|
||||
texture (`canvas.previewLatencyMs`, `canvas.refineLatencyMs`, on a monotonic clock, rendering
|
||||
included in the browser too; the GPU upload and presenting the frame come after; `null` until
|
||||
there was one). While a prototype plays, `presenter`
|
||||
there was one). `importReport` holds the Import report window's content (`{name, pages, layers, images, appended, placed, warnings, warningCount}`,
|
||||
`null` while none is open). The window opens only for a file the user opened or imported in the app
|
||||
(File ▸ Open or Import, Open recent, a recent file on the home screen, a drop, a paste, the system's open event).
|
||||
Emulated user input counts as the user's action: `ui.input` and `ui.key` (for example a Cmd+V paste or a click
|
||||
on a menu item) can open it. Commands sent through this channel with `engine.execute`, MCP, `window.new {path}`
|
||||
and files named on the command line never open it.
|
||||
`file.open` and `file.import` return the counts as `summary`. While a prototype plays, `presenter`
|
||||
(full window) and `preview` (the preview pane) hold the player's state: the `current` frame and its
|
||||
`name`, `start`, `history`, open `overlays`, how many delayed mouse triggers are `waiting`, the screen's `scroll` offset and `scrollRange`, the
|
||||
`device` (and whether it is turned to `landscape`, its `deviceColor`) and the `viewport` of the frame
|
||||
|
||||
+1
-1
@@ -227,7 +227,7 @@ Or in a project's `.mcp.json`:
|
||||
| `select {ids}` | Replace the selection (`[]` deselects). |
|
||||
| `export {ids, format, scale \| size, path?, bleed?, cropMarks?, bleedMarks?, cmyk?, contentsOnly?, includeBoundingBox?, svgOutlineText?, svgIdAttribute?, svgSimplifyStroke?}` | png, jpg, svg, webp, pdf, or `fig`: the whole file as `.fig`, experimental (see Exporting `.fig` files below); `size` is `2x`, `512w` or `512h`. The image covers what the layers paint (shadows towards their offset, strokes, unclipped children, a mask group's mask; hidden layers take no room; a slice is its area with everything under it); `contentsOnly: false` also draws every other layer showing in that area, `includeBoundingBox` exports the layers' own boxes; SVG: `svgOutlineText: false` keeps `<text>`, `svgIdAttribute` writes unique ids from layer names, `svgSimplifyStroke: false` writes every stroke as an outline. The same options go in export settings (`export.add` / `export.update`). PDF: frames one page each (sized in points), anything else together on one page; print options: `bleed` (points or a length like `"3mm"`, `"0.125in"`), crop and bleed marks, CMYK colour (see Exports below), also taken by `file.export`, `file.exportLayers`, `file.exportFramesPdf` and stored per export setting by `export.add` / `export.update`; `run_command` `file.exportFramesPdf {page?, path?}` writes every top-level frame of a page. Without `path`, images come back as MCP image content, SVG as text. Layers' own export settings: `run_command` with `export.add` / `export.update {index, size?, suffix?, format?}` / `export.remove` and `file.exportLayers {ids?, dir?, assets?}` (every layer once per setting, named `<layer><suffix>.<format>`, a slash in a layer name making a folder; `assets: true` exports the Inspect tab's assets instead: icons as SVG, images as PNG). |
|
||||
| `screenshot {ids?, scale?, maxSize?, window?}` | PNG of the page (with background) or of layers, scaled to fit `maxSize` (default 1600). `window: true` captures the app window in front when attached, a window id (see Windows below) that window. |
|
||||
| `open_file {path, variables?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.fig` (best effort, see below), `.svg` (see Importing SVG below) or an image; save as `.figcraft`. |
|
||||
| `open_file {path, variables?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.fig` (best effort, see below), `.svg` (see Importing SVG below) or an image; save as `.figcraft`. `open_file` returns `opened` (for a file opened anew, a `summary` of `name`, `pages`, `layers` and `images`, and `warnings` or `repairs`; a file that was already open just gives its `index`) and the document's `document` outline. |
|
||||
| `undo` / `redo` | Step through history. |
|
||||
| `pointer {kind, x, y, to?, steps?, tool?, shift, alt, cmd, pressure?, pressureTo?}` | Mouse gestures in page coordinates with the current tool: `click`, `double`, `drag`, `down`, `move`, `up`, `hover`. A dragged layer drops into the frame or auto layout slot under the pointer, except one inside a group, a boolean group or an instance (outside its slots), which only moves. `pressure` (0–1) is pen pressure; a drag goes from `pressure` to `pressureTo` (see Brushes and pen pressure). |
|
||||
| `key {key, shift, alt, cmd}` | A shortcut or key as if typed on the canvas (every default shortcut, tool keys, Escape, Delete, arrows, number keys for opacity, arrows that pan the view when nothing is selected; while editing text, the text shortcuts and then text editing keys). Keys are names like `A`, `0`, `Enter`, `ArrowLeft`, `=`, `/`, or egui's (`Plus`, `Questionmark`); `cmd` is Cmd on macOS and Ctrl elsewhere. |
|
||||
|
||||
Reference in New Issue
Block a user