mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
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:
1 parent
e3b61fc883
commit
1c50d538c3
5 files changed
+79
-4
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}”"),
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in new issue
Block a user