From 77ebac813a3ad0010db92042c986832c0d98f7a0 Mon Sep 17 00:00:00 2001 From: Markus Friedel <20045158+MarkusFriedel@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:15:08 +0200 Subject: [PATCH] LR-LIB-ALBUM: "Include selected photos" option in New Album (#1249) New Album always put the selection into the new album. Like Lightroom's Create Collection, the dialog now has an "Include selected photos" checkbox: ticked by default (today's behaviour), shown only when something is selected and never for a folder. dialog.newAlbum takes includeSelected; album.create already had addSelected. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/locales/de.json | 1 + crates/ui-egui/src/headless.rs | 2 +- crates/ui-egui/src/menus.rs | 17 +++++++--- crates/ui-egui/src/panels/dialogs.rs | 17 ++++++++-- crates/ui-egui/src/panels/left.rs | 4 +-- crates/ui-egui/src/state.rs | 5 ++- crates/ui-egui/src/tests_panels.rs | 51 +++++++++++++++++++++++++++- docs/parity-checklist.md | 2 +- 8 files changed, 86 insertions(+), 13 deletions(-) diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index 10cc7e0e..475f27e0 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -703,6 +703,7 @@ "In memory — nothing is saved": "Im Arbeitsspeicher — nichts wird gespeichert", "In use": "In Verwendung", "In-memory session: these settings last until LightCraft quits.": "Sitzung im Arbeitsspeicher: Diese Einstellungen gelten bis zum Beenden von LightCraft.", + "Include selected photos": "Ausgewählte Fotos einschließen", "Include subfolders": "Unterordner einbeziehen", "Increase Brush Feather": "Weiche Pinselkante vergrößern", "Increase Brush Size": "Pinselgröße vergrößern", diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index 8cb91b9b..9c1b6c01 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -1678,7 +1678,7 @@ mod tests { } assert_eq!(h.app.ui.dialog, None, "Return confirmed the dialog ({typed})"); }; - confirm(&mut h, Dialog::NewAlbum { name: String::new(), folder: false, parent: None }, "Weddings"); + confirm(&mut h, Dialog::NewAlbum { name: String::new(), folder: false, parent: None, include_selected: true }, "Weddings"); assert!(album(&h, "Weddings").is_some(), "New Album"); confirm(&mut h, Dialog::RenameAlbum { id: garden, name: "Garden".into() }, "Flowers"); assert_eq!(album(&h, "Flowers"), Some(garden), "Rename Album"); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index daf0f08e..0cb49d83 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1069,13 +1069,22 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option { - app.ui.dialog = - Some(Dialog::NewAlbum { name: p.get("name").and_then(Value::as_str).unwrap_or("").into(), folder: true, parent: parent_param(p) }); + app.ui.dialog = Some(Dialog::NewAlbum { + name: p.get("name").and_then(Value::as_str).unwrap_or("").into(), + folder: true, + parent: parent_param(p), + include_selected: false, + }); Ok(Value::Null) } "dialog.newAlbum" => { - app.ui.dialog = - Some(Dialog::NewAlbum { name: p.get("name").and_then(Value::as_str).unwrap_or("").into(), folder: false, parent: parent_param(p) }); + // {name?, parent?, includeSelected? (true)} + app.ui.dialog = Some(Dialog::NewAlbum { + name: p.get("name").and_then(Value::as_str).unwrap_or("").into(), + folder: false, + parent: parent_param(p), + include_selected: p.get("includeSelected").and_then(Value::as_bool).unwrap_or(true), + }); Ok(Value::Null) } "dialog.autoStack" => { diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index 72528fca..9456dc2b 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -608,7 +608,18 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { confirm = true; } } - Dialog::NewAlbum { name, .. } | Dialog::RenameAlbum { name, .. } => { + Dialog::NewAlbum { name, folder, include_selected, .. } => { + if dialog_field(ui, "field:albumName", name, "Name") { + confirm = true; + } + // as in Lightroom, the option is only there when something is selected + if !*folder && !app.session.targets(&json!({})).is_empty() { + ui.add_space(6.0); + let r = ui.checkbox(include_selected, crate::i18n::tr("Include selected photos")); + crate::widgets::register(ui.ctx(), "check:albumIncludeSelected", r.rect); + } + } + Dialog::RenameAlbum { name, .. } => { if dialog_field(ui, "field:albumName", name, "Name") { confirm = true; } @@ -1520,8 +1531,8 @@ pub fn confirm_dialog(app: &mut LightcraftApp, dlg: &Dialog) -> Result { - created_in(app, "album.create", json!({"name": name, "folder": folder, "addSelected": !folder, "parent": parent})) + Dialog::NewAlbum { name, folder, parent, include_selected } => { + created_in(app, "album.create", json!({"name": name, "folder": folder, "addSelected": !folder && *include_selected, "parent": parent})) } Dialog::RenameAlbum { id, name } => app.run("album.rename", json!({"id": id, "name": name})), Dialog::TextPrompt { value, command, params, key, .. } => { diff --git a/crates/ui-egui/src/panels/left.rs b/crates/ui-egui/src/panels/left.rs index a14332c8..108a57db 100644 --- a/crates/ui-egui/src/panels/left.rs +++ b/crates/ui-egui/src/panels/left.rs @@ -533,7 +533,7 @@ fn sections_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, on: Option, + #[serde(default = "yes")] + include_selected: bool, }, RenameAlbum { id: u64, diff --git a/crates/ui-egui/src/tests_panels.rs b/crates/ui-egui/src/tests_panels.rs index 25edf3bf..4d8634cc 100644 --- a/crates/ui-egui/src/tests_panels.rs +++ b/crates/ui-egui/src/tests_panels.rs @@ -2129,7 +2129,7 @@ fn right_click_add_to_album_offers_a_new_album_when_there_is_none() { h.step(); h.step(); match &mut h.app.ui.dialog { - Some(crate::state::Dialog::NewAlbum { name, folder: false, parent: None }) => *name = "Trip".into(), + Some(crate::state::Dialog::NewAlbum { name, folder: false, parent: None, .. }) => *name = "Trip".into(), other => panic!("New Album dialog expected, got {other:?}"), } h.step(); @@ -2141,3 +2141,52 @@ fn right_click_add_to_album_offers_a_new_album_when_there_is_none() { 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); } + +/// New Album's "Include selected photos" (as in Lightroom): on by default; unticked, the new album +/// starts empty; with nothing selected the option isn't shown, and a folder never has it. +#[test] +fn new_album_can_leave_the_selected_photos_out() { + let mut h = demo([1400.0, 900.0], json!({"view": "photoGrid", "leftPanel": true})); + let has = |h: &Headless, id: &str| h.app.widgets.iter().any(|(w, _)| w == id); + let photo = lightcraft_catalog::PhotoId(h.app.session.visible_cloned()[1].0); + h.app.session.execute("library.select", &json!({"ids": [photo.0]})).unwrap(); + let create = |h: &mut Headless, command: &str, name: &str| { + h.app.run(command, json!({"name": name})).unwrap(); + h.step(); + h.step(); + }; + let confirm = |h: &mut Headless| { + let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T); + assert_eq!(r["ok"], true, "{r}"); + h.step(); + h.step(); + assert!(h.app.ui.dialog.is_none(), "{:?}", h.app.ui.dialog); + }; + let photos = |h: &Headless, name: &str| h.app.session.catalog.albums().find(|a| a.name == name).expect("the album was created").photos.clone(); + + // ticked by default: the selection goes in, as before + create(&mut h, "dialog.newAlbum", "With"); + assert!(has(&h, "check:albumIncludeSelected"), "the option is offered with a selection"); + confirm(&mut h); + assert_eq!(photos(&h, "With"), vec![photo]); + + // unticked: an empty album + create(&mut h, "dialog.newAlbum", "Without"); + let r = h.request("ui.clickWidget", json!({"id": "check:albumIncludeSelected"}), T); + assert_eq!(r["ok"], true, "{r}"); + h.step(); + confirm(&mut h); + assert!(photos(&h, "Without").is_empty(), "nothing was added"); + + // a folder never offers it + create(&mut h, "dialog.newFolder", "Folder"); + assert!(!has(&h, "check:albumIncludeSelected"), "folders hold no photos"); + confirm(&mut h); + + // nothing selected: no option, an empty album + h.app.session.selection = lightcraft_engine::Selection { ids: vec![], active: None }; + create(&mut h, "dialog.newAlbum", "Nothing"); + assert!(!has(&h, "check:albumIncludeSelected"), "no selection, no option"); + confirm(&mut h); + assert!(photos(&h, "Nothing").is_empty()); +} diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 77ae26e4..23e31d0d 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -94,7 +94,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | 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`, `crates/engine/src/tests_albums_local.rs`, `crates/ui-egui/src/tests_panels.rs` | photos dragged from the grid onto an album row are added to it (LR-BEHAV-DRAGDROP); albums hold library photos (issue #684): `album.create {addSelected}`, `album.addPhotos` and `album.toggleTarget` (B) skip browsed Local photos and return `skipped` (a toast says so), a Local-only selection is an error and leaves Photo ▸ Add to Album, the grid's right-click Add to Album, a folder row's Add Selected Photos and a drop onto an album row off with the hint to Add to My Photos first (`button:contextAddToAlbum`, `button:contextAlbum:`); 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:contextNewAlbum`) | +| LR-LIB-ALBUM | Albums | P0 | ✅ | `cmd:album.create`, `cmd:album.rename`, `cmd:album.delete`, `cmd:album.addPhotos`, `cmd:album.removePhotos`, `cmd:dialog.newAlbum`, `crates/engine/src/tests_albums_local.rs`, `crates/ui-egui/src/tests_panels.rs` | photos dragged from the grid onto an album row are added to it (LR-BEHAV-DRAGDROP); albums hold library photos (issue #684): `album.create {addSelected}`, `album.addPhotos` and `album.toggleTarget` (B) skip browsed Local photos and return `skipped` (a toast says so), a Local-only selection is an error and leaves Photo ▸ Add to Album, the grid's right-click Add to Album, a folder row's Add Selected Photos and a drop onto an album row off with the hint to Add to My Photos first (`button:contextAddToAlbum`, `button:contextAlbum:`); 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:contextNewAlbum`); New Album's "Include selected photos" (`check:albumIncludeSelected`, on by default; `dialog.newAlbum {includeSelected}`) leaves the selection out when unticked, and, as in Lightroom, is only shown when something is selected (never for a folder) | | 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 |