mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 14:41:52 +00:00
Recovery: restoring documents keeps their linked-file lines and warnings
After Restore in the dialog Data Recovery shows at launch, the status bar wrote the restore count over the line about the restored document's linked files, and the warnings each copy restored with were never shown. The status bar now shows the count followed by the first of the restored documents' notes: each document's lines about its linked files, then its warnings, what reading the copy left out first, named with the document's title when more than one was restored. Hovering the message lists them all. The count is the number of documents restored. The questions about each restored document's missing or modified linked files now come one document at a time, with that document made the current one, so a later document's question no longer replaces an earlier one's. When a copy can't be restored, the status bar shows the error and its hover keeps the count and the restored documents' notes.
This commit is contained in:
@@ -719,7 +719,8 @@ pub fn confirm(app: &mut VectorcraftApp) -> DialogResult {
|
||||
/// After a dialog closes, and each frame: a Missing Fonts dialog that another dialog replaced waits
|
||||
/// for its turn again, the search a Missing Fonts or Find Linked Files dialog started stops once
|
||||
/// that dialog is gone, and with no dialog open the choice about a stopped plug-in that another
|
||||
/// dialog replaced opens again, else the next document's Missing Fonts dialog (one dialog at a
|
||||
/// dialog replaced opens again, else the questions about the next restored document's linked files
|
||||
/// ([`crate::recovery::ask_next`]), else the next document's Missing Fonts dialog (one dialog at a
|
||||
/// time).
|
||||
pub(crate) fn settle(app: &mut VectorcraftApp) {
|
||||
crate::placing::settle_swatch_paste(app);
|
||||
@@ -727,6 +728,7 @@ pub(crate) fn settle(app: &mut VectorcraftApp) {
|
||||
missing_fonts::stop_when_closed(app);
|
||||
find_links::stop_when_closed(app);
|
||||
safe_mode::reopen(app);
|
||||
crate::recovery::ask_next(app);
|
||||
missing_fonts::open_next(app);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,14 +54,7 @@ fn after_open_for(app: &mut VectorcraftApp, r: &Value, notes: Vec<String>, web:
|
||||
let rows = |k: &str| r[k].as_array().cloned().unwrap_or_default();
|
||||
let (missing, modified) = (rows("missingLinks"), ids(&rows("modifiedLinks")));
|
||||
let notice = if missing.is_empty() || web {
|
||||
let unreadable = (!missing.is_empty()).then(|| {
|
||||
crate::i18n::msg_n(
|
||||
missing.len() as u64,
|
||||
"{n} linked file can't be read here: its preview shows",
|
||||
"{n} linked files can't be read here: their previews show",
|
||||
&[],
|
||||
)
|
||||
});
|
||||
let unreadable = (!missing.is_empty()).then(|| unreadable_status(missing.len()));
|
||||
ask_update(app, modified).or(unreadable)
|
||||
} else {
|
||||
open(app, missing, modified, ¬es);
|
||||
@@ -71,6 +64,40 @@ fn after_open_for(app: &mut VectorcraftApp, r: &Value, notes: Vec<String>, web:
|
||||
super::settle(app);
|
||||
}
|
||||
|
||||
/// The status bar's line for `n` linked files the web can't read.
|
||||
fn unreadable_status(n: usize) -> String {
|
||||
crate::i18n::msg_n(
|
||||
n as u64,
|
||||
"{n} linked file can't be read here: its preview shows",
|
||||
"{n} linked files can't be read here: their previews show",
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
/// What the linked files of a document that opened (`document.open`'s result `r`) need, without
|
||||
/// asking yet → the status bar's lines about them, the missing files to ask about and the ids of
|
||||
/// the modified images to offer for update ([`ask_next`]). The web reads no linked files, so the
|
||||
/// missing ones get a line there; without Update Links: Ask When Modified the modified ones get a
|
||||
/// line instead of the offer.
|
||||
pub(crate) fn questions(app: &VectorcraftApp, r: &Value) -> (Vec<String>, Vec<Value>, Vec<Value>) {
|
||||
questions_for(app, r, cfg!(target_arch = "wasm32"))
|
||||
}
|
||||
|
||||
fn questions_for(app: &VectorcraftApp, r: &Value, web: bool) -> (Vec<String>, Vec<Value>, Vec<Value>) {
|
||||
let rows = |k: &str| r[k].as_array().cloned().unwrap_or_default();
|
||||
let (mut missing, mut modified) = (rows("missingLinks"), ids(&rows("modifiedLinks")));
|
||||
let mut lines = vec![];
|
||||
if web && !missing.is_empty() {
|
||||
lines.push(unreadable_status(missing.len()));
|
||||
missing.clear();
|
||||
}
|
||||
if !modified.is_empty() && app.session.prefs.update_links != "askWhenModified" {
|
||||
lines.push(changed_on_disk_status(modified.len()));
|
||||
modified.clear();
|
||||
}
|
||||
(lines, missing, modified)
|
||||
}
|
||||
|
||||
fn open(app: &mut VectorcraftApp, missing: Vec<Value>, modified: Vec<Value>, notes: &[String]) {
|
||||
let fields = json!({ "missing": missing, "modified": modified, "notes": notes, "applyToAll": false, "path": "" });
|
||||
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
||||
@@ -85,7 +112,7 @@ pub(super) fn notes(d: &Dialog) -> Vec<String> {
|
||||
/// Ask about the missing files `rest`, one at a time, then offer the modified ones `modified` for
|
||||
/// update. A line about the modified files is followed by the open's `notes`
|
||||
/// ([`crate::io::show_opened`]).
|
||||
pub(super) fn ask_next(app: &mut VectorcraftApp, rest: Vec<Value>, modified: Vec<Value>, notes: Vec<String>) {
|
||||
pub(crate) fn ask_next(app: &mut VectorcraftApp, rest: Vec<Value>, modified: Vec<Value>, notes: Vec<String>) {
|
||||
if rest.is_empty() {
|
||||
app.ui.dialog = None;
|
||||
if let Some(notice) = ask_update(app, modified) {
|
||||
@@ -309,6 +336,23 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// A restored document's linked files: the web gets a line about the missing ones, and
|
||||
/// without Update Links: Ask When Modified the modified ones get a line; the rest are asked
|
||||
/// about.
|
||||
#[test]
|
||||
fn the_questions_about_linked_files_wait_or_become_lines() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let r = json!({ "missingLinks": [{"name": "a.png", "path": "/x/a.png", "ids": [1]}], "modifiedLinks": [{"ids": [2]}, {"ids": [3]}] });
|
||||
app.session.prefs.update_links = "askWhenModified".into();
|
||||
assert_eq!(questions_for(&app, &r, false), (vec![], r["missingLinks"].as_array().unwrap().clone(), vec![json!(2), json!(3)]));
|
||||
let changed = "2 linked images changed on disk: Update Links shows the new versions".to_string();
|
||||
app.session.prefs.update_links = "manually".into();
|
||||
assert_eq!(questions_for(&app, &r, false), (vec![changed.clone()], r["missingLinks"].as_array().unwrap().clone(), vec![]));
|
||||
let unreadable = "1 linked file can't be read here: its preview shows".to_string();
|
||||
assert_eq!(questions_for(&app, &r, true), (vec![unreadable, changed], vec![], vec![]));
|
||||
assert!(app.ui.dialog.is_none(), "nothing is asked yet");
|
||||
}
|
||||
|
||||
/// The question about modified linked files uses the form the language's plural rule picks for
|
||||
/// the count.
|
||||
#[test]
|
||||
|
||||
@@ -26,13 +26,13 @@ pub(crate) use export::{
|
||||
export_files, exported_with_notes, file_url, open_in_app, open_original, reveal_path, run_to_file, save_command_output_named,
|
||||
};
|
||||
pub(crate) use open::report_installed;
|
||||
pub(crate) use open::{EXPORT_PRESETS, import_presets, open_with_on, opened, show_opened};
|
||||
pub(crate) use open::{EXPORT_PRESETS, import_presets, open_notes, open_with_on, opened, opened_after_notice, show_opened};
|
||||
pub use open::{
|
||||
MAX_RECENT_FILES, OpensAs, ask_revert, install_module, install_plugin, new_from_template, note_recent, open_bytes, open_dialog, open_document,
|
||||
open_path, open_path_with, open_reporting, open_with, opens_as, place_bytes, recent_files, report_open_error,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use open::{open_file, opened_after_notice, opened_with_notes};
|
||||
pub(crate) use open::{open_file, opened_with_notes};
|
||||
pub(crate) use save::save_current;
|
||||
pub use save::{ask_format_options, ask_pdf_options, remember_active_artboard, remember_view, save, save_pdf};
|
||||
pub(crate) use save::{export_path, is_web, notes_of, report_saved, show_with_notes, target_path, write_named, write_reporting, write_to};
|
||||
|
||||
@@ -162,7 +162,7 @@ pub(crate) fn show_opened(app: &mut VectorcraftApp, notice: Option<String>, note
|
||||
|
||||
/// The notes of the open `r`, those of what reading the file left out (its document's
|
||||
/// `import_losses`) first, each group in the order the import gave them.
|
||||
fn open_notes(app: &VectorcraftApp, r: &Value) -> Vec<String> {
|
||||
pub(crate) fn open_notes(app: &VectorcraftApp, r: &Value) -> Vec<String> {
|
||||
let notes = notes_of(r);
|
||||
let index = r["index"].as_u64().and_then(|i| usize::try_from(i).ok());
|
||||
let losses = index.and_then(|i| app.session.documents().get(i)).map(|st| st.import_losses.as_slice()).unwrap_or_default();
|
||||
|
||||
@@ -29,6 +29,17 @@ pub struct Timer {
|
||||
/// The recovery folders that failure messages named, latest last, at most [`NAMED`]: replies to
|
||||
/// automation clients hide those outside their read roots.
|
||||
pub named: Vec<String>,
|
||||
/// The restored documents whose linked files are still to be asked about, next first
|
||||
/// ([`ask_next`]).
|
||||
links: Vec<LinkQuestions>,
|
||||
}
|
||||
|
||||
/// The questions about the linked files of a restored document (by uid): the missing files, then
|
||||
/// the modified images (ids) to offer for update ([`crate::dialogs::missing_links::ask_next`]).
|
||||
struct LinkQuestions {
|
||||
uid: u64,
|
||||
missing: Vec<Value>,
|
||||
modified: Vec<Value>,
|
||||
}
|
||||
|
||||
/// How many recovery folders [`Timer::named`] keeps.
|
||||
@@ -187,28 +198,91 @@ fn named(app: &mut VectorcraftApp, folder: &str) {
|
||||
}
|
||||
|
||||
/// The startup dialog's OK (Restore) or Discard: every copy it lists.
|
||||
///
|
||||
/// After Restore the status bar shows how many documents were restored, followed by the first of
|
||||
/// the restored documents' notes: for each document in the dialog's order, the lines about its
|
||||
/// linked files, then what reading the copy left out, then its other warnings, each named with
|
||||
/// its document's title when more than one was restored. Hovering the message lists them all. The
|
||||
/// questions about the documents' linked files are asked one document at a time ([`ask_next`]).
|
||||
/// When a copy can't be restored, the status bar shows the error, and its hover lists the count
|
||||
/// and the notes of the documents that were restored.
|
||||
pub fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
app.ui.dialog = None;
|
||||
let id = if d.bool("discard") { "file.recovery.discard" } else { "file.recovery.restore" };
|
||||
let discard = d.bool("discard");
|
||||
let id = if discard { "file.recovery.discard" } else { "file.recovery.restore" };
|
||||
let files: Vec<String> =
|
||||
d.fields.get("copies").and_then(Value::as_array).into_iter().flatten().filter_map(|c| c["file"].as_str()).map(str::to_string).collect();
|
||||
let (mut done, mut errors) = (vec![], vec![]);
|
||||
let (mut done, mut errors, mut opened) = (vec![], vec![], vec![]);
|
||||
for file in files {
|
||||
match app.run(id, json!({ "file": file })) {
|
||||
Ok(r) => {
|
||||
if let Some(restored) = r["restored"].as_array().and_then(|v| v.first()) {
|
||||
crate::dialogs::missing_links::after_open(app, restored, Vec::new());
|
||||
for restored in r["restored"].as_array().into_iter().flatten() {
|
||||
opened.push(restored_notes(app, restored));
|
||||
}
|
||||
let failed = r["failed"].as_array().into_iter().flatten();
|
||||
errors.extend(failed.filter_map(|f| f["error"].as_str()).map(str::to_string));
|
||||
done.push(r);
|
||||
}
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
return Err(errors.join("; "));
|
||||
if discard {
|
||||
if !errors.is_empty() {
|
||||
return Err(errors.join("; "));
|
||||
}
|
||||
app.status(recovered_status(true, done.len()));
|
||||
return Ok(json!(done));
|
||||
}
|
||||
let summary = recovered_status(false, opened.len());
|
||||
let titled = opened.len() > 1;
|
||||
let restored = !opened.is_empty();
|
||||
let notes: Vec<String> = opened
|
||||
.into_iter()
|
||||
.flat_map(|(title, notes)| {
|
||||
notes.into_iter().map(move |n| if titled { crate::i18n::fill_once("{path}: {e}", &[("path", &title), ("e", &n)]) } else { n })
|
||||
})
|
||||
.collect();
|
||||
let result = if errors.is_empty() {
|
||||
let status = match notes.first() {
|
||||
Some(first) => crate::io::opened_after_notice(&summary, notes.len(), first),
|
||||
None => summary,
|
||||
};
|
||||
crate::io::show_with_notes(app, status, notes);
|
||||
Ok(json!(done))
|
||||
} else {
|
||||
let e = errors.join("; ");
|
||||
if restored {
|
||||
crate::io::show_with_notes(app, e.clone(), std::iter::once(summary).chain(notes).collect());
|
||||
}
|
||||
Err(e)
|
||||
};
|
||||
crate::dialogs::settle(app);
|
||||
result
|
||||
}
|
||||
|
||||
/// A document Restore just opened (`file.recovery.restore`'s entry `r`, the current document) →
|
||||
/// its title and notes ([`confirm`]). Its Missing Fonts dialog and the questions about its linked
|
||||
/// files wait for their turn.
|
||||
fn restored_notes(app: &mut VectorcraftApp, r: &Value) -> (String, Vec<String>) {
|
||||
crate::dialogs::missing_fonts::after_open(app, r);
|
||||
let (mut notes, missing, modified) = crate::dialogs::missing_links::questions(app, r);
|
||||
let uid = r["index"].as_u64().and_then(|i| usize::try_from(i).ok()).and_then(|i| app.session.documents().get(i)).map(|st| st.uid);
|
||||
if let Some(uid) = uid.filter(|_| !missing.is_empty() || !modified.is_empty()) {
|
||||
app.recovery.links.push(LinkQuestions { uid, missing, modified });
|
||||
}
|
||||
notes.extend(crate::io::open_notes(app, r));
|
||||
(r["title"].as_str().unwrap_or_default().to_string(), notes)
|
||||
}
|
||||
|
||||
/// With no dialog open: the questions about the linked files of the next restored document that
|
||||
/// is still open, with that document made the current one (the answers change it).
|
||||
pub(crate) fn ask_next(app: &mut VectorcraftApp) {
|
||||
while app.ui.dialog.is_none() && !app.recovery.links.is_empty() {
|
||||
let q = app.recovery.links.remove(0);
|
||||
let Some(i) = app.session.documents().iter().position(|st| st.uid == q.uid) else { continue };
|
||||
crate::chrome::show_document(app, i);
|
||||
crate::dialogs::missing_links::ask_next(app, q.missing, q.modified, Vec::new());
|
||||
}
|
||||
app.status(recovered_status(id == "file.recovery.discard", done.len()));
|
||||
Ok(json!(done))
|
||||
}
|
||||
|
||||
/// The status bar's line for `n` recovered documents discarded or restored.
|
||||
|
||||
@@ -115,6 +115,118 @@ fn launching_after_a_crash_offers_the_copies() {
|
||||
assert!(next.ui.dialog.is_none());
|
||||
}
|
||||
|
||||
/// A 40×30 PNG of one color.
|
||||
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
|
||||
}
|
||||
|
||||
/// The copies a crash left of `n` documents, each with an image linked to a file in `dir`. The
|
||||
/// files are deleted and the images' bytes are gone from the store, so each copy restores with a
|
||||
/// warning about its image and a missing linked file. → the store.
|
||||
fn crashed_with_lost_images(dir: &std::path::Path, n: u8) -> Arc<MemoryStore> {
|
||||
let store = Arc::new(MemoryStore::default());
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services { recovery_store: Some(store.clone()), ..Default::default() });
|
||||
app.services.read = Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string())));
|
||||
for k in 0..n {
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
let p = dir.join(format!("{k}.png"));
|
||||
std::fs::write(&p, png([200, k * 100, 0])).unwrap();
|
||||
app.run("file.place", json!({"path": p.to_string_lossy()})).unwrap();
|
||||
}
|
||||
app.run("file.recovery.save", json!({})).unwrap();
|
||||
drop(app);
|
||||
for k in 0..n {
|
||||
std::fs::remove_file(dir.join(format!("{k}.png"))).unwrap();
|
||||
}
|
||||
for name in store.list().unwrap().into_iter().filter(|n| n.ends_with(".blob")) {
|
||||
store.remove(&name).unwrap();
|
||||
}
|
||||
store
|
||||
}
|
||||
|
||||
/// The app launched after the crash, with the Recover Documents dialog open → the app and the
|
||||
/// titles the restored documents get, in the dialog's order.
|
||||
fn launched(store: &Arc<MemoryStore>) -> (VectorcraftApp, Vec<String>) {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services { recovery_store: Some(store.clone()), ..Default::default() });
|
||||
recovery::frame(&mut app, 0.0);
|
||||
let d = app.ui.dialog.as_ref().expect("the Recover Documents dialog");
|
||||
let titles = d.fields["copies"].as_array().unwrap().iter().map(|c| format!("{} [Recovered]", c["title"].as_str().unwrap())).collect();
|
||||
(app, titles)
|
||||
}
|
||||
|
||||
/// Is `note` the warning about a restored document's image that couldn't be read?
|
||||
fn lost_image(note: &str) -> bool {
|
||||
note.starts_with("the image “") && note.contains("” of the recovered document couldn't be read")
|
||||
}
|
||||
|
||||
/// Restore keeps the warnings each restored document opened with: the status bar names the first
|
||||
/// after the count, and its hover lists them all, each named with its document. The questions
|
||||
/// about the missing linked files come one document at a time, with that document current.
|
||||
#[test]
|
||||
fn restore_keeps_each_documents_notes_and_asks_about_its_links_in_turn() {
|
||||
let dir = scratch("restore-notes");
|
||||
let store = crashed_with_lost_images(&dir, 2);
|
||||
let (mut app, titles) = launched(&store);
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
let (status, notes) = app.ui.status_notes.clone().expect("the notes are kept");
|
||||
assert_eq!(status, app.ui.status);
|
||||
assert_eq!(notes.len(), 2, "{notes:?}");
|
||||
for (note, title) in notes.iter().zip(&titles) {
|
||||
let lost = note.strip_prefix(&format!("{title}: ")).unwrap_or_default();
|
||||
assert!(lost_image(lost), "{note}");
|
||||
}
|
||||
assert_eq!(status, format!("Restored 2 recovered documents: save them to keep them. Opened with 2 notes: {}", notes[0]));
|
||||
// The first document's missing file is asked about first, and the second's waits.
|
||||
for title in &titles {
|
||||
let d = app.ui.dialog.clone().expect("the missing file's question");
|
||||
assert_eq!(d.kind, dialogs::missing_links::KIND);
|
||||
assert_eq!(&app.session.doc().unwrap().title(), title);
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("discard".into(), json!(true));
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
}
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(app.ui.status, status, "answering the questions keeps the line");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// With one document restored, its notes are not named with its title.
|
||||
#[test]
|
||||
fn restoring_one_document_shows_its_notes_after_the_count() {
|
||||
let dir = scratch("restore-one");
|
||||
let store = crashed_with_lost_images(&dir, 1);
|
||||
let (mut app, _) = launched(&store);
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
let (status, notes) = app.ui.status_notes.clone().expect("the notes are kept");
|
||||
assert!(notes.len() == 1 && lost_image(¬es[0]), "{notes:?}");
|
||||
assert_eq!(status, format!("Restored 1 recovered document: save it to keep it. Opened with 1 note: {}", notes[0]));
|
||||
assert_eq!(app.ui.status, status);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A copy that can't be restored after another was: the status bar shows the error, and its hover
|
||||
/// still lists the count and the notes of the document that was restored.
|
||||
#[test]
|
||||
fn a_failed_restore_keeps_the_notes_of_the_documents_restored_before_it() {
|
||||
let dir = scratch("restore-partial");
|
||||
let store = crashed_with_lost_images(&dir, 2);
|
||||
let (mut app, _) = launched(&store);
|
||||
let second = app.ui.dialog.as_ref().unwrap().fields["copies"][1]["file"].as_str().unwrap().to_string();
|
||||
store.write(&format!("{second}.vectorcraft"), b"not a document").unwrap();
|
||||
let e = dialogs::confirm(&mut app).unwrap_err();
|
||||
assert_eq!(app.session.documents().len(), 1);
|
||||
assert_eq!(app.ui.status, e);
|
||||
let (status, notes) = app.ui.status_notes.clone().expect("the notes are kept");
|
||||
assert_eq!(status, e);
|
||||
assert_eq!(notes.len(), 2, "{notes:?}");
|
||||
assert_eq!(notes[0], "Restored 1 recovered document: save it to keep it");
|
||||
assert!(lost_image(¬es[1]), "{}", notes[1]);
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(dialogs::missing_links::KIND), "its missing file is asked about");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discard_at_launch_deletes_the_copies_and_cancel_keeps_them() {
|
||||
let store = Arc::new(MemoryStore::default());
|
||||
|
||||
@@ -813,7 +813,12 @@ instance keeps, so instances started side by side for screenshots don't get it).
|
||||
saved}]`, from `file.recovery.list`) and `discard`. `ui.dialog.confirm` restores them all (`file.recovery.restore`
|
||||
each, opening "<name> [Recovered]" documents), `discard: true` then confirm deletes them, `ui.dialog.cancel` keeps them
|
||||
for the next launch. While copies are written in the background `ui.inspect` lists them under `background`
|
||||
("Saving recovery data for <name>").
|
||||
("Saving recovery data for <name>"). After a restore the status bar shows the count followed by the first of the
|
||||
restored documents' notes: each document's lines about its linked files, then its warnings (what reading the copy left
|
||||
out first), named with the document's title when more than one was restored. Hovering the message lists them all. The
|
||||
questions about each document's missing or modified linked files open one document at a time, with that document made
|
||||
the current one. When a copy can't be restored, confirm returns the error and the status bar shows it; its hover lists
|
||||
the count and the notes of the documents that were restored.
|
||||
|
||||
Native save options: File → Save As to a native or `.ai` file opens the `saveOptions` dialog after the save panel
|
||||
(heading "VectorCraft Options" or "PDF-compatible .ai Options"). Fields: `separateArtboards`, `all` (true: every
|
||||
|
||||
@@ -1607,6 +1607,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Preferences › File Handling › Data Recovery › Folder has a Choose… button on the desktop that picks the folder Data Recovery keeps its copies in ([Illustrator reference, Set Data Recovery preferences](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf)); a picked folder that Data Recovery cannot create its folders in or list is refused with a status message, and the field keeps its value. OK applies the same check to a folder typed into the field (VectorCraft's own rule): when it fails, the dialog stays open, the status bar says why and no preference changes; an empty field (the default folder) and the folder already saved are not checked. Preferences › Plug-ins & Scratch Disks is where additional plug-ins folders are set ([Illustrator preferences help](https://helpx.adobe.com/illustrator/desktop/get-started/preferences-and-settings/set-app-preferences.html)); its Additional Plug-ins Folder has a Choose… button on the desktop as well, VectorCraft's own, matching Data Recovery's, and VectorCraft's own rule refuses a picked or typed folder whose contents can't be listed. Additional Fonts Folder and Templates Folder stay typed only.
|
||||
`prefs.set` applies the dialog's check to `recoveryFolder` and `pluginsFolder` for every caller (VectorCraft's own rule): a folder that fails returns an error and no preference changes; an empty value and the folder already saved are not checked.
|
||||
When a Data Recovery copy can't be written, the status bar and the log show the error (with the document's title when one document's copy fails), the folder copies go to, whether that is the default recovery folder or the folder set in Preferences, and the preference that picks another folder (`recoveryFolder`; VectorCraft's own message).
|
||||
After Restore in the dialog Data Recovery shows at launch, the status bar keeps the restored documents' notes (VectorCraft's own rule): it shows the count followed by the first note, and hovering it lists each document's lines about its linked files and its warnings, named with the document's title when more than one was restored. The questions about the documents' missing or modified linked files come one document at a time, with that document made the current one, and a copy that can't be restored shows its error while the hover keeps the count and the restored documents' notes (`tests/recovery.rs`).
|
||||
A name typed in the desktop save panel without the file type's extension gets it, and when a file of that name exists, which the panel did not ask about, Save, Save As, Save a Copy, Save as Template, the exports and every other write through a save panel ask before replacing it (dialog `replaceFile`; Cancel writes nothing).
|
||||
A name returned by the desktop save panel with the extension of a format the save does not write has it replaced with the saved format's: Save As of a new document saves `card.ait` and `card.png` as `card.vectorcraft`, and Save as Template saves `card.pdf` as `card.vctemplate`. An extension that names no format stays part of the name (`notes.v2` is saved as `notes.v2.vectorcraft`).
|
||||
Export As, Save as PDF, Save for Office Documents, Save for Web, Text Export Options, print to file, the presets' Export… and the library, swatch and Document Info saves replace a typed extension of another format than the one they write in the same way: Export As PNG writes `card.svg` as `card.png`, Save as PDF writes `card.png` as `card.pdf`, and `document.exportSelection` with a `format` suggests and adds that format's extension. An extension of the written format stays as typed in any letter case (`photo.JPEG` for JPEG), Export As SVG writes `art.svgz` compressed, and `document.exportSelection` without a `format` writes the format the typed extension names (`tests/saveext.rs`).
|
||||
|
||||
Reference in New Issue
Block a user