mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3560 from dotCooCoo/feat/package-off-ui-thread
Package: File › Package collects and writes the package on the background thread
This commit is contained in:
@@ -50,7 +50,7 @@ mod object;
|
||||
pub(crate) mod objects_on_path;
|
||||
mod opacitymask;
|
||||
mod overprint;
|
||||
mod package;
|
||||
pub mod package;
|
||||
mod paint;
|
||||
mod panelcmds;
|
||||
pub mod params;
|
||||
|
||||
@@ -306,6 +306,51 @@ impl<'a> Collector<'a> {
|
||||
}
|
||||
|
||||
fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let package = prepare(s, p)?;
|
||||
match &mut s.package_hand_off {
|
||||
Some(slot) => {
|
||||
*slot = Some(package);
|
||||
Ok(Value::Null)
|
||||
}
|
||||
None => package.write(&mut |path, bytes| write_file(path, bytes)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A package of the active document as `file.package` takes it from the session: the options, the
|
||||
/// names, the report's Document Info text and the document as it is now
|
||||
/// ([`Session::prepare_package`]). [`Package::write`] collects and writes it without the session,
|
||||
/// on any thread.
|
||||
pub struct Package {
|
||||
o: Options,
|
||||
doc_path: String,
|
||||
stem: String,
|
||||
name: String,
|
||||
root: Option<PathBuf>,
|
||||
info: Option<String>,
|
||||
doc: std::sync::Arc<Document>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Run `file.package` with `params` as a command (checked and journaled as any other) that
|
||||
/// collects and writes nothing → the package it prepared, which [`Package::write`] collects
|
||||
/// and writes (on the desktop with Background Export on, File › Package… does this on the
|
||||
/// background thread).
|
||||
pub fn prepare_package(&mut self, params: &Value) -> Result<Package> {
|
||||
self.package_hand_off = Some(None);
|
||||
let r = self.execute(C, params);
|
||||
let handed = self.package_hand_off.take().flatten();
|
||||
match (r, handed) {
|
||||
(Err(e), _) => Err(e),
|
||||
(Ok(_), Some(package)) => Ok(package),
|
||||
// Every run that succeeds hands one over; otherwise the package is prepared here.
|
||||
(Ok(_), None) => prepare(self, params),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What `file.package` reads from the session: the options, the checks of the document and the
|
||||
/// name, the report's Document Info text and the document.
|
||||
fn prepare(s: &mut Session, p: &Value) -> Result<Package> {
|
||||
let o = Options {
|
||||
copy_links: bool_or(p, "copyLinks", true),
|
||||
links_folder: bool_or(p, "linksFolder", true),
|
||||
@@ -322,8 +367,23 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let folder = str_param(p, "folder");
|
||||
let root = folder.map(|f| Path::new(f).join(&name));
|
||||
let info = if o.report { Some(super::docinfo::report(s, false)?) } else { None };
|
||||
let st = s.doc()?;
|
||||
let mut doc = (*st.doc).clone();
|
||||
let doc = s.doc()?.doc.clone();
|
||||
Ok(Package { o, doc_path, stem, name, root, info, doc })
|
||||
}
|
||||
|
||||
impl Package {
|
||||
/// Collect the linked files and fonts and write the package's files with `write` into
|
||||
/// `folder/name`, making its folders first, or into the zip without a folder → the reply of
|
||||
/// `file.package`.
|
||||
pub fn write(self, write: &mut dyn FnMut(&str, &[u8]) -> Result<()>) -> Result<Value> {
|
||||
collect_and_write(self, write)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Package::write`].
|
||||
fn collect_and_write(package: Package, write: &mut dyn FnMut(&str, &[u8]) -> Result<()>) -> Result<Value> {
|
||||
let Package { o, doc_path, stem, name, root, info, doc } = package;
|
||||
let mut doc = (*doc).clone();
|
||||
let doc_name = format!("{stem}.{}", vectorcraft_format::EXTENSION);
|
||||
// Prepare original links for the destination *before* rebasing copied links.
|
||||
// Recalculating afterward would overwrite the portable relative paths.
|
||||
@@ -366,7 +426,7 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(dir) = path.parent() {
|
||||
create_dir(&dir.to_string_lossy())?;
|
||||
}
|
||||
write_file(&path.to_string_lossy(), bytes)?;
|
||||
write(&path.to_string_lossy(), bytes)?;
|
||||
}
|
||||
out["folder"] = json!(root.to_string_lossy());
|
||||
}
|
||||
|
||||
@@ -1378,6 +1378,9 @@ pub struct Session {
|
||||
/// While `command.batch` runs: the documents its steps closed or replaced (Revert), which an
|
||||
/// error brings back; `None` otherwise.
|
||||
pub(crate) batch_stash: Option<Vec<DocState>>,
|
||||
/// While [`Session::prepare_package`] runs `file.package`: the package the command prepared,
|
||||
/// which it leaves here instead of writing it; `None` otherwise.
|
||||
pub(crate) package_hand_off: Option<Option<cmd::package::Package>>,
|
||||
/// Where Data Recovery keeps its copies ([`cmd::recovery`]).
|
||||
pub recovery: cmd::recovery::Recovery,
|
||||
/// While the active tool's actions run: their commands aren't "a command from outside the
|
||||
@@ -1466,6 +1469,7 @@ impl Session {
|
||||
journal_note: Default::default(),
|
||||
note_depth: 1,
|
||||
batch_stash: None,
|
||||
package_hand_off: None,
|
||||
recovery: Default::default(),
|
||||
in_tool_actions: false,
|
||||
previewing: false,
|
||||
|
||||
@@ -124,6 +124,80 @@ fn without_a_folder_the_zip_has_the_same_files() {
|
||||
assert_eq!(f["files"], r["files"]);
|
||||
}
|
||||
|
||||
/// The reply of a prepared package's [`cmd::package::Package::write`] and the files it gave the
|
||||
/// writer, by path.
|
||||
fn write_prepared(package: cmd::package::Package) -> (Value, BTreeMap<String, Vec<u8>>) {
|
||||
let mut files = BTreeMap::new();
|
||||
let mut write = |path: &str, bytes: &[u8]| {
|
||||
files.insert(path.to_string(), bytes.to_vec());
|
||||
Ok(())
|
||||
};
|
||||
let r = package.write(&mut write).unwrap_or_else(|e| panic!("{e}"));
|
||||
(r, files)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prepared_package_writes_what_file_package_writes() {
|
||||
let dir = Folder::new("package-prepared");
|
||||
let mut s = poster(&dir);
|
||||
run(&mut s, "text.create", json!({"x": 10, "y": 80, "text": "Gone", "font": "No Such Font Family"}));
|
||||
save(&mut s, &dir.file("poster.vectorcraft"));
|
||||
std::fs::remove_file(dir.file("art/blue.png")).unwrap();
|
||||
open(&mut s, &dir.file("poster.vectorcraft"));
|
||||
let p = json!({"folder": dir.file("out")});
|
||||
let want = run(&mut s, "file.package", p.clone());
|
||||
assert_eq!((want["missingLinks"].clone(), want["skippedFonts"][0]["font"].clone()), (json!(["blue.png"]), json!("No Such Font Family Regular")));
|
||||
let root = std::path::Path::new(&dir.file("out")).join("poster Folder");
|
||||
let on_disk: BTreeMap<String, Vec<u8>> = want["files"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|f| root.join(f.as_str().unwrap()))
|
||||
.map(|f| (f.to_string_lossy().into_owned(), std::fs::read(&f).unwrap()))
|
||||
.collect();
|
||||
assert_eq!(on_disk.len(), 4, "{want}");
|
||||
std::fs::remove_dir_all(&root).unwrap();
|
||||
// A prepared package is journaled as the command, and nothing is written until `Package::write` runs.
|
||||
let journaled = s.journal.len();
|
||||
let package = s.prepare_package(&p).unwrap();
|
||||
assert_eq!((s.journal.len(), s.journal.last().map(|(id, _)| id.as_str())), (journaled + 1, Some("file.package")));
|
||||
assert!(!root.exists());
|
||||
let (got, files) = write_prepared(package);
|
||||
assert_eq!(got, want);
|
||||
assert_eq!(files.keys().collect::<Vec<_>>(), on_disk.keys().collect::<Vec<_>>());
|
||||
for (path, bytes) in &on_disk {
|
||||
assert!(files[path] == *bytes, "{path} differs");
|
||||
}
|
||||
// Without a folder, the package is the same zip and no file is written.
|
||||
let want = run(&mut s, "file.package", json!({}));
|
||||
let (got, files) = write_prepared(s.prepare_package(&json!({})).unwrap());
|
||||
assert!(files.is_empty(), "{:?}", files.keys());
|
||||
let unzipped = |r: &Value| unzip(&vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap());
|
||||
assert_eq!(unzipped(&got), unzipped(&want));
|
||||
for key in ["files", "links", "fonts", "missingLinks", "skippedFonts", "warnings", "name", "bytes"] {
|
||||
assert_eq!(got[key], want[key], "{key}");
|
||||
}
|
||||
// The checks come first, as the command's do.
|
||||
let e = s.prepare_package(&json!({"name": "../up"})).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(e.contains("can't name a folder"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prepared_package_holds_the_document_as_it_was() {
|
||||
let dir = Folder::new("package-snapshot");
|
||||
let mut s = poster(&dir);
|
||||
let nodes = s.doc().unwrap().doc.node_count();
|
||||
let package = s.prepare_package(&json!({})).unwrap();
|
||||
run(&mut s, "select.all", json!({}));
|
||||
run(&mut s, "edit.clear", json!({}));
|
||||
run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}));
|
||||
let (r, _) = write_prepared(package);
|
||||
let zip = unzip(&vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap());
|
||||
let doc = vectorcraft_format::load(&zip["poster Folder/poster.vectorcraft"]).unwrap();
|
||||
assert_eq!(doc.node_count(), nodes);
|
||||
assert_eq!(r["links"], 2, "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_leave_out_links_fonts_and_the_report() {
|
||||
let dir = Folder::new("package-options");
|
||||
|
||||
@@ -5,12 +5,18 @@
|
||||
//!
|
||||
//! Fields: `folder` (the location), `name` (the package folder), `copyLinks`, `linksFolder`,
|
||||
//! `relink`, `copyFonts`, `report`. OK runs `file.package` with them, then offers to show the
|
||||
//! package folder (`file.showPackage`).
|
||||
//! package folder (`file.showPackage`). On the desktop with Background Export on, the package is
|
||||
//! collected and written on the background thread ([`crate::background`]) after the dialog closes.
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_engine::EngineError;
|
||||
|
||||
use super::confirm::ask;
|
||||
use super::{DialogSpec, form};
|
||||
use crate::background::{self, Writer};
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
use crate::{VectorcraftApp, io, widgets};
|
||||
@@ -110,8 +116,38 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
return Ok(r);
|
||||
}
|
||||
}
|
||||
let r = app.run("file.package", params)?;
|
||||
app.ui.dialog = None;
|
||||
// Background Export (Preferences › File Handling): the linked files and fonts are collected
|
||||
// and the package is written from the document as it is now, while editing goes on. The jobs
|
||||
// started before it (a save started above, an earlier package to the same folder) are written
|
||||
// first: the worker runs its jobs in order, and a package made at once waits for them.
|
||||
let enabled = app.session.prefs.background_export;
|
||||
if !enabled {
|
||||
background::wait_all(app);
|
||||
}
|
||||
let package = app.session.prepare_package(¶ms).map_err(|e| e.to_string()).inspect_err(|e| app.ui.status = e.clone())?;
|
||||
let label = format!("Packaging {}", d.str("name"));
|
||||
let later = Rc::new(Cell::new(false));
|
||||
let in_background = later.clone();
|
||||
let work = move |write: Writer| {
|
||||
let mut write = |path: &str, bytes: &[u8]| write(path, bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")));
|
||||
package.write(&mut write).map_err(|e| e.to_string())
|
||||
};
|
||||
let r = background::run(app, enabled, label, None, work, move |app, r| packaged(app, r, web, in_background.get()))?;
|
||||
if r["background"] == true {
|
||||
later.set(true);
|
||||
app.ui.dialog = None;
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Report the package `r` (`file.package`'s reply): the status bar, the web's download, and the
|
||||
/// offer to show the package folder. `later`: it was written in the background, after the dialog
|
||||
/// closed, and a dialog open now is another one, which stays (the status bar names the folder).
|
||||
fn packaged(app: &mut VectorcraftApp, r: Result<Value, String>, web: bool, later: bool) -> Result<Value, String> {
|
||||
let r = r.inspect_err(|e| app.ui.status = e.clone())?;
|
||||
if !later {
|
||||
app.ui.dialog = None;
|
||||
}
|
||||
let n = r["files"].as_array().map_or(0, Vec::len);
|
||||
let missing = r["missingLinks"].as_array().map_or(0, Vec::len);
|
||||
// The fonts left out: their license doesn't allow it, or they aren't on this computer.
|
||||
@@ -145,7 +181,9 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
&[("count", &n_s), ("folder", &folder), ("note", &shown_note)],
|
||||
)
|
||||
};
|
||||
ask(app, tl!("Package created"), &detail, "file.showPackage", json!({ "folder": folder }));
|
||||
if app.ui.dialog.is_none() {
|
||||
ask(app, tl!("Package created"), &detail, "file.showPackage", json!({ "folder": folder }));
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
use crate::background;
|
||||
use crate::tests_background::{Files, Gate, written};
|
||||
|
||||
/// A fresh folder for one test (removed when dropped).
|
||||
struct Folder(std::path::PathBuf);
|
||||
@@ -89,6 +92,153 @@ fn package_writes_the_folder_then_offers_to_show_it() {
|
||||
assert_eq!(opened.borrow().first().map(std::path::PathBuf::from), Some(root));
|
||||
}
|
||||
|
||||
/// [`saved`] with a thread-safe writer, the background thread's, that records the files it is
|
||||
/// given once `gate` opens.
|
||||
fn saved_with_worker(dir: &Folder, gate: &Gate) -> (VectorcraftApp, Files) {
|
||||
let mut app = saved(dir);
|
||||
let (files, g) = (Files::default(), gate.clone());
|
||||
let recorded = files.clone();
|
||||
app.services.write_shared = Some(Arc::new(move |p: &str, b: &[u8]| {
|
||||
g.wait();
|
||||
recorded.lock().unwrap().push((p.to_string(), b.to_vec()));
|
||||
Ok(())
|
||||
}));
|
||||
(app, files)
|
||||
}
|
||||
|
||||
/// Open the Package dialog for a package in `out`.
|
||||
fn package_dialog(app: &mut VectorcraftApp, out: &str) {
|
||||
app.run("ui.packageDialog", json!({})).unwrap();
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("folder".into(), json!(out));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_is_written_in_the_background_as_it_is_at_once() {
|
||||
let dir = Folder::new("background");
|
||||
let gate = Gate::default();
|
||||
let (mut app, files) = saved_with_worker(&dir, &gate);
|
||||
let out = dir.path("out");
|
||||
let root = std::path::Path::new(&out).join("card Folder");
|
||||
// With Background Export off, the package is written at once through the UI thread's writer.
|
||||
app.session.execute("prefs.set", &json!({"values": {"backgroundExport": false}})).unwrap();
|
||||
package_dialog(&mut app, &out);
|
||||
let at_once = confirm(&mut app).unwrap();
|
||||
let want: Vec<(String, Vec<u8>)> = at_once["files"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|f| root.join(f.as_str().unwrap()))
|
||||
.map(|p| (p.to_string_lossy().into_owned(), std::fs::read(&p).unwrap()))
|
||||
.collect();
|
||||
assert_eq!(want.len(), 3, "the document, its linked file and the report: {at_once}");
|
||||
let (status, detail) = (app.ui.status.clone(), app.ui.dialog.as_ref().unwrap().str("detail"));
|
||||
std::fs::remove_dir_all(&root).unwrap();
|
||||
app.ui.dialog = None;
|
||||
// With Background Export on, the dialog closes at once, and the same files are written later.
|
||||
app.session.execute("prefs.set", &json!({"values": {"backgroundExport": true}})).unwrap();
|
||||
package_dialog(&mut app, &out);
|
||||
let r = confirm(&mut app).unwrap();
|
||||
assert_eq!(r["background"], true, "{r}");
|
||||
assert!(app.ui.dialog.is_none(), "closed");
|
||||
assert_eq!(app.ui.status, "Packaging card Folder…");
|
||||
assert!(written(&files).is_empty(), "nothing written yet");
|
||||
gate.open();
|
||||
background::wait_all(&mut app);
|
||||
assert_eq!(*files.lock().unwrap(), want);
|
||||
assert_eq!(app.ui.status, status);
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!((d.str("__command"), d.str("detail")), ("file.showPackage".to_string(), detail));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edits_after_ok_do_not_reach_the_package() {
|
||||
let dir = Folder::new("snapshot");
|
||||
let gate = Gate::default();
|
||||
let (mut app, files) = saved_with_worker(&dir, &gate);
|
||||
let nodes = app.session.active().unwrap().doc.node_count();
|
||||
package_dialog(&mut app, &dir.path("out"));
|
||||
assert_eq!(confirm(&mut app).unwrap()["background"], true);
|
||||
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.run("edit.clear", json!({})).unwrap();
|
||||
// A dialog opened meanwhile stays open; the status bar reports the package.
|
||||
app.run("ui.packageDialog", json!({})).unwrap();
|
||||
gate.open();
|
||||
background::wait_all(&mut app);
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some("package"));
|
||||
assert!(app.ui.status.starts_with("Packaged 3 file(s) in "), "{}", app.ui.status);
|
||||
let files = files.lock().unwrap();
|
||||
let (_, bytes) = files.iter().find(|(p, _)| p.ends_with("card.vectorcraft")).unwrap();
|
||||
assert_eq!(vectorcraft_format::load(bytes).unwrap().node_count(), nodes, "the document as it was at OK");
|
||||
assert!(files.iter().any(|(p, _)| p.ends_with("pic.png")), "its linked file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dirty_documents_save_is_written_before_its_package() {
|
||||
let dir = Folder::new("dirty");
|
||||
let gate = Gate::default();
|
||||
let (mut app, files) = saved_with_worker(&dir, &gate);
|
||||
let (doc, out) = (dir.path("card.vectorcraft"), dir.path("out"));
|
||||
// With Background Export on, the save and the package both run in the background, and the
|
||||
// worker writes the save first.
|
||||
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
package_dialog(&mut app, &out);
|
||||
// A thread opens the gate after 10 s at the latest, so a confirm that waits for the save ends.
|
||||
let opener = gate.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||
opener.open();
|
||||
});
|
||||
assert_eq!(confirm(&mut app).unwrap()["background"], true);
|
||||
assert_eq!(app.background.jobs.len(), 2, "the save and the package");
|
||||
gate.open();
|
||||
background::wait_all(&mut app);
|
||||
let paths = written(&files);
|
||||
assert_eq!(paths.first(), Some(&doc), "{paths:?}");
|
||||
assert_eq!(paths.len(), 4, "{paths:?}");
|
||||
assert!(!app.session.active().unwrap().is_dirty());
|
||||
// With Background Export off, the package is made at once and waits for the background save.
|
||||
let gate = Gate::default();
|
||||
let (mut app, files) = saved_with_worker(&dir, &gate);
|
||||
app.session.execute("prefs.set", &json!({"values": {"backgroundExport": false}})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
package_dialog(&mut app, &out);
|
||||
let opener = gate.clone();
|
||||
let release = std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
opener.open();
|
||||
});
|
||||
confirm(&mut app).unwrap();
|
||||
release.join().unwrap();
|
||||
assert_eq!(written(&files), [doc]);
|
||||
assert!(app.background.jobs.is_empty() && !app.session.active().unwrap().is_dirty());
|
||||
assert!(app.ui.status.starts_with("Packaged 3 file(s) in "), "the package's status came last: {}", app.ui.status);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_package_made_at_once_waits_for_one_in_the_background() {
|
||||
let dir = Folder::new("twice");
|
||||
let gate = Gate::default();
|
||||
let (mut app, files) = saved_with_worker(&dir, &gate);
|
||||
let out = dir.path("out");
|
||||
package_dialog(&mut app, &out);
|
||||
assert_eq!(confirm(&mut app).unwrap()["background"], true);
|
||||
// With Background Export turned off, the same package made again is written after the one in
|
||||
// the background.
|
||||
app.session.execute("prefs.set", &json!({"values": {"backgroundExport": false}})).unwrap();
|
||||
package_dialog(&mut app, &out);
|
||||
let opener = gate.clone();
|
||||
let release = std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
opener.open();
|
||||
});
|
||||
confirm(&mut app).unwrap();
|
||||
assert!(app.background.jobs.is_empty(), "the package in the background was written first");
|
||||
release.join().unwrap();
|
||||
assert_eq!(written(&files).len(), 3);
|
||||
assert!(std::path::Path::new(&out).join("card Folder/card.vectorcraft").is_file());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_web_downloads_a_zip() {
|
||||
let dir = Folder::new("web");
|
||||
|
||||
@@ -4256,6 +4256,7 @@ axis Both И двете
|
||||
@msg Saving Запазване
|
||||
@msg Saving {_1} Записване на {_1}
|
||||
@msg Exporting {_1} Експортиране на {_1}
|
||||
@msg Packaging {_1} Пакетиране на {_1}
|
||||
@msg Saving recovery data for {_1} Записване на данни за възстановяване на {_1}
|
||||
@msg Discarded {_1} recovered document(s) Изхвърлени са {_1} възстановени документ(и)
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Възстановени са {_1} документ(и): запазете ги, за да ги съхраните
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4652,6 +4652,7 @@ imageTrace Outline Obrys
|
||||
@msg Saving Ukládání
|
||||
@msg Saving {_1} Ukládání {_1}
|
||||
@msg Exporting {_1} Exportování {_1}
|
||||
@msg Packaging {_1} Balení {_1}
|
||||
@msg Saving recovery data for {_1} Ukládání dat pro obnovení pro {_1}
|
||||
@msg Discarded {_1} recovered document(s) Zahozeno obnovených dokumentů: {_1}
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Obnoveno dokumentů: {_1}; pokud je chcete zachovat, uložte je
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4352,6 +4352,7 @@ imageTrace Outline Kontur
|
||||
@msg Saving Gemmer
|
||||
@msg Saving {_1} Gemmer {_1}
|
||||
@msg Exporting {_1} Eksporterer {_1}
|
||||
@msg Packaging {_1} Pakker {_1}
|
||||
@msg Saving recovery data for {_1} Gemmer gendannelsesdata for {_1}
|
||||
@msg Discarded {_1} recovered document(s) Forkastede {_1} gendannet(e) dokument(er)
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Genvandt {_1} gendannet(e) dokument(er): gem dem for at beholde dem
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4275,6 +4275,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg Saving Wird gespeichert
|
||||
@msg Saving {_1} {_1} wird gespeichert
|
||||
@msg Exporting {_1} {_1} wird exportiert
|
||||
@msg Packaging {_1} {_1} wird verpackt
|
||||
@msg Saving recovery data for {_1} Wiederherstellungsdaten für {_1} werden gespeichert
|
||||
@msg Discarded {_1} recovered document(s) {_1} wiederhergestellte(s) Dokument(e) verworfen
|
||||
@msg Restored {_1} recovered document(s): save them to keep them {_1} wiederhergestellte(s) Dokument(e) geöffnet: Speichern Sie sie, um sie zu behalten
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4326,6 +4326,7 @@ theme Appearance Εμφάνιση
|
||||
@msg Saving Αποθήκευση σε εξέλιξη
|
||||
@msg Saving {_1} Αποθήκευση {_1}
|
||||
@msg Exporting {_1} Εξαγωγή {_1}
|
||||
@msg Packaging {_1} Συσκευασία {_1}
|
||||
@msg Saving recovery data for {_1} Αποθήκευση δεδομένων ανάκτησης για {_1}
|
||||
@msg Discarded {_1} recovered document(s) Απορρίφθηκαν {_1} ανακτημένα έγγραφα
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Επαναφέρθηκαν {_1} ανακτημένα έγγραφα: αποθηκεύστε τα για να διατηρηθούν
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2045 and column 19.
|
@@ -4351,6 +4351,7 @@ axis Both Ambos
|
||||
@msg Saving Guardando
|
||||
@msg Saving {_1} Guardando {_1}
|
||||
@msg Exporting {_1} Exportando {_1}
|
||||
@msg Packaging {_1} Empaquetando {_1}
|
||||
@msg Saving recovery data for {_1} Guardando datos de recuperación de {_1}
|
||||
@msg Discarded {_1} recovered document(s) Se descartaron {_1} documento(s) recuperado(s)
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Se restauraron {_1} documento(s) recuperado(s): guárdalos para conservarlos
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4561,6 +4561,7 @@ imageTrace Outline Ääriviiva
|
||||
@msg Saving Tallennetaan
|
||||
@msg Saving {_1} Tallennetaan {_1}
|
||||
@msg Exporting {_1} Viedään {_1}
|
||||
@msg Packaging {_1} Paketoidaan {_1}
|
||||
@msg Saving recovery data for {_1} Tallennetaan palautustietoja: {_1}
|
||||
@msg Discarded {_1} recovered document(s) Hylättiin {_1} palautettu(a) asiakirja(a)
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Palautettiin {_1} asiakirja(a): tallenna ne säilyttääksesi ne
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4348,6 +4348,7 @@ axis Both Les deux
|
||||
@msg Saving Enregistrement
|
||||
@msg Saving {_1} Enregistrement de {_1}
|
||||
@msg Exporting {_1} Exportation de {_1}
|
||||
@msg Packaging {_1} Assemblage de {_1}
|
||||
@msg Saving recovery data for {_1} Enregistrement des données de récupération de {_1}
|
||||
@msg Discarded {_1} recovered document(s) {_1} document(s) récupéré(s) supprimé(s)
|
||||
@msg Restored {_1} recovered document(s): save them to keep them {_1} document(s) récupéré(s) restauré(s) : enregistrez-les pour les conserver
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4589,6 +4589,7 @@ imageTrace Outline Körvonal
|
||||
@msg Exported {what} Exportálva: {what}
|
||||
@msg Exported {what} with {_1} note(s): {first} Exportálva: {what}; megjegyzések: {_1}, az első: {first}
|
||||
@msg Exporting {_1} Exportálás: {_1}
|
||||
@msg Packaging {_1} Csomagolás: {_1}
|
||||
@msg file version {0} is newer than this VectorCraft supports ({VERSION}) a fájl verziója újabb, mint amit ez a VectorCraft támogat: {0} (támogatott: {VERSION})
|
||||
@msg Flatten Transparency: select objects to flatten Átlátszóság lapítása: jelöld ki a lapítandó objektumokat
|
||||
@msg Flatten Transparency: {e} Átlátszóság lapítása: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4352,6 +4352,7 @@ imageTrace Outline Kontur
|
||||
@msg Saving Menyimpan
|
||||
@msg Saving {_1} Menyimpan {_1}
|
||||
@msg Exporting {_1} Mengekspor {_1}
|
||||
@msg Packaging {_1} Mengemas {_1}
|
||||
@msg Saving recovery data for {_1} Menyimpan data pemulihan untuk {_1}
|
||||
@msg Discarded {_1} recovered document(s) {_1} dokumen hasil pemulihan dibuang
|
||||
@msg Restored {_1} recovered document(s): save them to keep them {_1} dokumen hasil pemulihan dipulihkan: simpan agar tidak hilang
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3816 and column 19.
|
@@ -4391,6 +4391,7 @@ axis Both Entrambi
|
||||
@msg Saving Salvataggio
|
||||
@msg Saving {_1} Salvataggio di {_1}
|
||||
@msg Exporting {_1} Esportazione di {_1}
|
||||
@msg Packaging {_1} Creazione del pacchetto {_1}
|
||||
@msg Saving recovery data for {_1} Salvataggio dei dati di recupero per {_1}
|
||||
@msg Discarded {_1} recovered document(s) Documenti recuperati scartati: {_1}
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Documenti recuperati ripristinati: {_1}. Salvali per conservarli
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4293,6 +4293,7 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg SVG parse error: {0} SVG を解析できませんでした: {0}
|
||||
@msg decimals must be {_1}–{_2} (got {_3}) 小数点以下の桁数は {_1} から {_2} までにしてください (指定値: {_3})
|
||||
@msg Exporting {_1} {_1} を書き出し中
|
||||
@msg Packaging {_1} {_1} をパッケージ中
|
||||
@msg Exported {what} {what} を書き出しました
|
||||
@msg "{0}" is not a colour: write it as #rrggbb 「{0}」はカラーではありません: #rrggbb の形式で指定してください
|
||||
@msg could not decode image: {0} 画像をデコードできませんでした: {0}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4108,6 +4108,7 @@ effect Regular ჩვეულებრივი
|
||||
@msg Saving შენახვა
|
||||
@msg Saving {_1} {_1}-ის შენახვა
|
||||
@msg Exporting {_1} ექსპორტირდება: {_1}
|
||||
@msg Packaging {_1} იფუთება: {_1}
|
||||
@msg Saving recovery data for {_1} {_1}-ის აღდგენის მონაცემების შენახვა
|
||||
@msg Discarded {_1} recovered document(s) უარყოფილია აღდგენილი დოკუმენტები: {_1}
|
||||
@msg Restored {_1} recovered document(s): save them to keep them აღდგენილია დოკუმენტები: {_1}: შესანარჩუნებლად შეინახეთ ისინი
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4225,6 +4225,7 @@ theme Appearance 외관
|
||||
@msg SVG parse error: {0} SVG 구문 분석 오류: {0}
|
||||
@msg decimals must be {_1}–{_2} (got {_3}) 소수 자릿수는 {_1}~{_2}이어야 합니다({_3} 입력됨)
|
||||
@msg Exporting {_1} {_1} 내보내는 중
|
||||
@msg Packaging {_1} {_1} 패키지 만드는 중
|
||||
@msg Exported {what} {what}을(를) 내보냈습니다
|
||||
@msg "{0}" is not a colour: write it as #rrggbb "{0}"은(는) 색상이 아닙니다: #rrggbb 형식으로 입력하세요
|
||||
@msg could not decode image: {0} 이미지를 디코딩할 수 없습니다: {0}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4364,6 +4364,7 @@ imageTrace Outline Contour
|
||||
@msg Saving Bezig met opslaan
|
||||
@msg Saving {_1} Bezig met opslaan van {_1}
|
||||
@msg Exporting {_1} Bezig met exporteren van {_1}
|
||||
@msg Packaging {_1} Bezig met verpakken van {_1}
|
||||
@msg Saving recovery data for {_1} Bezig met opslaan van herstelgegevens voor {_1}
|
||||
@msg Discarded {_1} recovered document(s) {_1} hersteld(e) document(en) verworpen
|
||||
@msg Restored {_1} recovered document(s): save them to keep them {_1} hersteld(e) document(en) teruggezet: sla ze op om ze te behouden
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4477,6 +4477,7 @@ imageTrace Outline Kontur
|
||||
@msg Saving Zapisywanie
|
||||
@msg Saving {_1} Zapisywanie {_1}
|
||||
@msg Exporting {_1} Eksportowanie {_1}
|
||||
@msg Packaging {_1} Pakowanie {_1}
|
||||
@msg Saving recovery data for {_1} Zapisywanie danych odzyskiwania dla {_1}
|
||||
@msg Discarded {_1} recovered document(s) Odrzucone odzyskane dokumenty: {_1}
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Przywrócone odzyskane dokumenty: {_1} — zapisz je, aby je zachować
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3922,6 +3922,7 @@ axis Both Ambos
|
||||
@msg Saving Salvando
|
||||
@msg Saving {_1} Salvando {_1}
|
||||
@msg Exporting {_1} Exportando {_1}
|
||||
@msg Packaging {_1} Empacotando {_1}
|
||||
@msg Saving recovery data for {_1} Salvando dados de recuperação de {_1}
|
||||
@msg Discarded {_1} recovered document(s) {_1} documento(s) recuperado(s) descartado(s)
|
||||
@msg Restored {_1} recovered document(s): save them to keep them {_1} documento(s) recuperado(s) restaurado(s): salve-os para mantê-los
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4387,6 +4387,7 @@ axis Both Оба
|
||||
@msg Saving Сохранение
|
||||
@msg Saving {_1} Сохранение {_1}
|
||||
@msg Exporting {_1} Экспорт {_1}
|
||||
@msg Packaging {_1} Упаковка {_1}
|
||||
@msg Saving recovery data for {_1} Сохранение данных восстановления для {_1}
|
||||
@msg Discarded {_1} recovered document(s) Отменено восстановленных документов: {_1}
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Восстановлено документов: {_1}: сохраните их, чтобы оставить
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4262,6 +4262,7 @@ axis Both Båda
|
||||
@msg Saving Sparar
|
||||
@msg Saving {_1} Sparar {_1}
|
||||
@msg Exporting {_1} Exporterar {_1}
|
||||
@msg Packaging {_1} Paketerar {_1}
|
||||
@msg Saving recovery data for {_1} Sparar återställningsdata för {_1}
|
||||
@msg Discarded {_1} recovered document(s) Kastade {_1} återställda dokument
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Återställde {_1} återskapade dokument: spara dem för att behålla dem
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4108,6 +4108,7 @@ axis Both Her İkisi
|
||||
@msg Saving Kaydediliyor
|
||||
@msg Saving {_1} {_1} kaydediliyor
|
||||
@msg Exporting {_1} {_1}'yi dışa aktarma
|
||||
@msg Packaging {_1} {_1} paketleniyor
|
||||
@msg Saving recovery data for {_1} {_1} için kurtarma verileri kaydediliyor
|
||||
@msg Discarded {_1} recovered document(s) Atılan {_1} kurtarılan belge(ler)
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Geri yüklenen {_1} kurtarılan belge(ler): saklamak için bunları kaydedin
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4375,6 +4375,7 @@ axis Both Обидві
|
||||
@msg Saving Збереження
|
||||
@msg Saving {_1} Збереження {_1}
|
||||
@msg Exporting {_1} Експорт {_1}
|
||||
@msg Packaging {_1} Пакування {_1}
|
||||
@msg Saving recovery data for {_1} Збереження даних відновлення для {_1}
|
||||
@msg Discarded {_1} recovered document(s) Скасовано відновлених документів: {_1}
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Відновлено документів: {_1}: збережіть їх, щоб залишити
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 733 and column 40.
|
@@ -4236,6 +4236,7 @@ theme Appearance Giao diện
|
||||
@msg Exported {what} Đã xuất {what}
|
||||
@msg Exported {what} with {_1} note(s): {first} Đã xuất {what} với {_1} ghi chú: {first}
|
||||
@msg Exporting {_1} Đang xuất {_1}
|
||||
@msg Packaging {_1} Đang đóng gói {_1}
|
||||
@msg file version {0} is newer than this VectorCraft supports ({VERSION}) phiên bản tệp {0} mới hơn VectorCraft hỗ trợ ({VERSION})
|
||||
@msg Flatten Transparency: select objects to flatten Dẹp phẳng độ trong suốt: hãy chọn đối tượng để dẹp phẳng
|
||||
@msg Flatten Transparency: {e} Dẹp phẳng độ trong suốt: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4028 and column 10.
|
@@ -4024,6 +4024,7 @@ imageTrace Outline 轮廓
|
||||
@msg SVG parse error: {0} SVG 解析错误:{0}
|
||||
@msg decimals must be {_1}–{_2} (got {_3}) 小数位数必须为 {_1}–{_2} (实际为 {_3})
|
||||
@msg Exporting {_1} 正在导出 {_1}
|
||||
@msg Packaging {_1} 正在打包 {_1}
|
||||
@msg Exported {what} 已导出 {what}
|
||||
@msg "{0}" is not a colour: write it as #rrggbb "{0}" 不是颜色:请写为 #rrggbb
|
||||
@msg could not decode image: {0} 无法解码图像:{0}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2010,6 +2010,7 @@ file-filter Templates 範本
|
||||
@msg SVG parse error: {0} SVG 剖析錯誤:{0}
|
||||
@msg decimals must be {_1}–{_2} (got {_3}) 小數位數必須為 {_1}–{_2} (收到 {_3})
|
||||
@msg Exporting {_1} 正在匯出 {_1}
|
||||
@msg Packaging {_1} 正在封裝 {_1}
|
||||
@msg Exported {what} 已匯出 {what}
|
||||
@msg "{0}" is not a colour: write it as #rrggbb "{0}" 不是顏色:請寫成 #rrggbb
|
||||
@msg could not decode image: {0} 無法解碼影像:{0}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -11,17 +11,17 @@ use vectorcraft_geom::{Rect, shapes};
|
||||
|
||||
use crate::{Services, VectorcraftApp, background, io};
|
||||
|
||||
type Files = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
|
||||
pub(super) type Files = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
|
||||
|
||||
/// Holds the background writer until opened.
|
||||
#[derive(Clone, Default)]
|
||||
struct Gate(Arc<AtomicBool>);
|
||||
pub(super) struct Gate(Arc<AtomicBool>);
|
||||
|
||||
impl Gate {
|
||||
fn open(&self) {
|
||||
pub(super) fn open(&self) {
|
||||
self.0.store(true, Ordering::SeqCst);
|
||||
}
|
||||
fn wait(&self) {
|
||||
pub(super) fn wait(&self) {
|
||||
while !self.0.load(Ordering::SeqCst) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
@@ -56,7 +56,7 @@ fn paths(n: usize) -> Document {
|
||||
d
|
||||
}
|
||||
|
||||
fn written(files: &Files) -> Vec<String> {
|
||||
pub(super) fn written(files: &Files) -> Vec<String> {
|
||||
files.lock().unwrap().iter().map(|(p, _)| p.clone()).collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -865,6 +865,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- File › Place reads Word `.docx` documents as area type with the same formatting, taken from the document defaults, the paragraph and character styles (their toggle properties reversing each other) and direct formatting, the theme's fonts included. Table rows are paragraphs with their cells separated by tabs; hidden and deleted text, field instructions, pictures, headers, footers and notes are left out (`file.place`, `crates/engine/src/tests_textimport.rs`).
|
||||
- A Word paragraph justified with a low, medium or high kashida length places justified with Insert Kashidas set to Short, Medium or Long (`file.place`, `crates/engine/src/tests_textimport.rs`).
|
||||
- On the desktop, Copy and Cut make the system clipboard's formats (text, SVG, PDF, PNG) from a copy of the clipboard on the thread that writes background saves and exports, and write them when they are ready; while that thread has a save, an export, a print or a recovery copy to finish, they are made and written at once. Until they are written, Paste in VectorCraft takes the internal clipboard, and while a text field or the Type tool has the keyboard the app waits for them. Another Copy or Cut, or a copy of text in VectorCraft, drops them.
|
||||
- On the desktop with Enable Background Export on (Preferences › File Handling), File › Package… collects the linked files and fonts and writes the package from the document as it was at OK on the thread that writes background saves and exports, with "Packaging" in the status bar and the busy spinner until it is written; a save of unsaved changes that Package starts is written first. `file.package` over MCP and scripts replies as before.
|
||||
- Missing linked files can be looked for in a folder and its subfolders: Find in Folder… in the Linked File Not Found dialog and in the Links panel menu lists the files with each one's name, those with the size and contents saved with the link first, and relinks the files chosen, copying an image's file into the document's folder when asked, without replacing a file there (`links.findFiles`, `links.relink {copy}`). In the Links panel it looks for the selected linked files when any are selected.
|
||||
- On Windows, a document opened from a mapped network drive finds its links stored with the share's own path (`\\server\share\…`), as the same document opened from the share does; links on other computers stay missing.
|
||||
- On Windows, a link on a drive mapped to a network share other than the document's (`Y:\…`) is missing, with the reason that the path is on another computer, as a link stored with that share's own path is; links on this computer's drives and on the document's share are followed.
|
||||
|
||||
Reference in New Issue
Block a user