LRC-LIB-CATALOG-SWITCH: a library missing at launch is reported instead of being made anew with demo photos (#3239)

When the library last opened was missing at launch (its drive or network share not connected yet, or moved), LightCraft created an empty library at that path, seeded it with the demo photos and opened it. The launch plan now marks the library remembered in Settings, and when it holds no library the session opens nothing and the can't-open window offers Try Again, Choose Another Library, Continue Without Saving and Quit. The default location, --library and LIGHTCRAFT_LIBRARY are still created when missing, as before.
This commit is contained in:
Bruno-Alumn
2026-10-29 14:53:12 +01:00
parent 77c0a6adfa
commit 8ca7280e2b
6 changed files with 78 additions and 19 deletions
+14 -3
View File
@@ -253,7 +253,7 @@ pub(crate) fn services(cc: &eframe::CreationContext<'_>, log_file: Option<&std::
pub(crate) fn open_session(in_memory: bool, launch: &Launch, seed_demo: bool) -> (Session, Option<LibraryProblem>) {
let (mut s, problem) = match launch {
Launch::Ask => (Session::new().with_fs().with_default_denoise_models().with_system_clock(), None),
Launch::Open { dir, .. } => open_library_session(in_memory, dir.clone(), seed_demo),
Launch::Open { dir, remembered, .. } => open_library_session(in_memory, dir.clone(), *remembered, seed_demo),
};
s.face_models_dir = lightcraft_engine::config::default_face_models_dir();
s.slide_templates_dir = lightcraft_engine::config::config_dir().map(|d| d.join(lightcraft_engine::slide_template::DIR_NAME));
@@ -267,7 +267,9 @@ pub(crate) fn open_session(in_memory: bool, launch: &Launch, seed_demo: bool) ->
/// If the library can't be opened the session is empty and in memory — never seeded with the
/// demo photos, never written anywhere — and the problem is returned: the window then says so
/// and offers Try Again / Choose Another Library… / Continue Without Saving / Quit (issue #100).
fn open_library_session(in_memory: bool, dir: Option<std::path::PathBuf>, seed_demo: bool) -> (Session, Option<LibraryProblem>) {
/// So is a `remembered` library (the one last opened) that isn't there any more: opening would
/// make a new, empty one in its place, e.g. on an unmounted drive's path (issue #3239).
fn open_library_session(in_memory: bool, dir: Option<std::path::PathBuf>, remembered: bool, seed_demo: bool) -> (Session, Option<LibraryProblem>) {
if in_memory {
return (if seed_demo { Session::with_demo() } else { Session::new() }.with_fs().with_default_denoise_models().with_system_clock(), None);
}
@@ -276,9 +278,18 @@ fn open_library_session(in_memory: bool, dir: Option<std::path::PathBuf>, seed_d
log::warn!("no library location (set --library or LIGHTCRAFT_LIBRARY)");
return (unopened(), Some(LibraryProblem::new("", "There is no home folder to keep the library in. Choose a folder for it.")));
};
if remembered && !lightcraft_engine::library::is_library(&dir) {
log::error!("library {} is missing", dir.display());
let why = if dir.exists() {
"There is no LightCraft library in this folder any more."
} else {
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected."
};
return (unopened(), Some(LibraryProblem::new(dir.to_string_lossy(), why)));
}
let t0 = std::time::Instant::now();
let mut s = Session::new().with_fs().with_default_denoise_models().with_system_clock();
match s.open_library(&dir, seed_demo) {
match s.open_library(&dir, seed_demo && !remembered) {
Ok(r) => {
let (replayed, torn) = (r.replayed, r.torn_bytes);
log::info!(
+30 -1
View File
@@ -29,7 +29,7 @@ fn launch_arguments_never_walk_a_drive_or_the_home_folder() {
/// adds to Apple Photos on a worker (headless sessions wait instead).
#[test]
fn the_desktop_session_adds_to_apple_photos_in_the_background() {
let (s, problem) = open_session(true, &Launch::Open { dir: None, alt_asks: false }, false);
let (s, problem) = open_session(true, &Launch::Open { dir: None, alt_asks: false, remembered: false }, false);
assert!(problem.is_none());
assert!(s.apple_photos_background);
assert!(!lightcraft_engine::Session::new().apple_photos_background);
@@ -63,6 +63,35 @@ fn asking_at_launch_opens_nothing_and_still_saves_the_settings() {
let _ = std::fs::remove_dir_all(&d);
}
/// Issue #3239: the library remembered in Settings whose folder is missing at launch (a disk or
/// network share not connected) is reported in the library problem window. Nothing is created
/// there (not the folder, not its parents) and no demo photos are added.
#[test]
fn a_remembered_library_missing_at_launch_is_reported_not_made_anew() {
let d = dir("missing-library");
let gone = d.join("gone");
let lib = gone.join("Lib");
let launch = Launch::Open { dir: Some(lib.clone()), alt_asks: true, remembered: true };
let (s, problem) = open_session(false, &launch, true);
let problem = problem.expect("the missing library is reported");
assert_eq!(problem.path, lib.to_string_lossy());
assert!(problem.can_retry, "Try Again once the drive is back");
assert!(!gone.exists(), "nothing is created where the library was");
assert!(s.library.is_none() && s.catalog.is_empty(), "no library, no demo photos");
// a remembered folder that is there but holds no library is reported too
std::fs::create_dir_all(&lib).unwrap();
let (s, problem) = open_session(false, &launch, true);
assert!(problem.is_some() && s.library.is_none() && s.catalog.is_empty());
assert!(!lightcraft_engine::library::is_library(&lib), "nothing written in it");
// the default location (or --library) is still made when it's missing, with the demo photos
let fresh = d.join("fresh").join("Lib");
let (s, problem) = open_session(false, &Launch::Open { dir: Some(fresh.clone()), alt_asks: true, remembered: false }, true);
assert!(problem.is_none() && s.library.is_some() && !s.catalog.is_empty(), "{problem:?}");
assert!(lightcraft_engine::library::is_library(&fresh));
drop(s);
let _ = std::fs::remove_dir_all(&d);
}
/// Issue #103: a damaged ui.json (which holds the library location) is kept aside and
/// reported, not silently replaced by the defaults.
#[test]
+8 -4
View File
@@ -274,7 +274,9 @@ pub const ALT_SECONDS: f64 = 2.0;
pub enum Launch {
/// Open this library folder (`None`: there is nowhere to keep one; the problem window says
/// so). `alt_asks`: holding Alt/Option as the window appears offers the recent libraries.
Open { dir: Option<PathBuf>, alt_asks: bool },
/// `remembered`: `dir` is the library last opened (Settings), not the default location, so a
/// missing one is reported instead of created anew (issue #3239).
Open { dir: Option<PathBuf>, alt_asks: bool, remembered: bool },
/// Open nothing yet: the window lists the recent libraries ([`LibraryProblem::choose`]).
Ask,
}
@@ -284,16 +286,18 @@ pub enum Launch {
/// `settings` is `None` without a settings file; `memory` (`--memory`) never asks.
pub fn launch_plan(memory: bool, flag: Option<PathBuf>, env: Option<PathBuf>, settings: Option<&AppSettings>, default: Option<PathBuf>) -> Launch {
if memory {
return Launch::Open { dir: None, alt_asks: false };
return Launch::Open { dir: None, alt_asks: false, remembered: false };
}
if let Some(dir) = flag.or(env) {
return Launch::Open { dir: Some(dir), alt_asks: false };
return Launch::Open { dir: Some(dir), alt_asks: false, remembered: false };
}
if settings.is_some_and(|s| s.at_launch == LaunchLibrary::Ask) {
return Launch::Ask;
}
let last = settings.map(|s| s.library_path.clone()).filter(|p| !p.is_empty()).map(PathBuf::from);
Launch::Open { dir: last.or(default), alt_asks: true }
// the default location is made when it's missing (first launch); a remembered library isn't
let remembered = last.as_ref().is_some_and(|l| default.as_ref() != Some(l));
Launch::Open { dir: last.or(default), alt_asks: true, remembered }
}
/// One library in the launch window's list.
+15 -10
View File
@@ -50,30 +50,35 @@ fn path(s: &str) -> Option<PathBuf> {
Some(PathBuf::from(s))
}
/// `--library`, then `LIGHTCRAFT_LIBRARY`, then the setting; `--memory` never asks.
/// `--library`, then `LIGHTCRAFT_LIBRARY`, then the setting; `--memory` never asks. Only the
/// library remembered in Settings is `remembered` (issue #3239: a missing one is reported, not
/// made anew), and not when it is the default location.
#[test]
fn the_launch_plan_puts_the_flag_and_the_variable_before_the_setting() {
let ask = AppSettings { at_launch: LaunchLibrary::Ask, library_path: "/last".into(), ..AppSettings::default() };
let last = AppSettings { library_path: "/last".into(), recent_libraries: vec!["/last".into(), "/older".into()], ..AppSettings::default() };
let at_default = AppSettings { library_path: "/default".into(), ..AppSettings::default() };
let fresh = AppSettings::default();
let open = |dir: &str, alt_asks| Launch::Open { dir: path(dir), alt_asks };
let open = |dir: &str, alt_asks, remembered| Launch::Open { dir: path(dir), alt_asks, remembered };
// --library, then LIGHTCRAFT_LIBRARY, whatever the setting says; neither asks on Alt
for s in [None, Some(&ask), Some(&last)] {
assert_eq!(launch_plan(false, path("/flag"), path("/env"), s, path("/default")), open("/flag", false));
assert_eq!(launch_plan(false, None, path("/env"), s, path("/default")), open("/env", false));
assert_eq!(launch_plan(false, path("/flag"), path("/env"), s, path("/default")), open("/flag", false, false));
assert_eq!(launch_plan(false, None, path("/env"), s, path("/default")), open("/env", false, false));
}
// Ask me asks, with or without a recent list
assert_eq!(launch_plan(false, None, None, Some(&ask), path("/default")), Launch::Ask);
let ask_empty = AppSettings { at_launch: LaunchLibrary::Ask, ..AppSettings::default() };
assert_eq!(launch_plan(false, None, None, Some(&ask_empty), None), Launch::Ask);
// Open the last library: the last one, else the default place; Alt can still ask
assert_eq!(launch_plan(false, None, None, Some(&last), path("/default")), open("/last", true));
assert_eq!(launch_plan(false, None, None, Some(&fresh), path("/default")), open("/default", true));
assert_eq!(launch_plan(false, None, None, None, path("/default")), open("/default", true));
assert_eq!(launch_plan(false, None, None, None, None), Launch::Open { dir: None, alt_asks: true });
assert_eq!(launch_plan(false, None, None, Some(&last), path("/default")), open("/last", true, true));
assert_eq!(launch_plan(false, None, None, Some(&last), None), open("/last", true, true));
assert_eq!(launch_plan(false, None, None, Some(&at_default), path("/default")), open("/default", true, false));
assert_eq!(launch_plan(false, None, None, Some(&fresh), path("/default")), open("/default", true, false));
assert_eq!(launch_plan(false, None, None, None, path("/default")), open("/default", true, false));
assert_eq!(launch_plan(false, None, None, None, None), Launch::Open { dir: None, alt_asks: true, remembered: false });
// --memory never asks and opens no folder
assert_eq!(launch_plan(true, None, None, Some(&ask), path("/default")), Launch::Open { dir: None, alt_asks: false });
assert_eq!(launch_plan(true, path("/flag"), None, Some(&last), None), Launch::Open { dir: None, alt_asks: false });
assert_eq!(launch_plan(true, None, None, Some(&ask), path("/default")), Launch::Open { dir: None, alt_asks: false, remembered: false });
assert_eq!(launch_plan(true, path("/flag"), None, Some(&last), None), Launch::Open { dir: None, alt_asks: false, remembered: false });
}
/// Each listed library is checked when the window opens; the last one opened is selected.
File diff suppressed because one or more lines are too long
+10
View File
@@ -1,5 +1,15 @@
# What's new in LightCraft
## 2026-10-29
### A library on a disconnected drive is reported at launch, not replaced
- When the library LightCraft last opened is missing at launch, because its drive or network share isn't connected
yet or it was moved, the window now says it couldn't open the library and offers Try Again, Choose Another
Library…, Continue Without Saving and Quit. Before, LightCraft quietly made a new, empty library at that path,
filled it with the demo photos and opened it, so it looked as if the photos and edits were gone. Nothing is
written there now. A library given with `--library` or `LIGHTCRAFT_LIBRARY`, and the default location on first
launch, are still created as before.
## 2026-10-24
### Relative-date filters and smart albums follow the clock