diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index c086d95..55e2524 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -1664,6 +1664,7 @@ impl NoteCraftApp { return; } } + self.tell_kept_ink(); if let Some(ed) = self.editor.as_mut() { ed.blocks.retain(|b| !matches!(b, Block::Image { src, .. } if crate::inkpad::is_old_sheet(src))); ed.rebuild_body(); @@ -3856,6 +3857,7 @@ impl NoteCraftApp { Ok(()) => self.say(crate::i18n::format_message_id("ui.took_the_last_stroke_back_count_left", &[("count", &left.to_string())])), Err(e) => self.complain(&crate::i18n::format_message_id("ui.cannot_write_the_drawing_error", &[("error", &e.to_string())])), } + self.tell_kept_ink(); // The page stops naming ink it no longer has. self.sync_ink_reference(); } @@ -4190,6 +4192,15 @@ impl NoteCraftApp { self.say(crate::tl_id!("ui.write_anywhere_on_the_page")); } + /// Says where the page's ink file was kept, when writing the ink kept it first because it + /// held more than could be read back as strokes (issue #157). + fn tell_kept_ink(&mut self) { + let Some(kept) = self.pad.as_mut().and_then(|pad| pad.kept_original.take()) else { return }; + let shown = self.shown(&kept); + log::warn!("the page's ink file held more than its strokes; the original was kept: {kept:?}"); + self.say(crate::i18n::format_message_id("ui.kept_the_original_drawing_file_path", &[("path", &shown)])); + } + /// Writes the drawing again with no pen readings in it, for sending somewhere else. fn ink_export_plain(&mut self) { let Some(pad) = self.pad.as_ref() else { @@ -6026,6 +6037,7 @@ impl NoteCraftApp { Ok(()) => self.say(crate::i18n::format_message_id("ui.drawing_saved_count_strokes", &[("count", &strokes.to_string())])), Err(e) => self.complain(&crate::i18n::format_message_id("ui.cannot_write_the_drawing_error", &[("error", &e.to_string())])), } + self.tell_kept_ink(); self.sync_ink_reference(); } if let Some(href) = follow { diff --git a/crates/ui-egui/src/inkpad.rs b/crates/ui-egui/src/inkpad.rs index b91f81f..77818a1 100644 --- a/crates/ui-egui/src/inkpad.rs +++ b/crates/ui-egui/src/inkpad.rs @@ -84,6 +84,11 @@ pub struct Pad { pub dirty: bool, /// How many strokes in the file were a picture only, with no readings to give back. pub flat: usize, + /// The file holds more than `drawing` does: it could not be read, or some of its lines are + /// a picture only. Before the file is first written over, it is kept beside the ink. + pub incomplete: bool, + /// Where that file was kept, once it has been, for the window to say so. + pub kept_original: Option, /// The stroke being drawn now, if the pen is down. drawing_now: Option, /// The strokes being held, by their place in the drawing. Never written to the file: @@ -128,13 +133,21 @@ impl Pad { /// drawn on, so a page nobody has drawn on stays as clean as it was. pub fn of_page(page_dir: &Path) -> Pad { let file = page_dir.join(FILE); - let (drawing, flat) = - std::fs::read_to_string(&file).ok().and_then(|text| svg::read(&text).ok()).unwrap_or_else(|| (Drawing::new(SHEET_W, SHEET_H), 0)); + let read = std::fs::read_to_string(&file).ok().and_then(|text| svg::read(&text).ok()); + // A file that is there but could not be read, or that holds lines with no readings, + // would lose them the first time the ink is written (issue #157). + let incomplete = match &read { + Some((_, flat)) => *flat > 0, + None => file.exists(), + }; + let (drawing, flat) = read.unwrap_or_else(|| (Drawing::new(SHEET_W, SHEET_H), 0)); Pad { file, drawing, dirty: false, flat, + incomplete, + kept_original: None, drawing_now: None, held: Vec::new(), loop_now: Vec::new(), @@ -154,6 +167,13 @@ impl Pad { if let Some(parent) = self.file.parent() { std::fs::create_dir_all(parent)?; } + // The file holds what this drawing does not: it is kept, as it was, before it is + // written over or taken away, and only then (issue #157). + if self.incomplete && self.file.is_file() { + let kept = self.keep_original()?; + self.kept_original = Some(kept); + } + self.incomplete = false; // A page drawn on and then rubbed clean loses its file rather than keeping an // empty one, so a page with no ink has no ink in its folder either. if self.drawing.is_empty() { @@ -169,6 +189,26 @@ impl Pad { Ok(()) } + /// Copies the ink file as it is to `ink.svg.bak`, or the first free `ink-N.svg.bak`, beside + /// it. Not an `.svg`, so nothing takes the copy for the page's ink or for a picture of it. + fn keep_original(&self) -> std::io::Result { + let folder = self.file.parent().map(Path::to_path_buf).unwrap_or_default(); + for n in 1usize..=10_000 { + let name = if n == 1 { format!("{FILE}.bak") } else { format!("ink-{n}.svg.bak") }; + let kept = folder.join(name); + match std::fs::OpenOptions::new().write(true).create_new(true).open(&kept) { + Ok(mut out) => { + let mut original = std::fs::File::open(&self.file)?; + std::io::copy(&mut original, &mut out)?; + return Ok(kept); + } + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(e) => return Err(e), + } + } + Err(std::io::Error::new(std::io::ErrorKind::AlreadyExists, "every name for keeping the original ink file is taken")) + } + /// Writes it again as a plain picture, beside the ink, for sending somewhere else. /// Returns where it was written. pub fn save_plain(&self) -> std::io::Result { @@ -836,6 +876,47 @@ mod tests { assert!(pad.file.ends_with(FILE), "the ink sits beside the page: {}", pad.file.display()); } + /// Issue #157: an ink file that holds lines with no readings, or that cannot be read at all, + /// is kept as it was before new ink is written over it, and only once. + #[test] + fn ink_that_cannot_all_be_read_is_kept_before_it_is_written_over() { + let original = ""; + for (label, text) in [("a picture's path", original), ("not an SVG at all", "this is not a drawing")] { + let t = TempDir::new("ink-keep-original"); + let page = t.path().to_path_buf(); + std::fs::write(page.join(FILE), text).expect("an ink file of someone else's"); + let mut pad = Pad::of_page(&page); + assert!(pad.incomplete, "{label}: the file holds more than the strokes read from it"); + + assert!(pad.drawing.add(stroke(10.0))); + pad.dirty = true; + pad.save().expect("saved"); + let kept = pad.kept_original.take().expect("the original was kept"); + assert_eq!(kept, page.join("ink.svg.bak"), "{label}"); + assert_eq!(std::fs::read_to_string(&kept).expect("kept"), text, "{label}: kept as it was"); + assert!(!crate::inkpad::is_old_sheet("ink.svg.bak")); + + // Saving again does not keep another copy: the file is NoteCraft's own now. + assert!(pad.drawing.add(stroke(20.0))); + pad.save().expect("saved again"); + assert_eq!(pad.kept_original, None, "{label}"); + assert!(!page.join("ink-2.svg.bak").exists(), "{label}"); + } + + // A file of NoteCraft's own, read whole, is written over as before, with nothing kept. + let t = TempDir::new("ink-keep-own"); + let page = t.path().to_path_buf(); + let mut pad = Pad::of_page(&page); + assert!(pad.drawing.add(stroke(10.0))); + pad.save().expect("saved"); + let mut again = Pad::of_page(&page); + assert!(!again.incomplete); + assert!(again.drawing.add(stroke(30.0))); + again.save().expect("saved"); + assert_eq!(again.kept_original, None); + assert!(!page.join("ink.svg.bak").exists()); + } + #[test] fn ink_is_written_beside_the_page_and_comes_back_as_strokes() { let t = TempDir::new("ink-layer-save"); diff --git a/i18n/ar-XB.tsv b/i18n/ar-XB.tsv index b02b804..c2d0c7a 100644 --- a/i18n/ar-XB.tsv +++ b/i18n/ar-XB.tsv @@ -335,6 +335,7 @@ @id ui.json [[RLI]]$$$JSON@@@[[PDI]] @id ui.justification_goes_on_a_paragraph_of_text [[RLI]]$$$Justification goes on a paragraph of text@@@[[PDI]] @id ui.keep_this_window_above_the_others [[RLI]]$$$Keep this window above the others@@@[[PDI]] +@id ui.kept_the_original_drawing_file_path [[RLI]]$$$This page's drawing file held lines NoteCraft cannot edit. The file as it was is kept as {path}.@@@[[PDI]] @id ui.label [[RLI]]$$$label@@@[[PDI]] @id ui.label_not_built [[RLI]]$$${label} (not built)@@@[[PDI]] @id ui.label_not_built_yet [[RLI]]$$${label}: not built yet@@@[[PDI]] diff --git a/i18n/ar.tsv b/i18n/ar.tsv index 7be6cdd..7f8166b 100644 --- a/i18n/ar.tsv +++ b/i18n/ar.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text التبرير ينطبق على فقرة نصية @id ui.keep_this_window_above_the_others إبقاء هذه النافذة فوق النوافذ الأخرى +@id ui.kept_the_original_drawing_file_path احتوى ملف الرسم في هذه الصفحة على خطوط لا يستطيع NoteCraft تحريرها. تم الاحتفاظ بالملف الأصلي باسم {path}. @id ui.label تسمية @id ui.label_not_built {label} (غير مبنٍ) @id ui.label_not_built_yet {label}: لم يُبنَ بعد diff --git a/i18n/de.tsv b/i18n/de.tsv index 2b2e0b4..f24d5da 100644 --- a/i18n/de.tsv +++ b/i18n/de.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text Der Textausrichtung bezieht sich auf einen Absatz @id ui.keep_this_window_above_the_others Dieses Fenster immer im Vordergrund halten +@id ui.kept_the_original_drawing_file_path Die Zeichnungsdatei dieser Seite enthielt Linien, die NoteCraft nicht bearbeiten kann. Die ursprüngliche Datei ist als {path} aufbewahrt. @id ui.label Beschriftung @id ui.label_not_built {label} (nicht implementiert) @id ui.label_not_built_yet {label}: noch nicht implementiert diff --git a/i18n/en-XA.tsv b/i18n/en-XA.tsv index 0628976..109fb19 100644 --- a/i18n/en-XA.tsv +++ b/i18n/en-XA.tsv @@ -335,6 +335,7 @@ @id ui.json $$$JSON@@@ @id ui.justification_goes_on_a_paragraph_of_text $$$Justification goes on a paragraph of text@@@ @id ui.keep_this_window_above_the_others $$$Keep this window above the others@@@ +@id ui.kept_the_original_drawing_file_path $$$This page's drawing file held lines NoteCraft cannot edit. The file as it was is kept as {path}.@@@ @id ui.label $$$label@@@ @id ui.label_not_built $$${label} (not built)@@@ @id ui.label_not_built_yet $$${label}: not built yet@@@ diff --git a/i18n/en.tsv b/i18n/en.tsv index df315b2..d57ab07 100644 --- a/i18n/en.tsv +++ b/i18n/en.tsv @@ -335,6 +335,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text Justification goes on a paragraph of text @id ui.keep_this_window_above_the_others Keep this window above the others +@id ui.kept_the_original_drawing_file_path This page's drawing file held lines NoteCraft cannot edit. The file as it was is kept as {path}. @id ui.label label @id ui.label_not_built {label} (not built) @id ui.label_not_built_yet {label}: not built yet diff --git a/i18n/es.tsv b/i18n/es.tsv index 3bd6d76..fe183e3 100644 --- a/i18n/es.tsv +++ b/i18n/es.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text La justificación se aplica a un párrafo de texto @id ui.keep_this_window_above_the_others Mantener esta ventana por encima de las demás +@id ui.kept_the_original_drawing_file_path El archivo de dibujo de esta página tenía trazos que NoteCraft no puede editar. El archivo original se conserva como {path}. @id ui.label label @id ui.label_not_built {label} (no compilado) @id ui.label_not_built_yet {label}: aún no compilado diff --git a/i18n/fa.tsv b/i18n/fa.tsv index 70560db..65aa78f 100644 --- a/i18n/fa.tsv +++ b/i18n/fa.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text تراز متن در یک بند اعمال می‌شود @id ui.keep_this_window_above_the_others این پنجره را بالای سایر پنجره‌ها نگه دارید +@id ui.kept_the_original_drawing_file_path فایل طراحی این صفحه خط‌هایی داشت که NoteCraft نمی‌تواند ویرایش کند. فایل اصلی با نام {path} نگه داشته شد. @id ui.label برچسب @id ui.label_not_built {label} (ساخته نشده) @id ui.label_not_built_yet {label}: هنوز ساخته نشده diff --git a/i18n/fr.tsv b/i18n/fr.tsv index 7cf35a9..e2916a5 100644 --- a/i18n/fr.tsv +++ b/i18n/fr.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text La justification s'applique à un paragraphe de texte @id ui.keep_this_window_above_the_others Garder cette fenêtre au-dessus des autres +@id ui.kept_the_original_drawing_file_path Le fichier de dessin de cette page contenait des traits que NoteCraft ne peut pas modifier. Le fichier d’origine est conservé sous {path}. @id ui.label label @id ui.label_not_built {label} (non implémenté) @id ui.label_not_built_yet {label} : pas encore implémenté diff --git a/i18n/he.tsv b/i18n/he.tsv index b785dbe..cd26005 100644 --- a/i18n/he.tsv +++ b/i18n/he.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text יישור מתייחס לפסקת טקסט @id ui.keep_this_window_above_the_others החזק חלון זה מעל האחרים +@id ui.kept_the_original_drawing_file_path קובץ הציור של דף זה הכיל קווים ש-NoteCraft אינו יכול לערוך. הקובץ המקורי נשמר בשם {path}. @id ui.label תווית @id ui.label_not_built {label} (לא בנוי) @id ui.label_not_built_yet {label}: טרם נבנה diff --git a/i18n/hi.tsv b/i18n/hi.tsv index ee05efa..ec0167c 100644 --- a/i18n/hi.tsv +++ b/i18n/hi.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text पंक्तिबद्धता पाठ के पैराग्राफ पर लागू होती है @id ui.keep_this_window_above_the_others इस विंडो को अन्य विंडोओं से ऊपर रखें +@id ui.kept_the_original_drawing_file_path इस पेज की ड्रॉइंग फ़ाइल में ऐसी रेखाएँ थीं जिन्हें NoteCraft संपादित नहीं कर सकता। मूल फ़ाइल {path} के रूप में रखी गई है। @id ui.label लेबल @id ui.label_not_built {label} (बिल्ड नहीं हुआ) @id ui.label_not_built_yet {label}: अभी तक बिल्ड नहीं हुआ diff --git a/i18n/ja.tsv b/i18n/ja.tsv index a496216..235d648 100644 --- a/i18n/ja.tsv +++ b/i18n/ja.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text 段落のテキストに対して適用されます @id ui.keep_this_window_above_the_others このウィンドウを他のウィンドウの上に固定する +@id ui.kept_the_original_drawing_file_path このページの描画ファイルには NoteCraft で編集できない線が含まれていました。元のファイルは {path} として保存されています。 @id ui.label ラベル @id ui.label_not_built {label} (未実装) @id ui.label_not_built_yet {label}: まだ実装されていません diff --git a/i18n/ko.tsv b/i18n/ko.tsv index 620a040..77c6547 100644 --- a/i18n/ko.tsv +++ b/i18n/ko.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text 정렬은 텍스트 단락에 적용됩니다 @id ui.keep_this_window_above_the_others 이 창을 다른 창 위에 고정 +@id ui.kept_the_original_drawing_file_path 이 페이지의 그리기 파일에 NoteCraft에서 편집할 수 없는 선이 있었습니다. 원래 파일은 {path}(으)로 보관되었습니다. @id ui.label 레이블 @id ui.label_not_built {label} (미구현) @id ui.label_not_built_yet {label}: 아직 구현되지 않음 diff --git a/i18n/ru.tsv b/i18n/ru.tsv index ffa3923..5069989 100644 --- a/i18n/ru.tsv +++ b/i18n/ru.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text Выравнивание применяется к абзацу текста @id ui.keep_this_window_above_the_others Держать это окно поверх остальных +@id ui.kept_the_original_drawing_file_path Файл рисунка этой страницы содержал линии, которые NoteCraft не может изменить. Исходный файл сохранён как {path}. @id ui.label метка @id ui.label_not_built {label} (не реализовано) @id ui.label_not_built_yet {label}: ещё не реализовано diff --git a/i18n/ur.tsv b/i18n/ur.tsv index cb94567..9eebae7 100644 --- a/i18n/ur.tsv +++ b/i18n/ur.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text جسٹیفیکیشن متن کے ایک پیراگراف پر لاگو ہوتی ہے @id ui.keep_this_window_above_the_others اس ونڈو کو دوسروں کے اوپر رکھیں +@id ui.kept_the_original_drawing_file_path اس صفحے کی ڈرائنگ فائل میں ایسی لکیریں تھیں جنہیں NoteCraft مدون نہیں کر سکتا۔ اصل فائل {path} کے نام سے محفوظ رکھی گئی ہے۔ @id ui.label لیبل @id ui.label_not_built {label} (بنایا نہیں گیا) @id ui.label_not_built_yet {label}: ابھی تک نہیں بنایا گیا diff --git a/i18n/zh.tsv b/i18n/zh.tsv index 9520169..f7f96cb 100644 --- a/i18n/zh.tsv +++ b/i18n/zh.tsv @@ -322,6 +322,7 @@ @id ui.json JSON @id ui.justification_goes_on_a_paragraph_of_text 对齐方式应用于一段文本 @id ui.keep_this_window_above_the_others 保持此窗口位于其他窗口之上 +@id ui.kept_the_original_drawing_file_path 此页面的绘图文件包含 NoteCraft 无法编辑的线条。原始文件已保存为 {path}。 @id ui.label 标签 @id ui.label_not_built {label}(未构建) @id ui.label_not_built_yet {label}:尚未构建