Local: Remove from Local hides a sidebar location; a row brings hidden ones back (#24)

Right-clicking a top-level Local location (built-in or the browsed folder) offers
Remove from Local. It only hides the shortcut: the folder, its photos and catalog
membership are untouched. The hidden list is saved with the UI state. While any
location is hidden, "Show N hidden locations" under Browse Folder… restores them,
and choosing a hidden folder with Browse Folder… puts it back. Both actions are
commands (local.hide, local.restoreHidden).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
Clément LandClaude Sonnet 5.5 committed 2026-10-04 11:10:24 +02:00
1 parent e3b61fc883
commit 1c50d538c3
5 files changed
+79 -4

No files matched your search

+33
View File
@@ -788,6 +788,39 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// Local: Remove from Local hides a sidebar location (nothing on disk changes) and the
/// "Show hidden locations" row puts it back.
#[test]
fn local_location_can_be_hidden_and_restored() {
let mut h = demo([1300.0, 900.0]);
let t = Duration::from_secs(10);
let dir = std::env::temp_dir().join(format!("lc-ui-hide-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.to_string_lossy().to_string();
let ids = |h: &mut Headless| -> Vec<String> {
let w = h.request("ui.widgets", json!({"filter": "source:local:"}), t);
w["result"].as_array().unwrap().iter().filter_map(|x| x["id"].as_str().map(String::from)).collect()
};
h.request("ui.set", json!({"leftPanel": true}), t);
h.request("engine.execute", json!({"command": "library.browse", "params": {"path": path}}), t);
h.settle(SETTLE);
assert!(ids(&mut h).contains(&format!("source:local:{path}")));
assert!(!ids(&mut h).iter().any(|i| i == "source:local:restoreHidden"));
let r = h.request("engine.execute", json!({"command": "local.hide", "params": {"path": path}}), t);
assert_eq!(r["ok"], true, "{r}");
h.settle(SETTLE);
let after = ids(&mut h);
assert!(!after.contains(&format!("source:local:{path}")), "{after:?}");
assert!(after.iter().any(|i| i == "source:local:restoreHidden"), "{after:?}");
assert!(dir.is_dir(), "the folder itself is untouched");
assert_eq!(h.request("ui.clickWidget", json!({"id": "source:local:restoreHidden"}), t)["ok"], true);
h.settle(SETTLE);
assert!(h.app.ui.hidden_locations.is_empty());
assert!(ids(&mut h).contains(&format!("source:local:{path}")));
let _ = std::fs::remove_dir_all(&dir);
}
/// Versions panel: resting on a version previews it in the loupe without changing the photo;
/// a click restores it.
#[test]
+19
View File
@@ -53,6 +53,8 @@ pub const UI_COMMANDS: &[UiCommand] = &[
("view.back", "Back to Grid", Some("Escape"), ""),
("tool.done", "Done", Some("Enter"), ""),
("view.filterBar", "Filter Bar", Some("Shift+F"), "View"),
("local.hide", "Remove from Local", None, ""),
("local.restoreHidden", "Show Hidden Local Locations", None, ""),
("view.fullScreenPreview", "Full Screen Preview", Some("F"), "View"),
("view.enterFullScreen", "Enter Full Screen", Some("Cmd+Shift+F"), "View"),
("view.infoOverlay", "Cycle Info Overlay", Some("Cmd+I"), "View"),
@@ -553,6 +555,23 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
app.ui.crop_overlay_orient = (app.ui.crop_overlay_orient + 1) % 4;
Ok(json!({"orientation": app.ui.crop_overlay_orient}))
}
"local.hide" => match p.get("path").and_then(Value::as_str) {
Some(path) => {
if !app.ui.hidden_locations.iter().any(|h| h == path) {
app.ui.hidden_locations.push(path.to_string());
}
Ok(json!({"hidden": app.ui.hidden_locations}))
}
None => Err("local.hide needs a path".into()),
},
"local.restoreHidden" => {
// one path, or (no path) every hidden location
match p.get("path").and_then(Value::as_str) {
Some(path) => app.ui.hidden_locations.retain(|h| h != path),
None => app.ui.hidden_locations.clear(),
}
Ok(json!({"hidden": app.ui.hidden_locations}))
}
"view.filterBar" => {
app.ui.filter_bar = !app.ui.filter_bar;
if app.ui.filter_bar && !matches!(app.ui.view, ViewMode::PhotoGrid | ViewMode::SquareGrid) {
+23 -3
View File
@@ -189,15 +189,29 @@ fn local_section(app: &mut LightcraftApp, ui: &mut egui::Ui) {
places.push((name, b.path.clone()));
}
let current = browsing.as_ref().map(|b| b.path.clone());
// hidden locations stay reachable: breadcrumbs, Browse Folder…, and the row below
places.retain(|(_, p)| !app.ui.hidden_locations.contains(p));
for (name, path) in places {
folder_tree(app, ui, &name, &path, 0.0, current.as_deref());
}
if app.services.pick_folder.is_some() && row(app, ui, "local:browse", Icon::Plus, "Browse Folder…", None, false, 0.0).clicked() {
let picked = app.services.pick_folder.as_mut().and_then(|f| f());
if let Some(path) = picked
&& let Err(e) = app.run("library.browse", json!({"path": path}))
if let Some(path) = picked {
// choosing a folder again puts it back in the list
let _ = app.run("local.restoreHidden", json!({"path": path}));
if let Err(e) = app.run("library.browse", json!({"path": path})) {
app.toast(ui.ctx(), e);
}
}
}
let hidden = app.ui.hidden_locations.len();
if hidden > 0 {
let label = format!("Show {hidden} hidden location{}", if hidden == 1 { "" } else { "s" });
if row(app, ui, "local:restoreHidden", Icon::Folder, &label, None, false, 0.0)
.on_hover_text("Put the locations you removed from Local back (no files change)")
.clicked()
{
app.toast(ui.ctx(), e);
let _ = app.run("local.restoreHidden", json!({}));
}
}
ui.add_space(10.0);
@@ -257,6 +271,12 @@ fn folder_tree(app: &mut LightcraftApp, ui: &mut egui::Ui, name: &str, path: &st
app.toast(ui.ctx(), e);
}
resp.context_menu(|ui| {
if indent == 0.0
&& ui.button("Remove from Local").on_hover_text("Hides this shortcut only; the folder and its photos stay as they are").clicked()
{
let _ = app.run("local.hide", json!({"path": path}));
ui.close();
}
if ui.button("Rename Folder…").clicked() {
app.ui.dialog = Some(crate::state::Dialog::TextPrompt {
title: format!("Rename “{name}”"),
+3
View File
@@ -230,6 +230,8 @@ pub struct UiState {
pub grid_info: String,
/// Photo counts next to sources and albums in the left panel.
pub show_counts: bool,
/// Local sidebar locations hidden with “Remove from Local” (folders on disk are untouched).
pub hidden_locations: Vec<String>,
/// Copies opened in an external editor this session (reloaded when the window is focused
/// again), and whether the window had focus last frame.
#[serde(skip)]
@@ -500,6 +502,7 @@ impl Default for UiState {
show_filenames: true,
grid_info: "filename".into(),
show_counts: true,
hidden_locations: Vec::new(),
dragging_control: None,
external_edits: Vec::new(),
was_focused: true,
+1 -1
View File
@@ -108,7 +108,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
| LR-IMP-RAWDEFAULT | Raw defaults | P1 | ✅ | `cmd:library.preferences`, `crates/engine/src/import.rs`, `crates/ui-egui/src/panels/settings.rs` | LightCraft default / a preset / per camera (make + model); non-raw default too; the preset look counts as unedited and Reset returns to it |
| LR-IMP-MIGRATE | Migrate other catalogs | OOS | 🚫 | | |
| LR-IMP-PROFILES | Import profiles & presets | P1 | ✅ | `cmd:file.importPresets`, `cmd:preset.import`, `cmd:profile.import`, `crates/engine/src/preset_import.rs`, `crates/engine/src/preset_luminar.rs` | presets: .lcpreset, XMP, classic .lrtemplate, photos carrying edits ("DNG presets"), Luminar looks (.lmp, .mplumpack collections; sliders with a counterpart), .zip bundles, folders (folder → group), drag & drop; masks carried over; unmapped settings reported; profiles: .cube 3D LUTs; Adobe profile formats deliberately unsupported |
| LR-IMP-LOCAL | Work on files in place | P0 | ✅ | `cmd:library.browse`, `cmd:photo.addToLibrary`, `cmd:library.import` (mode add), `crates/engine/src/cmd/browse.rs`, `crates/ui-egui/src/panels/left.rs` (`local_section`), `crates/ui-egui/src/panels/grid.rs` (`folder_header`) | Local: browse Pictures / Desktop / Downloads / Home or any folder without adding it (breadcrumb, Include subfolders, Add N to My Photos); browsed photos stay out of All Photos, albums and counts; edits go to XMP sidecars; importing promotes them |
| LR-IMP-LOCAL | Work on files in place | P0 | ✅ | `cmd:library.browse`, `cmd:photo.addToLibrary`, `cmd:library.import` (mode add), `cmd:local.hide`, `cmd:local.restoreHidden`, `crates/engine/src/cmd/browse.rs`, `crates/ui-egui/src/panels/left.rs` (`local_section`), `crates/ui-egui/src/panels/grid.rs` (`folder_header`) | Local: browse Pictures / Desktop / Downloads / Home or any folder without adding it (breadcrumb, Include subfolders, Add N to My Photos); browsed photos stay out of All Photos, albums and counts; edits go to XMP sidecars; importing promotes them; right-click a top-level location → Remove from Local hides the shortcut only (nothing on disk or in the catalog changes), saved with the UI state, and “Show N hidden locations” (or Browse Folder… on it) brings it back |
| LR-IMP-SIDECAR-SPLIT | Separate XMP sidecar variants | P2 | ⬜ | `cmd:library.xmpPreferences` | sidecar naming option exists (stem/full), no split sidecars |
| LR-IMP-FORMATS | Supported formats | P0 | 🟡 | `crates/codecs/src/lib.rs`, `crates/raw/src/lib.rs` | JPEG, PNG, TIFF, WebP, JXL, PSD, GIF, BMP; DNG (incl. lossy / Smart Preview DNG), CR2, ARW, NEF, RAF, RW2, PEF, ORF. Missing: CR3, compressed NEF/RAF/ORF, RW2 v4 (preview only: imported with `preview_only` = the decoder's reason, rendered as a rendered JPEG, flagged by a grid badge and loupe/Edit/Info notices, `previewOnly` in `catalog.query`; Reload clears it once the file decodes), HEIC/AVIF decode |
| LR-IMP-CULL-AT-IMPORT | Culling analysis at import | P2 | 🟡 | `cmd:photo.analyze` | run Assisted Culling on the imported photos (they're selected after an import); not automatic |