From 4721627d3168c9f2cf1930ba1e381f29e9147cac Mon Sep 17 00:00:00 2001 From: Jason Baldwin <33934381+bitshiftza@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:29:08 +0200 Subject: [PATCH] =?UTF-8?q?LR-LIB-ALBUM:=20right-click=20=E2=96=B8=20Add?= =?UTF-8?q?=20to=20Album=20offers=20New=20Album=E2=80=A6=20(#676)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grid context menu's Add to Album submenu listed only the existing plain albums, so in a library without one it came up empty. The Photo menu's submenu already ends with New Album…, and album.create adds the selection by default; the context menu now has the same entry (after a separator when albums exist). The submenu and the entry are registered as button:contextAddToAlbum / button:contextNewAlbum for the control channel. Test: right_click_add_to_album_offers_a_new_album_when_there_is_none deletes every plain album, right-clicks a thumbnail, hovers Add to Album, clicks New Album…, names and confirms it; the album exists and holds the photo. Fails on main with "no widget button:contextAddToAlbum". --- crates/ui-egui/src/panels/grid.rs | 14 ++++++++++- crates/ui-egui/src/tests_panels.rs | 40 ++++++++++++++++++++++++++++++ docs/parity.md | 2 +- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/grid.rs b/crates/ui-egui/src/panels/grid.rs index 4264e429..e1a962b3 100644 --- a/crates/ui-egui/src/panels/grid.rs +++ b/crates/ui-egui/src/panels/grid.rs @@ -913,14 +913,26 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { } }); ui.menu_button(crate::i18n::tr("Set Color Label"), |ui| label_menu(app, ui)); - ui.menu_button(crate::i18n::tr("Add to Album"), |ui| { + let add = ui.menu_button(crate::i18n::tr("Add to Album"), |ui| { let albums: Vec<_> = app.session.catalog.albums().filter(|a| !a.folder && !a.is_smart()).map(|a| (a.id.0, a.name.clone())).collect(); + let any = !albums.is_empty(); for (aid, name) in albums { if ui.button(name).clicked() { let _ = app.run("album.addPhotos", json!({"id": aid})); } } + if any { + ui.separator(); + } + // as in the Photo menu: a new album holding the selection, also when there is none yet + // to pick (issue #676: the submenu came up empty) + let new = ui.button(crate::i18n::tr("New Album…")); + register(ui.ctx(), "button:contextNewAlbum", new.rect); + if new.clicked() { + let _ = app.run("dialog.newAlbum", json!({})); + } }); + register(ui.ctx(), "button:contextAddToAlbum", add.response.rect); // 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()) diff --git a/crates/ui-egui/src/tests_panels.rs b/crates/ui-egui/src/tests_panels.rs index 1f5cd4d4..ce7cc277 100644 --- a/crates/ui-egui/src/tests_panels.rs +++ b/crates/ui-egui/src/tests_panels.rs @@ -1182,3 +1182,43 @@ fn the_tracklog_toast_shows_after_a_while() { assert!(h.app.ui.toast.as_ref().is_some_and(|t| t.0.starts_with("Tagged")), "{:?}", h.app.ui.toast); let _ = std::fs::remove_dir_all(&dir); } + +/// Issue #676: right-click ▸ Add to Album offers New Album… like the Photo menu does, also when +/// the library has no album yet (the submenu used to come up empty); the new album holds the photo. +#[test] +fn right_click_add_to_album_offers_a_new_album_when_there_is_none() { + let mut h = demo([1400.0, 900.0], json!({"view": "photoGrid", "leftPanel": true})); + let plain: Vec = h.app.session.catalog.albums().filter(|a| !a.folder && !a.is_smart()).map(|a| a.id.0).collect(); + for id in plain { + h.app.session.execute("album.delete", &json!({"id": id})).unwrap(); + } + assert!(h.app.session.catalog.albums().all(|a| a.folder || a.is_smart()), "no plain album is left"); + let photo = lightcraft_catalog::PhotoId(h.app.session.visible_cloned()[1].0); + h.step(); + let thumb = widget(&h, &format!("thumb:{}", photo.0)); + let r = h.request("ui.click", json!({"x": thumb.center().x, "y": thumb.center().y, "button": "right"}), T); + assert_eq!(r["ok"], true, "{r}"); + h.step(); + h.step(); + assert!(h.app.session.selection.contains(photo), "right-click selects the photo"); + let r = h.request("ui.hoverWidget", json!({"id": "button:contextAddToAlbum"}), T); + assert_eq!(r["ok"], true, "{r}"); + h.step(); + h.step(); + let r = h.request("ui.clickWidget", json!({"id": "button:contextNewAlbum"}), T); + assert_eq!(r["ok"], true, "{r}"); + h.step(); + h.step(); + match &mut h.app.ui.dialog { + Some(crate::state::Dialog::NewAlbum { name, folder: false, parent: None }) => *name = "Trip".into(), + other => panic!("New Album dialog expected, got {other:?}"), + } + h.step(); + let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T); + assert_eq!(r["ok"], true, "{r}"); + h.step(); + h.step(); + let trip = h.app.session.catalog.albums().find(|a| a.name == "Trip").expect("the album was created"); + assert!(trip.photos.contains(&photo), "the right-clicked photo is in it: {:?}", trip.photos); + assert!(h.app.ui.dialog.is_none(), "{:?}", h.app.ui.dialog); +} diff --git a/docs/parity.md b/docs/parity.md index ad82e3d2..e5b89a49 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -157,7 +157,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | LR-LIB-ALLPHOTOS | All photos | P0 | ✅ | `cmd:library.source`, `crates/ui-egui/src/panels/left.rs` | | | LR-LIB-RECENT-ADDED | Recently added | P1 | ✅ | `cmd:library.source` (`recentlyAdded`), `crates/engine/src/view.rs` (`RECENT_DAYS`) | photos imported in the 30 days up to the latest import, newest import first, grouped by import day | | LR-LIB-BYDATE | Browse by date | P1 | ✅ | `cmd:library.filter` (`date`), `crates/ui-egui/src/panels/left.rs` (`date_row`), `crates/catalog/src/query.rs` (`date_groups`) | year → month → day tree with library-wide counts; a row filters the current source by that date. Active filters show as removable chips under the grid header (`crates/ui-egui/src/panels/chips.rs`, with “+N more” and Clear all), the header counts “N of M photos” (matching of the source's total), a badge on the top-bar filter icon counts them, and an empty result says no photos match the active filters | -| LR-LIB-ALBUM | Albums | P0 | ✅ | `cmd:album.create`, `cmd:album.rename`, `cmd:album.delete`, `cmd:album.addPhotos`, `cmd:album.removePhotos`, `cmd:dialog.newAlbum` | photos dragged from the grid onto an album row are added to it (LR-BEHAV-DRAGDROP) | +| LR-LIB-ALBUM | Albums | P0 | ✅ | `cmd:album.create`, `cmd:album.rename`, `cmd:album.delete`, `cmd:album.addPhotos`, `cmd:album.removePhotos`, `cmd:dialog.newAlbum`, `crates/ui-egui/src/panels/grid.rs` (`context_menu`) | photos dragged from the grid onto an album row are added to it (LR-BEHAV-DRAGDROP); right-click a photo ▸ Add to Album lists the albums and ends with New Album…, which creates one holding the selection — also when the library has no album yet (issue #676; `button:contextAddToAlbum`, `button:contextNewAlbum`) | | LR-LIB-FOLDER | Folders of albums | P0 | ✅ | `cmd:album.create` (`folder`, `parent`), `cmd:album.move`, `cmd:album.reorder`, `cmd:album.sort`, `cmd:dialog.newAlbum` / `cmd:dialog.newFolder` / `cmd:dialog.smartAlbum` / `cmd:dialog.newSmartAlbum` (`parent`), `crates/ui-egui/src/panels/left.rs` (`albums_tree`, `folder_menu`, `disclosure_triangle`), `crates/ui-egui/src/tests_panels.rs` | Delete removes a folder and its nested albums as one undoable edit, keeps photos in the library and returns a deleted album view to All Photos (issue #501); folders nest and fold with the same disclosure triangle as Local / By Date / Folders / Keywords (`albumToggle:`; the row folds them too; open by default); right-click a folder ▸ New ▸ Create Album… / Create Smart Album… / Create Smart Album from Filter… / Create Folder… creates inside it (the `dialog.new*` commands take `parent`), opening the folders down to the new row; creating is only allowed inside folders; moving: drag an album or folder row onto a folder (outlined when allowed; a closed folder opens after 0.6 s under the pointer; the "Albums" header takes an item that sits in a folder back to the top level while it is dragged), the Move to menu or `album.move` — one undo step each; not onto a plain album, itself or something inside it; drop on the top / bottom half of an album or the edge of a folder to place it there by hand (`cmd:album.reorder`, a line shows the place; ordered per folder, folders stay first, new albums go after the ordered ones; right-click ▸ Sort Contents A–Z or + ▸ Sort Albums A–Z = `cmd:album.sort`) | | LR-LIB-SMARTALBUM | Smart albums | P1 | ✅ | `cmd:album.createSmart`, `cmd:album.setRules`, `crates/catalog/src/query.rs`, `crates/ui-egui/src/panels/filterbar.rs`, `crates/ui-egui/src/lib.rs` (`album_counts`) | saved filters (rating/flag/label/kind/edited/keyword/camera/lens/date range/text/album) or a rule set from the rule editor (match all / any / none, nested groups; see LRC-LIB-SMARTCOLL), live; sidebar counts are cached per catalog revision (and per minute while an “in the last…” rule is used), counted without building id lists | | LR-LIB-SHARED-ALBUM | Shared albums | P2 | ⬜ | | needs a sharing service |