Merge PR #907
This commit is contained in:
commit
33a29dcdc3
|
|
@ -115,6 +115,9 @@ fn library_error(dir: &str, e: lightcraft_engine::EngineError) -> String {
|
|||
lightcraft_engine::EngineError::LibraryInUse(why) => format!(
|
||||
"{dir}: {why}\nTo work with the library while the app has it open, start the app with `--control PORT` and use `lightcraft-cli mcp --connect 127.0.0.1:PORT`."
|
||||
),
|
||||
e @ lightcraft_engine::EngineError::LightroomCatalog { .. } => format!(
|
||||
"{e}\nOpen a LightCraft library with `--library DIR` (an empty folder makes a new one) and run `library.importLightroom` with the catalog's path."
|
||||
),
|
||||
e => format!("{dir}: {e}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ mod import_move;
|
|||
pub mod library;
|
||||
mod lightroom_archive;
|
||||
pub mod lightroom_catalog;
|
||||
pub mod lightroom_folder;
|
||||
pub mod lightroom_job;
|
||||
mod lightroom_sqlite;
|
||||
pub mod logging;
|
||||
|
|
@ -88,6 +89,11 @@ pub enum EngineError {
|
|||
/// Another process (the app, `lightcraft-cli`, another computer) has the library open.
|
||||
#[error("{0}")]
|
||||
LibraryInUse(String),
|
||||
/// The folder (or file) asked for is a Lightroom Classic catalog or part of one, not a
|
||||
/// LightCraft library (issue #891): nothing was created or opened. `catalog`: the `.lrcat`
|
||||
/// to import with `library.importLightroom`, when one was found.
|
||||
#[error("{} is a Lightroom Classic catalog, not a LightCraft library; import it into a LightCraft library with File ▸ Import Lightroom Catalog…{}", path.display(), catalog.as_ref().map_or(String::new(), |c| format!(" ({})", c.display())))]
|
||||
LightroomCatalog { path: std::path::PathBuf, catalog: Option<std::path::PathBuf> },
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
|
@ -892,6 +898,8 @@ mod tests_libops;
|
|||
#[cfg(test)]
|
||||
mod tests_library;
|
||||
#[cfg(test)]
|
||||
mod tests_lightroom_folder;
|
||||
#[cfg(test)]
|
||||
mod tests_merge;
|
||||
#[cfg(test)]
|
||||
mod tests_organize;
|
||||
|
|
|
|||
|
|
@ -252,8 +252,16 @@ impl Session {
|
|||
/// The library is locked for this session ([`lightcraft_catalog::lock`]): if another process
|
||||
/// has it open, this fails with [`EngineError::LibraryInUse`] and nothing is read or changed.
|
||||
/// Reopening the library this session already has open keeps its lock.
|
||||
///
|
||||
/// A Lightroom Classic catalog folder, one of its `.lrdata` / `.lrcat-data` folders or an
|
||||
/// `.lrcat` file is refused with [`EngineError::LightroomCatalog`] before anything is
|
||||
/// created ([`crate::lightroom_folder`]); a LightCraft library always opens, whatever else
|
||||
/// its folder holds.
|
||||
pub fn open_library(&mut self, dir: impl AsRef<Path>, seed_demo: bool) -> Result<&LoadReport> {
|
||||
let dir = dir.as_ref().to_path_buf();
|
||||
if let Some(place) = crate::lightroom_folder::lightroom_catalog_at(&dir) {
|
||||
return Err(EngineError::LightroomCatalog { path: dir, catalog: place.catalog });
|
||||
}
|
||||
let open = || FsStore::open(&dir).map_err(|e| EngineError::Other(format!("can't open library {}: {e}", dir.display())));
|
||||
let stores = LibraryStores { catalog: Box::new(open()?), files: Box::new(open()?), on_disk: true, dir: dir.clone() };
|
||||
// the library open in this session (same directory): hand its lock over
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
//! Recognise a Lightroom Classic catalog where a LightCraft library was asked for (issue #891).
|
||||
//!
|
||||
//! A Lightroom catalog folder holds `Name.lrcat` (SQLite) next to `Name Previews.lrdata`,
|
||||
//! `Name Smart Previews.lrdata`, `Name Helper.lrdata`, `Name.lrcat-data`, the `-wal` / `-shm` /
|
||||
//! `.lock` files of an open catalog and `Backups/`. Opening any of these as a library would create
|
||||
//! an empty LightCraft library inside Lightroom's folder; [`Session::open_library`] refuses with
|
||||
//! [`EngineError::LightroomCatalog`] instead, and the app offers File ▸ Import Lightroom Catalog….
|
||||
//!
|
||||
//! What counts (only names are looked at; no catalog is opened or read):
|
||||
//! - a LightCraft library (`catalog.snap` or `catalog.log` in the folder) is never a Lightroom
|
||||
//! catalog, whatever else the folder holds: it opens as before;
|
||||
//! - a path named `*.lrcat` or one of its companions (`*.lrcat-wal`, `*.lrcat-shm`,
|
||||
//! `*.lrcat-journal`, `*.lrcat.lock`);
|
||||
//! - a folder named `*.lrdata` or `*.lrcat-data` (Lightroom's previews and data);
|
||||
//! - a folder holding a `*.lrcat` at its top level (the catalog folder itself), or `Backups/`
|
||||
//! next to one.
|
||||
//!
|
||||
//! The folder scan is bounded ([`SCAN_LIMIT`] entries, top level only) and every I/O error just
|
||||
//! means "not found": this never fails and never panics.
|
||||
//!
|
||||
//! [`Session::open_library`]: crate::Session::open_library
|
||||
//! [`EngineError::LightroomCatalog`]: crate::EngineError::LightroomCatalog
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The most directory entries looked at in one folder.
|
||||
pub const SCAN_LIMIT: usize = 10_000;
|
||||
|
||||
/// A Lightroom Classic catalog (or part of one) found where a library was asked for.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct LightroomPlace {
|
||||
/// The `.lrcat` file to import, when one was found.
|
||||
pub catalog: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Companion files of an open catalog, after the `.lrcat` (lower case).
|
||||
const COMPANIONS: [&str; 4] = ["-wal", "-shm", "-journal", ".lock"];
|
||||
/// Lightroom's own `.lrdata` folders: `Name<suffix>.lrdata` belongs to `Name.lrcat`.
|
||||
const LRDATA_SUFFIXES: [&str; 4] = [" Smart Previews", " Previews", " Helper", " Sync"];
|
||||
|
||||
/// `path`'s file name, lower-cased (lossy: only used to compare with ASCII suffixes).
|
||||
fn lower_name(path: &Path) -> Option<String> {
|
||||
path.file_name().map(|n| n.to_string_lossy().to_lowercase())
|
||||
}
|
||||
|
||||
/// `name` ends with `suffix`, ignoring ASCII case; returns the part before it (`None` for a
|
||||
/// non-UTF-8 name, or when the cut would not be on a character boundary).
|
||||
fn strip_suffix_nocase<'a>(name: &'a str, suffix: &str) -> Option<&'a str> {
|
||||
let cut = name.len().checked_sub(suffix.len())?;
|
||||
let tail = name.get(cut..)?;
|
||||
tail.eq_ignore_ascii_case(suffix).then(|| name.get(..cut)).flatten()
|
||||
}
|
||||
|
||||
/// `name` is `*.lrcat` (any case).
|
||||
fn is_lrcat_name(name: &str) -> bool {
|
||||
name.to_lowercase().ends_with(".lrcat")
|
||||
}
|
||||
|
||||
/// A LightCraft library lives in `dir` (its catalog journal is there).
|
||||
pub fn is_lightcraft_library(dir: &Path) -> bool {
|
||||
[lightcraft_catalog::journal::SNAPSHOT, lightcraft_catalog::journal::LOG].iter().any(|f| dir.join(f).is_file())
|
||||
}
|
||||
|
||||
/// The first `*.lrcat` file (by name) at the top level of `dir`, looking at no more than
|
||||
/// [`SCAN_LIMIT`] entries. Unreadable folders and entries are skipped.
|
||||
pub fn find_lrcat(dir: &Path) -> Option<PathBuf> {
|
||||
let entries = std::fs::read_dir(dir).ok()?;
|
||||
let mut found: Option<PathBuf> = None;
|
||||
for entry in entries.take(SCAN_LIMIT).flatten() {
|
||||
let name = entry.file_name();
|
||||
let Some(name) = name.to_str() else { continue };
|
||||
if !is_lrcat_name(name) {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
// (follows a symlink; a folder named `*.lrcat` is not a catalog)
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
if found.as_ref().is_none_or(|f| path < *f) {
|
||||
found = Some(path);
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// `candidate` if it is a file, else the first catalog in `dir`.
|
||||
fn catalog_or_scan(candidate: Option<PathBuf>, dir: Option<&Path>) -> Option<PathBuf> {
|
||||
candidate.filter(|c| c.is_file()).or_else(|| dir.and_then(find_lrcat))
|
||||
}
|
||||
|
||||
/// Whether `path`, given as a library folder, is a Lightroom Classic catalog or part of one (see
|
||||
/// the module docs). `None`: not Lightroom's, open it as a library.
|
||||
pub fn lightroom_catalog_at(path: &Path) -> Option<LightroomPlace> {
|
||||
let is_dir = path.is_dir();
|
||||
if is_dir && is_lightcraft_library(path) {
|
||||
return None;
|
||||
}
|
||||
let parent = path.parent().filter(|p| !p.as_os_str().is_empty());
|
||||
let name = path.file_name().and_then(|n| n.to_str());
|
||||
let lower = lower_name(path).unwrap_or_default();
|
||||
// the catalog file itself (a folder named `*.lrcat` is looked into below)
|
||||
if lower.ends_with(".lrcat") && !is_dir {
|
||||
return Some(LightroomPlace { catalog: path.is_file().then(|| path.to_path_buf()) });
|
||||
}
|
||||
// an open catalog's companion files: `Name.lrcat-wal` → `Name.lrcat`
|
||||
for c in COMPANIONS {
|
||||
if lower.ends_with(&format!(".lrcat{c}")) && !is_dir {
|
||||
let candidate = name.and_then(|n| strip_suffix_nocase(n, c)).and_then(|n| parent.map(|p| p.join(n)));
|
||||
return Some(LightroomPlace { catalog: catalog_or_scan(candidate, parent) });
|
||||
}
|
||||
}
|
||||
// `Name.lrcat-data` → `Name.lrcat`
|
||||
if let Some(stem) = name.and_then(|n| strip_suffix_nocase(n, "-data"))
|
||||
&& is_lrcat_name(stem)
|
||||
{
|
||||
let candidate = parent.map(|p| p.join(stem));
|
||||
return Some(LightroomPlace { catalog: catalog_or_scan(candidate, parent) });
|
||||
}
|
||||
// `Name Previews.lrdata` (and Smart Previews, Helper, Sync) → `Name.lrcat`
|
||||
if let Some(stem) = name.and_then(|n| strip_suffix_nocase(n, ".lrdata")) {
|
||||
let base = LRDATA_SUFFIXES.iter().find_map(|s| strip_suffix_nocase(stem, s)).unwrap_or(stem);
|
||||
let candidate = parent.map(|p| p.join(format!("{base}.lrcat")));
|
||||
return Some(LightroomPlace { catalog: catalog_or_scan(candidate, parent) });
|
||||
}
|
||||
if lower.ends_with(".lrdata") || lower.ends_with(".lrcat-data") {
|
||||
// (a non-UTF-8 name)
|
||||
return Some(LightroomPlace { catalog: parent.and_then(find_lrcat) });
|
||||
}
|
||||
if is_dir {
|
||||
// the catalog folder
|
||||
if let Some(catalog) = find_lrcat(path) {
|
||||
return Some(LightroomPlace { catalog: Some(catalog) });
|
||||
}
|
||||
// `Backups/` next to a catalog
|
||||
if lower == "backups"
|
||||
&& let Some(catalog) = parent.and_then(find_lrcat)
|
||||
{
|
||||
return Some(LightroomPlace { catalog: Some(catalog) });
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
//! Issue #891: Open Library given a Lightroom Classic catalog (its folder, `.lrdata` /
|
||||
//! `.lrcat-data` folders, the `.lrcat` or its companion files) refuses it, creating nothing, and
|
||||
//! names the catalog to import; a LightCraft library still opens, whatever else its folder holds.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::lightroom_folder::{LightroomPlace, SCAN_LIMIT, lightroom_catalog_at};
|
||||
use crate::{EngineError, Session};
|
||||
|
||||
fn temp_dir(tag: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("lc-engine-lrfolder-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// A Lightroom catalog folder as Lightroom Classic leaves it (fake contents: only names count).
|
||||
fn lightroom_folder(root: &Path) -> PathBuf {
|
||||
let dir = root.join("Lightroom");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("Lightroom Catalog.lrcat"), b"not really SQLite").unwrap();
|
||||
for f in ["Lightroom Catalog.lrcat-wal", "Lightroom Catalog.lrcat-shm", "Lightroom Catalog.lrcat.lock"] {
|
||||
std::fs::write(dir.join(f), b"").unwrap();
|
||||
}
|
||||
for d in [
|
||||
"Lightroom Catalog Previews.lrdata",
|
||||
"Lightroom Catalog Smart Previews.lrdata",
|
||||
"Lightroom Catalog Helper.lrdata",
|
||||
"Lightroom Catalog.lrcat-data",
|
||||
"Backups",
|
||||
] {
|
||||
std::fs::create_dir_all(dir.join(d)).unwrap();
|
||||
}
|
||||
dir
|
||||
}
|
||||
|
||||
fn names(dir: &Path) -> Vec<std::ffi::OsString> {
|
||||
let mut v: Vec<_> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().file_name()).collect();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
/// Refused with the catalog named, and nothing created or changed anywhere in `root`.
|
||||
fn refused(root: &Path, path: &Path, catalog: &Path) {
|
||||
let before = names(&root.join("Lightroom"));
|
||||
let mut s = Session::new();
|
||||
match s.open_library(path, true) {
|
||||
Err(EngineError::LightroomCatalog { path: p, catalog: c }) => {
|
||||
assert_eq!(p, path);
|
||||
assert_eq!(c.as_deref(), Some(catalog), "{}", path.display());
|
||||
}
|
||||
other => panic!("{}: {other:?}", path.display()),
|
||||
}
|
||||
assert!(s.library.is_none());
|
||||
assert_eq!(names(&root.join("Lightroom")), before, "nothing written next to the catalog");
|
||||
if path.is_dir() {
|
||||
assert!(!path.join("catalog.snap").exists() && !path.join("catalog.lock").exists(), "no library created in {}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_part_of_a_lightroom_catalog_is_refused() {
|
||||
let root = temp_dir("parts");
|
||||
let dir = lightroom_folder(&root);
|
||||
let catalog = dir.join("Lightroom Catalog.lrcat");
|
||||
for p in [
|
||||
dir.clone(),
|
||||
catalog.clone(),
|
||||
dir.join("Lightroom Catalog Previews.lrdata"),
|
||||
dir.join("Lightroom Catalog Smart Previews.lrdata"),
|
||||
dir.join("Lightroom Catalog Helper.lrdata"),
|
||||
dir.join("Lightroom Catalog.lrcat-data"),
|
||||
dir.join("Lightroom Catalog.lrcat-wal"),
|
||||
dir.join("Lightroom Catalog.lrcat-shm"),
|
||||
dir.join("Lightroom Catalog.lrcat.lock"),
|
||||
dir.join("Backups"),
|
||||
] {
|
||||
refused(&root, &p, &catalog);
|
||||
}
|
||||
let e = Session::new().open_library(&dir, false).unwrap_err().to_string();
|
||||
assert!(e.contains("Lightroom Classic catalog, not a LightCraft library") && e.contains("Import Lightroom Catalog"), "{e}");
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_lrdata_folder_whose_catalog_was_renamed_finds_the_one_next_to_it() {
|
||||
let root = temp_dir("renamed");
|
||||
let dir = root.join("Lightroom");
|
||||
std::fs::create_dir_all(dir.join("Old Name Previews.lrdata")).unwrap();
|
||||
std::fs::write(dir.join("New Name.lrcat"), b"x").unwrap();
|
||||
refused(&root, &dir.join("Old Name Previews.lrdata"), &dir.join("New Name.lrcat"));
|
||||
// no catalog anywhere: still refused (it is Lightroom's data), with nothing to import
|
||||
let lone = root.join("lone");
|
||||
std::fs::create_dir_all(lone.join("Gone Previews.lrdata")).unwrap();
|
||||
assert_eq!(lightroom_catalog_at(&lone.join("Gone Previews.lrdata")), Some(LightroomPlace { catalog: None }));
|
||||
assert_eq!(lightroom_catalog_at(&lone.join("Gone.lrcat")), Some(LightroomPlace { catalog: None }), "a missing .lrcat path");
|
||||
assert!(!lone.join("Gone.lrcat").exists());
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_folder_still_becomes_a_library() {
|
||||
let root = temp_dir("empty");
|
||||
let lib = root.join("New Library");
|
||||
assert_eq!(lightroom_catalog_at(&lib), None, "not there yet");
|
||||
let mut s = Session::new();
|
||||
s.open_library(&lib, false).unwrap();
|
||||
assert!(s.library.is_some());
|
||||
// a folder with other files (and a folder named like a catalog, which is not one) too
|
||||
let other = root.join("Other");
|
||||
std::fs::create_dir_all(other.join("weird.lrcat")).unwrap();
|
||||
std::fs::write(other.join("notes.txt"), b"x").unwrap();
|
||||
assert_eq!(lightroom_catalog_at(&other), None);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
/// Precedence: a LightCraft library (its catalog journal) opens, even with an `.lrcat` beside it.
|
||||
#[test]
|
||||
fn a_lightcraft_library_with_a_catalog_inside_still_opens() {
|
||||
let root = temp_dir("both");
|
||||
let lib = root.join("Lightroom");
|
||||
{
|
||||
let mut s = Session::new();
|
||||
s.open_library(&lib, true).unwrap();
|
||||
assert!(!s.catalog.is_empty());
|
||||
s.close_library().unwrap();
|
||||
}
|
||||
std::fs::write(lib.join("Lightroom Catalog.lrcat"), b"x").unwrap();
|
||||
std::fs::create_dir_all(lib.join("Lightroom Catalog Previews.lrdata")).unwrap();
|
||||
assert_eq!(lightroom_catalog_at(&lib), None);
|
||||
let mut s = Session::new();
|
||||
s.open_library(&lib, false).unwrap();
|
||||
assert!(!s.catalog.is_empty(), "the LightCraft library opened");
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
/// Never crash, never scan without end: a huge folder, unreadable paths, odd names.
|
||||
#[test]
|
||||
fn odd_folders_never_fail_the_check() {
|
||||
let root = temp_dir("odd");
|
||||
// the catalog after the scan limit isn't looked for (sorted names put it last on most file systems; either answer is fine, the check returns)
|
||||
let big = root.join("big");
|
||||
std::fs::create_dir_all(&big).unwrap();
|
||||
for i in 0..SCAN_LIMIT + 10 {
|
||||
std::fs::write(big.join(format!("{i:06}.jpg")), b"").unwrap();
|
||||
}
|
||||
let _ = lightroom_catalog_at(&big);
|
||||
assert_eq!(lightroom_catalog_at(&root.join("missing/deeper")), None);
|
||||
assert_eq!(lightroom_catalog_at(Path::new("")), None);
|
||||
assert_eq!(lightroom_catalog_at(Path::new("/")), None);
|
||||
assert!(lightroom_catalog_at(Path::new("x.LRCAT")).is_some(), "case-insensitive");
|
||||
// a one-letter name with a multibyte character never cuts inside it
|
||||
let _ = lightroom_catalog_at(&root.join("é"));
|
||||
let _ = lightroom_catalog_at(&root.join("ataé-data"));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let bad = root.join(std::ffi::OsStr::from_bytes(b"caf\xe9 Previews.lrdata"));
|
||||
std::fs::create_dir_all(&bad).unwrap();
|
||||
std::fs::write(root.join("Cat.lrcat"), b"x").unwrap();
|
||||
assert_eq!(lightroom_catalog_at(&bad), Some(LightroomPlace { catalog: Some(root.join("Cat.lrcat")) }), "non-UTF-8 name");
|
||||
let lr = root.join(std::ffi::OsStr::from_bytes(b"\xff\xfe"));
|
||||
std::fs::create_dir_all(&lr).unwrap();
|
||||
std::fs::write(lr.join(std::ffi::OsStr::from_bytes(b"\xff.lrcat")), b"x").unwrap();
|
||||
std::fs::write(lr.join("b.lrcat"), b"x").unwrap();
|
||||
assert_eq!(lightroom_catalog_at(&lr), Some(LightroomPlace { catalog: Some(lr.join("b.lrcat")) }), "non-UTF-8 entries skipped");
|
||||
// a symlink to a catalog folder is one too; a dangling one is not
|
||||
std::os::unix::fs::symlink(&lr, root.join("link")).unwrap();
|
||||
assert!(lightroom_catalog_at(&root.join("link")).is_some());
|
||||
std::os::unix::fs::symlink(root.join("nowhere"), root.join("dangling")).unwrap();
|
||||
assert_eq!(lightroom_catalog_at(&root.join("dangling")), None);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
|
@ -2041,5 +2041,7 @@
|
|||
"Downloading model": "Modell wird heruntergeladen",
|
||||
"Finding faces": "Gesichter werden gesucht",
|
||||
"Synchronizing folder": "Ordner wird synchronisiert",
|
||||
"Looking for changes": "Änderungen werden gesucht"
|
||||
"Looking for changes": "Änderungen werden gesucht",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Dies ist ein Lightroom-Katalog, keine LightCraft-Bibliothek. Stattdessen in eine LightCraft-Bibliothek importieren?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "Der Import liest den Katalog, ohne ihn zu ändern, und fügt seine Fotos, Bearbeitungen, Stichwörter und Sammlungen der jetzt geöffneten Bibliothek hinzu."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1886,5 +1886,7 @@
|
|||
"Downloading model": "Descargando modelo",
|
||||
"Finding faces": "Buscando caras",
|
||||
"Synchronizing folder": "Sincronizando carpeta",
|
||||
"Looking for changes": "Buscando cambios"
|
||||
"Looking for changes": "Buscando cambios",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Esto es un catálogo de Lightroom, no una biblioteca de LightCraft. ¿Importarlo a una biblioteca de LightCraft?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "La importación lee el catálogo sin modificarlo y añade sus fotos, ediciones, palabras clave y colecciones a la biblioteca abierta ahora."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1886,5 +1886,7 @@
|
|||
"Panels": "Panels",
|
||||
"Reset Panel Layout": "Reset Panel Layout",
|
||||
"Resize panel window": "Resize panel window",
|
||||
"Resize panels": "Resize panels"
|
||||
"Resize panels": "Resize panels",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Ceci est un catalogue Lightroom, pas une bibliothèque LightCraft. L’importer plutôt dans une bibliothèque LightCraft ?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "L’importation lit le catalogue sans le modifier et ajoute ses photos, retouches, mots-clés et collections à la bibliothèque ouverte."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1888,5 +1888,7 @@
|
|||
"Downloading model": "モデルをダウンロード中",
|
||||
"Finding faces": "顔を検索中",
|
||||
"Synchronizing folder": "フォルダを同期中",
|
||||
"Looking for changes": "変更を確認中"
|
||||
"Looking for changes": "変更を確認中",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "これは Lightroom カタログで、LightCraft ライブラリではありません。代わりに LightCraft ライブラリに読み込みますか?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "読み込みはカタログを変更せずに読み取り、写真・編集・キーワード・コレクションを現在開いているライブラリに追加します。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1886,5 +1886,7 @@
|
|||
"Downloading model": "Baixando modelo",
|
||||
"Finding faces": "Procurando rostos",
|
||||
"Synchronizing folder": "Sincronizando pasta",
|
||||
"Looking for changes": "Procurando alterações"
|
||||
"Looking for changes": "Procurando alterações",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Este é um catálogo do Lightroom, não uma biblioteca do LightCraft. Importá-lo para uma biblioteca do LightCraft?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "A importação lê o catálogo sem alterá-lo e adiciona as fotos, edições, palavras-chave e coleções dele à biblioteca aberta agora."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1600,5 +1600,7 @@
|
|||
"Finding faces": "Поиск лиц",
|
||||
"Quit Anyway": "Всё равно выйти",
|
||||
"Synchronizing folder": "Синхронизация папки",
|
||||
"Looking for changes": "Поиск изменений"
|
||||
"Looking for changes": "Поиск изменений",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Это каталог Lightroom, а не библиотека LightCraft. Импортировать его в библиотеку LightCraft?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "Импорт читает каталог, не изменяя его, и добавляет его фото, правки, ключевые слова и коллекции в открытую сейчас библиотеку."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2080,5 +2080,7 @@
|
|||
"Panels": "Panels",
|
||||
"Reset Panel Layout": "Reset Panel Layout",
|
||||
"Resize panel window": "Resize panel window",
|
||||
"Resize panels": "Resize panels"
|
||||
"Resize panels": "Resize panels",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Це каталог Lightroom, а не бібліотека LightCraft. Імпортувати його до бібліотеки LightCraft?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "Імпорт читає каталог, не змінюючи його, і додає його фото, редагування, ключові слова та колекції до відкритої зараз бібліотеки."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1886,5 +1886,7 @@
|
|||
"Downloading model": "正在下载模型",
|
||||
"Finding faces": "正在查找人脸",
|
||||
"Synchronizing folder": "正在同步文件夹",
|
||||
"Looking for changes": "正在查找更改"
|
||||
"Looking for changes": "正在查找更改",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "这是 Lightroom 目录,不是 LightCraft 图库。要改为将其导入 LightCraft 图库吗?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "导入只读取目录、不做任何更改,并将其中的照片、编辑、关键字和收藏夹添加到当前打开的图库。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1886,5 +1886,7 @@
|
|||
"Downloading model": "正在下載模型",
|
||||
"Finding faces": "正在尋找臉孔",
|
||||
"Synchronizing folder": "正在同步資料夾",
|
||||
"Looking for changes": "正在尋找變更"
|
||||
"Looking for changes": "正在尋找變更",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "這是 Lightroom 目錄,不是 LightCraft 照片庫。要改為將其匯入 LightCraft 照片庫嗎?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "匯入只讀取目錄、不做任何變更,並將其中的照片、編輯、關鍵字和收藏夾加入目前開啟的照片庫。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ mod tests_menubar;
|
|||
#[cfg(test)]
|
||||
mod tests_offline;
|
||||
#[cfg(test)]
|
||||
mod tests_open_lightroom;
|
||||
#[cfg(test)]
|
||||
mod tests_panels;
|
||||
#[cfg(test)]
|
||||
mod tests_preview_limit;
|
||||
|
|
|
|||
|
|
@ -227,7 +227,12 @@ fn inspect_report(data: lightcraft_engine::lightroom_catalog::CatalogImport) ->
|
|||
})
|
||||
}
|
||||
|
||||
fn terminal(app: &mut LightcraftApp, ctx: &egui::Context, kind: Kind, value: Value) {
|
||||
/// The task is over: keep its result (with the catalog's `path`) for `status` and say how it went.
|
||||
fn terminal(app: &mut LightcraftApp, ctx: &egui::Context, task: &LightroomTask, mut value: Value) {
|
||||
let kind = task.kind;
|
||||
if let Some(o) = value.as_object_mut() {
|
||||
o.entry("path").or_insert_with(|| json!(task.path));
|
||||
}
|
||||
let message = if let Some(e) = value.get("error").and_then(Value::as_str) {
|
||||
let warning = value.get("indexWarning").and_then(Value::as_str).map_or(String::new(), |w| format!("; index warning: {w}"));
|
||||
if matches!(kind, Kind::Inspect) {
|
||||
|
|
@ -343,26 +348,26 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
return;
|
||||
}
|
||||
Err(mpsc::TryRecvError::Disconnected) => {
|
||||
terminal(app, ctx, task.kind, json!({"error": "Lightroom worker stopped unexpectedly"}));
|
||||
terminal(app, ctx, &task, json!({"error": "Lightroom worker stopped unexpectedly"}));
|
||||
return;
|
||||
}
|
||||
};
|
||||
match message {
|
||||
Message::Inspected(result) => match result {
|
||||
Ok(report) if !task.cancel.load(Ordering::Relaxed) => terminal(app, ctx, task.kind, report),
|
||||
Ok(_) => terminal(app, ctx, task.kind, json!({"cancelled": true})),
|
||||
Err(e) if task.cancel.load(Ordering::Relaxed) || e.contains("cancelled") => terminal(app, ctx, task.kind, json!({"cancelled": true})),
|
||||
Err(e) => terminal(app, ctx, task.kind, json!({"error": e})),
|
||||
Ok(report) if !task.cancel.load(Ordering::Relaxed) => terminal(app, ctx, &task, report),
|
||||
Ok(_) => terminal(app, ctx, &task, json!({"cancelled": true})),
|
||||
Err(e) if task.cancel.load(Ordering::Relaxed) || e.contains("cancelled") => terminal(app, ctx, &task, json!({"cancelled": true})),
|
||||
Err(e) => terminal(app, ctx, &task, json!({"error": e})),
|
||||
},
|
||||
Message::Prepared(result) => {
|
||||
let prepared = match result {
|
||||
Ok(p) => p,
|
||||
Err(e) if task.cancel.load(Ordering::Relaxed) || e.contains("cancelled") => {
|
||||
terminal(app, ctx, task.kind, json!({"cancelled": true}));
|
||||
terminal(app, ctx, &task, json!({"cancelled": true}));
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
terminal(app, ctx, task.kind, json!({"error": e}));
|
||||
terminal(app, ctx, &task, json!({"error": e}));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
@ -371,7 +376,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
terminal(
|
||||
app,
|
||||
ctx,
|
||||
task.kind,
|
||||
&task,
|
||||
if task.cancel.load(Ordering::Relaxed) {
|
||||
json!({"cancelled": true})
|
||||
} else {
|
||||
|
|
@ -394,7 +399,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
let (tx, rx) = mpsc::channel();
|
||||
let commit_error = committed.as_ref().err().map(ToString::to_string);
|
||||
let Some(finalization) = finalization else {
|
||||
terminal(app, ctx, task.kind, json!({"error": commit_error.clone().unwrap_or_else(|| "Lightroom import did not commit".into())}));
|
||||
terminal(app, ctx, &task, json!({"error": commit_error.clone().unwrap_or_else(|| "Lightroom import did not commit".into())}));
|
||||
return;
|
||||
};
|
||||
if let Err(e) = spawn_finalize(finalization, tx, ctx.clone()) {
|
||||
|
|
@ -402,7 +407,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
if let Some(error) = commit_error {
|
||||
result["error"] = json!(error);
|
||||
}
|
||||
terminal(app, ctx, task.kind, result);
|
||||
terminal(app, ctx, &task, result);
|
||||
return;
|
||||
}
|
||||
task.rx = rx;
|
||||
|
|
@ -418,14 +423,14 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
let pending = app.lightroom_last.take().unwrap_or(Value::Null);
|
||||
let report = pending.get("report").cloned().unwrap_or(Value::Null);
|
||||
match result {
|
||||
Ok(()) if pending.get("error").is_none() => terminal(app, ctx, task.kind, report),
|
||||
Ok(()) => terminal(app, ctx, task.kind, json!({"report": report, "error": pending["error"].clone()})),
|
||||
Ok(()) if pending.get("error").is_none() => terminal(app, ctx, &task, report),
|
||||
Ok(()) => terminal(app, ctx, &task, json!({"report": report, "error": pending["error"].clone()})),
|
||||
Err(e) => {
|
||||
let mut value = json!({"report": report, "indexWarning": e});
|
||||
if let Some(error) = pending.get("error") {
|
||||
value["error"] = error.clone();
|
||||
}
|
||||
terminal(app, ctx, task.kind, value);
|
||||
terminal(app, ctx, &task, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -509,7 +514,7 @@ mod tests {
|
|||
tx.send(Message::Prepared(Err("cancelled".into()))).unwrap();
|
||||
h.step();
|
||||
assert!(h.app.lightroom.is_none());
|
||||
assert_eq!(h.app.lightroom_last, Some(serde_json::json!({"cancelled": true})));
|
||||
assert_eq!(h.app.lightroom_last, Some(serde_json::json!({"cancelled": true, "path": "fixture.lrcat"})));
|
||||
assert!(h.app.session.activity.list().is_empty());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
Dialog::Merge { opts } => opts.title(),
|
||||
Dialog::Settings { .. } => "Settings",
|
||||
Dialog::ConfirmDelete { .. } => "Delete Photos",
|
||||
Dialog::LightroomCatalog { .. } => "Lightroom Classic Catalog",
|
||||
Dialog::RemoveFolder { disk: true, .. } => "Remove Disk from Library",
|
||||
Dialog::RemoveFolder { .. } => "Remove Folder from Library",
|
||||
Dialog::SynchronizeFolder { .. } => "Synchronize Folder",
|
||||
|
|
@ -165,6 +166,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
Dialog::SmartRules { .. } => 680.0,
|
||||
Dialog::AllMetadata { .. } => 620.0,
|
||||
Dialog::FaceModel { .. } => 460.0,
|
||||
Dialog::LightroomCatalog { .. } => 440.0,
|
||||
_ => 380.0,
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
|
|
@ -919,6 +921,16 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
ui.label(egui::RichText::new(crate::i18n::tr("They can be restored from Recently Deleted until it is emptied.")).color(t.text_dim));
|
||||
}
|
||||
d @ Dialog::SynchronizeFolder { .. } => crate::sync::body(app, ui, d),
|
||||
Dialog::LightroomCatalog { folder, catalog } => {
|
||||
ui.label(crate::i18n::tr("This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?"));
|
||||
ui.label(egui::RichText::new(catalog.as_deref().unwrap_or(folder.as_str())).color(t.text_dim));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::tr(
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.",
|
||||
))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
}
|
||||
Dialog::RemoveFolder { name, count, path, disk } => {
|
||||
ui.label(crate::i18n::tr_format!(
|
||||
"Remove “{name}” and its {count} photo{} from the library?",
|
||||
|
|
@ -1030,6 +1042,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
Dialog::KeywordTag { editing: None, .. } => "Create",
|
||||
Dialog::KeywordTag { .. } => "Save",
|
||||
Dialog::RemoveFolder { .. } => "Remove",
|
||||
Dialog::LightroomCatalog { .. } => "Import Lightroom Catalog…",
|
||||
Dialog::SynchronizeFolder { .. } => "Synchronize",
|
||||
Dialog::SamModel { then: Some(_), .. } if sam_installed => "Continue",
|
||||
Dialog::SamModel { .. } if sam_installed || sam_running || sam_nowhere => "",
|
||||
|
|
@ -1413,6 +1426,10 @@ pub fn confirm_dialog(app: &mut LightcraftApp, dlg: &Dialog) -> Result<serde_jso
|
|||
Dialog::Merge { opts } => crate::merge::start_final(app, opts),
|
||||
Dialog::Import { opts } => crate::import::start(app, opts),
|
||||
Dialog::ConfirmDelete { .. } => app.run("photo.delete", json!({})),
|
||||
// the catalog found: no file dialog; none: File ▸ Import Lightroom Catalog… asks for it
|
||||
Dialog::LightroomCatalog { catalog, .. } => {
|
||||
app.run("file.importLightroom", catalog.as_ref().map_or_else(|| json!({}), |c| json!({"path": c})))
|
||||
}
|
||||
Dialog::RemoveFolder { path, disk, .. } => app.run("library.removeFolder", json!({"path": path, "disk": disk})),
|
||||
d @ Dialog::SynchronizeFolder { .. } => crate::sync::confirm(app, d),
|
||||
Dialog::About | Dialog::Shortcuts | Dialog::Settings { .. } => Ok(serde_json::Value::Null),
|
||||
|
|
|
|||
|
|
@ -624,6 +624,11 @@ fn interface_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
|
||||
/// `app.openLibrary {path?}`: close the current library and open (or create) the one at `path`,
|
||||
/// or a folder chosen in a dialog. Remembered as the library to open at launch.
|
||||
///
|
||||
/// A Lightroom Classic catalog (its folder, an `.lrdata` / `.lrcat-data` folder or the `.lrcat`
|
||||
/// itself, see [`lightcraft_engine::lightroom_folder`]) is not opened: the current library stays
|
||||
/// and a dialog offers Import Lightroom Catalog… instead; returns `{opened: false,
|
||||
/// lightroomCatalog: <.lrcat path or null>, path}`.
|
||||
pub fn open_library(app: &mut LightcraftApp, p: &Value) -> Result<Value, String> {
|
||||
if crate::lightroom_import::is_running(app) {
|
||||
return Err("wait for Lightroom catalog import to finish before switching libraries".into());
|
||||
|
|
@ -642,6 +647,18 @@ pub fn open_library(app: &mut LightcraftApp, p: &Value) -> Result<Value, String>
|
|||
}
|
||||
}
|
||||
};
|
||||
// A Lightroom catalog folder (or part of one) is not a library: before closing the library
|
||||
// open now, offer to import it instead (issue #891). The engine refuses it too.
|
||||
if let Some(place) = lightcraft_engine::lightroom_folder::lightroom_catalog_at(std::path::Path::new(&path)) {
|
||||
let catalog = place.catalog.map(|c| c.to_string_lossy().into_owned());
|
||||
let err = lightcraft_engine::EngineError::LightroomCatalog { path: path.clone().into(), catalog: catalog.clone().map(Into::into) };
|
||||
// the library problem window (no library could be opened) shows the error instead
|
||||
if app.library_problem.is_some() {
|
||||
return Err(err.to_string());
|
||||
}
|
||||
app.ui.dialog = Some(crate::state::Dialog::LightroomCatalog { folder: path.clone(), catalog: catalog.clone() });
|
||||
return Ok(json!({"opened": false, "lightroomCatalog": catalog, "path": path}));
|
||||
}
|
||||
app.session.close_library().map_err(|e| e.to_string())?;
|
||||
app.session.open_library(&path, false).map_err(|e| e.to_string())?;
|
||||
app.lightroom_last = None;
|
||||
|
|
|
|||
|
|
@ -725,6 +725,13 @@ pub enum Dialog {
|
|||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
},
|
||||
/// Open Library was given a Lightroom Classic catalog (issue #891): offer File ▸ Import
|
||||
/// Lightroom Catalog… with `catalog` (the `.lrcat` found; `None`: ask for it) instead.
|
||||
/// `folder`: what was chosen.
|
||||
LightroomCatalog {
|
||||
folder: String,
|
||||
catalog: Option<String>,
|
||||
},
|
||||
/// Confirm moving photos to Recently Deleted.
|
||||
ConfirmDelete {
|
||||
count: usize,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
//! Issue #891: File ▸ Open Library… given a Lightroom Classic catalog folder doesn't make a
|
||||
//! LightCraft library inside it. The library open now stays, and a dialog offers Import Lightroom
|
||||
//! Catalog… with the catalog found, which starts the import of that `.lrcat`.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::headless::Headless;
|
||||
use crate::state::Dialog;
|
||||
use crate::{LightcraftApp, Services};
|
||||
|
||||
const T: Duration = Duration::from_secs(20);
|
||||
|
||||
fn has(h: &Headless, id: &str) -> bool {
|
||||
h.app.widgets.iter().any(|(w, _)| w == id)
|
||||
}
|
||||
|
||||
fn names(dir: &std::path::Path) -> Vec<std::ffi::OsString> {
|
||||
let mut v: Vec<_> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().file_name()).collect();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_library_on_a_lightroom_catalog_offers_its_import() {
|
||||
let dir = std::env::temp_dir().join(format!("lc-ui-openlrcat-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let lib = dir.join("LightCraft Library");
|
||||
let lr = dir.join("Lightroom");
|
||||
std::fs::create_dir_all(lr.join("Lightroom Catalog Previews.lrdata")).unwrap();
|
||||
std::fs::create_dir_all(lr.join("Lightroom Catalog.lrcat-data")).unwrap();
|
||||
std::fs::create_dir_all(lr.join("Backups")).unwrap();
|
||||
let lrcat = lr.join("Lightroom Catalog.lrcat");
|
||||
std::fs::write(&lrcat, b"not really SQLite").unwrap();
|
||||
let before = names(&lr);
|
||||
|
||||
let mut session = lightcraft_engine::Session::new().with_fs();
|
||||
session.open_library(&lib, true).unwrap();
|
||||
let photos = session.catalog.len();
|
||||
assert!(photos > 0);
|
||||
let picked = lr.to_string_lossy().to_string();
|
||||
let png = |img: &lightcraft_raster::Rgba8| {
|
||||
lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(img), &lightcraft_codecs::EncodeMeta::default()).unwrap_or_default()
|
||||
};
|
||||
let services = Services {
|
||||
pick_folder: Some(Box::new(move || Some(picked.clone()))),
|
||||
png: Some(Box::new(png)),
|
||||
write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))),
|
||||
..Default::default()
|
||||
};
|
||||
let mut h = Headless::new(LightcraftApp::new(session, services), [1200.0, 800.0], 1.0);
|
||||
h.step();
|
||||
|
||||
// File ▸ Open Library…, the Lightroom folder chosen in the folder dialog
|
||||
let r = h.app.run("app.openLibrary", json!({})).unwrap();
|
||||
assert_eq!(r["opened"], false, "{r}");
|
||||
assert_eq!(r["lightroomCatalog"], json!(lrcat.to_string_lossy()), "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
assert!(
|
||||
matches!(&h.app.ui.dialog, Some(Dialog::LightroomCatalog { catalog: Some(c), .. }) if *c == lrcat.to_string_lossy()),
|
||||
"{:?}",
|
||||
h.app.ui.dialog
|
||||
);
|
||||
assert!(has(&h, "button:dialogOk") && has(&h, "button:dialogCancel"));
|
||||
// the library open before is still open, and nothing was made in Lightroom's folder
|
||||
assert_eq!(h.app.session.library.as_ref().map(|l| l.dir.clone()), Some(lib.clone()));
|
||||
assert_eq!(h.app.session.catalog.len(), photos);
|
||||
assert_eq!(names(&lr), before);
|
||||
if let Some(p) = std::env::var_os("LIGHTCRAFT_TEST_SHOTS") {
|
||||
let r = h.request("ui.screenshot", json!({"path": std::path::Path::new(&p).join("prompt-891.png").to_string_lossy(), "headless": true}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
}
|
||||
|
||||
// Cancel: nothing happens
|
||||
assert_eq!(h.request("ui.clickWidget", json!({"id": "button:dialogCancel"}), T)["ok"], true);
|
||||
h.step();
|
||||
assert!(h.app.ui.dialog.is_none());
|
||||
assert!(h.app.lightroom.is_none() && h.app.lightroom_last.is_none());
|
||||
|
||||
// a previews folder given by path (control channel, agents): the same offer
|
||||
let r = h.app.run("app.openLibrary", json!({"path": lr.join("Lightroom Catalog Previews.lrdata").to_string_lossy()})).unwrap();
|
||||
assert_eq!(r["lightroomCatalog"], json!(lrcat.to_string_lossy()), "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
// Import Lightroom Catalog…: the import of that catalog starts (no file dialog), into the
|
||||
// library open now. (This fake catalog then fails to read: only its path matters here.)
|
||||
assert_eq!(h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T)["ok"], true);
|
||||
h.step();
|
||||
assert!(h.app.ui.dialog.is_none());
|
||||
h.step_until(T, |h| h.app.lightroom.is_none());
|
||||
let last = h.app.lightroom_last.clone().unwrap_or_default();
|
||||
assert_eq!(last["path"], json!(lrcat), "{last}");
|
||||
assert_eq!(h.app.session.library.as_ref().map(|l| l.dir.clone()), Some(lib.clone()));
|
||||
assert_eq!(names(&lr), before, "the catalog is only read");
|
||||
drop(h);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
|
@ -114,6 +114,18 @@ are `button:libraryRetry`, `button:libraryChoose`, `button:libraryTemporary` (Co
|
|||
`button:libraryQuit`. A temporary session shows a banner (`indicator:temporarySession`, `button:libraryReopen`) and
|
||||
writes nothing. `app.openLibrary` opening a library ends it.
|
||||
|
||||
### Open Library on a Lightroom catalog
|
||||
|
||||
`app.openLibrary` never makes a LightCraft library inside a Lightroom Classic catalog (issue #891): given the catalog
|
||||
folder (it holds a `*.lrcat`), one of its `*.lrdata` / `*.lrcat-data` folders or `Backups/`, the `.lrcat` itself or
|
||||
its `-wal` / `-shm` / `.lock` files, it keeps the library open now and returns `{opened: false, lightroomCatalog:
|
||||
<the .lrcat found, or null>, path}`; the window shows a dialog whose `button:dialogOk` (Import Lightroom Catalog…, or
|
||||
`ui.dialog.confirm`) runs `file.importLightroom` with that catalog (no catalog found: it asks for one) and
|
||||
`button:dialogCancel` (`ui.dialog.cancel`) does nothing. A folder that is a LightCraft library (`catalog.snap` / `catalog.log`) always
|
||||
opens as one. With no library open (the library problem window) it is an error instead. The engine refuses the same
|
||||
places (`EngineError::LightroomCatalog`, so `lightcraft-cli --library` and `mcp --library` too). `library.importLightroom
|
||||
{status: true}` reports the catalog's `path` after the task ends as well as while it runs.
|
||||
|
||||
## Headless rendering (no window, no GPU)
|
||||
|
||||
The egui UI can be rasterized on the CPU (`crates/ui-egui/src/softpaint.rs`, driven by
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ File → Import Lightroom Catalog… opens `.lrcat` directly. Lightroom, Python,
|
|||
and a C SQLite runtime are not required. The source database is read-only, including committed
|
||||
`-wal` pages; a catalog that changes during the read is rejected. LightCraft owns subsequent edits.
|
||||
|
||||
File → Open Library… given a Lightroom catalog folder (or an `.lrdata` / `.lrcat-data` folder, or the `.lrcat`)
|
||||
doesn't make a LightCraft library there: it offers this import instead, with the catalog found
|
||||
(`crates/engine/src/lightroom_folder.rs`, issue #891). `lightcraft-cli --library` refuses those places too.
|
||||
|
||||
Equivalent commands:
|
||||
|
||||
```text
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,13 @@
|
|||
# What's new in LightCraft
|
||||
|
||||
## 2026-10-11
|
||||
|
||||
### Open Library knows a Lightroom catalog
|
||||
- Choosing a Lightroom Classic catalog folder in File → Open Library… (or one of its `Previews.lrdata` /
|
||||
`.lrcat-data` folders, or the `.lrcat`) no longer makes an empty LightCraft library inside it (issue #891). The
|
||||
library open now stays, and LightCraft says "This is a Lightroom catalog, not a LightCraft library" and offers
|
||||
**Import Lightroom Catalog…** with that catalog, or Cancel. `lightcraft-cli --library` refuses those folders too.
|
||||
|
||||
## 2026-10-10
|
||||
|
||||
### Activity stack
|
||||
|
|
|
|||
Loading…
Reference in New Issue