mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 14:07:34 -06:00
Merge PR #3322: LRC-LIB-CATALOG-SWITCH: a library missing at launch is reported instead of being made anew with demo photos
docs/parity-checklist.md: the PR's edit to the LRC-LIB-CATALOG-SWITCH row applied to main's row. docs/whats-new.md: the PR's 2026-10-29 section added above main's sections.
This commit is contained in:
@@ -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,14 @@ 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 && let Some(why) = lightcraft_ui_egui::panels::library_problem::missing_library_reason(&dir) {
|
||||
log::error!("library {} can't be opened: {why}", dir.display());
|
||||
// Try Again must not make one there either (an empty mount point, issue #3239)
|
||||
return (unopened(), Some(LibraryProblem { existing_only: true, ..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!(
|
||||
|
||||
@@ -69,7 +69,7 @@ fn a_librarys_catalog_given_at_launch_opens_that_library() {
|
||||
/// 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);
|
||||
@@ -103,6 +103,74 @@ 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!(problem.existing_only, "Try Again makes no library there either");
|
||||
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.as_ref().is_some_and(|p| p.existing_only) && s.library.is_none() && s.catalog.is_empty(), "{problem:?}");
|
||||
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 #3239: a remembered library whose folder can't be looked into (no permission) is
|
||||
/// reported as unreadable at launch, not as gone, and nothing is opened or written there.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn a_remembered_library_it_may_not_read_at_launch_says_so() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let d = dir("denied-library");
|
||||
let lib = d.join("Lib");
|
||||
let mut s = lightcraft_engine::Session::new().with_fs();
|
||||
s.open_library(&lib, false).unwrap();
|
||||
s.close_library().unwrap();
|
||||
drop(s);
|
||||
let names = |dir: &std::path::Path| {
|
||||
let mut names: Vec<_> = std::fs::read_dir(dir).unwrap().flatten().map(|e| e.file_name()).collect();
|
||||
names.sort();
|
||||
names
|
||||
};
|
||||
let before = names(&lib);
|
||||
std::fs::set_permissions(&lib, std::fs::Permissions::from_mode(0o000)).unwrap();
|
||||
if std::fs::read_dir(&lib).is_ok() {
|
||||
std::fs::set_permissions(&lib, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
lightcraft_testkit::skip!(System, "running as root: a folder with mode 000 is still readable");
|
||||
return;
|
||||
}
|
||||
let launch = Launch::Open { dir: Some(lib.clone()), alt_asks: true, remembered: true };
|
||||
let (s, problem) = open_session(false, &launch, true);
|
||||
std::fs::set_permissions(&lib, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let problem = problem.expect("the unreadable library is reported");
|
||||
assert!(problem.error.starts_with("This folder can't be read:"), "{}", problem.error);
|
||||
assert!(problem.error.to_lowercase().contains("permission denied"), "{}", problem.error);
|
||||
assert!(problem.existing_only, "Try Again makes no library there either");
|
||||
assert!(s.library.is_none() && s.catalog.is_empty(), "no library, no demo photos");
|
||||
assert_eq!(names(&lib), before, "nothing written there");
|
||||
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]
|
||||
|
||||
@@ -58,6 +58,22 @@ pub fn is_library(dir: &Path) -> bool {
|
||||
dir.join(lightcraft_catalog::journal::SNAPSHOT).is_file() || dir.join(lightcraft_catalog::journal::LOG).is_file()
|
||||
}
|
||||
|
||||
/// [`is_library`] that tells "no library there" from "can't tell": `Ok(false)` only when the
|
||||
/// folder or its catalog files are definitely missing; a lookup that fails otherwise (a folder it
|
||||
/// may not read, a share that timed out) is the error, so a library that is there but can't be
|
||||
/// read isn't reported as gone (issue #3239).
|
||||
pub fn library_state(dir: &Path) -> std::io::Result<bool> {
|
||||
for name in [lightcraft_catalog::journal::SNAPSHOT, lightcraft_catalog::journal::LOG] {
|
||||
match std::fs::metadata(dir.join(name)) {
|
||||
Ok(m) if m.is_file() => return Ok(true),
|
||||
Ok(_) => {}
|
||||
Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory) => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// The default library location: `$LIGHTCRAFT_LIBRARY` if set, else `~/Pictures/LightCraft Library`
|
||||
/// (`%USERPROFILE%\Pictures\LightCraft Library` on Windows).
|
||||
pub fn default_dir() -> Option<PathBuf> {
|
||||
|
||||
@@ -1020,6 +1020,9 @@
|
||||
"LightCraft could not start": "LightCraft se nepodařilo spustit",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft nedokázal najít místo pro uložení knihovny.",
|
||||
"LightCraft couldn't open the library at": "LightCraft nedokázal otevřít knihovnu v umístění",
|
||||
"There is no LightCraft library in this folder any more.": "V této složce už žádná knihovna LightCraft není.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "Knihovna tu není: byla přesunuta nebo přejmenována, nebo není připojen její disk či síťové úložiště.",
|
||||
"This folder can't be read:": "Tuto složku nelze přečíst:",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft tento model nezná, a proto předpokládá:",
|
||||
"LightCraft on GitHub": "LightCraft na GitHubu",
|
||||
"LightCraft website": "Web LightCraft",
|
||||
|
||||
@@ -801,6 +801,9 @@
|
||||
"LightCraft could not start": "LightCraft konnte nicht starten",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft konnte keinen Speicherort für deine Bibliothek finden.",
|
||||
"LightCraft couldn't open the library at": "LightCraft konnte die Bibliothek nicht öffnen unter",
|
||||
"There is no LightCraft library in this folder any more.": "In diesem Ordner ist keine LightCraft-Bibliothek mehr.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "Die Bibliothek ist nicht da: Sie wurde verschoben oder umbenannt, oder ihr Laufwerk bzw. ihre Netzwerkfreigabe ist nicht verbunden.",
|
||||
"This folder can't be read:": "Dieser Ordner kann nicht gelesen werden:",
|
||||
"LightCraft no longer crashes at launch on Windows PCs whose Vulkan driver is broken (issue #136, e.g. some Intel UHD": "LightCraft stürzt beim Start auf Windows-PCs mit defektem Vulkan-Treiber nicht mehr ab (Issue #136, etwa bestimmte Intel-UHD-630-Treiber).",
|
||||
"LightCraft on GitHub": "LightCraft auf GitHub",
|
||||
"LightCraft website": "LightCraft-Website",
|
||||
|
||||
@@ -806,6 +806,9 @@
|
||||
"LightCraft could not start": "No se pudo iniciar LightCraft",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft no encontró dónde guardar la biblioteca.",
|
||||
"LightCraft couldn't open the library at": "LightCraft no pudo abrir la biblioteca en",
|
||||
"There is no LightCraft library in this folder any more.": "Ya no hay ninguna biblioteca de LightCraft en esta carpeta.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "La biblioteca no está: se movió o se le cambió el nombre, o su disco o carpeta de red no está conectado.",
|
||||
"This folder can't be read:": "No se puede leer esta carpeta:",
|
||||
"LightCraft on GitHub": "LightCraft en GitHub",
|
||||
"LightCraft website": "Sitio web de LightCraft",
|
||||
"LightCraft — Second Window": "LightCraft: segunda ventana",
|
||||
|
||||
@@ -1040,6 +1040,9 @@
|
||||
"LightCraft could not start": "LightCraft ei voinut käynnistyä",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft ei löytänyt paikkaa kirjastosi säilytykselle.",
|
||||
"LightCraft couldn't open the library at": "LightCraft ei voinut avata kirjastoa kohteessa",
|
||||
"There is no LightCraft library in this folder any more.": "Tässä kansiossa ei ole enää LightCraft-kirjastoa.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "Kirjastoa ei löydy: se on siirretty tai nimetty uudelleen, tai sen asemaa tai verkkojakoa ei ole yhdistetty.",
|
||||
"This folder can't be read:": "Tätä kansiota ei voi lukea:",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft ei tunne tätä mallia, joten se oletti:",
|
||||
"LightCraft on GitHub": "LightCraft GitHubissa",
|
||||
"LightCraft stopped": "LightCraft pysähtyi",
|
||||
|
||||
@@ -759,6 +759,8 @@
|
||||
"LightCraft could not start": "LightCraft n'a pas pu démarrer",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft n'a pas trouvé où conserver votre bibliothèque.",
|
||||
"LightCraft couldn't open the library at": "LightCraft n'a pas pu ouvrir la bibliothèque à l'emplacement",
|
||||
"There is no LightCraft library in this folder any more.": "Ce dossier ne contient plus de bibliothèque LightCraft.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "La bibliothèque est introuvable : elle a été déplacée ou renommée, ou son disque ou son partage réseau n'est pas connecté.",
|
||||
"LightCraft on GitHub": "LightCraft sur GitHub",
|
||||
"LightCraft website": "Site web de LightCraft",
|
||||
"LightCraft — Second Window": "LightCraft : deuxième fenêtre",
|
||||
|
||||
@@ -1041,6 +1041,9 @@
|
||||
"LightCraft could not start": "LightCraft gagal dijalankan",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft tidak dapat menemukan lokasi untuk menyimpan pustaka Anda.",
|
||||
"LightCraft couldn't open the library at": "LightCraft tidak dapat membuka pustaka di",
|
||||
"There is no LightCraft library in this folder any more.": "Folder ini tidak lagi berisi pustaka LightCraft.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "Pustaka tidak ada di sini: pustaka dipindahkan atau diganti namanya, atau drive atau berbagi jaringannya tidak terhubung.",
|
||||
"This folder can't be read:": "Folder ini tidak dapat dibaca:",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft tidak mengenali model ini, jadi menggunakan asumsi berikut:",
|
||||
"LightCraft on GitHub": "LightCraft di GitHub",
|
||||
"LightCraft stopped": "LightCraft berhenti",
|
||||
|
||||
@@ -761,6 +761,9 @@
|
||||
"LightCraft could not start": "Impossibile avviare LightCraft",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft non ha trovato dove salvare la tua libreria.",
|
||||
"LightCraft couldn't open the library at": "LightCraft non è riuscito ad aprire la libreria in",
|
||||
"There is no LightCraft library in this folder any more.": "In questa cartella non c'è più una libreria LightCraft.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "La libreria non c'è: è stata spostata o rinominata, oppure il suo disco o la sua condivisione di rete non è collegata.",
|
||||
"This folder can't be read:": "Impossibile leggere questa cartella:",
|
||||
"LightCraft on GitHub": "LightCraft su GitHub",
|
||||
"LightCraft website": "Sito web di LightCraft",
|
||||
"LightCraft — Second Window": "LightCraft: seconda finestra",
|
||||
|
||||
@@ -1634,6 +1634,9 @@
|
||||
"Your library couldn't be opened": "ライブラリを開けませんでした",
|
||||
"LightCraft couldn't find where to keep your library.": "ライブラリの保存場所が見つかりませんでした。",
|
||||
"LightCraft couldn't open the library at": "次の場所にあるライブラリを開けませんでした:",
|
||||
"There is no LightCraft library in this folder any more.": "このフォルダーには LightCraft ライブラリがもうありません。",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "ライブラリが見つかりません。移動されたか名前が変更されたか、ドライブまたはネットワーク共有が接続されていません。",
|
||||
"This folder can't be read:": "このフォルダーを読み取れません:",
|
||||
"Try Again": "再試行",
|
||||
"Choose Another Library…": "別のライブラリを選択…",
|
||||
"Continue Without Saving": "保存せずに続行",
|
||||
|
||||
@@ -1120,6 +1120,9 @@
|
||||
"LightCraft could not start": "LightCraft ვერ გაეშვა",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft-მა ვერ იპოვა, სად შეინახოს თქვენი ბიბლიოთეკა.",
|
||||
"LightCraft couldn't open the library at": "LightCraft-მა ვერ გახსნა ბიბლიოთეკა მისამართზე",
|
||||
"There is no LightCraft library in this folder any more.": "ამ საქაღალდეში LightCraft-ის ბიბლიოთეკა აღარ არის.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "ბიბლიოთეკა აქ არ არის: ის გადაიტანეს ან გადაარქვეს სახელი, ან მისი დისკი ან ქსელური საქაღალდე არ არის მიერთებული.",
|
||||
"This folder can't be read:": "ამ საქაღალდის წაკითხვა ვერ ხერხდება:",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft-მა ეს მოდელი არ იცის, ამიტომ ივარაუდა:",
|
||||
"LightCraft no longer crashes at launch on Windows PCs whose Vulkan driver is broken (issue #136, e.g. some Intel UHD": "LightCraft აღარ ითიშება გაშვებისას Windows კომპიუტერებზე, რომელთა Vulkan დრაივერი გაუმართავია (ხარვეზი #136, მაგ. ზოგიერთი Intel UHD",
|
||||
"LightCraft on GitHub": "LightCraft GitHub-ზე",
|
||||
|
||||
@@ -743,6 +743,9 @@
|
||||
"LightCraft could not start": "Nie można uruchomić LightCraft",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft nie znalazł miejsca na przechowywanie biblioteki.",
|
||||
"LightCraft couldn't open the library at": "LightCraft nie mógł otworzyć biblioteki w lokalizacji",
|
||||
"There is no LightCraft library in this folder any more.": "W tym folderze nie ma już biblioteki LightCraft.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "Biblioteki tu nie ma: została przeniesiona lub zmieniono jej nazwę albo jej dysk lub udział sieciowy nie jest podłączony.",
|
||||
"This folder can't be read:": "Nie można odczytać tego folderu:",
|
||||
"LightCraft on GitHub": "LightCraft w serwisie GitHub",
|
||||
"LightCraft website": "Strona internetowa LightCraft",
|
||||
"LightCraft — Second Window": "LightCraft — drugie okno",
|
||||
|
||||
@@ -770,6 +770,9 @@
|
||||
"LightCraft could not start": "Não foi possível iniciar o LightCraft",
|
||||
"LightCraft couldn't find where to keep your library.": "O LightCraft não conseguiu encontrar onde guardar sua biblioteca.",
|
||||
"LightCraft couldn't open the library at": "O LightCraft não conseguiu abrir a biblioteca em",
|
||||
"There is no LightCraft library in this folder any more.": "Esta pasta não tem mais uma biblioteca do LightCraft.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "A biblioteca não está aqui: foi movida ou renomeada, ou o disco ou compartilhamento de rede dela não está conectado.",
|
||||
"This folder can't be read:": "Não é possível ler esta pasta:",
|
||||
"LightCraft on GitHub": "LightCraft no GitHub",
|
||||
"LightCraft website": "Site do LightCraft",
|
||||
"LightCraft — Second Window": "LightCraft — segunda janela",
|
||||
|
||||
@@ -831,6 +831,9 @@
|
||||
"LightCraft could not start": "Не удалось запустить LightCraft",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft не удалось найти место для хранения вашей библиотеки.",
|
||||
"LightCraft couldn't open the library at": "LightCraft не удалось открыть библиотеку в",
|
||||
"There is no LightCraft library in this folder any more.": "В этой папке больше нет библиотеки LightCraft.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "Библиотеки здесь нет: её переместили или переименовали, либо её диск или сетевая папка не подключены.",
|
||||
"This folder can't be read:": "Не удаётся прочитать эту папку:",
|
||||
"LightCraft on GitHub": "LightCraft на GitHub",
|
||||
"LightCraft website": "Сайт LightCraft",
|
||||
"LightCraft — Second Window": "LightCraft — второе окно",
|
||||
|
||||
@@ -1137,6 +1137,9 @@
|
||||
"LightCraft could not start": "LightCraft kunde inte starta",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft hittade ingen plats att spara ditt bibliotek på.",
|
||||
"LightCraft couldn't open the library at": "LightCraft kunde inte öppna biblioteket på",
|
||||
"There is no LightCraft library in this folder any more.": "Det finns inte längre något LightCraft-bibliotek i den här mappen.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "Biblioteket finns inte där: det har flyttats eller bytt namn, eller så är dess enhet eller nätverksresurs inte ansluten.",
|
||||
"This folder can't be read:": "Den här mappen kan inte läsas:",
|
||||
"LightCraft Default": "LightCraft-standard",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft känner inte till den här modellen och antog därför:",
|
||||
"LightCraft Help": "LightCraft-hjälp",
|
||||
|
||||
@@ -1629,6 +1629,9 @@
|
||||
"Your library couldn't be opened": "Не вдалося відкрити вашу бібліотеку",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft не вдалося знайти місце для бібліотеки.",
|
||||
"LightCraft couldn't open the library at": "LightCraft не вдалося відкрити бібліотеку в",
|
||||
"There is no LightCraft library in this folder any more.": "У цій папці більше немає бібліотеки LightCraft.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "Бібліотеки тут немає: її перемістили чи перейменували, або її диск чи мережеву папку не підключено.",
|
||||
"This folder can't be read:": "Не вдається прочитати цю папку:",
|
||||
"Try Again": "Спробувати знову",
|
||||
"Choose Another Library…": "Вибрати іншу бібліотеку…",
|
||||
"Continue Without Saving": "Продовжити без збереження",
|
||||
|
||||
@@ -958,6 +958,9 @@
|
||||
"LightCraft could not start": "LightCraft không thể khởi động",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft không tìm được nơi lưu thư viện của bạn.",
|
||||
"LightCraft couldn't open the library at": "LightCraft không mở được thư viện tại",
|
||||
"There is no LightCraft library in this folder any more.": "Thư mục này không còn thư viện LightCraft nào.",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "Thư viện không có ở đây: nó đã bị di chuyển hoặc đổi tên, hoặc ổ đĩa hay ổ mạng của nó chưa được kết nối.",
|
||||
"This folder can't be read:": "Không đọc được thư mục này:",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft không biết mô hình này, nên đã giả định:",
|
||||
"LightCraft no longer crashes at launch on Windows PCs whose Vulkan driver is broken (issue #136, e.g. some Intel UHD": "LightCraft không còn sự cố khi khởi động trên PC Windows có driver Vulkan lỗi (issue #136, ví dụ một số Intel UHD",
|
||||
"LightCraft on GitHub": "LightCraft trên GitHub",
|
||||
|
||||
@@ -867,6 +867,9 @@
|
||||
"LightCraft Website": "LightCraft 网站",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft 找不到保存图库的位置。",
|
||||
"LightCraft couldn't open the library at": "LightCraft 无法打开以下位置的图库:",
|
||||
"There is no LightCraft library in this folder any more.": "此文件夹中已经没有 LightCraft 图库。",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "图库不在这里:它已被移动或重命名,或者它所在的驱动器或网络共享未连接。",
|
||||
"This folder can't be read:": "无法读取此文件夹:",
|
||||
"LightCraft on GitHub": "GitHub 上的 LightCraft",
|
||||
"LightCraft website": "LightCraft 网站",
|
||||
"LightCraft — Second Window": "LightCraft — 第二窗口",
|
||||
|
||||
@@ -868,6 +868,9 @@
|
||||
"LightCraft Website": "LightCraft 網站",
|
||||
"LightCraft couldn't find where to keep your library.": "LightCraft 找不到儲存照片庫的位置。",
|
||||
"LightCraft couldn't open the library at": "LightCraft 無法開啟此位置的照片庫:",
|
||||
"There is no LightCraft library in this folder any more.": "這個資料夾中已經沒有 LightCraft 照片庫。",
|
||||
"The library isn't there: it was moved or renamed, or its drive or network share isn't connected.": "照片庫不在這裡:它已被移動或重新命名,或是它所在的磁碟機或網路共用資料夾未連線。",
|
||||
"This folder can't be read:": "無法讀取這個資料夾:",
|
||||
"LightCraft on GitHub": "GitHub 上的 LightCraft",
|
||||
"LightCraft website": "LightCraft 網站",
|
||||
"LightCraft — Second Window": "LightCraft — 第二視窗",
|
||||
|
||||
@@ -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.
|
||||
@@ -341,6 +345,11 @@ pub struct LibraryProblem {
|
||||
/// Try Again reopens `path` as a library folder (`false` in the browser, whose storage isn't
|
||||
/// a folder: reloading the page tries again).
|
||||
pub can_retry: bool,
|
||||
/// Try Again opens `path` only once it holds a library again, and never makes one there: set
|
||||
/// for the library remembered in Settings that was missing at launch, whose folder can be
|
||||
/// the empty mount point of a share that isn't connected yet (issue #3239). Choose Another
|
||||
/// Library… still creates one where asked.
|
||||
pub existing_only: bool,
|
||||
/// The window lists the recent libraries to choose from instead (issue #1290); `error` then
|
||||
/// says why the last one tried didn't open.
|
||||
pub chooser: Option<Chooser>,
|
||||
@@ -710,6 +719,37 @@ fn unavailable(path: &str) -> Option<Unavailable> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Why the library at `dir` that has to exist already (a remembered one, issue #3239) can't be
|
||||
/// opened (`None`: it's there): the folder is there without a catalog, or the folder itself is
|
||||
/// missing. A folder that can't be looked into (no permission, a share that timed out) says so
|
||||
/// instead: the library may well be there.
|
||||
pub fn missing_library_reason(dir: &Path) -> Option<String> {
|
||||
match lightcraft_engine::library::library_state(dir) {
|
||||
Ok(true) => None,
|
||||
Ok(false) if dir.exists() => Some(crate::i18n::tr("There is no LightCraft library in this folder any more.").to_string()),
|
||||
Ok(false) => {
|
||||
Some(crate::i18n::tr("The library isn't there: it was moved or renamed, or its drive or network share isn't connected.").to_string())
|
||||
}
|
||||
Err(e) => Some(format!("{} {e}", crate::i18n::tr("This folder can't be read:"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try Again: reopen the problem's library. One that must exist already (`existing_only`) is
|
||||
/// opened only once its folder holds a library again (a share remounted): until then the window
|
||||
/// stays with the reason, and nothing is created there (issue #3239).
|
||||
fn retry(app: &mut LightcraftApp, problem: LibraryProblem) {
|
||||
if problem.existing_only
|
||||
&& let Some(why) = missing_library_reason(Path::new(&problem.path))
|
||||
{
|
||||
if let Some(p) = app.library_problem.as_mut() {
|
||||
p.error = why;
|
||||
p.dismissed = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
open(app, Some(problem.path));
|
||||
}
|
||||
|
||||
/// What the user chose.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Choice {
|
||||
@@ -807,6 +847,10 @@ fn open(app: &mut LightcraftApp, path: Option<String>) {
|
||||
}
|
||||
}
|
||||
if let Some(path) = path {
|
||||
// another library failed: Try Again may make that one, as it always could
|
||||
if p.path != path {
|
||||
p.existing_only = false;
|
||||
}
|
||||
p.path = path;
|
||||
}
|
||||
p.error = e;
|
||||
@@ -1073,7 +1117,7 @@ fn act(app: &mut LightcraftApp, choice: Choice) {
|
||||
let Some(problem) = app.library_problem.clone() else { return };
|
||||
let chosen = problem.chooser.as_ref().and_then(|c| c.selected.and_then(|i| c.libraries.get(i)).map(|l| (l.clone(), c.over)));
|
||||
match choice {
|
||||
Choice::Retry => open(app, Some(problem.path)),
|
||||
Choice::Retry => retry(app, problem),
|
||||
Choice::Choose => open(app, None),
|
||||
Choice::Open => {
|
||||
let Some((library, over)) = chosen.filter(|(l, _)| l.unavailable.is_none()) else { return };
|
||||
|
||||
@@ -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.
|
||||
@@ -1027,3 +1032,103 @@ fn found_again(multipass: bool) {
|
||||
drop(h);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Issue #3239: Try Again on a remembered library that was missing at launch makes no library in
|
||||
/// its folder when the folder is there but empty (a share's mount point before the share is
|
||||
/// connected): the window stays and says so. Once the folder holds the library again, it opens.
|
||||
#[test]
|
||||
fn try_again_on_a_missing_remembered_library_makes_nothing_in_an_empty_folder() {
|
||||
let dir = temp("retry-existing");
|
||||
let mount = dir.join("Share");
|
||||
std::fs::create_dir_all(&mount).unwrap();
|
||||
let path = mount.to_string_lossy().into_owned();
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::new().with_fs(), Services::default());
|
||||
app.library_problem = Some(LibraryProblem { existing_only: true, ..LibraryProblem::new(path.clone(), "missing") });
|
||||
let mut h = Headless::new(app, [1300.0, 900.0], 1.0);
|
||||
settle(&mut h);
|
||||
assert_eq!(h.request("ui.clickWidget", json!({"id": "button:libraryRetry"}), T)["ok"], true);
|
||||
settle(&mut h);
|
||||
assert!(!lightcraft_engine::library::is_library(&mount), "no catalog is made in the empty folder");
|
||||
assert_eq!(std::fs::read_dir(&mount).unwrap().count(), 0, "nothing is written there");
|
||||
assert!(h.app.session.library.is_none());
|
||||
let p = h.app.library_problem.as_ref().expect("the window stays");
|
||||
assert_eq!(p.error, "There is no LightCraft library in this folder any more.");
|
||||
assert!(has(&h, "button:libraryRetry"));
|
||||
// the share is connected again: Try Again opens the library in it
|
||||
make_library(&mount);
|
||||
assert_eq!(h.request("ui.clickWidget", json!({"id": "button:libraryRetry"}), T)["ok"], true);
|
||||
settle(&mut h);
|
||||
assert_eq!(h.app.session.library.as_ref().map(|l| l.dir.to_string_lossy().into_owned()), Some(path));
|
||||
assert!(h.app.library_problem.is_none());
|
||||
drop(h);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Issue #3239: why Try Again didn't open a remembered library is in the interface language,
|
||||
/// both when its folder is empty and when the folder itself is gone.
|
||||
#[test]
|
||||
fn try_again_says_why_the_remembered_library_is_missing_in_the_interface_language() {
|
||||
let dir = temp("retry-lang");
|
||||
let mount = dir.join("Share");
|
||||
std::fs::create_dir_all(&mount).unwrap();
|
||||
let path = mount.to_string_lossy().into_owned();
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::new().with_fs(), Services::default());
|
||||
app.ui.language = crate::i18n::Locale::De;
|
||||
app.library_problem = Some(LibraryProblem { existing_only: true, ..LibraryProblem::new(path, "missing") });
|
||||
let mut h = Headless::new(app, [1300.0, 900.0], 1.0);
|
||||
settle(&mut h);
|
||||
assert_eq!(h.request("ui.clickWidget", json!({"id": "button:libraryRetry"}), T)["ok"], true);
|
||||
settle(&mut h);
|
||||
let error = h.app.library_problem.as_ref().map(|p| p.error.clone()).unwrap_or_default();
|
||||
assert_eq!(error, "In diesem Ordner ist keine LightCraft-Bibliothek mehr.");
|
||||
std::fs::remove_dir(&mount).unwrap();
|
||||
assert_eq!(h.request("ui.clickWidget", json!({"id": "button:libraryRetry"}), T)["ok"], true);
|
||||
settle(&mut h);
|
||||
let error = h.app.library_problem.as_ref().map(|p| p.error.clone()).unwrap_or_default();
|
||||
crate::i18n::set_language(crate::i18n::Locale::En);
|
||||
assert!(error.starts_with("Die Bibliothek ist nicht da:"), "{error}");
|
||||
assert!(!mount.exists(), "nothing is made where it was");
|
||||
drop(h);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Issue #3239: a remembered library whose folder can't be looked into (no permission) isn't
|
||||
/// reported as gone: Try Again says the folder can't be read, and nothing is written there.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn try_again_on_a_remembered_library_it_may_not_read_says_so() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = temp("retry-denied");
|
||||
let locked = dir.join("Locked");
|
||||
let path = make_library(&locked);
|
||||
let names = |dir: &Path| {
|
||||
let mut names: Vec<_> = std::fs::read_dir(dir).unwrap().flatten().map(|e| e.file_name()).collect();
|
||||
names.sort();
|
||||
names
|
||||
};
|
||||
let before = names(&locked);
|
||||
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
|
||||
let readable = std::fs::read_dir(&locked).is_ok();
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::new().with_fs(), Services::default());
|
||||
app.library_problem = Some(LibraryProblem { existing_only: true, ..LibraryProblem::new(path, "missing") });
|
||||
let mut h = Headless::new(app, [1300.0, 900.0], 1.0);
|
||||
settle(&mut h);
|
||||
if !readable {
|
||||
assert_eq!(h.request("ui.clickWidget", json!({"id": "button:libraryRetry"}), T)["ok"], true);
|
||||
settle(&mut h);
|
||||
}
|
||||
let error = h.app.library_problem.as_ref().map(|p| p.error.clone()).unwrap_or_default();
|
||||
let library = h.app.session.library.is_some();
|
||||
drop(h);
|
||||
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
if readable {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
lightcraft_testkit::skip!(System, "running as root: a folder with mode 000 is still readable");
|
||||
return;
|
||||
}
|
||||
assert!(error.starts_with("This folder can't be read:"), "{error}");
|
||||
assert!(error.to_lowercase().contains("permission denied"), "{error}");
|
||||
assert!(!library, "nothing opened");
|
||||
assert_eq!(names(&locked), before, "nothing written there");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,18 @@
|
||||
# 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, and Try Again opens the library once its drive or share is back: on an empty folder left in
|
||||
its place, such as a share's mount point, it says so again instead of making a new library there. A library
|
||||
folder LightCraft isn't allowed to read is reported as unreadable, with the system's reason, not as gone. A
|
||||
library given with `--library` or `LIGHTCRAFT_LIBRARY`, and the default location on first launch, are still
|
||||
created as before.
|
||||
|
||||
## 2026-10-28
|
||||
|
||||
### Local's standard places can't be renamed by mistake
|
||||
|
||||
Reference in New Issue
Block a user