commit
695eb6acc5
|
|
@ -803,6 +803,7 @@
|
|||
"Local folders": "Lokale Ordner",
|
||||
"Local library — no cloud account needed": "Lokale Bibliothek — kein Cloud-Konto erforderlich",
|
||||
"Locate Missing File…": "Fehlende Datei suchen…",
|
||||
"Locate Missing Files…": "Fehlende Dateien suchen…",
|
||||
"Locate Photo": "Foto suchen",
|
||||
"Location": "Ort",
|
||||
"Lock": "Sperren",
|
||||
|
|
|
|||
|
|
@ -781,6 +781,7 @@
|
|||
"Local folders": "Carpetas locales",
|
||||
"Local library — no cloud account needed": "Biblioteca local: no requiere cuenta en la nube",
|
||||
"Locate Missing File…": "Localizar archivo que falta…",
|
||||
"Locate Missing Files…": "Localizar archivos que faltan…",
|
||||
"Locate Photo": "Localizar foto",
|
||||
"Location": "Ubicación",
|
||||
"Lock": "Bloquear",
|
||||
|
|
|
|||
|
|
@ -761,6 +761,7 @@
|
|||
"Local folders": "Dossiers locaux",
|
||||
"Local library — no cloud account needed": "Bibliothèque locale : aucun compte cloud nécessaire",
|
||||
"Locate Missing File…": "Localiser le fichier manquant…",
|
||||
"Locate Missing Files…": "Localiser les fichiers manquants…",
|
||||
"Locate Photo": "Localiser la photo",
|
||||
"Location": "Lieu",
|
||||
"Lock": "Verrouiller",
|
||||
|
|
|
|||
|
|
@ -866,6 +866,7 @@
|
|||
"Local folders": "ローカルフォルダ",
|
||||
"Local library — no cloud account needed": "ローカルのライブラリ(クラウドアカウント不要)",
|
||||
"Locate Missing File…": "見つからないファイルを指定…",
|
||||
"Locate Missing Files…": "見つからないファイルを指定…",
|
||||
"Location": "保存場所",
|
||||
"Long Edge": "長辺",
|
||||
"Looking for photos…": "写真を検索中…",
|
||||
|
|
|
|||
|
|
@ -772,6 +772,7 @@
|
|||
"Local folders": "Pastas locais",
|
||||
"Local library — no cloud account needed": "Biblioteca local — não precisa de conta na nuvem",
|
||||
"Locate Missing File…": "Localizar arquivo ausente…",
|
||||
"Locate Missing Files…": "Localizar arquivos ausentes…",
|
||||
"Locate Photo": "Localizar foto",
|
||||
"Location": "Local",
|
||||
"Lock": "Travar",
|
||||
|
|
|
|||
|
|
@ -637,6 +637,7 @@
|
|||
"Local folders": "Локальные папки",
|
||||
"Local library — no cloud account needed": "Локальная библиотека — учётная запись в облаке не нужна",
|
||||
"Locate Missing File…": "Указать отсутствующий файл…",
|
||||
"Locate Missing Files…": "Указать отсутствующие файлы…",
|
||||
"Locate Photo": "Найти фото",
|
||||
"Location": "Местоположение",
|
||||
"Long Edge": "Длинная сторона",
|
||||
|
|
|
|||
|
|
@ -1024,6 +1024,7 @@
|
|||
"Local folders": "Локальні теки",
|
||||
"Local library — no cloud account needed": "Локальна бібліотека — хмарний обліковий запис не потрібен",
|
||||
"Locate Missing File…": "Знайти відсутній файл…",
|
||||
"Locate Missing Files…": "Знайти відсутні файли…",
|
||||
"Long Edge": "Довгий край",
|
||||
"Looking for photos…": "Пошук фото…",
|
||||
"Luminance Range": "Діапазон яскравості",
|
||||
|
|
|
|||
|
|
@ -858,6 +858,7 @@
|
|||
"Local folders": "本地文件夹",
|
||||
"Local library — no cloud account needed": "本地图库 — 无需云账户",
|
||||
"Locate Missing File…": "定位缺失的文件…",
|
||||
"Locate Missing Files…": "定位缺失的文件…",
|
||||
"Locate Photo": "定位照片",
|
||||
"Location": "位置",
|
||||
"Long Edge": "长边",
|
||||
|
|
|
|||
|
|
@ -858,6 +858,7 @@
|
|||
"Local folders": "本機資料夾",
|
||||
"Local library — no cloud account needed": "本機照片庫,無需雲端帳號",
|
||||
"Locate Missing File…": "尋找遺失的檔案…",
|
||||
"Locate Missing Files…": "尋找遺失的檔案…",
|
||||
"Locate Photo": "尋找照片",
|
||||
"Location": "位置",
|
||||
"Long Edge": "長邊",
|
||||
|
|
|
|||
|
|
@ -1349,9 +1349,16 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
|||
Ok(Value::Null)
|
||||
}
|
||||
"file.findMissing" => {
|
||||
let within = match p.get("within") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::String(path)) if !path.is_empty() => Some(path.clone()),
|
||||
Some(Value::String(_)) => return Some(Err("file.findMissing: `within` must not be empty".into())),
|
||||
Some(value) => return Some(Err(format!("file.findMissing: `within` must be a folder path, not {value}"))),
|
||||
};
|
||||
let title = if within.is_some() { "Locate Missing Files…" } else { "Find Missing Photos…" };
|
||||
let folder = match p.get("folder").and_then(Value::as_str) {
|
||||
Some(f) => Some(f.to_string()),
|
||||
None => match crate::pick::ask(app, id, p, "folder", PickRequest::folder(crate::i18n::tr("Find Missing Photos…")), |s| {
|
||||
None => match crate::pick::ask(app, id, p, "folder", PickRequest::folder(crate::i18n::tr(title)), |s| {
|
||||
s.pick_folder.as_mut().and_then(|f| f()).map(|x| vec![x])
|
||||
}) {
|
||||
Picked::Now(v) => v.into_iter().next(),
|
||||
|
|
@ -1365,7 +1372,10 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
|||
if app.tasks.is_running(LABEL) {
|
||||
return Some(Err("Find Missing Photos is already searching".into()));
|
||||
}
|
||||
let candidates = lightcraft_engine::cmd::missing::find_candidates(&app.session.catalog);
|
||||
let mut candidates = lightcraft_engine::cmd::missing::find_candidates(&app.session.catalog);
|
||||
if let Some(within) = within {
|
||||
candidates.retain(|candidate| lightcraft_catalog::query::folder_within(&candidate.path, &within));
|
||||
}
|
||||
let work = move || lightcraft_engine::cmd::missing::plan_find_missing(&candidates, &folder);
|
||||
let done = |app: &mut LightcraftApp, ctx: &egui::Context, plan: Result<lightcraft_engine::cmd::missing::FindPlan, String>| {
|
||||
// relinked under the session as it is now: photos relinked meanwhile and files
|
||||
|
|
|
|||
|
|
@ -1743,6 +1743,7 @@ fn row_menu(app: &mut LightcraftApp, resp: &egui::Response, n: &FolderNode) {
|
|||
} else if n.path != "/" {
|
||||
resp.context_menu(|ui| {
|
||||
folder_label_menu(app, ui, n);
|
||||
locate_missing_item(app, ui, n);
|
||||
synchronize_item(app, ui, n, &n.name);
|
||||
ui.separator();
|
||||
if ui
|
||||
|
|
@ -1757,6 +1758,19 @@ fn row_menu(app: &mut LightcraftApp, resp: &egui::Response, n: &FolderNode) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Locate Missing Files… of a Folders row: choose where this folder moved, then let the existing
|
||||
/// background finder relink only this folder's photos (including the folders inside it).
|
||||
fn locate_missing_item(app: &mut LightcraftApp, ui: &mut egui::Ui, n: &FolderNode) {
|
||||
let r = ui.button(crate::i18n::tr("Locate Missing Files…"));
|
||||
register(ui.ctx(), "folderLocateMissing", r.rect);
|
||||
if r.clicked() {
|
||||
if let Err(e) = app.run("file.findMissing", json!({"within": n.path})) {
|
||||
app.toast(ui.ctx(), e);
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronize Folder… of a Folders row (widget `folderSynchronize`): opens its dialog, which
|
||||
/// scans the folder in the background.
|
||||
fn synchronize_item(app: &mut LightcraftApp, ui: &mut egui::Ui, n: &FolderNode, name: &str) {
|
||||
|
|
@ -1848,6 +1862,7 @@ fn folder_menu_for_library(app: &mut LightcraftApp, resp: &egui::Response, n: &F
|
|||
ui.close();
|
||||
}
|
||||
folder_label_menu(app, ui, n);
|
||||
locate_missing_item(app, ui, n);
|
||||
synchronize_item(app, ui, n, &label);
|
||||
ui.separator();
|
||||
if ui
|
||||
|
|
|
|||
|
|
@ -1487,6 +1487,10 @@ fn folders_app(paths: &[&str]) -> Headless {
|
|||
}
|
||||
|
||||
fn folders_app_sized(paths: &[&str], size: [f32; 2]) -> Headless {
|
||||
folders_app_with_services(paths, size, Services { png: None, ..Default::default() })
|
||||
}
|
||||
|
||||
fn folders_app_with_services(paths: &[&str], size: [f32; 2], services: Services) -> Headless {
|
||||
use lightcraft_catalog::{Op, Photo, Source};
|
||||
let mut session = lightcraft_engine::Session::new();
|
||||
for path in paths {
|
||||
|
|
@ -1494,7 +1498,7 @@ fn folders_app_sized(paths: &[&str], size: [f32; 2]) -> Headless {
|
|||
let p = Photo::new(id, Source::File { path: (*path).into() }, "x.jpg", "JPEG", 60, 40, "2026-01-01T10:00:00");
|
||||
session.catalog.apply(Op::AddPhoto { photo: Box::new(p) }).unwrap();
|
||||
}
|
||||
let app = LightcraftApp::new(session, Services { png: None, ..Default::default() });
|
||||
let app = LightcraftApp::new(session, services);
|
||||
let mut h = Headless::new(app, size, 1.0);
|
||||
let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
|
|
@ -1699,6 +1703,59 @@ fn right_click_opens_a_folders_menu_from_the_triangle_too() {
|
|||
assert!(!popup_open(&h), "the startup disk offers nothing");
|
||||
}
|
||||
|
||||
/// Issue #1194: a folder's menu chooses the folder's new location, then the bulk finder relinks
|
||||
/// only the missing photos from that folder and the folders inside it.
|
||||
#[test]
|
||||
fn a_folder_menu_locates_its_missing_tree_only() {
|
||||
struct Cleanup(std::path::PathBuf);
|
||||
impl Drop for Cleanup {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
let base = std::env::temp_dir().join(format!("lc-ui-locate-folder-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
let _cleanup = Cleanup(base.clone());
|
||||
let new_trip = base.join("new/trip");
|
||||
std::fs::create_dir_all(new_trip.join("day2")).unwrap();
|
||||
for path in [new_trip.join("a.jpg"), new_trip.join("day2/b.jpg"), new_trip.join("c.jpg")] {
|
||||
std::fs::write(path, b"photo").unwrap();
|
||||
}
|
||||
|
||||
let root = base.to_string_lossy().replace('\\', "/");
|
||||
let old = format!("{root}/old");
|
||||
let old_trip = format!("{old}/trip");
|
||||
let paths = [format!("{old_trip}/a.jpg"), format!("{old_trip}/day2/b.jpg"), format!("{old}/other/c.jpg")];
|
||||
let picked = new_trip.to_string_lossy().to_string();
|
||||
let services = Services { pick_folder: Some(Box::new(move || Some(picked.clone()))), png: None, ..Default::default() };
|
||||
let refs: Vec<&str> = paths.iter().map(String::as_str).collect();
|
||||
let mut h = folders_app_with_services(&refs, [1400.0, 900.0], services);
|
||||
|
||||
click(&mut h, &format!("libraryFolderToggle:{old}"));
|
||||
right_click(&mut h, &format!("source:libfolder:{old_trip}"));
|
||||
assert!(has(&h, "folderLocateMissing"), "the folder menu offers the action");
|
||||
click(&mut h, "folderLocateMissing");
|
||||
assert!(h.step_until(T, |h| h.app.ui.last_find_missing.is_some()), "the background search finished");
|
||||
let result = h.app.ui.last_find_missing.as_ref().unwrap();
|
||||
assert_eq!(result["found"].as_array().map(Vec::len), Some(2), "{result}");
|
||||
assert_eq!(result["missing"], 0, "{result}");
|
||||
|
||||
let sources: Vec<String> = h
|
||||
.app
|
||||
.session
|
||||
.catalog
|
||||
.photos()
|
||||
.filter_map(|photo| match &photo.source {
|
||||
lightcraft_catalog::Source::File { path } => Some(path.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(sources.contains(&new_trip.join("a.jpg").to_string_lossy().to_string()), "{sources:?}");
|
||||
assert!(sources.contains(&new_trip.join("day2/b.jpg").to_string_lossy().to_string()), "{sources:?}");
|
||||
assert!(sources.contains(&format!("{old}/other/c.jpg")), "another missing folder stays untouched: {sources:?}");
|
||||
}
|
||||
|
||||
/// Right-click a folder ▸ Set Color Label ▸ a colour: the folder's row shows the label's dot (a
|
||||
/// row without a label shows none), and None takes it off again.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -715,7 +715,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06-
|
|||
| LRC-LIB-PEOPLE | People view | P2 | 🟡 | `cmd:view.people`, `cmd:view.person`, `cmd:faces.person`, `cmd:faces.unnamed`, `cmd:faces.nameFaces`, `crates/ui-egui/src/panels/people.rs`, `crates/ui-egui/src/panels/person.rs`, `crates/ui-egui/src/panels/unnamed.rs` | Named People cards (face close-up, the photo count on the picture, name) for the photos the active filters let through, with the filter chips shown above them, sized by the thumbnail slider. Below them the **Unnamed faces**: every face nobody has named as a cropped picture, look-alikes next to each other once recognition has looked at them, a suggested name on a face the named ones recognise; click or Shift-click to select, type a name, Enter names the whole group in one undo step. A click on a person opens their page: only cropped faces, then **More** (unnamed faces that look like them, best first; click to confirm, × to hide). Every face is shown by the detector's box where the scan has one, so faces are equally close. Until recognition is set up a line at the top offers the next step (Settings ▸ Faces, or Turn on), as does the loupe's name box. No clusters with separators between groups, no persistent reject, no merge of two people
|
||||
| LRC-LIB-COMMENTS | Comments panel | P2 | ⬜ | | |
|
||||
| LRC-LIB-VISUALSEARCH | Find similar photos | P2 | ✅ | `cmd:library.findSimilar`, `crates/pipeline/src/cull.rs` (`signature`) | photo menu / Photo ▸ Find Similar Photos: look-alike photos (composition and tones), most similar first, as a filter (`only`) |
|
||||
| LRC-LIB-MISSING | Missing files & relink | P1 | ✅ | `cmd:library.missing`, `cmd:photo.relink`, `cmd:library.findMissing`, `cmd:file.findMissing`, `cmd:photo.locate`, `crates/engine/src/cmd/missing.rs` (`checked_path`) | File → Find Missing Photos… (anywhere in a folder: same name, size and content hash; renamed files by size + content hash; same name and size alone only when unambiguous — look-alikes are skipped and reported; searched on a worker thread, one undo step), photo menu → Locate Missing File…; unreadable files show "!" in the grid and a reason in the loupe; undo never moves files. Missing Photos in the sidebar (counted on a worker thread; the view, the Info panel, the grid's context menu and thumbnails read file availability from a cache a worker thread fills, so an offline NAS never stalls a frame); it covers library photos only — Local browse records are never checked, for the count, the view, `library.missing` and Find Missing alike |
|
||||
| LRC-LIB-MISSING | Missing files & relink | P1 | ✅ | `cmd:library.missing`, `cmd:photo.relink`, `cmd:library.findMissing`, `cmd:file.findMissing`, `cmd:photo.locate`, `crates/engine/src/cmd/missing.rs` (`checked_path`), `crates/ui-egui/src/panels/left.rs` (`locate_missing_item`) | File → Find Missing Photos… (anywhere in a folder: same name, size and content hash; renamed files by size + content hash; same name and size alone only when unambiguous — look-alikes are skipped and reported; searched on a worker thread, one undo step), photo menu → Locate Missing File…; a folder or disk row's Locate Missing Files… chooses its new location and scopes the same recursive background finder to that folder and its subfolders. Unreadable files show "!" in the grid and a reason in the loupe; undo never moves files. Missing Photos in the sidebar (counted on a worker thread; the view, the Info panel, the grid's context menu and thumbnails read file availability from a cache a worker thread fills, so an offline NAS never stalls a frame); it covers library photos only — Local browse records are never checked, for the count, the view, `library.missing` and Find Missing alike |
|
||||
| LRC-LIB-CONVERT | Convert to DNG | P2 | ✅ | `cmd:photo.convertToDng`, `crates/engine/src/cmd/convert.rs` | Photo ▸ Convert to DNG: lossless DNG next to the raw with the settings embedded, photo (and its virtual copies) relinked; originals kept; undoable; the DNG is decoded and compared with the raw data, written to a temp file, synced, read back and only then given a free name (never replacing a file) — a failed write leaves no DNG and the photo on its raw (issue #106) |
|
||||
| LRC-LIB-PREVIEWS | Build / discard previews | P1 | ✅ | `cmd:library.buildPreviews`, `cmd:library.previewProgress`, `cmd:library.cancelPreviews`, `cmd:library.clearPreviews`, `crates/engine/src/cmd/previews.rs`, `crates/preview/src/lib.rs` | File ▸ Previews: build standard-sized (Settings → Performance size) or 1:1 previews of the selected / visible photos in the background (a row in the activity stack, ✕ to stop), discard the cache; disk thumbnail + view cache; no smart previews (offline proxies) |
|
||||
| LRC-LIB-SLIDESHOW-IMPROMPTU | Impromptu slideshow | P2 | ✅ | `cmd:view.slideshow` | see LR-VIEW-SLIDESHOW |
|
||||
|
|
@ -804,6 +804,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
|||
| 2026-10-12 | minor | LR-BEHAV-ACCESS: AccessKit tree audit test; text fields named, sliders expose range and Increment / Decrement; status stays 🟡 |
|
||||
| 2026-10-12 | minor | LR-PREF-DISPLAY: on Linux, previews follow the display profile the desktop assigns in colord |
|
||||
| 2026-10-12 | minor | LR-IMP-FORMATS: transparent areas are flattened onto white in linear light instead of rendering black (issue #1212); status unchanged (🟡) |
|
||||
| 2026-10-12 | minor | LRC-LIB-MISSING: folder and disk context menus locate a moved tree recursively without relinking missing photos outside it (issue #1194) |
|
||||
| 2026-10-11 | minor | KEY-SLIDER-TYPE: honor immediate Return/Escape and keep typed keys in the value editor; add regression references |
|
||||
| 2026-10-11 | minor | LR-PROF-CAMERACOLOR: raws with a black-and-white camera JPEG take its tone curve (issue #577) |
|
||||
| 2026-10-11 | minor | LR-VIEW-ZOOM and LR-VIEW-COMPARE: Compare and Reference are sharp at 1:1 (zoom windows per pane, two originals kept decoded while they are open); statuses unchanged |
|
||||
|
|
|
|||
Loading…
Reference in New Issue