Merge pull request #6182 from dotCooCoo/feat/relink-replace-scopes

Links: Relink can replace all instances, the folder's missing links or each link separately
This commit is contained in:
BFlat
2026-11-08 14:41:00 +08:00
committed by GitHub
40 changed files with 909 additions and 39 deletions
+154 -19
View File
@@ -68,7 +68,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Relink",
[],
None,
"{ids?: [image or placed document ids] (default: the selected ones; with folder and nothing selected, every missing link), path | folder, copy?: false (with path, for images of a saved document: copy the file into the document's folder and link to the copy; the names Name.png, Name 2.png … Name 99.png are tried in turn until one is free, which gets the copy, or is a file with the same contents, which is used), preserve?: transforms|bounds|fileDimensions|fit|fill (how the new file takes each image's place this time, as links.placementOptions describes; default: each image's Placement Options; fit keeps the new file's aspect ratio inside the old bounds: Replace)} link images (or placed documents, each showing the same artboard or page of the new file) to the file at path, or each to the file of its link's name in folder; embedded images become linked; each keeps its bounds. One undo step → {relinked: [ids], notFound: [file names], copied?: [{from, to}], kept?: [{from, to}] (with copy)}",
"{ids?: [image or placed document ids] (default: the selected ones; with folder and nothing selected, every missing link), path | folder, scope?: selected (default: ids) | all (ids and every other object in the layers linked to the same file as one of them) | missingInFolder (with path: ids, then every other missing link whose file name is in path's folder, each to the file of its name there; names not there are in notFound; with copy, only images are looked for) | each (with paths), paths?: [{ids, path}] (with scope each, in place of ids, path and folder: each link to its own file; an id in two entries is an error), copy?: false (with path or paths, for images of a saved document: copy the file into the document's folder and link to the copy; the names Name.png, Name 2.png … Name 99.png are tried in turn until one is free, which gets the copy, or is a file with the same contents, which is used), preserve?: transforms|bounds|fileDimensions|fit|fill (how the new file takes each image's place this time, as links.placementOptions describes; default: each image's Placement Options; fit keeps the new file's aspect ratio inside the old bounds: Replace)} link images (or placed documents, each showing the same artboard or page of the new file) to the file at path, or each to the file of its link's name in folder; embedded images become linked; each keeps its bounds. One undo step → {relinked: [ids], notFound: [file names], copied?: [{from, to}], kept?: [{from, to}] (with copy)}",
has_doc,
relink
),
@@ -1125,6 +1125,62 @@ fn update_where(s: &mut Session, ids: Option<Vec<NodeId>>, path: Option<&str>) -
Ok(json!({ "updated": updated, "missing": missing }))
}
/// Which links `links.relink` points at the new file (the Replace choice of Relink).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Scope {
/// Each link its own file (`paths`).
Each,
/// The objects given or selected.
Selected,
/// Every object in the layers linked to the same file as one given or selected.
All,
/// The objects given or selected, then every other missing link whose file name is in the new
/// file's folder.
MissingInFolder,
}
impl Scope {
fn from_id(id: &str) -> Option<Self> {
match id {
"each" => Some(Scope::Each),
"selected" => Some(Scope::Selected),
"all" => Some(Scope::All),
"missingInFolder" => Some(Scope::MissingInFolder),
_ => None,
}
}
}
/// The error for `scope` and `paths` given one without the other, or `paths` beside ids, path or
/// folder.
fn each_mismatch(cmd: &str) -> EngineError {
bad(cmd, "scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder")
}
/// `links.relink {scope: "each", paths}`: each entry's ids with its absolute file, entries naming
/// one file merged. A malformed entry, an id in two entries or an empty list is an error.
fn each_paths(p: &Value, cmd: &str) -> Result<Vec<(Vec<NodeId>, String)>> {
let entries = p.get("paths").and_then(Value::as_array).filter(|a| !a.is_empty()).ok_or_else(|| each_mismatch(cmd))?;
let mut seen = BTreeSet::new();
let mut out: Vec<(Vec<NodeId>, String)> = vec![];
for (i, e) in entries.iter().enumerate() {
let malformed = || bad(cmd, format!("paths entry {i} needs path and ids (object ids no other entry has)"));
let path = e.get("path").and_then(Value::as_str).filter(|s| !s.is_empty()).ok_or_else(malformed)?;
let ids = e.get("ids").and_then(Value::as_array).filter(|a| !a.is_empty()).ok_or_else(malformed)?;
let mut these = vec![];
for v in ids {
let id = v.as_u64().map(NodeId).filter(|id| seen.insert(*id)).ok_or_else(malformed)?;
these.push(id);
}
let path = absolute_path(path);
match out.iter_mut().find(|(_, p)| *p == path) {
Some((ids, _)) => ids.extend(these),
None => out.push((these, path)),
}
}
Ok(out)
}
fn relink(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "links.relink";
// How the new file takes the image's place this time (default: its Placement Options).
@@ -1132,13 +1188,26 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
None => None,
Some(v) => Some(Preserve::from_id(v).ok_or_else(|| bad(C, format!("preserve `{v}` is not transforms|bounds|fileDimensions|fit|fill")))?),
};
let scope = match p.get("scope").filter(|v| !v.is_null()) {
None => Scope::Selected,
Some(v) => v.as_str().and_then(Scope::from_id).ok_or_else(|| bad(C, format!("scope {v} is not each|selected|all|missingInFolder")))?,
};
let st = s.doc()?;
let folder_param = str_param(p, "folder");
match scope {
Scope::Each if p.get("path").is_some() || folder_param.is_some() || p.get("ids").is_some() => return Err(each_mismatch(C)),
Scope::Selected | Scope::All | Scope::MissingInFolder if p.get("paths").is_some() => return Err(each_mismatch(C)),
Scope::MissingInFolder if folder_param.is_some() => {
return Err(bad(C, "scope missingInFolder takes path: the folder of that file is searched for the other missing links"));
}
_ => {}
}
let each = if scope == Scope::Each { Some(each_paths(p, C)?) } else { None };
// With copy: the saved document's folder, which the file is copied into.
let copy_to = match (bool_or(p, "copy", false), str_param(p, "path")) {
let copy_to = match (bool_or(p, "copy", false), str_param(p, "path").is_some() || each.is_some()) {
(false, _) => None,
(true, None) => return Err(bad(C, "copy needs path: the file to copy into the document's folder")),
(true, Some(_)) => {
(true, false) => return Err(bad(C, "copy needs path: the file to copy into the document's folder")),
(true, true) => {
let doc = st.path.as_deref().map(absolute_path).ok_or_else(|| bad(C, "save the document first: copy puts the file in its folder"))?;
Some(Path::new(&doc).parent().map(Path::to_path_buf).ok_or_else(|| bad(C, format!("{doc} has no folder")))?)
}
@@ -1151,9 +1220,10 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
_ => None,
})
};
let ids: Vec<NodeId> = match ids_param(p, "ids") {
Some(ids) => ids,
None => {
let mut ids: Vec<NodeId> = match (&each, ids_param(p, "ids")) {
(Some(each), _) => each.iter().flat_map(|(ids, _)| ids.iter().copied()).collect(),
(None, Some(ids)) => ids,
(None, None) => {
let selected: Vec<NodeId> = st.selection.objects.iter().copied().filter(|id| image(id).is_some()).collect();
match folder_param {
Some(_) if selected.is_empty() => {
@@ -1169,15 +1239,37 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
if ids.is_empty() {
return Err(bad(C, "select the images to relink, or pass ids"));
}
// All Instances: the other objects in the layers linked to the same files.
if scope == Scope::All {
let paths: BTreeSet<&str> = ids.iter().filter_map(|id| image(id).flatten()).map(|l| l.path.as_str()).collect();
let given: BTreeSet<NodeId> = ids.iter().copied().collect();
let mut more = vec![];
st.doc.walk(|n| {
if let Some((l, _, _)) = linked(n)
&& paths.contains(l.path.as_str())
&& !given.contains(&n.id)
{
more.push(n.id);
}
});
ids.extend(more);
}
if copy_to.is_some() && ids.iter().any(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(NodeKind::PlacedDocument(_)))) {
return Err(bad(C, "copy takes images only: relink placed documents where their files are"));
}
// (ids, the file) per file to read.
let mut files: Vec<(Vec<NodeId>, String)> = vec![];
let mut not_found = vec![];
match (str_param(p, "path"), folder_param) {
(Some(path), _) => files.push((ids, absolute_path(path))),
(None, Some(dir)) => {
match (each, str_param(p, "path"), folder_param) {
(Some(each), _, _) => files = each,
(None, Some(path), _) if scope == Scope::MissingInFolder => {
let path = absolute_path(path);
let given: BTreeSet<NodeId> = ids.iter().copied().collect();
files.push((ids, path));
missing_in_folder(st, &given, copy_to.is_some(), &mut files, &mut not_found);
}
(None, Some(path), _) => files.push((ids, absolute_path(path))),
(None, None, Some(dir)) => {
for id in ids {
let Some(link) = image(&id).flatten() else { continue };
let path = absolute_path(&Path::new(dir).join(link.name()).to_string_lossy());
@@ -1188,10 +1280,10 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
}
}
}
(None, None) => return Err(bad(C, "give path (a file) or folder")),
(None, None, None) => return Err(bad(C, "give path (a file) or folder")),
}
let mut reads = vec![];
let (mut copied, mut kept) = (vec![], vec![]);
// Every file is read and checked before any is copied.
let mut checked = vec![];
for (ids, path) in files {
let bytes = read_file(&path)?;
// Images show the file; placed documents each show their artboard or page of it.
@@ -1215,20 +1307,28 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
for ((page, bounding, page_box), ids) in documents {
sources.push((page, ids, super::place::document::source(&path, &bytes, Some(&path), page, bounding, page_box, C)?));
}
let to = match &copy_to {
Some(dir) => {
let (to, new) = copy_into_folder(&path, &bytes, dir)?;
// The bytes are kept only to be copied, the file as it was checked.
let link = link_info(&path, &bytes);
let bytes = copy_to.is_some().then_some(bytes);
checked.push((path, link, bytes, images, img, sources));
}
let mut reads = vec![];
let (mut copied, mut kept): (Vec<Value>, Vec<Value>) = (vec![], vec![]);
for (path, link, bytes, images, img, sources) in checked {
let link = match (&copy_to, bytes) {
(Some(dir), Some(bytes)) => {
let (to, new) = copy_or_remove(&path, &bytes, dir, &copied)?;
let link = link_info(&to, &bytes);
let pair = json!({ "from": path, "to": to });
if new {
copied.push(pair)
} else {
kept.push(pair)
}
to
link
}
None => path,
_ => link,
};
let link = link_info(&to, &bytes);
if let Some(img) = img {
reads.push((images, Read::Image((link.clone(), img))));
}
@@ -1250,6 +1350,41 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
Ok(out)
}
/// `links.relink {scope: "missingInFolder"}`: the other missing links of `st` (not `given`; only
/// images when `images_only`, as copy takes), each added to `files` with the file of its name in
/// the folder of the first file there; names not there go to `not_found`.
fn missing_in_folder(
st: &crate::DocState,
given: &BTreeSet<NodeId>,
images_only: bool,
files: &mut Vec<(Vec<NodeId>, String)>,
not_found: &mut Vec<String>,
) {
let Some(dir) = files.first().and_then(|(_, p)| Path::new(p).parent()).map(Path::to_path_buf) else { return };
for g in groups(&st.doc, None).into_iter().filter(|g| !(images_only && g.document.is_some())) {
let rest: Vec<NodeId> = g.ids.iter().copied().filter(|id| !given.contains(id)).collect();
if rest.is_empty() || probe(&g, st.path.as_deref()).0 != Status::Missing {
continue;
}
let found = absolute_path(&dir.join(g.link.name()).to_string_lossy());
match files.iter_mut().find(|(_, p)| *p == found) {
Some((ids, _)) => ids.extend(rest),
None if file_stamp(&found).is_some() => files.push((rest, found)),
None => not_found.push(g.link.name().to_string()),
}
}
}
/// [`copy_into_folder`]; when it fails, the copies `copied` (`{from, to}`) the same call made
/// before are removed, since nothing is relinked.
fn copy_or_remove(from: &str, bytes: &[u8], dir: &Path, copied: &[Value]) -> Result<(String, bool)> {
copy_into_folder(from, bytes, dir).inspect_err(|_| {
for to in copied.iter().filter_map(|pair| pair.get("to").and_then(Value::as_str)) {
let _ = std::fs::remove_file(to);
}
})
}
/// `links.relink {copy}`: put the file at `from` (`bytes`, as just read) into the folder `dir`,
/// never replacing a file there ([`fileio::copy_new`]) → where it is now, and whether it was
/// copied (`false`: a file there with the same contents, of its name or a numbered one, is used).
+145
View File
@@ -480,6 +480,151 @@ fn a_missing_link_renders_its_preview_and_relinks() {
assert!(s.execute("links.relink", &json!({"ids": [id.0]})).is_err(), "path or folder");
}
/// The path of the file image `id` links to.
fn link_path(s: &Session, id: NodeId) -> String {
image(s, id).link.map(|l| l.path).unwrap_or_default()
}
/// Replace: All Instances relinks every image linked to the same file as the one given, in one
/// undo step; Selected Instances (the default) only the one given.
#[test]
fn relink_all_instances_takes_every_image_of_the_same_file() {
let dir = Folder::new("relink-all");
let (pic, other) = (dir.file("photo.png"), dir.file("other.png"));
write(&pic, &png(60, 30, RED));
write(&other, &png(60, 30, RED));
let mut s = session();
let (a, b, c) = (place(&mut s, &pic), place(&mut s, &pic), place(&mut s, &other));
let blue = dir.file("new/blue.png");
write(&blue, &png(60, 30, BLUE));
let r = s.execute("links.relink", &json!({"ids": [a.0], "path": blue, "scope": "selected"})).unwrap();
assert_eq!(r, json!({"relinked": [a.0], "notFound": []}));
assert_eq!(link_path(&s, b), pic, "selected instances: the other copy keeps its file");
s.execute("edit.undo", &json!({})).unwrap();
let r = s.execute("links.relink", &json!({"ids": [a.0], "path": blue, "scope": "all"})).unwrap();
assert_eq!(r, json!({"relinked": [a.0, b.0], "notFound": []}));
assert_eq!((link_path(&s, a), link_path(&s, b), link_path(&s, c)), (blue.clone(), blue.clone(), other.clone()));
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!((link_path(&s, a), link_path(&s, b)), (pic.clone(), pic.clone()), "one undo step");
// The selection stands in for ids.
s.execute("select.set", &json!({"ids": [b.0]})).unwrap();
let r = s.execute("links.relink", &json!({"path": blue, "scope": "all"})).unwrap();
assert_eq!(r["relinked"], json!([b.0, a.0]));
}
/// Replace: Missing Links from This Folder relinks the given links to the chosen file, then every
/// other missing link to the file of its name in that file's folder, in one undo step; names not
/// there are listed in notFound and stay missing.
#[test]
fn relink_missing_links_from_this_folder_finds_the_others_by_name() {
let dir = Folder::new("relink-missing-folder");
let names = ["a.png", "b.png", "c.png", "d.png"];
let mut s = session();
let ids: Vec<NodeId> = names
.iter()
.map(|n| {
write(&dir.file(n), &png(60, 30, RED));
place(&mut s, &dir.file(n))
})
.collect();
let doc_path = dir.file("poster.vectorcraft");
save(&mut s, &doc_path);
// d.png stays where it was: it isn't missing.
for n in &names[..3] {
std::fs::remove_file(dir.file(n)).unwrap();
}
open(&mut s, &doc_path);
let found = dir.file("found/a new name.png");
write(&found, &png(60, 30, BLUE));
write(&dir.file("found/b.png"), &png(60, 30, BLUE));
write(&dir.file("found/d.png"), &png(60, 30, BLUE));
let r = s.execute("links.relink", &json!({"ids": [ids[0].0], "path": found, "scope": "missingInFolder"})).unwrap();
assert_eq!(r, json!({"relinked": [ids[0].0, ids[1].0], "notFound": ["c.png"]}));
let b_found = dir.0.join("found").join("b.png").to_string_lossy().into_owned();
assert_eq!((link_path(&s, ids[0]), link_path(&s, ids[1])), (found.clone(), b_found));
assert_eq!((link_path(&s, ids[2]), link_path(&s, ids[3])), (dir.file("c.png"), dir.file("d.png")));
let c = s.execute("links.check", &json!({})).unwrap();
assert_eq!(c["missing"], 1, "{c}");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.execute("links.check", &json!({})).unwrap()["missing"], 3, "one undo step");
}
/// Replace: Each Link Separately takes one file for each link, in one undo step.
#[test]
fn relink_each_link_separately_takes_a_file_for_each() {
let dir = Folder::new("relink-each");
let (one, two) = (dir.file("one.png"), dir.file("two.png"));
write(&one, &png(60, 30, RED));
write(&two, &png(60, 30, RED));
let mut s = session();
let (a, b, c) = (place(&mut s, &one), place(&mut s, &one), place(&mut s, &two));
let (x, y) = (dir.file("new/x.png"), dir.file("new/y.png"));
write(&x, &png(60, 30, BLUE));
write(&y, &png(30, 30, BLUE));
let paths = json!([{"ids": [a.0, b.0], "path": x}, {"ids": [c.0], "path": y}]);
let r = s.execute("links.relink", &json!({"scope": "each", "paths": paths})).unwrap();
assert_eq!(r, json!({"relinked": [a.0, b.0, c.0], "notFound": []}));
assert_eq!((link_path(&s, a), link_path(&s, b), link_path(&s, c)), (x.clone(), x.clone(), y.clone()));
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!((link_path(&s, a), link_path(&s, c)), (one.clone(), two.clone()), "one undo step");
// Two entries naming the same file read it once.
let paths = json!([{"ids": [a.0], "path": y}, {"ids": [c.0], "path": y}]);
let r = s.execute("links.relink", &json!({"scope": "each", "paths": paths})).unwrap();
assert_eq!(r["relinked"], json!([a.0, c.0]));
}
/// With copy, a file that can't be shown stops the call before any file is copied into the
/// document's folder.
#[test]
fn relink_each_with_copy_copies_nothing_when_a_file_cant_be_read() {
let dir = Folder::new("relink-each-copy");
let (one, two) = (dir.file("doc/one.png"), dir.file("doc/two.png"));
write(&one, &png(60, 30, RED));
write(&two, &png(60, 30, RED));
let mut s = session();
let (a, c) = (place(&mut s, &one), place(&mut s, &two));
save(&mut s, &dir.file("doc/poster.vectorcraft"));
let (x, bad) = (dir.file("new/x.png"), dir.file("new/broken.png"));
write(&x, &png(60, 30, BLUE));
write(&bad, b"not an image");
let paths = json!([{"ids": [a.0], "path": x}, {"ids": [c.0], "path": bad}]);
assert!(s.execute("links.relink", &json!({"scope": "each", "paths": paths, "copy": true})).is_err());
assert!(!Path::new(&dir.file("doc/x.png")).exists(), "no copy is left behind");
assert_eq!(link_path(&s, a), one);
}
/// A scope or paths that doesn't fit is refused, and nothing is relinked.
#[test]
fn relink_refuses_scopes_and_paths_that_dont_fit() {
let dir = Folder::new("relink-bad-scope");
let pic = dir.file("photo.png");
write(&pic, &png(60, 30, RED));
let mut s = session();
let a = place(&mut s, &pic);
let blue = dir.file("blue.png");
write(&blue, &png(60, 30, BLUE));
let bad = [
json!({"ids": [a.0], "path": blue, "scope": "everything"}),
json!({"ids": [a.0], "path": blue, "scope": 3}),
json!({"scope": "each"}),
json!({"scope": "each", "paths": []}),
json!({"scope": "each", "paths": [{"ids": [a.0]}]}),
json!({"scope": "each", "paths": [{"path": blue}]}),
json!({"scope": "each", "paths": [{"ids": ["one"], "path": blue}]}),
json!({"scope": "each", "paths": [{"ids": [a.0], "path": blue}, {"ids": [a.0], "path": pic}]}),
json!({"scope": "each", "paths": [{"ids": [a.0], "path": blue}], "path": blue}),
json!({"scope": "each", "paths": [{"ids": [a.0], "path": blue}], "ids": [a.0]}),
json!({"scope": "all", "paths": [{"ids": [a.0], "path": blue}]}),
json!({"ids": [a.0], "paths": [{"ids": [a.0], "path": blue}]}),
json!({"ids": [a.0], "folder": dir.0.to_string_lossy(), "scope": "missingInFolder"}),
json!({"ids": [a.0], "scope": "missingInFolder"}),
];
for p in bad {
assert!(s.execute("links.relink", &p).is_err(), "{p}");
assert_eq!(link_path(&s, a), pic, "{p} relinks nothing");
}
}
/// A link stored as a classic Mac OS path (`Disk:Folder:file`, as older `.ai` files keep them) is
/// named by its last component: the Links panel shows that name and its folder, Relink to Folder
/// looks for that name, and the document's own folder is searched for it. The stored path is kept.
@@ -488,6 +488,35 @@ fn relink_points_a_placed_document_at_another_file() {
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
}
/// Missing Links from This Folder finds a missing placed document by name, except with copy, which
/// takes images only: the placed document then stays as it is.
#[test]
fn relink_missing_in_folder_finds_placed_documents_except_with_copy() {
let dir = Folder::new("placed-missing-folder");
let (doc_file, pic) = (dir.file("art/a.vectorcraft"), dir.file("art/photo.png"));
write(&pic, &crate::tests_links::png(60, 30, RED));
source(&doc_file, 100.0, 50.0, RED, 0);
let mut s = session();
let (placed_id, image_id) = (place(&mut s, &doc_file), place(&mut s, &pic));
let doc_path = dir.file("poster.vectorcraft");
save(&mut s, &doc_path);
std::fs::remove_file(&doc_file).unwrap();
std::fs::remove_file(&pic).unwrap();
open(&mut s, &doc_path);
let found = dir.0.join("found");
let chosen = found.join("renamed.png").to_string_lossy().into_owned();
write(&chosen, &crate::tests_links::png(60, 30, BLUE));
source(&found.join("a.vectorcraft").to_string_lossy(), 100.0, 50.0, BLUE, 0);
let p = json!({"ids": [image_id.0], "path": chosen, "scope": "missingInFolder", "copy": true});
let r = s.execute("links.relink", &p).unwrap();
assert_eq!(r["relinked"], json!([image_id.0]), "{r}");
assert_eq!(placed(&s, placed_id).link.path, doc_file, "copy leaves the placed document");
s.execute("edit.undo", &json!({})).unwrap();
let p = json!({"ids": [image_id.0], "path": chosen, "scope": "missingInFolder"});
let r = s.execute("links.relink", &p).unwrap();
assert_eq!(r["relinked"], json!([image_id.0, placed_id.0]), "{r}");
}
/// A file made from scratch at `path` in `format` (`pdf`, or `ai`: a PDF-compatible .ai): one
/// 100×50 page per color of `pages`, filled with it (`extra` more rectangles on each change the
/// file's size).
+4
View File
@@ -243,6 +243,10 @@ fn structured_junk() {
("addon.select.byLinkStatus", json!({"status": "embedded", "ids": {"a": 1}, "add": [1], "zoom": "yes"})),
("addon.select.byLinkStatus", json!({"status": ["modified", "embedded"], "ids": [], "add": true, "zoom": true})),
("addon.select.byLinkStatus", json!({"status": [[["linked"]]], "ids": null})),
("links.relink", json!({"scope": "each", "paths": [{"ids": [2, u64::MAX, -1, "x"], "path": ""}, null, 7, {"path": [1]}]})),
("links.relink", json!({"scope": ["all"], "path": "/no/such/file.png", "ids": {"a": 1}})),
("links.relink", json!({"scope": "missingInFolder", "path": "", "folder": [], "paths": "x"})),
("links.relink", json!({"scope": "all", "path": "/no/such/file.png", "ids": [2, u64::MAX, -1, 1.5]})),
("object.transform", json!({"matrix": [0, 0, 0, 0, 0, 0]})),
("object.transform", json!({"matrix": [1e308, 1e308, 1e308, 1e308, 1e308, 1e308]})),
("object.transform", json!({"matrix": [1, 2, 3]})),
+2
View File
@@ -92,6 +92,7 @@ pub mod raster_effects;
pub mod rearrange_artboards;
pub mod recolor;
mod recovery;
pub mod relink;
pub mod rename_artboards;
pub(crate) mod revolve;
pub(crate) mod revolve_gizmo;
@@ -368,6 +369,7 @@ registry! {
OfficeExport: [office_export::KIND] => office_export::SPEC,
DxfOptions: [dxf_options::KIND] => dxf_options::SPEC,
PlacementOptions: [placement_options::KIND] => placement_options::SPEC,
Relink: [relink::KIND] => relink::SPEC,
Package: [package::KIND] => package::SPEC,
ActionBatch: [action_batch::KIND] => action_batch::SPEC,
ActionSetOptions: [action_set_options::KIND] => action_set_options::SPEC,
+316
View File
@@ -0,0 +1,316 @@
//! Links panel → Relink…: the selected links, and the Replace choice of which links the new file
//! takes. Selected Instances relinks the selected images and placed documents, All Instances every
//! object linked to the same files, and Missing Links from This Folder the selected ones and then
//! every other missing link whose file name is in the new file's folder. Each Link Separately asks
//! for a file for each link in turn, one OK each, and relinks them all at the last one. Every
//! choice is one `links.relink` call: one undo step.
//!
//! Fields: `ids` (the images and placed documents), `links` (`[{name, ids}]`, one row per linked
//! file, as `links.check` lists them, then one per embedded image), `scope` (each|selected|all|missingInFolder; default
//! selected), `path` (the file; picked when empty) and `paths` (with each: the files chosen so
//! far, one per row of `links` in order; the next OK takes `path` or picks one for the next row).
use serde_json::{Value, json};
use super::DialogSpec;
use super::form;
use crate::state::Dialog;
use crate::theme::{self, Tokens};
use crate::{VectorcraftApp, widgets};
pub(super) const KIND: &str = "relink";
const CMD: &str = "links.relink";
/// (value, label) of each Replace choice.
const SCOPES: [(&str, &str); 4] = [
("each", "Each Link Separately"),
("selected", "Selected Instances"),
("all", "All Instances"),
("missingInFolder", "Missing Links from This Folder"),
];
const LABEL: f32 = 70.0;
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Relink").into(),
body,
confirm,
ok: Some("Relink…"),
min_width: 400.0,
max_width: Some(460.0),
..DialogSpec::FORM
};
/// Open the dialog on the images and placed documents `ids` (default: the selected ones).
pub fn open(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let ids = match p.get("ids").filter(|v| !v.is_null()) {
Some(v) => v.as_array().cloned().ok_or_else(|| format!("`ids` must be an array of object ids, not {v}"))?,
None => crate::panels::links::selected_ids(app).into_iter().map(Value::from).collect(),
};
if ids.is_empty() {
return Err("select the images to relink, or pass ids".into());
}
let check = app.session.execute("links.check", &json!({ "ids": ids })).map_err(|e| e.to_string())?;
// One row per file: objects showing different content of one file (pages of a PDF) share it.
let mut by_path: Vec<(Value, Value, Vec<Value>)> = vec![];
for l in check["links"].as_array().into_iter().flatten() {
let ids = l["ids"].as_array().cloned().unwrap_or_default();
match by_path.iter_mut().find(|(path, _, _)| *path == l["path"]) {
Some((_, _, all)) => all.extend(ids),
None => by_path.push((l["path"].clone(), l["name"].clone(), ids)),
}
}
let mut links: Vec<Value> = by_path.into_iter().map(|(_, name, ids)| json!({ "name": name, "ids": ids })).collect();
// Embedded images have no link: each gets a row of its own, named as the Links panel lists it.
let linked: Vec<&Value> = links.iter().flat_map(|l| l["ids"].as_array().into_iter().flatten()).collect();
let mut embedded: Vec<Value> = vec![];
for id in ids.iter().filter(|id| !linked.contains(id)) {
if !embedded.contains(id) {
embedded.push(id.clone());
}
}
if !embedded.is_empty() {
let list = app.session.execute("links.list", &json!({})).map_err(|e| e.to_string())?;
let rows = list["links"].as_array().cloned().unwrap_or_default();
for id in embedded {
let name = rows.iter().find(|r| r["id"] == id).map_or(Value::Null, |r| r["name"].clone());
links.push(json!({ "name": name, "ids": [id] }));
}
}
app.ui.dialog = Some(Dialog::new(KIND, json!({ "ids": ids, "links": links, "scope": "selected", "path": "", "paths": [] })));
Ok(Value::Null)
}
fn rows(d: &Dialog, key: &str) -> Vec<Value> {
d.fields.get(key).and_then(Value::as_array).cloned().unwrap_or_default()
}
/// The file the next OK uses: `path`, else one picked (none: cancelled).
fn file(app: &mut VectorcraftApp, d: &Dialog) -> Result<String, String> {
match d.str("path") {
p if !p.is_empty() => Ok(p),
_ => {
let pick = crate::FilePick { filters: crate::FilePick::filters(vectorcraft_engine::cmd::fileio::place_filters()), ..Default::default() };
crate::picks::open(app, &pick).ok_or_else(|| "cancelled".to_string())
}
}
}
/// OK: relink with the chosen scope. With each, a file for the next link: the dialog stays open
/// until every link has one, then relinks them all.
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let scope = d.str("scope");
let r = if scope == "each" {
let links = rows(d, "links");
let mut paths = rows(d, "paths");
if paths.len() < links.len() {
paths.push(json!(file(app, d)?));
}
if paths.len() < links.len() {
let mut next = d.clone();
next.fields.insert("paths".into(), json!(paths));
next.fields.insert("path".into(), json!(""));
app.ui.dialog = Some(next);
return Ok(json!({ "next": links.get(paths.len()).map_or(Value::Null, |l| l["name"].clone()) }));
}
let entries: Vec<Value> = links.iter().zip(&paths).map(|(l, p)| json!({ "ids": l["ids"], "path": p })).collect();
app.run(CMD, json!({ "scope": "each", "paths": entries }))?
} else {
let path = file(app, d)?;
app.run(CMD, json!({ "ids": d.fields.get("ids").cloned().unwrap_or(Value::Null), "path": path, "scope": scope }))?
};
app.ui.dialog = None;
let missed: Vec<&str> = r["notFound"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
app.status(crate::panels::links::relinked_status_missed(r["relinked"].as_array().map_or(0, Vec::len), &missed));
Ok(r)
}
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
let t = Tokens::get(ui.ctx());
let links = rows(d, "links");
let paths = rows(d, "paths");
let each = d.str("scope") == "each";
widgets::list_box(ui, |ui| {
ui.set_width(ui.available_width());
for (i, l) in links.iter().enumerate() {
ui.horizontal(|ui| {
ui.add_space(6.0);
// With each: the link the next OK picks a file for is in bold.
let font = if each && i == paths.len() { theme::semibold(12.0) } else { egui::FontId::proportional(12.0) };
ui.label(egui::RichText::new(l["name"].as_str().unwrap_or_default()).font(font).color(t.text_strong));
if let Some(p) = paths.get(i).and_then(Value::as_str).filter(|_| each) {
let name = std::path::Path::new(p).file_name().map_or(p.into(), |n| n.to_string_lossy());
ui.add(egui::Label::new(egui::RichText::new(format!("→ {name}")).size(11.5).color(t.text_dim)).truncate()).on_hover_text(p);
}
});
}
});
ui.add_space(8.0);
form::choice(ui, d, "scope", tl!("Replace:"), (LABEL, 260.0), &SCOPES);
false
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_engine::Session;
fn frame(app: &mut VectorcraftApp) -> String {
crate::tests_labels::painted_text(app, |app, ui| super::super::show(app, ui.ctx()))
}
fn png(rgb: [u8; 3]) -> Vec<u8> {
let mut out = vec![];
let [r, g, b] = rgb;
image::RgbaImage::from_pixel(40, 30, image::Rgba([r, g, b, 255]))
.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png)
.unwrap();
out
}
/// A fresh folder with the PNG files `names` → (app with a document, folder).
fn setup(name: &str, names: &[&str]) -> (VectorcraftApp, std::path::PathBuf) {
let dir = std::env::temp_dir().join(format!("vectorcraft-relink-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("new")).unwrap();
for n in names {
std::fs::write(dir.join(n), png([200, 0, 0])).unwrap();
}
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.services.read = Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string())));
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
(app, dir)
}
fn place(app: &mut VectorcraftApp, path: &std::path::Path) -> u64 {
app.run("file.place", json!({"path": path.to_string_lossy()})).unwrap()["ids"][0].as_u64().unwrap()
}
fn link_path(app: &VectorcraftApp, id: u64) -> String {
let st = app.session.active().unwrap();
match st.doc.node(vectorcraft_doc::NodeId(id)).map(|n| &n.kind) {
Some(vectorcraft_doc::NodeKind::Image(im)) => im.link.as_ref().map(|l| l.path.clone()).unwrap_or_default(),
_ => String::new(),
}
}
fn new_file(dir: &std::path::Path, name: &str) -> String {
let p = dir.join("new").join(name);
std::fs::write(&p, png([0, 0, 200])).unwrap();
p.to_string_lossy().into_owned()
}
#[test]
fn relink_opens_with_the_replace_choice_and_all_instances_relinks_every_copy() {
let (mut app, dir) = setup("all", &["photo.png", "other.png"]);
let (a, b, c) = (place(&mut app, &dir.join("photo.png")), place(&mut app, &dir.join("photo.png")), place(&mut app, &dir.join("other.png")));
app.run("select.set", json!({"ids": [a]})).unwrap();
app.services.pick_open = None;
assert!(!crate::menus::enabled(&app, "ui.relinkDialog"), "no file picker (the web)");
let picked = new_file(&dir, "blue.png");
let answer = picked.clone();
app.services.pick_open = Some(Box::new(move |_: &crate::FilePick| Some(answer.clone())));
assert!(crate::menus::enabled(&app, "ui.relinkDialog"));
app.run("ui.relinkDialog", json!({})).unwrap();
let d = app.ui.dialog.as_ref().expect("the dialog");
assert_eq!((d.kind.as_str(), d.str("scope"), d.fields["links"].as_array().unwrap().len()), (KIND, "selected".to_string(), 1));
let text = frame(&mut app);
for label in ["Relink", "photo.png", "Replace:", "Selected Instances", "Relink…", "Cancel"] {
assert!(text.contains(label), "{label} in {text}");
}
app.ui.dialog.as_mut().unwrap().fields.insert("scope".into(), json!("all"));
// OK picks the file.
let r = super::super::confirm(&mut app).unwrap();
assert_eq!(r["relinked"], json!([a, b]));
assert!(app.ui.dialog.is_none());
assert_eq!((link_path(&app, a), link_path(&app, b)), (picked.clone(), picked.clone()));
assert_eq!(link_path(&app, c), dir.join("other.png").to_string_lossy());
app.run("edit.undo", json!({})).unwrap();
assert_eq!(link_path(&app, b), dir.join("photo.png").to_string_lossy(), "one undo step");
// Nothing selected and no ids: nothing to relink.
app.run("select.set", json!({"ids": []})).unwrap();
assert!(!crate::menus::enabled(&app, "ui.relinkDialog"));
assert!(app.run("ui.relinkDialog", json!({})).is_err());
assert!(app.run("ui.relinkDialog", json!({"ids": 3})).is_err(), "ids is a list");
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn each_link_separately_asks_for_a_file_for_each_link_then_relinks_them_all() {
let (mut app, dir) = setup("each", &["one.png", "two.png"]);
let (a, b) = (place(&mut app, &dir.join("one.png")), place(&mut app, &dir.join("two.png")));
let (x, y) = (new_file(&dir, "x.png"), new_file(&dir, "y.png"));
let mut answers = vec![y.clone(), x.clone()];
app.services.pick_open = Some(Box::new(move |_: &crate::FilePick| answers.pop()));
app.run("ui.relinkDialog", json!({"ids": [a, b]})).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("scope".into(), json!("each"));
let r = super::super::confirm(&mut app).unwrap();
assert_eq!(r, json!({"next": "two.png"}));
let d = app.ui.dialog.as_ref().expect("asks for the next link's file");
assert_eq!(d.fields["paths"], json!([x]));
assert_eq!(link_path(&app, a), dir.join("one.png").to_string_lossy(), "nothing relinked yet");
assert!(frame(&mut app).contains("→ x.png"));
let r = super::super::confirm(&mut app).unwrap();
assert_eq!(r["relinked"], json!([a, b]));
assert!(app.ui.dialog.is_none());
assert_eq!((link_path(&app, a), link_path(&app, b)), (x.clone(), y.clone()));
app.run("edit.undo", json!({})).unwrap();
assert_eq!(
(link_path(&app, a), link_path(&app, b)),
(dir.join("one.png").to_string_lossy().into_owned(), dir.join("two.png").to_string_lossy().into_owned()),
"one undo step"
);
let _ = std::fs::remove_dir_all(dir);
}
/// An embedded image in the selection gets a row of its own, so Each Link Separately asks for
/// its file too.
#[test]
fn embedded_images_get_their_own_rows() {
let (mut app, dir) = setup("embedded", &["one.png"]);
let (a, b) = (place(&mut app, &dir.join("one.png")), place(&mut app, &dir.join("one.png")));
app.run("links.embed", json!({"ids": [b]})).unwrap();
app.run("ui.relinkDialog", json!({"ids": [a, b, b]})).unwrap();
let d = app.ui.dialog.as_ref().expect("the dialog");
let rows = d.fields["links"].as_array().unwrap();
assert_eq!((rows.len(), &rows[0]["ids"], &rows[1]["ids"]), (2, &json!([a]), &json!([b])));
let _ = std::fs::remove_dir_all(dir);
}
/// Objects showing different content of one file share its row, so Each Link Separately asks
/// for that file once.
#[test]
fn one_file_with_two_contents_is_one_row() {
let (mut app, dir) = setup("one-file", &["photo.png"]);
let a = place(&mut app, &dir.join("photo.png"));
std::fs::write(dir.join("photo.png"), png([0, 200, 0])).unwrap();
let b = place(&mut app, &dir.join("photo.png"));
app.run("ui.relinkDialog", json!({"ids": [a, b]})).unwrap();
let d = app.ui.dialog.as_ref().expect("the dialog");
assert_eq!(d.fields["links"], json!([{"name": "photo.png", "ids": [a, b]}]));
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn missing_links_from_this_folder_relinks_the_others_found_there() {
let (mut app, dir) = setup("missing", &["a.png", "b.png"]);
let (a, b) = (place(&mut app, &dir.join("a.png")), place(&mut app, &dir.join("b.png")));
let doc = dir.join("doc.vectorcraft").to_string_lossy().into_owned();
app.run("document.save", json!({"path": doc})).unwrap();
for n in ["a.png", "b.png"] {
std::fs::remove_file(dir.join(n)).unwrap();
}
app.run("file.close", json!({})).unwrap();
crate::io::open_path(&mut app, &doc).unwrap();
// The Linked File Not Found question is left for the Links panel.
app.ui.dialog = None;
let chosen = new_file(&dir, "renamed.png");
let found = new_file(&dir, "b.png");
app.run("ui.relinkDialog", json!({"ids": [a]})).unwrap();
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("scope".into(), json!("missingInFolder"));
d.fields.insert("path".into(), json!(chosen));
let r = super::super::confirm(&mut app).unwrap();
assert_eq!(r, json!({"relinked": [a, b], "notFound": []}));
assert_eq!((link_path(&app, a), link_path(&app, b)), (chosen, found));
let _ = std::fs::remove_dir_all(dir);
}
}
+8
View File
@@ -2638,6 +2638,14 @@ digits Malayalam Малаялам
Relink Свързване наново
Relink Linked Files to the Document Свързване наново на свързаните файлове към документа
Relink to Folder… Свързване наново към папка…
Each Link Separately Всяка връзка поотделно
Selected Instances Избраните екземпляри
All Instances Всички екземпляри
Missing Links from This Folder Липсващите връзки от тази папка
@msg scope {v} is not each|selected|all|missingInFolder scope {v} не е each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each и paths вървят заедно: paths изброява записи с ids и path вместо ids, path и folder
@msg paths entry {i} needs path and ids (object ids no other entry has) записът {i} в paths изисква path и ids (идентификатори на обекти, които няма друг запис)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder приема path: в папката на този файл се търсят другите липсващи връзки
Relink… Свързване наново…
Find Linked Files Търсене на свързани файлове
Find Linked Files in Folder… Търсене на свързани файлове в папка…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2067,6 +2067,14 @@ digits Malayalam Malajalam
Location Lokacija
No images Nema slika
Relink to Folder… Ponovo poveži s folderom…
Each Link Separately Svaku vezu zasebno
Selected Instances Odabrane instance
All Instances Sve instance
Missing Links from This Folder Nedostajuće veze iz ovog foldera
@msg scope {v} is not each|selected|all|missingInFolder scope {v} nije each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each i paths idu zajedno: paths navodi unose s ids i path, umjesto ids, path i folder
@msg paths entry {i} needs path and ids (object ids no other entry has) unos {i} u paths treba path i ids (ID-ove objekata koje nema nijedan drugi unos)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder uzima path: u folderu te datoteke traže se ostale nedostajuće veze
Show Missing Prikaži nedostajuće
Sort by Status Sortiraj po statusu
Clear Overrides Očisti zamjene
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -3325,6 +3325,14 @@ digits Malayalam Malaiàlam
Relink Torna a enllaçar
Relink Linked Files to the Document Torna a enllaçar els fitxers enllaçats al document
Relink to Folder… Torna a enllaçar a una carpeta…
Each Link Separately Cada enllaç per separat
Selected Instances Instàncies seleccionades
All Instances Totes les instàncies
Missing Links from This Folder Enllaços que falten d’aquesta carpeta
@msg scope {v} is not each|selected|all|missingInFolder scope {v} no és each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each i paths van junts: paths llista entrades amb ids i path, en lloc d’ids, path i folder
@msg paths entry {i} needs path and ids (object ids no other entry has) l’entrada {i} de paths necessita path i ids (identificadors d’objecte que cap altra entrada no tingui)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder pren path: a la carpeta d’aquest fitxer es cerquen els altres enllaços que falten
Relink… Torna a enllaçar…
Reload Plug-ins Recarrega els complements
Remember Import Matches Recorda les coincidències d’importació
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2093,6 +2093,14 @@ digits Malayalam Malajálamské
Location Umístění
No images Žádné obrázky
Relink to Folder… Znovu propojit ke složce…
Each Link Separately Každé propojení zvlášť
Selected Instances Vybrané instance
All Instances Všechny instance
Missing Links from This Folder Chybějící propojení z této složky
@msg scope {v} is not each|selected|all|missingInFolder scope {v} není each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each a paths patří k sobě: paths uvádí položky s ids a path místo ids, path a folder
@msg paths entry {i} needs path and ids (object ids no other entry has) položka {i} v paths potřebuje path a ids (ID objektů, které nemá žádná jiná položka)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder bere path: ve složce tohoto souboru se hledají ostatní chybějící propojení
Show Missing Zobrazit chybějící
Sort by Status Seřadit podle stavu
Clear Overrides Vymazat přepsání
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1899,6 +1899,14 @@ digits Malayalam Malayalam
Location Placering
No images Ingen billeder
Relink to Folder… Genopret link til mappe…
Each Link Separately Hvert link for sig
Selected Instances Valgte forekomster
All Instances Alle forekomster
Missing Links from This Folder Manglende links fra denne mappe
@msg scope {v} is not each|selected|all|missingInFolder scope {v} er ikke each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each og paths hører sammen: paths angiver poster med ids og path i stedet for ids, path og folder
@msg paths entry {i} needs path and ids (object ids no other entry has) post {i} i paths skal have path og ids (objekt-id'er, som ingen anden post har)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder tager path: der søges efter de andre manglende links i den fils mappe
Show Missing Vis manglende
Sort by Status Sortér efter status
Clear Overrides Ryd tilsidesættelser
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2665,6 +2665,14 @@ digits Malayalam Malayalam
Relink Neu verknüpfen
Relink Linked Files to the Document Verknüpfte Dateien neu mit dem Dokument verknüpfen
Relink to Folder… Mit Ordner neu verknüpfen…
Each Link Separately Jede Verknüpfung einzeln
Selected Instances Ausgewählte Instanzen
All Instances Alle Instanzen
Missing Links from This Folder Fehlende Verknüpfungen aus diesem Ordner
@msg scope {v} is not each|selected|all|missingInFolder scope {v} ist nicht each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each und paths gehören zusammen: paths listet Einträge mit ids und path anstelle von ids, path und folder
@msg paths entry {i} needs path and ids (object ids no other entry has) paths-Eintrag {i} braucht path und ids (Objekt-IDs, die kein anderer Eintrag hat)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder nimmt path: Im Ordner dieser Datei wird nach den anderen fehlenden Verknüpfungen gesucht
Relink… Neu verknüpfen…
Find Linked Files Verknüpfte Dateien suchen
Find Linked Files in Folder… Verknüpfte Dateien in Ordner suchen…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -3275,6 +3275,14 @@ digits Malayalam Μαλαγιάλαμ
Relink Επανασύνδεση
Relink Linked Files to the Document Επανασύνδεση συνδεδεμένων αρχείων με το έγγραφο
Relink to Folder… Επανασύνδεση σε φάκελο…
Each Link Separately Κάθε σύνδεση χωριστά
Selected Instances Επιλεγμένες παρουσίες
All Instances Όλες οι παρουσίες
Missing Links from This Folder Συνδέσεις που λείπουν από αυτόν τον φάκελο
@msg scope {v} is not each|selected|all|missingInFolder το scope {v} δεν είναι each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder τα scope each και paths πάνε μαζί: το paths απαριθμεί καταχωρίσεις με ids και path, αντί για ids, path και folder
@msg paths entry {i} needs path and ids (object ids no other entry has) η καταχώριση {i} του paths χρειάζεται path και ids (αναγνωριστικά αντικειμένων που δεν έχει άλλη καταχώριση)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links το scope missingInFolder δέχεται path: στον φάκελο αυτού του αρχείου αναζητούνται οι άλλες συνδέσεις που λείπουν
Relink… Επανασύνδεση…
Find Linked Files Εύρεση συνδεδεμένων αρχείων
Find Linked Files in Folder… Εύρεση συνδεδεμένων αρχείων σε φάκελο…
Can't render this file because it contains an unexpected character in line 168 and column 5.
+8
View File
@@ -2708,6 +2708,14 @@ digits Malayalam Malayálam
Relink Volver a vincular
Relink Linked Files to the Document Volver a vincular los archivos vinculados al documento
Relink to Folder… Volver a vincular a carpeta…
Each Link Separately Cada vínculo por separado
Selected Instances Instancias seleccionadas
All Instances Todas las instancias
Missing Links from This Folder Vínculos que faltan de esta carpeta
@msg scope {v} is not each|selected|all|missingInFolder scope {v} no es each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each y paths van juntos: paths enumera entradas con ids y path, en lugar de ids, path y folder
@msg paths entry {i} needs path and ids (object ids no other entry has) la entrada {i} de paths necesita path e ids (id. de objeto que ninguna otra entrada tenga)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder toma path: en la carpeta de ese archivo se buscan los demás vínculos que faltan
Relink… Volver a vincular…
Find Linked Files Buscar archivos vinculados
Find Linked Files in Folder… Buscar archivos vinculados en una carpeta…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2054,6 +2054,14 @@ digits Malayalam Malajalam
Location Sijainti
No images Ei kuvia
Relink to Folder… Linkitä uudelleen kansioon…
Each Link Separately Jokainen linkki erikseen
Selected Instances Valitut esiintymät
All Instances Kaikki esiintymät
Missing Links from This Folder Puuttuvat linkit tästä kansiosta
@msg scope {v} is not each|selected|all|missingInFolder scope {v} ei ole each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each ja paths kuuluvat yhteen: paths luettelee merkinnät, joissa on ids ja path, ids:n, path:n ja folder:n sijaan
@msg paths entry {i} needs path and ids (object ids no other entry has) paths-merkintä {i} tarvitsee path- ja ids-arvot (objektitunnukset, joita millään muulla merkinnällä ei ole)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder ottaa path-arvon: muita puuttuvia linkkejä haetaan tämän tiedoston kansiosta
Show Missing Näytä puuttuvat
Sort by Status Lajittele tilan mukaan
Clear Overrides Tyhjennä ohitukset
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2700,6 +2700,14 @@ digits Malayalam Malayalam
Relink Lier à nouveau
Relink Linked Files to the Document Lier à nouveau les fichiers liés au document
Relink to Folder… Lier à nouveau à un dossier…
Each Link Separately Chaque lien séparément
Selected Instances Occurrences sélectionnées
All Instances Toutes les occurrences
Missing Links from This Folder Liens manquants de ce dossier
@msg scope {v} is not each|selected|all|missingInFolder scope {v} n’est pas each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each et paths vont ensemble : paths liste des entrées avec ids et path, à la place de ids, path et folder
@msg paths entry {i} needs path and ids (object ids no other entry has) l’entrée {i} de paths nécessite path et ids (des identifiants d’objet qu’aucune autre entrée n’a)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder prend path : les autres liens manquants sont cherchés dans le dossier de ce fichier
Relink… Lier à nouveau…
Find Linked Files Rechercher des fichiers liés
Find Linked Files in Folder… Rechercher des fichiers liés dans un dossier…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -3624,6 +3624,14 @@
Relink दोबारा लिंक करें
Relink Linked Files to the Document लिंक्ड फ़ाइलें डॉक्यूमेंट से दोबारा लिंक करें
Relink to Folder… फ़ोल्डर से दोबारा लिंक करें…
Each Link Separately हर लिंक अलग से
Selected Instances चुने गए इंस्टेंस
All Instances सभी इंस्टेंस
Missing Links from This Folder इस फ़ोल्डर से मिसिंग लिंक
@msg scope {v} is not each|selected|all|missingInFolder scope {v}, each|selected|all|missingInFolder में से नहीं है
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each और paths साथ में दिए जाते हैं: paths में ids और path वाली एंट्री होती हैं, ids, path और folder की जगह
@msg paths entry {i} needs path and ids (object ids no other entry has) paths की एंट्री {i} को path और ids चाहिए (ऐसे ऑब्जेक्ट ids जो किसी दूसरी एंट्री में न हों)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder, path लेता है: उस फ़ाइल के फ़ोल्डर में बाकी मिसिंग लिंक खोजे जाते हैं
Relink… दोबारा लिंक करें…
Reload Plug-ins प्लग-इन फिर से लोड करें
Remember Import Matches याद रखें आयात मैच
Can't render this file because it is too large.
+8
View File
@@ -3231,6 +3231,14 @@ mark-style Japanese Japán
Relink Újracsatolás
Relink Linked Files to the Document Csatolt fájlok újracsatolása a dokumentumhoz
Relink to Folder… Újracsatolás mappából…
Each Link Separately Minden csatolás külön
Selected Instances Kijelölt példányok
All Instances Összes példány
Missing Links from This Folder Hiányzó csatolások ebből a mappából
@msg scope {v} is not each|selected|all|missingInFolder a scope {v} nem each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder a scope each és a paths együtt jár: a paths ids és path elemű bejegyzéseket sorol fel az ids, path és folder helyett
@msg paths entry {i} needs path and ids (object ids no other entry has) a paths {i} bejegyzéséhez path és ids kell (olyan objektumazonosítók, amelyek más bejegyzésben nincsenek)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links a scope missingInFolder path értéket vár: a többi hiányzó csatolást a fájl mappájában keresi
Relink… Újracsatolás…
Find Linked Files Csatolt fájlok keresése
Find Linked Files in Folder… Csatolt fájlok keresése mappában…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1899,6 +1899,14 @@ digits Malayalam Malayalam
Location Lokasi
No images Tidak ada gambar
Relink to Folder… Tautkan ulang ke folder…
Each Link Separately Setiap tautan terpisah
Selected Instances Instans yang dipilih
All Instances Semua instans
Missing Links from This Folder Tautan hilang dari folder ini
@msg scope {v} is not each|selected|all|missingInFolder scope {v} bukan each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each dan paths digunakan bersama: paths mencantumkan entri berisi ids dan path, sebagai ganti ids, path, dan folder
@msg paths entry {i} needs path and ids (object ids no other entry has) entri paths {i} memerlukan path dan ids (id objek yang tidak dimiliki entri lain)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder menerima path: tautan hilang lainnya dicari di folder file tersebut
Show Missing Tampilkan yang hilang
Sort by Status Urutkan berdasarkan status
Clear Overrides Hapus penimpaan
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1899
1900
1901
1902
1903
1904
1905 @msg
1906 @msg
1907 @msg
1908 @msg
1909
1910
1911
1912
+8
View File
@@ -2741,6 +2741,14 @@ digits Malayalam Malayalam
Relink Ricollega
Relink Linked Files to the Document Ricollega i file collegati al documento
Relink to Folder… Ricollega a cartella…
Each Link Separately Ogni collegamento separatamente
Selected Instances Istanze selezionate
All Instances Tutte le istanze
Missing Links from This Folder Collegamenti mancanti da questa cartella
@msg scope {v} is not each|selected|all|missingInFolder scope {v} non è each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each e paths vanno insieme: paths elenca voci con ids e path, al posto di ids, path e folder
@msg paths entry {i} needs path and ids (object ids no other entry has) la voce {i} di paths richiede path e ids (ID di oggetti che nessun'altra voce ha)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder accetta path: nella cartella di quel file si cercano gli altri collegamenti mancanti
Relink… Ricollega…
Find Linked Files Trova file collegati
Find Linked Files in Folder… Trova file collegati in una cartella…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2730,6 +2730,14 @@ mark-style Japanese 日本式
Release to Layers (Sequence) レイヤーに分配 (シーケンス)
Relink 再リンク
Relink to Folder… フォルダーに再リンク…
Each Link Separately リンクごとに個別
Selected Instances 選択したインスタンス
All Instances すべてのインスタンス
Missing Links from This Folder このフォルダーから見つからないリンク
@msg scope {v} is not each|selected|all|missingInFolder scope {v} は each|selected|all|missingInFolder のいずれでもありません
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each と paths は一緒に指定します: paths には ids、path、folder の代わりに、ids と path を持つエントリを並べます
@msg paths entry {i} needs path and ids (object ids no other entry has) paths のエントリ {i} には path と ids (ほかのエントリにないオブジェクト ID) が必要です
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder は path を取ります: そのファイルのフォルダーで、ほかの見つからないリンクを探します
Relink… 再リンク…
Find Linked Files リンクファイルを検索
Find Linked Files in Folder… フォルダー内のリンクファイルを検索…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1806,6 +1806,14 @@ digits Malayalam მალაიალამური
Location მდებარეობა
No images გამოსახულებები არ არის
Relink to Folder… საქაღალდეში ხელახლა დაკავშირება…
Each Link Separately თითოეული ბმული ცალ-ცალკე
Selected Instances არჩეული ეგზემპლარები
All Instances ყველა ეგზემპლარი
Missing Links from This Folder დაკარგული ბმულები ამ საქაღალდიდან
@msg scope {v} is not each|selected|all|missingInFolder scope {v} არ არის each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each და paths ერთად გამოიყენება: paths ჩამოთვლის ჩანაწერებს ids-ით და path-ით, ids-ის, path-ისა და folder-ის ნაცვლად
@msg paths entry {i} needs path and ids (object ids no other entry has) paths-ის ჩანაწერს {i} სჭირდება path და ids (ობიექტის ID-ები, რომლებიც სხვა ჩანაწერს არ აქვს)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder იღებს path-ს: სხვა დაკარგული ბმულები ამ ფაილის საქაღალდეში იძებნება
Show Missing დაკარგულების ჩვენება
Sort by Status სტატუსის მიხედვით სორტირება
Clear Overrides ცვლილებების გასუფთავება
Can't render this file because it is too large.
+8
View File
@@ -2719,6 +2719,14 @@ mark-style Japanese 일본식
Release to Layers (Sequence) 레이어로 풀기(순차)
Relink 다시 연결
Relink to Folder… 폴더로 다시 연결…
Each Link Separately 각 연결 따로
Selected Instances 선택한 인스턴스
All Instances 모든 인스턴스
Missing Links from This Folder 이 폴더에서 누락된 연결
@msg scope {v} is not each|selected|all|missingInFolder scope {v}은(는) each|selected|all|missingInFolder 중 하나가 아닙니다
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each와 paths는 함께 사용합니다: paths에는 ids, path, folder 대신 ids와 path가 있는 항목을 나열합니다
@msg paths entry {i} needs path and ids (object ids no other entry has) paths 항목 {i}에는 path와 ids(다른 항목에 없는 개체 ID)가 필요합니다
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder는 path를 받습니다: 그 파일의 폴더에서 다른 누락된 연결을 찾습니다
Relink… 다시 연결…
Find Linked Files 연결된 파일 찾기
Find Linked Files in Folder… 폴더에서 연결된 파일 찾기…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1913,6 +1913,14 @@ digits Malayalam Malayalam
Location Locatie
No images Geen afbeeldingen
Relink to Folder… Opnieuw koppelen aan map…
Each Link Separately Elke koppeling afzonderlijk
Selected Instances Geselecteerde instanties
All Instances Alle instanties
Missing Links from This Folder Ontbrekende koppelingen uit deze map
@msg scope {v} is not each|selected|all|missingInFolder scope {v} is niet each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each en paths horen bij elkaar: paths bevat items met ids en path, in plaats van ids, path en folder
@msg paths entry {i} needs path and ids (object ids no other entry has) item {i} van paths heeft path en ids nodig (object-id's die geen ander item heeft)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder neemt path: in de map van dat bestand wordt naar de andere ontbrekende koppelingen gezocht
Show Missing Ontbrekende weergeven
Sort by Status Sorteren op status
Clear Overrides Onderdrukkingen wissen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2013,6 +2013,14 @@ digits Malayalam Malajalam
Location Położenie
No images Brak obrazów
Relink to Folder… Połącz ponownie z folderem…
Each Link Separately Każde łącze osobno
Selected Instances Zaznaczone wystąpienia
All Instances Wszystkie wystąpienia
Missing Links from This Folder Brakujące łącza z tego folderu
@msg scope {v} is not each|selected|all|missingInFolder scope {v} nie jest jednym z each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each i paths występują razem: paths wymienia wpisy z ids i path zamiast ids, path i folder
@msg paths entry {i} needs path and ids (object ids no other entry has) wpis {i} w paths wymaga path i ids (identyfikatorów obiektów, których nie ma żaden inny wpis)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder przyjmuje path: w folderze tego pliku szukane są pozostałe brakujące łącza
Show Missing Pokaż brakujące
Sort by Status Sortuj według stanu
Clear Overrides Wyczyść zastąpienia
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2259,6 +2259,14 @@ digits Malayalam Malaiala
Release with Perspective Liberar com Perspectiva
Relink Revinculcar
Relink to Folder… Revinculcar Pasta…
Each Link Separately Cada Vínculo Separadamente
Selected Instances Instâncias Selecionadas
All Instances Todas as Instâncias
Missing Links from This Folder Vínculos Ausentes Desta Pasta
@msg scope {v} is not each|selected|all|missingInFolder scope {v} não é each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each e paths andam juntos: paths lista entradas com ids e path, no lugar de ids, path e folder
@msg paths entry {i} needs path and ids (object ids no other entry has) a entrada {i} de paths precisa de path e ids (ids de objeto que nenhuma outra entrada tenha)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder recebe path: os outros vínculos ausentes são procurados na pasta desse arquivo
Relink… Revinculcar…
Reload Plug-ins Recarregar Plug-ins
Remove Remover
Can't render this file because it contains an unexpected character in line 6 and column 47.
+8
View File
@@ -2736,6 +2736,14 @@ digits Malayalam Малаялам
Relink Восстановить связь
Relink Linked Files to the Document Восстановить связи связанных файлов с документом
Relink to Folder… Восстановить связь с папкой…
Each Link Separately Каждую связь отдельно
Selected Instances Выбранные экземпляры
All Instances Все экземпляры
Missing Links from This Folder Отсутствующие связи из этой папки
@msg scope {v} is not each|selected|all|missingInFolder scope {v} не является одним из each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each и paths задаются вместе: paths перечисляет записи с ids и path вместо ids, path и folder
@msg paths entry {i} needs path and ids (object ids no other entry has) записи {i} в paths нужны path и ids (идентификаторы объектов, которых нет в других записях)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder принимает path: в папке этого файла ищутся остальные отсутствующие связи
Relink… Восстановить связь…
Find Linked Files Найти связанные файлы
Find Linked Files in Folder… Найти связанные файлы в папке…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -2644,6 +2644,14 @@ digits Malayalam Malayalam
Relink Länka om
Relink Linked Files to the Document Länka om länkade filer till dokumentet
Relink to Folder… Länka om till mapp…
Each Link Separately Varje länk separat
Selected Instances Markerade instanser
All Instances Alla instanser
Missing Links from This Folder Saknade länkar från den här mappen
@msg scope {v} is not each|selected|all|missingInFolder scope {v} är inte each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each och paths hör ihop: paths listar poster med ids och path i stället för ids, path och folder
@msg paths entry {i} needs path and ids (object ids no other entry has) post {i} i paths behöver path och ids (objekt-id:n som ingen annan post har)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder tar path: de andra saknade länkarna söks i den filens mapp
Relink… Länka om…
Find Linked Files Sök länkade filer
Find Linked Files in Folder… Sök länkade filer i mapp…
Can't render this file because it contains an unexpected character in line 5 and column 25.
+8
View File
@@ -2514,6 +2514,14 @@ digits Malayalam Malayalam
Relink Yeniden bağla
Relink Linked Files to the Document Bağlantılı Dosyaları Belgeye Yeniden Bağlama
Relink to Folder… Klasöre Yeniden Bağlan…
Each Link Separately Her Bağlantıyı Ayrı Ayrı
Selected Instances Seçili Örnekler
All Instances Tüm Örnekler
Missing Links from This Folder Bu Klasördeki Eksik Bağlantılar
@msg scope {v} is not each|selected|all|missingInFolder scope {v}, each|selected|all|missingInFolder değerlerinden biri değil
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each ve paths birlikte kullanılır: paths, ids, path ve folder yerine ids ve path içeren girdileri listeler
@msg paths entry {i} needs path and ids (object ids no other entry has) paths girdisi {i} için path ve ids gerekir (başka hiçbir girdide olmayan nesne kimlikleri)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder path alır: diğer eksik bağlantılar o dosyanın klasöründe aranır
Relink… Yeniden bağla…
Find Linked Files Bağlantılı Dosyaları Bul
Find Linked Files in Folder… Klasördeki Bağlantılı Dosyaları Bul…
Can't render this file because it contains an unexpected character in line 5 and column 25.
+8
View File
@@ -2733,6 +2733,14 @@ digits Malayalam Малаялам
Relink Відновити зв’язок
Relink Linked Files to the Document Відновити зв’язані файли з документом
Relink to Folder… Відновити зв’язок із папкою…
Each Link Separately Кожен зв’язок окремо
Selected Instances Вибрані екземпляри
All Instances Усі екземпляри
Missing Links from This Folder Відсутні зв’язки з цієї папки
@msg scope {v} is not each|selected|all|missingInFolder scope {v} не є одним із each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each і paths задаються разом: paths перелічує записи з ids і path замість ids, path і folder
@msg paths entry {i} needs path and ids (object ids no other entry has) запису {i} у paths потрібні path і ids (ідентифікатори об’єктів, яких немає в інших записах)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder приймає path: у папці цього файлу шукаються інші відсутні зв’язки
Relink… Відновити зв’язок…
Find Linked Files Знайти пов’язані файли
Find Linked Files in Folder… Знайти пов’язані файли в папці…
Can't render this file because it contains an unexpected character in line 182 and column 5.
+8
View File
@@ -3007,6 +3007,14 @@ mark-style Japanese Kiểu Nhật
Relink Liên kết lại
Relink Linked Files to the Document Liên kết lại các tệp liên kết với tài liệu
Relink to Folder… Liên kết lại vào thư mục…
Each Link Separately Từng liên kết riêng
Selected Instances Các phiên bản đã chọn
All Instances Tất cả phiên bản
Missing Links from This Folder Liên kết thiếu từ thư mục này
@msg scope {v} is not each|selected|all|missingInFolder scope {v} không phải each|selected|all|missingInFolder
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each và paths đi cùng nhau: paths liệt kê các mục có ids và path, thay cho ids, path và folder
@msg paths entry {i} needs path and ids (object ids no other entry has) mục {i} của paths cần path và ids (ID đối tượng mà không mục nào khác có)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder nhận path: các liên kết thiếu khác được tìm trong thư mục của tệp đó
Relink… Liên kết lại…
Reload Plug-ins Tải lại plug-in
Remove Gỡ
Can't render this file because it contains an unexpected character in line 204 and column 5.
+8
View File
@@ -2612,6 +2612,14 @@ digits Malayalam 马拉雅拉姆数字
Relink 重新链接
Relink Linked Files to the Document 将链接文件重新链接到文档
Relink to Folder… 重新链接到文件夹…
Each Link Separately 逐个链接
Selected Instances 所选实例
All Instances 所有实例
Missing Links from This Folder 此文件夹中缺失的链接
@msg scope {v} is not each|selected|all|missingInFolder scope {v} 不是 each|selected|all|missingInFolder 之一
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each 与 paths 需一起使用:paths 列出包含 ids 和 path 的条目,用来代替 ids、path 和 folder
@msg paths entry {i} needs path and ids (object ids no other entry has) paths 条目 {i} 需要 path 和 ids(其他条目没有的对象 ID)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder 需要 path:将在该文件所在的文件夹中查找其他缺失的链接
Relink… 重新链接…
Find Linked Files 查找链接文件
Find Linked Files in Folder… 在文件夹中查找链接文件…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -3606,6 +3606,14 @@ digits Malayalam 馬來亞拉姆數字
Relink 重新連結
Relink Linked Files to the Document 將連結檔案重新連結至文件
Relink to Folder… 重新連結至資料夾…
Each Link Separately 逐一連結
Selected Instances 所選實例
All Instances 所有實例
Missing Links from This Folder 此資料夾中遺失的連結
@msg scope {v} is not each|selected|all|missingInFolder scope {v} 不是 each|selected|all|missingInFolder 其中之一
@msg scope each and paths go together: paths lists entries of ids and path, in place of ids, path and folder scope each 與 paths 須一起使用:paths 列出含 ids 與 path 的項目,用來取代 ids、path 與 folder
@msg paths entry {i} needs path and ids (object ids no other entry has) paths 項目 {i} 需要 path 與 ids(其他項目沒有的物件 ID)
@msg scope missingInFolder takes path: the folder of that file is searched for the other missing links scope missingInFolder 需要 path:會在該檔案所在的資料夾中尋找其他遺失的連結
Relink… 重新連結…
Find Linked Files 尋找連結檔案
Find Linked Files in Folder… 在資料夾中尋找連結檔案…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -851,6 +851,7 @@ fn run_ui_part11(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result
"links.editOriginal" => crate::panels::links::open_file(app, p, false),
"links.reveal" => crate::panels::links::open_file(app, p, true),
"ui.placementOptionsDialog" => crate::dialogs::placement_options::open(app, p),
"ui.relinkDialog" => crate::dialogs::relink::open(app, p),
"ui.findLinksInFolder" => {
crate::dialogs::find_links::find_in_folder_command(app, s("folder"), p.get("ids").and_then(Value::as_array).cloned())
}
+1
View File
@@ -281,6 +281,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
"ui.dxfOptionsDialog" => app.session.active().is_some(),
"links.editOriginal" | "links.reveal" => selected_image(app, |im| im.link.is_some()) || selected_placed(app),
"ui.placementOptionsDialog" => selected_image(app, |_| true) || selected_placed(app),
"ui.relinkDialog" => app.services.pick_open.is_some() && (selected_image(app, |_| true) || selected_placed(app)),
"ui.findLinksInFolder" => crate::picks::can(app, &crate::picks::PickRequest::Folder) && app.session.active().is_some(),
"ui.packageDialog" | "docInfo.save" => app.session.active().is_some(),
"ui.batchDialog" => !app.ui.action_sets.is_empty() && app.ui.action_sets.iter().any(|s| !s.actions.is_empty()),
+6
View File
@@ -1259,6 +1259,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{ids?} open Placement Options for images `ids` (default: the selected ones; dialog `placementOptions`: ids, preserve, align, clip, see links.placementOptions); OK runs links.placementOptions",
),
(
"ui.relinkDialog",
"Relink…",
"",
"{ids?: [image or placed document ids]} open Relink for `ids` (default: the selected ones; dialog `relink`: ids, links ([{name, ids}], one row per linked file), scope (Replace: each|selected (default)|all|missingInFolder, see links.relink), path (picked when empty), paths (with each: the files chosen so far, one per row of links)); OK runs links.relink with them, one undo step, except with each: OK takes path, or picks a file, for the next row of links, and the last row's OK relinks them all. An error when nothing is given or selected",
),
(
"ui.findLinksInFolder",
"Find Linked Files in Folder…",
+16 -19
View File
@@ -2,8 +2,8 @@
//! and embedded images, as rows with a thumbnail, name, page and status. Clicking a row selects
//! its image, Ctrl/Cmd-click adds or removes it, and Shift-click selects the images of the rows
//! from the last row clicked without Shift to it (with Ctrl/Cmd, adds them); the buttons Relink,
//! Go To, Update and Edit Original act on the selected rows; Link Info shows the details of the
//! first. The flyout adds Relink to Folder, Find in Folder (Find Linked Files:
//! Go To, Update and Edit Original act on the selected rows (Relink through its dialog, with the
//! Replace choice: `ui.relinkDialog`); Link Info shows the details of the first. The flyout adds Relink to Folder, Find in Folder (Find Linked Files:
//! `ui.findLinksInFolder`), Embed / Unembed, Placement Options, Select Linked / Embedded / Missing /
//! Modified (`addon.select.byLinkStatus`, Shift adds), Show filters, Sort, thumbnail size
//! and Show in Folder.
@@ -128,16 +128,19 @@ pub(crate) fn updated_status(n: usize) -> String {
crate::i18n::msg_n(n as u64, "Updated {n} image", "Updated {n} images", &[])
}
/// Relink images `ids` to a picked file.
pub(crate) fn relink(app: &mut VectorcraftApp, ids: Vec<u64>) {
crate::picks::button(app, move |app| relink_picked(app, ids.clone()));
/// The status bar's line for `n` relinked images, naming the files `missed` that weren't found.
pub(crate) fn relinked_status_missed(n: usize, missed: &[&str]) -> String {
if missed.is_empty() {
relinked_status(n)
} else {
let (one, other) = ("Relinked {n} image; not in that folder: {names}", "Relinked {n} images; not in that folder: {names}");
crate::i18n::msg_n(n as u64, one, other, &[("names", &missed.join(", "))])
}
}
fn relink_picked(app: &mut VectorcraftApp, ids: Vec<u64>) {
let pick = crate::FilePick { filters: crate::FilePick::filters(vectorcraft_engine::cmd::fileio::place_filters()), ..Default::default() };
let Some(path) = crate::picks::open(app, &pick) else { return };
let r = app.run("links.relink", json!({ "ids": ids, "path": path }));
report(app, r, |v| relinked_status(v["relinked"].as_array().map_or(0, Vec::len)));
/// The ids of the selected image objects and placed documents (Relink's dialog).
pub(crate) fn selected_ids(app: &VectorcraftApp) -> Vec<u64> {
selected_images(app)
}
/// Replace images `ids` with a picked file (Control bar, Properties): it keeps its own aspect
@@ -163,13 +166,7 @@ fn relink_to_picked_folder(app: &mut VectorcraftApp, ids: Vec<u64>) {
let r = app.run("links.relink", json!({ "ids": ids, "folder": folder }));
report(app, r, |v| {
let missed: Vec<&str> = v["notFound"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
let n = v["relinked"].as_array().map_or(0, Vec::len);
if missed.is_empty() {
relinked_status(n)
} else {
let (one, other) = ("Relinked {n} image; not in that folder: {names}", "Relinked {n} images; not in that folder: {names}");
crate::i18n::msg_n(n as u64, one, other, &[("names", &missed.join(", "))])
}
relinked_status_missed(v["relinked"].as_array().map_or(0, Vec::len), &missed)
});
}
@@ -426,7 +423,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
go_to(app, id);
}
if widgets::icon_button_enabled(ui, "link", tl!("Relink…"), false, !sel.is_empty() && can_pick(app), 24.0).clicked() {
relink(app, ids(&sel, |_| true));
crate::menus::invoke(app, "ui.relinkDialog", json!({ "ids": ids(&sel, |_| true) }));
}
if widgets::icon_button_enabled(ui, "link-2-off", tl!("Break Link"), false, !linked.is_empty(), 24.0).clicked() {
break_link(app, linked.clone());
@@ -548,7 +545,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
let any = !sel.is_empty();
let missing_any = list["missing"].as_u64().unwrap_or(0) > 0;
if menu_item(ui, tl!("Relink…"), any && can_pick(app), false) {
relink(app, ids(&sel, |_| true));
crate::menus::invoke(app, "ui.relinkDialog", json!({ "ids": ids(&sel, |_| true) }));
}
if menu_item(ui, tl!("Relink to Folder…"), (any || missing_any) && app.services.pick_folder.is_some(), false) {
relink_to_folder(app, ids(&sel, |_| true));
+10 -1
View File
@@ -685,7 +685,16 @@ art too large at that scale) keeps the dialog open.
Placement Options: the Links panel's flyout (or `ui.placementOptionsDialog {ids?}` for agents) opens the
`placementOptions` dialog for the selected images (fields `ids`, `preserve`: `transforms`/`bounds`/`fileDimensions`/
`fit`/`fill`, `align`: `topLeft` … `bottomRight`, `clip`). `ui.dialog.confirm` runs `links.placementOptions` with them as
one undo step. The Links panel (`window.panel {panel: "links"}`) shows `links.list` and the first selected image's
one undo step.
Relink: the Links panel's Relink… button and flyout item (or `ui.relinkDialog {ids?}` for agents) open the `relink`
dialog for the selected images and placed documents (fields `ids`, `links`: `[{name, ids}]`, one row per linked file,
`scope`: `each`/`selected`/`all`/`missingInFolder`, default `selected`, `path` and `paths`). With nothing selected and
no `ids`, it fails with "select the images to relink, or pass ids". `ui.dialog.set {field: "path", value}` then `ui.dialog.confirm` runs
`links.relink {ids, path, scope}` as one undo step; without `path` a file is picked. With `scope: "each"`, each confirm
takes `path` (or picks a file) for the next row of `links`, adds it to `paths` and keeps the dialog open, returning
`{next}` (the next row's name); the last row's confirm runs `links.relink {scope: "each", paths}` for every row as one
undo step. The Links panel (`window.panel {panel: "links"}`) shows `links.list` and the first selected image's
`links.info`; `links.editOriginal` and `links.reveal` (Edit › Edit Original, Show in Folder) hand the linked file to
the system and return `{path}`. Edit Original opens only a regular file whose extension is a format `document.open`
reads, and answers a control request with an error unless the file is a placed document (which opens in a tab).
+8
View File
@@ -4380,6 +4380,14 @@ path isn't followed) when it isn't. The desktop app also checks the active docum
comes back to the front, following the same preference. `links.check` reports every link's `status` (`ok`, `modified`,
`missing`), `links.update {ids?}` reads modified files again and `links.relink {ids?, path | folder}` points images at
another file (or each at the file of its name in a folder); images keep their bounds, one undo step each.
`links.relink`'s `scope` picks which links take the new file (Relink's Replace choice): `selected` (the default) relinks `ids`; `all` also relinks every other object in the layers linked to the same file as
one of them; `missingInFolder` relinks `ids` to `path`, then every other missing link to the file of its name in
`path`'s folder, and lists the names not there in `notFound` (with `copy`, only missing images are looked for); `each` takes `paths: [{ids, path}]` in place of `ids`,
`path` and `folder` and relinks each link to its own file. An unknown scope, `paths` without `each`, `each` with `ids`,
`path` or `folder`, an entry without `path` or `ids`, an id in two entries and `missingInFolder` with `folder` are
errors, and nothing is relinked. Each call is one undo step. The Links panel's Relink… opens the `relink` dialog
(`ui.relinkDialog`) with a Replace menu of these four choices. A PNG, JPEG,
another file (or each at the file of its name in a folder); images keep their bounds, one undo step each.
`links.update`, and `document.open` when it reads a modified file, also read the file again for the images and placed
documents inside symbols, graph designs, patterns and opacity masks that show the same content (by default also for
those that only that art shows); they have no ids, so the results do not list them. Saving a document also reads it
+1
View File
@@ -1348,6 +1348,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- 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.
- On the desktop with Enable Background Export on, Export for Screens and the Asset Export panel's Export encode and write their files (into the folder, or one ZIP in it) from the document as it was at OK on the thread that writes background saves and exports, with "Exporting" in the status bar and the export progress dialog, which counts the files and whose Cancel stops before the next file. `document.exportForScreens` and `assets.export` over MCP and scripts reply 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.
- The Links panel's Relink… opens a Relink dialog whose Replace menu picks which links take the new file: Selected Instances (the selected images, as before), All Instances (every image linked to the same file), Missing Links from This Folder (then every other missing link whose file name is in the new file's folder) and Each Link Separately (a file for each selected link in turn). Each relinks as one undo step (`links.relink {scope}`, `ui.relinkDialog`). Relink to Folder… stays as it is.
- Linked files inside symbols, graph designs, patterns and opacity masks are saved with their paths relative to the document, as linked files in the layers are, so they are found after the document and its files move to another folder together; on open each of those links takes the path where its file was found. Update Links, and opening with Update Links: Automatically, read a modified file again for that art too, and saving a document refreshes its placed copies inside symbols and patterns of the other open documents. Evidence: `links_outside_the_layers_find_their_files_after_the_folder_moves`, `update_links_reads_files_again_inside_symbols_patterns_and_masks`, `saving_a_document_refreshes_its_placed_copies_inside_symbols_and_patterns`.
- 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.