mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 05:57:09 -06:00
LRC-LIB-CATALOG-SWITCH: a library's catalog.snap or folder given at launch opens that library
Double-clicking a library's catalog.snap in Explorer (or giving it, or the library folder, as an argument) opened the last library used and treated the argument as photos to import. It now opens that library, through the usual launch path (the lock check for one open in another LightCraft), and makes it the library in use. A second library argument gets a notice. Never imported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 64a86cf3098ae4d33cef8c4f4a2ec486cdb148a4)
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
03c88c166c
commit
e47fc37267
@@ -790,7 +790,8 @@ fn open_library_session(in_memory: bool, dir: Option<std::path::PathBuf>, seed_d
|
||||
const HELP: &str = "\
|
||||
lightcraft — photo library + raw developer
|
||||
|
||||
USAGE: lightcraft [OPTIONS] [FILES/FOLDERS…] (files and folders are imported)
|
||||
USAGE: lightcraft [OPTIONS] [FILES/FOLDERS…] (files and folders are imported; a library's catalog.snap or
|
||||
folder opens that library)
|
||||
|
||||
OPTIONS:
|
||||
--library DIR open (or create) this library (env LIGHTCRAFT_LIBRARY; default: the last library opened, or
|
||||
@@ -817,17 +818,32 @@ ENVIRONMENT:
|
||||
|
||||
/// The launch arguments' files and folders to import (absolute), and a notice for each one that
|
||||
/// is not imported (see [`lightcraft_engine::import::launch_path`]; issue #374).
|
||||
fn launch_imports(args: &[String]) -> (Vec<String>, Vec<String>) {
|
||||
fn launch_imports(args: &[String]) -> LaunchArgs {
|
||||
let cwd = std::env::current_dir().ok();
|
||||
let home = std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }).filter(|h| !h.is_empty()).map(std::path::PathBuf::from);
|
||||
launch_imports_in(args, cwd.as_deref(), home.as_deref())
|
||||
}
|
||||
|
||||
fn launch_imports_in(args: &[String], cwd: Option<&std::path::Path>, home: Option<&std::path::Path>) -> (Vec<String>, Vec<String>) {
|
||||
/// What the launch arguments ask for: files and folders to import, a library to open (a library's `catalog.snap`
|
||||
/// or folder, double-clicked in the file manager), and a notice for each argument
|
||||
/// left alone.
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
struct LaunchArgs {
|
||||
files: Vec<String>,
|
||||
library: Option<std::path::PathBuf>,
|
||||
notices: Vec<String>,
|
||||
}
|
||||
|
||||
fn launch_imports_in(args: &[String], cwd: Option<&std::path::Path>, home: Option<&std::path::Path>) -> LaunchArgs {
|
||||
use lightcraft_engine::import::{LaunchPath, launch_path};
|
||||
let (mut files, mut notices) = (Vec::new(), Vec::new());
|
||||
let (mut files, mut notices, mut library) = (Vec::new(), Vec::new(), None);
|
||||
for a in args {
|
||||
match launch_path(a, cwd, home, lightcraft_engine::devices::dcim_in) {
|
||||
Some(LaunchPath::Library(dir)) if library.is_none() => library = Some(std::path::PathBuf::from(dir)),
|
||||
Some(LaunchPath::Library(dir)) => {
|
||||
log::warn!("one library at a time: not opening {dir} given at launch");
|
||||
notices.push(format!("{dir} was not opened: LightCraft opens one library at a time. Open it with File ▸ Open Library…"));
|
||||
}
|
||||
Some(LaunchPath::Import(p)) => files.push(p),
|
||||
Some(LaunchPath::Refused { path, why }) => {
|
||||
log::warn!("not importing {path} given at launch: {why}");
|
||||
@@ -837,7 +853,7 @@ fn launch_imports_in(args: &[String], cwd: Option<&std::path::Path>, home: Optio
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
(files, notices)
|
||||
LaunchArgs { files, library, notices }
|
||||
}
|
||||
|
||||
/// Where the log files live: `logs` in the settings folder, next to `ui.json` (see `logging`).
|
||||
@@ -920,7 +936,10 @@ fn main() -> eframe::Result {
|
||||
}
|
||||
// what the arguments name, resolved now: an empty or relative argument never becomes a drive
|
||||
// root, and a whole drive or home folder is not walked (issue #374)
|
||||
let (files, launch_notices) = launch_imports(&files);
|
||||
let LaunchArgs { files, library: launch_library, notices: launch_notices } = launch_imports(&files);
|
||||
// a library double-clicked (its catalog.snap or folder) is the one that opens, not the last one used
|
||||
let opened_by_argument = launch_library.is_some() && library_dir.is_none();
|
||||
let library_dir = library_dir.or(launch_library);
|
||||
let (prefs, prefs_warning, keep_prefs_file) = load_prefs(in_memory);
|
||||
// the saved language from the start, so a failed start is reported in it too
|
||||
if let Some(ui) = &prefs {
|
||||
@@ -1022,6 +1041,20 @@ fn main() -> eframe::Result {
|
||||
keep_file: keep_prefs_file,
|
||||
..Default::default()
|
||||
};
|
||||
// a library opened from its catalog file is the one in use from now on, as Open Library makes it
|
||||
// (after the snapshot above, so the writer saves it)
|
||||
if opened_by_argument
|
||||
&& problem.is_none()
|
||||
&& let Some(dir) = app.session.library.as_ref().filter(|l| l.on_disk).map(|l| l.dir.to_string_lossy().into_owned())
|
||||
{
|
||||
let settings = &mut app.ui.settings;
|
||||
if settings.library_path != dir && !settings.library_path.is_empty() {
|
||||
let previous = settings.library_path.clone();
|
||||
lightcraft_ui_egui::panels::settings::remember_library(&mut settings.recent_libraries, &previous);
|
||||
}
|
||||
lightcraft_ui_egui::panels::settings::remember_library(&mut settings.recent_libraries, &dir);
|
||||
settings.library_path = dir;
|
||||
}
|
||||
// after the snapshot above, so the writer saves the switched-off preference
|
||||
if let Some(notice) = gpu_crash {
|
||||
app.ui.settings.gpu = false;
|
||||
@@ -1161,17 +1194,44 @@ mod tests {
|
||||
let home = dir("launch");
|
||||
std::fs::create_dir_all(home.join("Trip")).unwrap();
|
||||
let args: Vec<String> = ["", " ", ".", "Trip", &home.to_string_lossy()].iter().map(|s| s.to_string()).collect();
|
||||
let (files, notices) = launch_imports_in(&args, Some(&home), Some(&home));
|
||||
let LaunchArgs { files, notices, library } = launch_imports_in(&args, Some(&home), Some(&home));
|
||||
assert_eq!(library, None);
|
||||
assert_eq!(files, [home.join("Trip").to_string_lossy().to_string()]);
|
||||
assert_eq!(notices.len(), 2, "{notices:?}");
|
||||
assert!(notices.iter().all(|n| n.contains("was not imported")), "{notices:?}");
|
||||
// the working folder is a drive root (a launcher's choice): `.` is never the whole drive
|
||||
let root = home.ancestors().last().unwrap().to_path_buf();
|
||||
let (files, notices) = launch_imports_in(&[".".to_string()], Some(&root), None);
|
||||
let LaunchArgs { files, notices, .. } = launch_imports_in(&[".".to_string()], Some(&root), None);
|
||||
assert!(files.iter().all(|f| f.ends_with("DCIM")) && files.len() + notices.len() == 1, "{files:?} {notices:?}");
|
||||
let _ = std::fs::remove_dir_all(&home);
|
||||
}
|
||||
|
||||
/// A library's catalog.snap (double-clicked in Explorer) or its folder given at
|
||||
/// launch opens that library and is never imported as photos; a second library is left with a notice.
|
||||
#[test]
|
||||
fn a_librarys_catalog_given_at_launch_opens_that_library() {
|
||||
let home = dir("launch-library");
|
||||
let lib = home.join("Wedding LC");
|
||||
let mut s = lightcraft_engine::Session::new().with_fs();
|
||||
s.open_library(&lib, false).unwrap();
|
||||
s.close_library().unwrap();
|
||||
let snap = lib.join("catalog.snap");
|
||||
assert!(snap.is_file(), "a library keeps its catalog.snap");
|
||||
let other = home.join("Other LC");
|
||||
let mut s = lightcraft_engine::Session::new().with_fs();
|
||||
s.open_library(&other, false).unwrap();
|
||||
s.close_library().unwrap();
|
||||
for arg in [snap.to_string_lossy().to_string(), lib.to_string_lossy().to_string(), "Wedding LC/catalog.snap".to_string()] {
|
||||
let got = launch_imports_in(std::slice::from_ref(&arg), Some(&home), Some(&home.join("elsewhere")));
|
||||
assert_eq!(got.library.as_deref(), Some(lib.as_path()), "{arg}");
|
||||
assert!(got.files.is_empty() && got.notices.is_empty(), "{arg}: never imported: {got:?}");
|
||||
}
|
||||
let got = launch_imports_in(&[snap.to_string_lossy().to_string(), other.to_string_lossy().to_string()], Some(&home), None);
|
||||
assert_eq!(got.library.as_deref(), Some(lib.as_path()), "the first one");
|
||||
assert_eq!(got.notices.len(), 1, "{got:?}");
|
||||
let _ = std::fs::remove_dir_all(&home);
|
||||
}
|
||||
|
||||
/// Issue #236: the window answers control requests on its UI thread, so the desktop session
|
||||
/// adds to Apple Photos on a worker (headless sessions wait instead).
|
||||
#[test]
|
||||
|
||||
@@ -471,6 +471,20 @@ pub enum LaunchPath {
|
||||
/// Not imported: `path` (absolute) is a whole drive or the whole home folder, which is never
|
||||
/// walked without being chosen in the app (issue #374).
|
||||
Refused { path: String, why: String },
|
||||
/// Open this (absolute) library folder: the argument was a library's `catalog.snap` or its folder, as a
|
||||
/// double-click on a catalog in the file manager gives. Never imported.
|
||||
Library(String),
|
||||
}
|
||||
|
||||
/// The library folder `path` names: a library's `catalog.snap` (its folder), or a folder holding one. `None` for
|
||||
/// anything else (photos, folders of photos).
|
||||
pub fn library_dir_of(path: &Path) -> Option<PathBuf> {
|
||||
let snapshot = lightcraft_catalog::journal::SNAPSHOT;
|
||||
let named = path.file_name().is_some_and(|n| n.to_string_lossy().eq_ignore_ascii_case(snapshot));
|
||||
if named && path.is_file() {
|
||||
return path.parent().map(Path::to_path_buf);
|
||||
}
|
||||
(path.is_dir() && path.join(snapshot).is_file()).then(|| path.to_path_buf())
|
||||
}
|
||||
|
||||
/// Resolve a launch argument for importing (issue #374). An empty argument is ignored (`None`);
|
||||
@@ -514,6 +528,9 @@ pub fn launch_path(arg: &str, cwd: Option<&Path>, home: Option<&Path>, dcim: imp
|
||||
if home.is_some_and(|h| key(&h.to_string_lossy()) == key(&shown)) {
|
||||
return Some(LaunchPath::Refused { path: shown, why: "the whole home folder".into() });
|
||||
}
|
||||
if let Some(dir) = library_dir_of(&abs) {
|
||||
return Some(LaunchPath::Library(dir.to_string_lossy().to_string()));
|
||||
}
|
||||
Some(LaunchPath::Import(shown))
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user