From 30ec48d9b3e44bb43d8208ce523bf966cd3e2442 Mon Sep 17 00:00:00 2001 From: Michael Anderson Date: Wed, 14 Oct 2026 11:20:53 -0500 Subject: [PATCH] LR-META-XMP: Retry automatic XMP sidecar writes that failed A sidecar write that failed because the folder was offline or read-only was only logged. Keep those photos waiting, write them again once their files are reachable, and tell the user. --- crates/engine/src/cmd/xmp.rs | 12 +++++ crates/engine/src/lib.rs | 4 ++ crates/engine/src/sidecar.rs | 56 +++++++++++++++++++-- crates/engine/src/tests_xmp.rs | 39 ++++++++++++++ crates/ui-egui/locales/de-formats.json | 1 + crates/ui-egui/locales/de.json | 1 + crates/ui-egui/locales/es-formats.json | 1 + crates/ui-egui/locales/es.json | 1 + crates/ui-egui/locales/fr-formats.json | 1 + crates/ui-egui/locales/fr.json | 1 + crates/ui-egui/locales/ja-formats.json | 1 + crates/ui-egui/locales/ja.json | 1 + crates/ui-egui/locales/pt-br-formats.json | 1 + crates/ui-egui/locales/pt-br.json | 1 + crates/ui-egui/locales/ru-formats.json | 1 + crates/ui-egui/locales/ru.json | 1 + crates/ui-egui/locales/uk-formats.json | 1 + crates/ui-egui/locales/uk.json | 1 + crates/ui-egui/locales/zh-hans-formats.json | 1 + crates/ui-egui/locales/zh-hans.json | 1 + crates/ui-egui/locales/zh-hant-formats.json | 1 + crates/ui-egui/locales/zh-hant.json | 1 + crates/ui-egui/src/lib.rs | 27 ++++++++++ crates/ui-egui/src/state.rs | 7 +++ docs/parity-checklist.md | 3 +- docs/whats-new.md | 6 +++ 26 files changed, 167 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/xmp.rs b/crates/engine/src/cmd/xmp.rs index cd89aa896..84987b15b 100644 --- a/crates/engine/src/cmd/xmp.rs +++ b/crates/engine/src/cmd/xmp.rs @@ -108,6 +108,18 @@ pub fn specs() -> Vec { always, prefs ), + cmd!( + "library.retrySidecars", + "Write Waiting XMP Sidecars", + [], + None, + "{} — writes again the automatic XMP sidecars that failed (folder offline, read-only or full) whose originals are reachable now → {written, waiting}", + always, + |s, _| { + let written = s.retry_unsaved_sidecars(); + Ok(json!({"written": written, "waiting": s.unsaved_sidecars()})) + } + ), cmd!("library.toggleAutoWriteXmp", "Automatically Write Changes into XMP", ["File"], None, "{} → {autoWrite}", always, |s, _| { let on = !s.xmp.auto_write; prefs(s, &json!({"autoWrite": on})) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 7e05fcffd..b127a0dd6 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -206,6 +206,9 @@ pub struct Session { /// Set by a command whose change must not rewrite the photo's XMP sidecar even with auto-write on /// (a catalog-only edit of data the sidecar writer does not emit); consumed when the command ends. pub(crate) skip_auto_write: bool, + /// Photos whose automatic XMP sidecar write failed (their folder offline, read-only or full), written again by + /// [`Session::retry_unsaved_sidecars`] once their file is reachable. + pub(crate) unsaved_sidecars: std::collections::BTreeSet, /// AI denoise: the model in use, the photos that have their picture and the work in progress. pub(crate) denoise: denoise::State, /// Where the host keeps face models (one folder each); `None` where there is no file system (the web). @@ -353,6 +356,7 @@ impl Session { pending_sidecar_repairs: Vec::new(), interaction: None, skip_auto_write: false, + unsaved_sidecars: Default::default(), denoise: Default::default(), face_models_dir: None, face_downloads: Default::default(), diff --git a/crates/engine/src/sidecar.rs b/crates/engine/src/sidecar.rs index c67a2c84f..2f48e7425 100644 --- a/crates/engine/src/sidecar.rs +++ b/crates/engine/src/sidecar.rs @@ -713,13 +713,61 @@ impl Session { } let owners = StemOwners::among(&self.catalog, &ids); for id in ids { - if self.catalog.photo(id).is_some_and(|p| file_path(p).is_some() && p.copy_of.is_none()) - && let Err(e) = self.save_sidecar_with(id, &owners) - { - log::warn!("auto-write XMP: {e}"); + if self.catalog.photo(id).is_some_and(|p| file_path(p).is_some() && p.copy_of.is_none()) { + match self.save_sidecar_with(id, &owners) { + Ok(_) => { + self.unsaved_sidecars.remove(&id); + } + Err(e) => { + // kept for a retry once the file is reachable: a network share that dropped out mustn't leave + // the sidecar silently behind the edit + log::warn!("auto-write XMP: {e}"); + self.unsaved_sidecars.insert(id); + } + } } } } + + /// How many photos have an automatic sidecar write waiting (it failed: the folder offline, read-only or full). + pub fn unsaved_sidecars(&self) -> usize { + self.unsaved_sidecars.len() + } + + /// Write the waiting sidecars whose originals are reachable again (the availability cache answers without + /// touching the disk for the others); photos that are gone, or with auto-write off, are dropped from the wait. + /// Returns how many were written. + pub fn retry_unsaved_sidecars(&mut self) -> usize { + if !self.xmp.auto_write { + self.unsaved_sidecars.clear(); + return 0; + } + let mut ready = Vec::new(); + for id in self.unsaved_sidecars.clone() { + match self.catalog.photo(id).filter(|p| p.copy_of.is_none()).and_then(|p| file_path(p).map(str::to_owned)) { + None => { + self.unsaved_sidecars.remove(&id); + } + Some(path) if self.media.availability.exists(&path) == Some(true) => ready.push(id), + Some(_) => {} + } + } + if ready.is_empty() { + return 0; + } + let owners = StemOwners::among(&self.catalog, &ready); + let mut written = 0; + for id in ready { + match self.save_sidecar_with(id, &owners) { + Ok(_) => { + self.unsaved_sidecars.remove(&id); + written += 1; + } + Err(e) => log::debug!("retry XMP: {e}"), + } + } + written + } } #[cfg(test)] diff --git a/crates/engine/src/tests_xmp.rs b/crates/engine/src/tests_xmp.rs index 58d0d7a14..3649b0b43 100644 --- a/crates/engine/src/tests_xmp.rs +++ b/crates/engine/src/tests_xmp.rs @@ -1209,3 +1209,42 @@ fn files_sharing_a_stem_get_their_own_sidecars() { let _ = std::fs::remove_dir_all(&src); let _ = std::fs::remove_dir_all(&lib); } + +/// An automatic sidecar write that fails (here the folder is read-only, as a network share that dropped out or went +/// read-only) waits instead of being forgotten, and `library.retrySidecars` writes it once the folder takes writes +/// again. +#[cfg(unix)] +#[test] +fn a_failed_auto_write_waits_and_is_written_on_retry() { + use std::os::unix::fs::PermissionsExt; + let src = temp_dir("auto-retry"); + write_png(&src.join("a.png"), 4); + let mut s = Session::new().with_fs(); + s.execute("library.import", &json!({"paths": [src.to_string_lossy()]})).unwrap(); + s.execute("library.xmpPreferences", &json!({"autoWrite": true})).unwrap(); + std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o555)).unwrap(); + // (root writes anyway: nothing to test there) + if std::fs::write(src.join(".probe"), b"x").is_ok() { + std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o755)).unwrap(); + return; + } + s.execute("photo.rate", &json!({"rating": 4})).unwrap(); + let sidecar = src.join("a.xmp"); + assert!(!sidecar.exists()); + assert_eq!(s.unsaved_sidecars(), 1, "the failed write waits"); + let r = s.execute("library.retrySidecars", &json!({})).unwrap(); + assert_eq!(r, json!({"written": 0, "waiting": 1}), "still read-only"); + std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o755)).unwrap(); + let r = s.execute("library.retrySidecars", &json!({})).unwrap(); + assert_eq!(r, json!({"written": 1, "waiting": 0})); + assert!(std::fs::read_to_string(&sidecar).unwrap().contains("4")); + // a later edit that writes fine leaves nothing waiting; turning auto-write off drops what still waits + std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o555)).unwrap(); + s.execute("photo.rate", &json!({"rating": 3})).unwrap(); + assert_eq!(s.unsaved_sidecars(), 1); + s.execute("library.xmpPreferences", &json!({"autoWrite": false})).unwrap(); + s.execute("library.retrySidecars", &json!({})).unwrap(); + assert_eq!(s.unsaved_sidecars(), 0); + std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o755)).unwrap(); + let _ = std::fs::remove_dir_all(&src); +} diff --git a/crates/ui-egui/locales/de-formats.json b/crates/ui-egui/locales/de-formats.json index dff2ddb75..dd7de11c8 100644 --- a/crates/ui-egui/locales/de-formats.json +++ b/crates/ui-egui/locales/de-formats.json @@ -61,6 +61,7 @@ "Import cancelled · {} photo{} added": "Import abgebrochen · {} Foto{} hinzugefügt", "Import failed: {e}": "Import fehlgeschlagen: {e}", "Import warnings ({n}):\n{warnings}": "Importwarnungen ({n}):\n{warnings}", + "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "Die XMP-Begleitdateien von {n} Fotos konnten nicht geschrieben werden (ist ihr Ordner offline?). LightCraft versucht es weiter.", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "XMP-Profil \"{profile}\" wurde für {path} nicht angewendet; Farben können von Lightroom abweichen.", "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "Lightroom-Geometrie wurde für {path} nicht beibehalten; {mode} wird neu berechnet.", "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "Lightroom-Geometrie für {path} wurde nicht beibehalten; gespeicherte Transformation wird ignoriert, weil Upright deaktiviert ist.", diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index 25c411ce0..b287fdf51 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -1617,6 +1617,7 @@ "Window": "Fenster", "Winter Blue": "Winterblau", "Without Saving and Quit (issue #100). It no longer quietly starts a demo session that looked like a reset library and": "Ohne Speichern fortfahren und Beenden (Issue #100). Sie startet keine unbemerkte Demo-Sitzung mehr, die wie eine zurückgesetzte Bibliothek wirkte und", + "Wrote the XMP sidecars that were waiting": "Ausstehende XMP-Begleitdateien geschrieben", "XMP Sidecar Preferences": "Einstellungen für XMP-Begleitdateien", "XMP sidecars": "XMP-Begleitdateien", "Year": "Jahr", diff --git a/crates/ui-egui/locales/es-formats.json b/crates/ui-egui/locales/es-formats.json index 452fc6621..df9cdee3a 100644 --- a/crates/ui-egui/locales/es-formats.json +++ b/crates/ui-egui/locales/es-formats.json @@ -61,6 +61,7 @@ "Import cancelled · {} photo{} added": "Importación cancelada · {0} foto{1} añadida{1}", "Import failed: {e}": "Error de importación: {e}", "Import warnings ({n}):\n{warnings}": "Avisos de importación ({n}):\n{warnings}", + "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "No se pudieron escribir los archivos XMP de {n} fotos (¿su carpeta está desconectada?). LightCraft lo sigue intentando.", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "El perfil XMP \"{profile}\" no se aplicó a {path}; los colores pueden diferir de Lightroom.", "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "La geometría de Lightroom no se conservó para {path}; {mode} se recalculará.", "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "La geometría de Lightroom no se conservó para {path}; la transformación guardada se ignora porque Vertical está desactivado.", diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index ba3e1e331..d9efd748b 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1581,6 +1581,7 @@ "Will be restored": "Se restaurará", "Window": "Ventana", "Winter Blue": "Azul invernal", + "Wrote the XMP sidecars that were waiting": "Se escribieron los archivos XMP pendientes", "XMP Sidecar Preferences": "Preferencias de archivos XMP complementarios", "XMP sidecars": "Archivos XMP complementarios", "Year": "Año", diff --git a/crates/ui-egui/locales/fr-formats.json b/crates/ui-egui/locales/fr-formats.json index d85e837a3..9e0fceba9 100644 --- a/crates/ui-egui/locales/fr-formats.json +++ b/crates/ui-egui/locales/fr-formats.json @@ -65,6 +65,7 @@ "Imported {n} preset{}": "{n} préréglage{} importé(s)", "Imported {n} preset{}, {f} file{} not readable": "{n} préréglage{} importé(s), {f} fichier{} illisible(s)", "Imported {profiles} profile{} (Profile browser ▸ their groups)": "{profiles} profil{} importé(s) (Navigateur de profils → leurs groupes)", + "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "Les fichiers annexes XMP de {n} photos n'ont pas pu être écrits (leur dossier est-il hors ligne ?). LightCraft continue d'essayer.", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "Le profil XMP « {profile} » n’a pas été appliqué à {path} ; les couleurs peuvent différer de Lightroom.", "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "La géométrie Lightroom de {path} n’a pas été conservée ; {mode} sera recalculé.", "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "La géométrie Lightroom de {path} n’a pas été conservée ; la transformation enregistrée est ignorée, car Upright est désactivé.", diff --git a/crates/ui-egui/locales/fr.json b/crates/ui-egui/locales/fr.json index b0becb154..fc2a1a1e9 100644 --- a/crates/ui-egui/locales/fr.json +++ b/crates/ui-egui/locales/fr.json @@ -1543,6 +1543,7 @@ "Will be restored": "Sera restaurée", "Window": "Fenêtre", "Winter Blue": "Bleu hivernal", + "Wrote the XMP sidecars that were waiting": "Fichiers annexes XMP en attente écrits", "XMP Sidecar Preferences": "Préférences des fichiers annexes XMP", "XMP sidecars": "Fichiers annexes XMP", "Year": "Année", diff --git a/crates/ui-egui/locales/ja-formats.json b/crates/ui-egui/locales/ja-formats.json index 211e47e29..dbac557d4 100644 --- a/crates/ui-egui/locales/ja-formats.json +++ b/crates/ui-egui/locales/ja-formats.json @@ -66,6 +66,7 @@ "Show {hidden} hidden location{}": "非表示の{hidden}か所を表示{:.0}", "Showing {n} of {}": "{}枚中{n}枚を表示", "Smart album: {}": "スマートアルバム:{}", + "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "{n} 枚の写真の XMP サイドカーを書き込めませんでした(フォルダーがオフラインではありませんか?)。LightCraft は再試行を続けます。", "Undo {}": "取り消す:{}", "Version {} — a clean-room, pure-Rust photo library and raw developer.": "バージョン{} — Rust製の写真管理・RAW現像アプリ", "Version: {}": "バージョン:{}", diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 7a282e676..efa5752c8 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1471,6 +1471,7 @@ "Will be restored": "復元されます", "Window": "ウィンドウ", "Winter Blue": "ウィンターブルー", + "Wrote the XMP sidecars that were waiting": "保留中の XMP サイドカーを書き込みました", "XMP Sidecar Preferences": "XMPサイドカーファイルの環境設定", "XMP sidecars": "XMPサイドカーファイル", "Year": "年", diff --git a/crates/ui-egui/locales/pt-br-formats.json b/crates/ui-egui/locales/pt-br-formats.json index e81969e0f..1417d88cb 100644 --- a/crates/ui-egui/locales/pt-br-formats.json +++ b/crates/ui-egui/locales/pt-br-formats.json @@ -87,6 +87,7 @@ "Show {hidden} hidden location{}": "Mostrar {hidden} local(is) oculto(s){:.0}", "Showing {n} of {}": "Mostrando {n} de {}", "Smart album: {}": "Álbum inteligente: {}", + "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "Não foi possível gravar os arquivos XMP de {n} fotos (a pasta está offline?). O LightCraft continua tentando.", "Undo {}": "Desfazer {}", "Updated “{name}”": "“{name}” atualizado", "Version {} — a clean-room, pure-Rust photo library and raw developer.": "Versão {} — uma biblioteca de fotos e revelador RAW em Rust puro, desenvolvida de forma independente.", diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index f8e4753e5..cee25024e 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1566,6 +1566,7 @@ "Will be restored": "Será restaurada", "Window": "Janela", "Winter Blue": "Azul invernal", + "Wrote the XMP sidecars that were waiting": "Os arquivos XMP pendentes foram gravados", "XMP Sidecar Preferences": "Preferências de sidecar XMP", "XMP sidecars": "Sidecars XMP", "Year": "Ano", diff --git a/crates/ui-egui/locales/ru-formats.json b/crates/ui-egui/locales/ru-formats.json index 38e28e356..a3f3bb08f 100644 --- a/crates/ui-egui/locales/ru-formats.json +++ b/crates/ui-egui/locales/ru-formats.json @@ -64,6 +64,7 @@ "Import cancelled · {} photo{} added": "Импорт отменён · добавлено фото: {}{:.0}", "Import failed: {e}": "Ошибка импорта: {e}", "Import warnings ({n}):\n{warnings}": "Предупреждения импорта ({n}):\n{warnings}", + "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "Не удалось записать XMP-файлы {n} фото (папка недоступна?). LightCraft продолжает попытки.", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "Профиль XMP \"{profile}\" не применён к {path}; цвета могут отличаться от Lightroom.", "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "Геометрия Lightroom для {path} не сохранена; режим {mode} будет пересчитан.", "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "Геометрия Lightroom для {path} не сохранена; сохранённое преобразование игнорируется, поскольку Upright выключен.", diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index c840218a0..aaa571798 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1309,6 +1309,7 @@ "Width × Height": "Ширина × высота", "Window": "Окно", "Winter Blue": "Зимняя синева", + "Wrote the XMP sidecars that were waiting": "Ожидавшие XMP-файлы записаны", "XMP Sidecar Preferences": "Настройки XMP-файлов", "XMP sidecars": "XMP-файлы", "Year": "Год", diff --git a/crates/ui-egui/locales/uk-formats.json b/crates/ui-egui/locales/uk-formats.json index 55636422f..0f9f360a5 100644 --- a/crates/ui-egui/locales/uk-formats.json +++ b/crates/ui-egui/locales/uk-formats.json @@ -65,6 +65,7 @@ "Imported {n} preset{}": "Імпортовано наборів налаштувань: {n}{:.0}", "Imported {n} preset{}, {f} file{} not readable": "Імпортовано наборів налаштувань: {n}{:.0}, файлів не читається: {f}{:.0}", "Imported {profiles} profile{} (Profile browser ▸ their groups)": "Імпортовано профілів: {profiles}{:.0} (Оглядач профілів → їхні групи)", + "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "Не вдалося записати XMP-файли {n} фото (папка недоступна?). LightCraft продовжує спроби.", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "Профіль XMP «{profile}» не застосовано до {path}; кольори можуть відрізнятися від Lightroom.", "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "Геометрію Lightroom для {path} не збережено; режим {mode} буде перераховано.", "Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "Геометрію Lightroom для {path} не збережено; збережене перетворення ігнорується, оскільки Upright вимкнено.", diff --git a/crates/ui-egui/locales/uk.json b/crates/ui-egui/locales/uk.json index 85a838098..62204d635 100644 --- a/crates/ui-egui/locales/uk.json +++ b/crates/ui-egui/locales/uk.json @@ -305,6 +305,7 @@ "Whites": "Білі тони", "Width": "Ширина", "Window": "Вікно", + "Wrote the XMP sidecars that were waiting": "XMP-файли, що очікували, записано", "Yellow": "Жовтий", "Yes": "Так", "Zoom": "Масштаб", diff --git a/crates/ui-egui/locales/zh-hans-formats.json b/crates/ui-egui/locales/zh-hans-formats.json index 782f8d731..7ea9340e5 100644 --- a/crates/ui-egui/locales/zh-hans-formats.json +++ b/crates/ui-egui/locales/zh-hans-formats.json @@ -66,6 +66,7 @@ "Show {hidden} hidden location{}": "显示 {hidden} 个隐藏位置{0:.0}", "Showing {n} of {}": "显示 {0} 张中的 {n} 张", "Smart album: {}": "智能相册:{0}", + "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "无法写入 {n} 张照片的 XMP 附属文件(其文件夹是否离线?)。LightCraft 会继续重试。", "Undo {}": "撤销 {0}", "Version {} — a clean-room, pure-Rust photo library and raw developer.": "版本 {0} — 净室实现的纯 Rust 照片图库与 RAW 编辑工具。", "Version: {}": "版本:{0}", diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index ad379f1ef..407be8654 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1547,6 +1547,7 @@ "Will be restored": "将恢复", "Window": "窗口", "Winter Blue": "冬日蓝调", + "Wrote the XMP sidecars that were waiting": "已写入等待中的 XMP 附属文件", "XMP Sidecar Preferences": "XMP 附属文件偏好设置", "XMP sidecars": "XMP 附属文件", "Year": "年", diff --git a/crates/ui-egui/locales/zh-hant-formats.json b/crates/ui-egui/locales/zh-hant-formats.json index 56db559c6..945585b6a 100644 --- a/crates/ui-egui/locales/zh-hant-formats.json +++ b/crates/ui-egui/locales/zh-hant-formats.json @@ -83,6 +83,7 @@ "Show {hidden} hidden location{}": "顯示 {hidden} 個隱藏的位置{:.0}", "Showing {n} of {}": "顯示 {n}/{} 張", "Smart album: {}": "智慧型相簿:{}", + "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "無法寫入 {n} 張照片的 XMP 附屬檔案(其資料夾是否離線?)。LightCraft 會持續重試。", "Undo {}": "復原 {}", "Updated “{name}”": "已更新「{name}」", "Version {} — a clean-room, pure-Rust photo library and raw developer.": "版本 {} — 以純 Rust 獨立實作的照片管理與 RAW 顯像程式。", diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 647d153d0..215f3c29c 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1547,6 +1547,7 @@ "Will be restored": "將還原", "Window": "視窗", "Winter Blue": "冬日藍調", + "Wrote the XMP sidecars that were waiting": "已寫入等待中的 XMP 附屬檔案", "XMP Sidecar Preferences": "XMP 附屬檔案偏好設定", "XMP sidecars": "XMP 附屬檔案", "Year": "年", diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 37fb86df3..d54f50e9c 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1092,6 +1092,33 @@ impl LightcraftApp { } ctx.request_repaint_after(std::time::Duration::from_secs(3)); } + // automatic XMP writes that failed (a network share offline): tried again every few seconds once their files + // are reachable, and said once when they start waiting and once when they're written + #[cfg(not(target_arch = "wasm32"))] + { + if self.session.unsaved_sidecars() > 0 { + let now = ctx.input(|i| i.time); + if now - self.ui.sidecar_retry_at >= 10.0 { + self.ui.sidecar_retry_at = now; + self.session.retry_unsaved_sidecars(); + } + ctx.request_repaint_after(std::time::Duration::from_secs(10)); + } + let waiting = self.session.unsaved_sidecars(); + if waiting > 0 && self.ui.sidecars_waiting == 0 { + self.toast_for( + ctx, + crate::i18n::tr_format!( + "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.", + n = waiting + ), + 8.0, + ); + } else if waiting == 0 && self.ui.sidecars_waiting > 0 { + self.toast(ctx, crate::i18n::tr("Wrote the XMP sidecars that were waiting")); + } + self.ui.sidecars_waiting = waiting; + } self.session.persist_if_dirty(); panels::denoise::pump(self, ctx); model_setup::pump(self, ctx); diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index cc583e7b3..30e7f6357 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -613,6 +613,11 @@ pub struct UiState { /// When the watched folder was last scanned (egui time). #[serde(skip)] pub auto_import_at: f64, + /// When the automatic XMP writes that failed were last tried again (egui time), and how many were waiting then. + #[serde(skip)] + pub sidecar_retry_at: f64, + #[serde(skip)] + pub sidecars_waiting: usize, /// The develop control whose slider is being dragged (geometry sliders show a grid). #[serde(skip)] pub dragging_control: Option, @@ -1101,6 +1106,8 @@ impl Default for UiState { external_edits: Vec::new(), was_focused: true, auto_import_at: 0.0, + sidecar_retry_at: 0.0, + sidecars_waiting: 0, search: String::new(), focus_search: false, renaming_mask: None, diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 7bb187811..574ec0587 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (integration batch 35: KEYC-SELECTONLYACTIVE, Select Only Active Photo ⌘⇧D; LR-BEHAV-AUTOSAVE, damaged snapshots repaired on open; LR-EDIT-OPTICS-PROFILE, Nikon Z 6II / Z 8 / Z f in-file distortion; LRC-LIB-CATALOG-IMPORT, WAL-only catalogs open; LR-EXP-DIM, enlargements stop at 512 MP; LR-IMP-CAMERA-COVERAGE, Canon EOS RP CR3 verified; model folder retry) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (LR-META-XMP: failed automatic sidecar writes are retried; integration batch 35: KEYC-SELECTONLYACTIVE, Select Only Active Photo ⌘⇧D; LR-BEHAV-AUTOSAVE, damaged snapshots repaired on open; LR-EDIT-OPTICS-PROFILE, Nikon Z 6II / Z 8 / Z f in-file distortion; LRC-LIB-CATALOG-IMPORT, WAL-only catalogs open; LR-EXP-DIM, enlargements stop at 512 MP; LR-IMP-CAMERA-COVERAGE, Canon EOS RP CR3 verified; model folder retry) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good @@ -790,6 +790,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | Date | Change | Summary | |---|---|---| +| 2026-10-14 | patch | LR-META-XMP: automatic sidecar writes that fail (folder offline or read-only) wait and are written again once the files are reachable, with a notice (`cmd:library.retrySidecars`); status unchanged | | 2026-10-13 | patch | LR-VIEW-COMPARE, LRC-LIB-COMPARE: ⌘/Ctrl- and Shift-clicks on the filmstrip in Compare change the selection as in the grid; the two photos shown stay selected and the active photo is unchanged (#187); statuses unchanged | | 2026-10-13 | minor | KEYC-SELECTONLYACTIVE: Edit → Select Only Active Photo (⌘⇧D) lands as a new row; status 🟡 (headless-tested, native macOS menu path unverified) | | 2026-10-13 | patch | LRC-LIB-CATALOG-IMPORT: a catalog whose tables are all still in its `-wal` file (main file schema format 0) opens instead of failing as an unsupported schema format; status unchanged | diff --git a/docs/whats-new.md b/docs/whats-new.md index 1bf44c3f4..6e5268cbe 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -2,6 +2,12 @@ ## 2026-10-14 +### XMP sidecars catch up after a network share comes back +- With automatic XMP writing on, an edit to a photo whose folder can't be written (a network share that dropped + out, a folder that went read-only) used to be written only to the library, with a line in the log, so the sidecar + stayed behind the edit. LightCraft now says once that the sidecars are waiting, tries again every few seconds once + the files are reachable, and says when they're written. `library.retrySidecars` does it on request. + ### Hard-edged Heal and Clone spots - A spot with Feather at 0 no longer draws black pixels exactly on its radius. This also fixes spots at 0% Opacity changing pixels along their edge. The hard edge now leaves pixels on the radius unchanged.