#2114: hidden Local locations come back one at a time

"Show N hidden locations" put every location removed from Local back at
once. With several hidden, the row now lists them (name, path on hover):
each comes back on its own, or all with Restore All. With one hidden, the
row restores it directly, as before. The command side already had
local.restoreHidden {path}; only the panel called it without a path.

The headless test hides a second place, opens the list from the row,
restores one, checks the other stays hidden, then restores the rest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
This commit is contained in:
Claude
2026-10-19 11:29:48 +00:00
parent d999e382ba
commit e2ea657668
4 changed files with 76 additions and 8 deletions
+32
View File
@@ -2833,8 +2833,39 @@ mod tests {
assert!(restore[1] >= 0.0 && restore[1] + restore[3] <= f64::from(size[1]), "the restore row is in view: {restore:?} {after_ids:?}");
assert!(h.app.session.browse.is_some(), "hiding does not stop browsing");
assert!(dir.is_dir(), "the folder itself is untouched");
// a second hidden place: the row lists them, and each comes back on its own (issue #2114:
// the row used to bring every hidden place back at once)
let dir2 = std::env::temp_dir().join(format!("lc-ui-hide-{}-b", std::process::id()));
let _ = std::fs::remove_dir_all(&dir2);
std::fs::create_dir_all(&dir2).unwrap();
let path2 = dir2.to_string_lossy().to_string();
for (command, params) in [("local.addRoot", json!({"path": path2})), ("local.hide", json!({"path": path2}))] {
let r = h.request("engine.execute", json!({"command": command, "params": params}), t);
assert_eq!(r["ok"], true, "{r}");
}
h.settle(SETTLE);
let menu = |h: &mut Headless| -> Vec<String> {
let w = h.request("ui.widgets", json!({"filter": "local:restoreHidden:"}), t);
w["result"].as_array().unwrap().iter().map(|x| x["id"].as_str().unwrap().to_string()).collect()
};
assert!(menu(&mut h).is_empty(), "the list opens from the row");
assert_eq!(h.request("ui.clickWidget", json!({"id": "source:local:restoreHidden"}), t)["ok"], true);
h.settle(SETTLE);
let items = menu(&mut h);
assert!(items.contains(&format!("local:restoreHidden:{path}")) && items.contains(&"local:restoreHidden:all".to_string()), "{items:?}");
assert_eq!(h.request("ui.clickWidget", json!({"id": format!("local:restoreHidden:{path}")}), t)["ok"], true);
h.settle(SETTLE);
assert!(!h.app.ui.hidden_locations.iter().any(|x| x == &path), "{:?}", h.app.ui.hidden_locations);
assert!(h.app.ui.hidden_locations.iter().any(|x| x == &path2), "the other place stays hidden");
assert!(ids(&mut h).iter().any(|i| i == "source:local:restoreHidden"), "still something to restore");
// one place left: the row brings it back itself; several: Restore All
assert_eq!(h.request("ui.clickWidget", json!({"id": "source:local:restoreHidden"}), t)["ok"], true);
h.settle(SETTLE);
if !h.app.ui.hidden_locations.is_empty() {
assert!(hidden_before, "{:?}", h.app.ui.hidden_locations);
assert_eq!(h.request("ui.clickWidget", json!({"id": "local:restoreHidden:all"}), t)["ok"], true);
h.settle(SETTLE);
}
assert!(h.app.ui.hidden_locations.is_empty());
assert!(!ids(&mut h).iter().any(|i| i == "source:local:restoreHidden"), "nothing left to restore");
if !hidden_before {
@@ -2842,6 +2873,7 @@ mod tests {
assert!(ids(&mut h).contains(&format!("source:local:{path}")));
}
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_dir_all(&dir2);
}
/// Local: a kept folder stays a root while its subfolders (and other locations) are
+37 -7
View File
@@ -793,6 +793,8 @@ fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize {
struct LocalNow {
places: LocalPlaces,
current: Option<String>,
/// The places removed from Local, as (name, path), in the order they were hidden.
hidden: Vec<(String, String)>,
}
/// Folders on this computer to browse without adding (Lightroom's Local): Pictures, Desktop,
@@ -817,11 +819,14 @@ fn local_now(app: &mut LightcraftApp, ui: &egui::Ui) -> Option<LocalNow> {
}
let browsing = app.session.browse.clone().filter(|_| app.session.source == LibrarySource::Folder);
let current = browsing.as_ref().map(|b| b.path.clone());
// a hidden built-in place keeps its name (Home, not the user's folder name)
let name_of = |path: &str| builtin.iter().find(|(_, p)| same_folder(p, path)).map_or_else(|| folder_label(path), |(n, _)| n.clone());
let hidden = app.ui.hidden_locations.iter().map(|h| (name_of(h), h.clone())).collect();
let places = local_places(builtin, &app.ui.local_roots, current.as_deref(), app.ui.local_browse_root.as_deref(), &app.ui.hidden_locations);
if current.is_some() {
app.ui.local_browse_root = places.browse_root.clone();
}
Some(LocalNow { places, current })
Some(LocalNow { places, current, hidden })
}
/// Local's rows: each place as a folder tree, Browse Folder…, and the way back for hidden places.
@@ -846,18 +851,43 @@ fn local_rows(app: &mut LightcraftApp, ui: &mut egui::Ui, now: &LocalNow) {
}
}
}
let hidden = app.ui.hidden_locations.len();
// the way back for hidden places: one comes back from the row itself; several are listed, so
// each comes back on its own, or all at once (issue #2114: the row used to bring every one back)
let hidden = now.hidden.len();
if hidden > 0 {
let label = crate::i18n::tr_format!("Show {hidden} hidden location{}", if hidden == 1 { "" } else { "s" }, hidden = hidden);
if row(app, ui, "local:restoreHidden", Icon::Folder, &label, None, false, 0.0)
.on_hover_text(crate::i18n::tr("Put the locations you removed from Local back (no files change)"))
.clicked()
{
let _ = app.run("local.restoreHidden", json!({}));
let resp = row(app, ui, "local:restoreHidden", Icon::Folder, &label, None, false, 0.0)
.on_hover_text(crate::i18n::tr("Put the locations you removed from Local back (no files change)"));
if let [(_, only)] = now.hidden.as_slice() {
if resp.clicked() {
let _ = app.run("local.restoreHidden", json!({"path": only}));
}
} else {
egui::Popup::menu(&resp).show(|ui| hidden_menu(app, ui, &now.hidden));
}
}
}
/// The list behind "Show N hidden locations": each place by name (its path on hover), then
/// Restore All.
fn hidden_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, hidden: &[(String, String)]) {
for (name, path) in hidden {
let r = ui.button(name).on_hover_text(path);
register(ui.ctx(), format!("local:restoreHidden:{path}"), r.rect);
if r.clicked() {
let _ = app.run("local.restoreHidden", json!({"path": path}));
ui.close();
}
}
ui.separator();
let r = ui.button(crate::i18n::tr("Restore All"));
register(ui.ctx(), "local:restoreHidden:all", r.rect);
if r.clicked() {
let _ = app.run("local.restoreHidden", json!({}));
ui.close();
}
}
/// Whether two paths name the same folder, however they are spelled (separators, trailing
/// slash, `.`/`..`, drive-letter case; see `lightcraft_catalog::query::folder_key`).
pub(crate) fn same_folder(a: &str, b: &str) -> bool {
File diff suppressed because one or more lines are too long
+6
View File
@@ -1,5 +1,11 @@
# What's new in LightCraft
## 2026-10-19
### Hidden Local locations come back one at a time
- With several locations removed from Local, "Show N hidden locations" now lists them, so each comes back on its own
(Restore All is there too). The row used to bring every hidden location back at once (issue #2114).
## 2026-10-14
### Model downloads time out on a name that doesn't resolve