MENU-CTX-ALBUM + MENU-CTX-GRID: export / move albums from their menu, set album cover

- Album rows: Export Album… (shows and selects the album, opens Export), Export Album with
  Preset (background), Move to (a folder or Top Level; never into itself or a subfolder).
- Photo context menu inside an album: Set as Album Cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-01 22:54:09 -04:00
co-authored by Claude Opus 5.5
parent cc6c0e457d
commit a8744c07e7
3 changed files with 67 additions and 7 deletions
+7 -3
View File
@@ -467,12 +467,16 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
}
}
});
// in a (non-smart) album: take the selection out of it
// in a (non-smart) album: take the selection out of it, or make this photo its cover
if let lightcraft_engine::LibrarySource::Album(aid) = app.session.source
&& app.session.catalog.album(aid).is_some_and(|a| !a.folder && !a.is_smart())
&& ui.button("Remove from Album").clicked()
{
let _ = app.run("album.removePhotos", json!({"id": aid.0}));
if ui.button("Remove from Album").clicked() {
let _ = app.run("album.removePhotos", json!({"id": aid.0}));
}
if ui.button("Set as Album Cover").clicked() {
let _ = app.run("album.setCover", json!({"id": aid.0, "photo": id.0}));
}
}
if ui.button("Rename…").clicked() {
let _ = app.run("dialog.rename", json!({}));
+56
View File
@@ -158,6 +158,45 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
if a.is_smart() && ui.button("Update Rules from Current Filter").clicked() {
let _ = app.run("album.setRules", json!({"id": a.id.0, "fromView": true}));
}
if !a.folder {
// export: show the album, select its photos, then the dialog / a preset
let show_all = |app: &mut LightcraftApp| {
let _ = app.run("library.source", json!({"kind": "album", "id": a.id.0}));
let _ = app.run("library.selectAll", json!({}));
};
let has_photos = app.session.catalog.album_count(a.id) > 0;
if ui.add_enabled(has_photos, egui::Button::new("Export Album…")).clicked() {
show_all(app);
let _ = app.run("dialog.export", json!({}));
}
ui.add_enabled_ui(has_photos, |ui| {
ui.menu_button("Export Album with Preset", |ui| {
for (p, _) in app.session.all_export_presets() {
if ui.button(&p.name).clicked() {
show_all(app);
if let Err(e) = app.run("app.export", json!({"preset": p.name, "background": true})) {
app.toast(ui.ctx(), e);
}
}
}
});
});
ui.separator();
}
// move into another folder (not into itself or one of its own subfolders)
let mut folders: Vec<(u64, String)> =
app.session.catalog.albums().filter(|f| f.folder && !is_within(app, f.id, a.id)).map(|f| (f.id.0, f.name.clone())).collect();
folders.sort_by_key(|(_, n)| n.to_lowercase());
ui.menu_button("Move to", |ui| {
if ui.add_enabled(a.parent.is_some(), egui::Button::new("Top Level")).clicked() {
let _ = app.run("album.move", json!({"id": a.id.0, "parent": null}));
}
for (fid, name) in &folders {
if ui.add_enabled(a.parent.map(|p| p.0) != Some(*fid), egui::Button::new(name)).clicked() {
let _ = app.run("album.move", json!({"id": a.id.0, "parent": fid}));
}
}
});
if ui.button("Rename…").clicked() {
app.ui.dialog = Some(crate::state::Dialog::RenameAlbum { id: a.id.0, name: a.name.clone() });
}
@@ -167,6 +206,23 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
});
}
/// Whether `id` is `ancestor` or lies inside it.
fn is_within(app: &LightcraftApp, id: lightcraft_catalog::AlbumId, ancestor: lightcraft_catalog::AlbumId) -> bool {
let mut cur = Some(id);
let mut guard = 0;
while let Some(c) = cur {
if c == ancestor {
return true;
}
cur = app.session.catalog.album(c).and_then(|x| x.parent);
guard += 1;
if guard > 64 {
break;
}
}
false
}
/// "Keywords": the library's keyword tree with photo counts (`a|b|c` keywords nest). A click
/// filters the grid by the keyword (children included), the triangle opens a level, and the
/// context menu renames, merges or deletes the keyword across the library.
+4 -4
View File
@@ -45,10 +45,10 @@ and prints the summary; `cargo xtask parity --write` refreshes the summary table
| V. Preferences (PREF) | 4 | 1 | 3 | 3 | 1/1 (100%) | 3/4 (75%) |
| W. Cloud & AI infrastructure (CLOUD / AI) | 0 | 1 | 1 | 2 | — | — |
| X. Cross-cutting behaviours (BEHAV) | 11 | 2 | 4 | 1 | 8/8 (100%) | 3/5 (60%) |
| Y. Menus | 57 | 15 | 13 | 8 | 42/47 (89%) | 13/23 (57%) |
| Y. Menus | 59 | 13 | 13 | 8 | 44/47 (94%) | 13/23 (57%) |
| Z. Keyboard shortcuts (desktop) | 56 | 13 | 11 | 1 | 47/52 (90%) | 9/23 (39%) |
| Lightroom Classic extras | 4 | 20 | 55 | 9 | — | 3/21 (14%) |
| **Total** | 267 | 77 | 156 | 37 | 185/198 (93%) | 72/144 (50%) |
| **Total** | 269 | 75 | 156 | 37 | 187/198 (94%) | 72/144 (50%) |
<!-- /parity:summary -->
## Top gaps
@@ -579,9 +579,9 @@ visible menu bar yet: the menu model is only exposed through the control channel
| MENU-HELP-SHORTCUTS | Keyboard shortcuts | P1 | ✅ | `cmd:app.shortcuts` | |
| MENU-HELP-FEEDBACK | Send feedback | P2 | ⬜ | | |
| MENU-HELP-SYSINFO | System info | P2 | ⬜ | `cmd:library.info` | library info only |
| MENU-CTX-GRID | Photo context menu | P0 | 🟡 | `crates/ui-egui/src/panels/grid.rs` (`context_menu`) | rate, flag, label, add to / remove from album, rename, virtual copy, version, stack, copy / paste / paste selected, reset, merge, rotate, show in Finder, export / export with preset, delete; no set-as-album-cover |
| MENU-CTX-GRID | Photo context menu | P0 | ✅ | `crates/ui-egui/src/panels/grid.rs` (`context_menu`) | rate, flag, label, add to / remove from album, rename, virtual copy, version, stack, copy / paste / paste selected, reset, merge, rotate, show in Finder, export / export with preset, set as album cover (in an album), delete |
| MENU-CTX-DETAIL | Loupe context menu | P1 | 🟡 | `crates/ui-egui/src/panels/detail.rs` | same as grid; no zoom submenu |
| MENU-CTX-ALBUM | Album / folder row menu | P0 | 🟡 | `crates/ui-egui/src/panels/left.rs` | add selected, rename, delete; no move-to, export album |
| MENU-CTX-ALBUM | Album / folder row menu | P0 | ✅ | `crates/ui-egui/src/panels/left.rs` (`folder_menu`), `cmd:album.move`, `cmd:dialog.export` | add selected, export album (dialog / preset), move to a folder or the top level, rename, delete; smart albums: update rules |
| MENU-CTX-MASK | Mask / component menu | P0 | 🟡 | `crates/ui-egui/src/panels/masking.rs` (`mask_menu`) | mask rows: duplicate (and invert), invert, show/hide, move, rename, delete; components: add/subtract only (no per-component menu) |
| MENU-CTX-PRESET | Preset menu | P1 | 🟡 | `crates/ui-egui/src/panels/presets.rs` | favourite, delete, export group; no rename/update/move |
| MENU-CTX-PROFILE | Profile favourites | P2 | ⬜ | | |