mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 14:07:34 -06:00
LR-LIB-ALBUM: album commands skip photos the library lacks and record nothing when they add nothing
album_targets kept ids the catalog doesn't have, so album.create {addSelected}, album.addPhotos and album.toggleTarget could name a missing photo in an album. It now drops them before anything changes.
album.addPhotos and album.toggleTarget also committed when nothing was added (every photo already in, or none left), which recorded an undo step and cleared redo. They now return without a commit; toggleTarget also makes no Quick Collection for nothing.
This commit is contained in:
@@ -35,10 +35,22 @@ pub(super) fn strs(p: &Value, key: &str) -> Vec<String> {
|
||||
|
||||
/// The targets an album can hold and how many were browsed (Local) photos, which album views
|
||||
/// never show (issue #684): those are skipped, and reported, instead of becoming invisible members.
|
||||
/// An id the catalog doesn't have (named outright, or kept selected after an undo took its photo
|
||||
/// away) is dropped before anything changes, and isn't counted: `skipped` stays the Local count
|
||||
/// the toasts name.
|
||||
pub(super) fn album_targets(s: &Session, targets: Vec<PhotoId>) -> (Vec<PhotoId>, usize) {
|
||||
let before = targets.len();
|
||||
let kept: Vec<PhotoId> = targets.into_iter().filter(|id| s.catalog.photo(*id).is_none_or(|p| !p.local)).collect();
|
||||
let skipped = before - kept.len();
|
||||
let mut skipped = 0;
|
||||
let kept: Vec<PhotoId> = targets
|
||||
.into_iter()
|
||||
.filter(|id| match s.catalog.photo(*id) {
|
||||
None => false,
|
||||
Some(p) if p.local => {
|
||||
skipped += 1;
|
||||
false
|
||||
}
|
||||
Some(_) => true,
|
||||
})
|
||||
.collect();
|
||||
(kept, skipped)
|
||||
}
|
||||
|
||||
|
||||
@@ -1027,7 +1027,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Add to Album",
|
||||
["Photo"],
|
||||
None,
|
||||
"{id: albumId, ids?: [photoIds]} (default: selection) → {added, skipped}; Local photos are skipped (albums hold library photos), only Local ones is an error",
|
||||
"{id: albumId, ids?: [photoIds]} (default: selection) → {added, skipped}; Local photos are skipped (albums hold library photos), only Local ones is an error; ids the library doesn't have are dropped; nothing new to add changes nothing (no undo step)",
|
||||
has_library_selection,
|
||||
|s, p| {
|
||||
let id = album_param(p, "id", "album.addPhotos")?;
|
||||
@@ -1047,6 +1047,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
}
|
||||
}
|
||||
let added = photos.len() - before;
|
||||
// all of them were there already (or none was left to add): the album is unchanged,
|
||||
// so no undo step, and redo stays
|
||||
if added == 0 {
|
||||
return Ok(json!({"added": 0, "skipped": skipped}));
|
||||
}
|
||||
let cover = al.cover.or(photos.first().copied());
|
||||
s.commit("Add to Album", Op::Batch { ops: vec![Op::SetAlbumPhotos { id, photos }, Op::SetAlbumCover { id, cover }] })?;
|
||||
Ok(json!({"added": added, "skipped": skipped}))
|
||||
@@ -1057,12 +1062,23 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Add to Target Album",
|
||||
[],
|
||||
None,
|
||||
"{ids?} — B: adds the photos to the target album (the Quick Collection unless one is set), or removes them when they're all in it → {album, added, skipped}; Local photos are skipped",
|
||||
"{ids?} — B: adds the photos to the target album (the Quick Collection unless one is set), or removes them when they're all in it → {album, added, toggled, skipped}; Local photos are skipped, ids the library doesn't have are dropped (none left: nothing changes and `added` is null)",
|
||||
has_library_selection,
|
||||
|s, p| {
|
||||
let targets =
|
||||
album_targets_or_err(s, "album.toggleTarget", crate::cmd::photo_ids(p, "album.toggleTarget")?.unwrap_or_else(|| s.selected()))?;
|
||||
let (targets, skipped) = targets;
|
||||
// nothing left to toggle (every id named is gone): no album is made or changed
|
||||
if targets.is_empty() {
|
||||
let existing = s
|
||||
.target_album
|
||||
.filter(|a| s.catalog.album(*a).is_some_and(|al| !al.is_smart() && !al.folder))
|
||||
.or_else(|| s.catalog.quick_collection());
|
||||
let al = existing.and_then(|a| s.catalog.album(a));
|
||||
return Ok(
|
||||
json!({"album": al.map(|a| a.id.0), "name": al.map(|a| a.name.clone()), "added": null, "count": al.map_or(0, |a| s.catalog.album_count(a.id)), "skipped": skipped}),
|
||||
);
|
||||
}
|
||||
let target = s.target_album.filter(|a| s.catalog.album(*a).is_some_and(|al| !al.is_smart() && !al.folder));
|
||||
let id = match target.or_else(|| s.catalog.quick_collection()) {
|
||||
Some(id) => id,
|
||||
@@ -1076,7 +1092,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
let al = s.catalog.album(id).ok_or_else(|| bad("album.toggleTarget", "no target album"))?;
|
||||
// looked up, not searched for: thousands toggled in a big album stay linear
|
||||
let mut held: std::collections::HashSet<PhotoId> = al.photos.iter().copied().collect();
|
||||
let all_in = !targets.is_empty() && targets.iter().all(|t| held.contains(t));
|
||||
let all_in = targets.iter().all(|t| held.contains(t));
|
||||
let photos: Vec<PhotoId> = if all_in {
|
||||
let targets: std::collections::HashSet<PhotoId> = targets.iter().copied().collect();
|
||||
al.photos.iter().copied().filter(|x| !targets.contains(x)).collect()
|
||||
@@ -1084,12 +1100,14 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
al.photos.iter().copied().chain(targets.iter().copied().filter(|t| held.insert(*t))).collect()
|
||||
};
|
||||
let name = al.name.clone();
|
||||
// photos that went in or came out: what a toast can say (not the ones that were in already)
|
||||
let toggled = al.photos.len().abs_diff(photos.len());
|
||||
let cover = al.cover.filter(|c| photos.contains(c)).or(photos.first().copied());
|
||||
s.commit(
|
||||
if all_in { "Remove from Target Album" } else { "Add to Target Album" },
|
||||
Op::Batch { ops: vec![Op::SetAlbumPhotos { id, photos }, Op::SetAlbumCover { id, cover }] },
|
||||
)?;
|
||||
Ok(json!({"album": id.0, "name": name, "added": !all_in, "count": s.catalog.album_count(id), "skipped": skipped}))
|
||||
Ok(json!({"album": id.0, "name": name, "added": !all_in, "toggled": toggled, "count": s.catalog.album_count(id), "skipped": skipped}))
|
||||
}
|
||||
),
|
||||
cmd!("album.setTarget", "Set as Target Album", [], None, "{id: albumId | null} (null = the Quick Collection)", always, |s, p| {
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use crate::{Selection, Session, command_specs};
|
||||
|
||||
mod album_missing;
|
||||
mod album_order;
|
||||
mod album_remove;
|
||||
mod albums_local;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//! Album commands only take photos the catalog has, and a call that changes nothing records
|
||||
//! nothing.
|
||||
//!
|
||||
//! Scenarios:
|
||||
//! - Given ids the catalog doesn't have (named outright, or still selected after an undo took the
|
||||
//! photos away), when they are added to an album, toggled into the target album, or used for a
|
||||
//! new album, then they reach no album.
|
||||
//! - Given nothing is left to add, or every photo is in the album already, then there is no undo
|
||||
//! step and redo is left alone.
|
||||
//! - Given a normal add or toggle, then one step is recorded and one undo restores the album.
|
||||
|
||||
use lightcraft_catalog::{AlbumId, PhotoId};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Selection, Session};
|
||||
|
||||
const GONE: PhotoId = PhotoId(987_654);
|
||||
|
||||
fn album(s: &mut Session, name: &str) -> AlbumId {
|
||||
AlbumId(s.execute("album.create", &json!({"name": name})).unwrap()["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
/// A demo session, an album, and a redo entry waiting (a rating set, then undone).
|
||||
fn with_redo() -> (Session, AlbumId, PhotoId) {
|
||||
let mut s = Session::with_demo();
|
||||
let photo = s.catalog.photos().next().unwrap().id;
|
||||
let a = album(&mut s, "Trip");
|
||||
s.execute("photo.rate", &json!({"ids": [photo.0], "rating": 4})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.redo.len(), 1);
|
||||
(s, a, photo)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn photos_the_catalog_lacks_reach_no_album() {
|
||||
let (mut s, a, photo) = with_redo();
|
||||
let (undo, redo) = (s.undo.len(), s.redo.len());
|
||||
let r = s.execute("album.addPhotos", &json!({"id": a.0, "ids": [GONE.0]})).unwrap();
|
||||
assert_eq!((r["added"].as_u64(), r["skipped"].as_u64()), (Some(0), Some(0)), "{r}");
|
||||
assert!(s.catalog.album(a).unwrap().photos.is_empty());
|
||||
assert_eq!((s.undo.len(), s.redo.len()), (undo, redo), "no step, redo kept");
|
||||
|
||||
// a mix: the one that is there goes in, the other doesn't
|
||||
let r = s.execute("album.addPhotos", &json!({"id": a.0, "ids": [GONE.0, photo.0]})).unwrap();
|
||||
assert_eq!(r["added"], 1, "{r}");
|
||||
assert_eq!(s.catalog.album(a).unwrap().photos, vec![photo]);
|
||||
assert_eq!(s.undo.len(), undo + 1);
|
||||
assert_eq!(s.redo.len(), 0, "a real change clears redo");
|
||||
|
||||
// toggling: nothing is made or changed for ids that are gone
|
||||
let r = s.execute("album.toggleTarget", &json!({"ids": [GONE.0]})).unwrap();
|
||||
assert!(r["added"].is_null(), "{r}");
|
||||
assert!(s.catalog.quick_collection().is_none(), "no Quick Collection for nothing");
|
||||
assert_eq!(s.undo.len(), undo + 1);
|
||||
|
||||
// a new album from a selection that is gone starts empty
|
||||
s.selection = Selection::single(GONE);
|
||||
let r = s.execute("album.create", &json!({"name": "New", "addSelected": true})).unwrap();
|
||||
let new = s.catalog.album(AlbumId(r["id"].as_u64().unwrap())).unwrap();
|
||||
assert!(new.photos.is_empty() && new.cover.is_none(), "{r}");
|
||||
}
|
||||
|
||||
/// Leaves one redo entry waiting: a rating set, then undone.
|
||||
fn leave_redo(s: &mut Session, photo: PhotoId) {
|
||||
s.execute("photo.rate", &json!({"ids": [photo.0], "rating": 4})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.redo.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggling_gone_photos_leaves_the_target_album_and_redo_alone() {
|
||||
let mut s = Session::with_demo();
|
||||
let photo = s.catalog.photos().next().unwrap().id;
|
||||
let a = album(&mut s, "Trip");
|
||||
s.execute("album.setTarget", &json!({"id": a.0})).unwrap();
|
||||
s.execute("album.toggleTarget", &json!({"ids": [photo.0]})).unwrap();
|
||||
leave_redo(&mut s, photo);
|
||||
let undo = s.undo.len();
|
||||
let r = s.execute("album.toggleTarget", &json!({"ids": [GONE.0]})).unwrap();
|
||||
assert_eq!(r["album"].as_u64(), Some(a.0), "{r}");
|
||||
assert!(r["added"].is_null(), "{r}");
|
||||
assert_eq!(s.catalog.album(a).unwrap().photos, vec![photo]);
|
||||
assert_eq!((s.undo.len(), s.redo.len()), (undo, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_photos_that_are_all_there_already_records_nothing() {
|
||||
let mut s = Session::with_demo();
|
||||
let photo = s.catalog.photos().next().unwrap().id;
|
||||
let a = album(&mut s, "Trip");
|
||||
s.execute("album.addPhotos", &json!({"id": a.0, "ids": [photo.0]})).unwrap();
|
||||
leave_redo(&mut s, photo);
|
||||
let undo = s.undo.len();
|
||||
let r = s.execute("album.addPhotos", &json!({"id": a.0, "ids": [photo.0]})).unwrap();
|
||||
assert_eq!(r["added"], 0, "{r}");
|
||||
assert_eq!((s.undo.len(), s.redo.len()), (undo, 1), "no step, redo kept");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_normal_add_or_toggle_is_one_step_and_one_undo_restores_it() {
|
||||
let (mut s, a, photo) = with_redo();
|
||||
let undo = s.undo.len();
|
||||
s.execute("album.addPhotos", &json!({"id": a.0, "ids": [photo.0]})).unwrap();
|
||||
assert_eq!(s.undo.len(), undo + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(s.catalog.album(a).unwrap().photos.is_empty());
|
||||
|
||||
s.execute("album.setTarget", &json!({"id": a.0})).unwrap();
|
||||
let undo = s.undo.len();
|
||||
let r = s.execute("album.toggleTarget", &json!({"ids": [GONE.0, photo.0]})).unwrap();
|
||||
assert_eq!((r["added"].as_bool(), r["toggled"].as_u64(), s.undo.len()), (Some(true), Some(1), undo + 1), "{r}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(s.catalog.album(a).unwrap().photos.is_empty());
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
// toggled off: also one step, one undo puts the photo back
|
||||
let undo = s.undo.len();
|
||||
let r = s.execute("album.toggleTarget", &json!({"ids": [photo.0]})).unwrap();
|
||||
assert_eq!((r["added"].as_bool(), r["toggled"].as_u64(), s.undo.len()), (Some(false), Some(1), undo + 1), "{r}");
|
||||
assert!(s.catalog.album(a).unwrap().photos.is_empty());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.catalog.album(a).unwrap().photos, vec![photo]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_toggle_counts_only_the_photos_it_added() {
|
||||
let mut s = Session::with_demo();
|
||||
let ids: Vec<PhotoId> = s.catalog.photos().take(3).map(|p| p.id).collect();
|
||||
let a = album(&mut s, "Trip");
|
||||
s.execute("album.setTarget", &json!({"id": a.0})).unwrap();
|
||||
s.execute("album.addPhotos", &json!({"id": a.0, "ids": [ids[0].0, ids[1].0]})).unwrap();
|
||||
let all: Vec<u64> = ids.iter().map(|p| p.0).collect();
|
||||
let r = s.execute("album.toggleTarget", &json!({"ids": all})).unwrap();
|
||||
assert_eq!((r["added"].as_bool(), r["toggled"].as_u64()), (Some(true), Some(1)), "two were in already: {r}");
|
||||
assert_eq!(s.catalog.album(a).unwrap().photos.len(), 3);
|
||||
}
|
||||
@@ -710,8 +710,9 @@ fn brush_key(app: &mut LightcraftApp, ctx: &egui::Context) -> bool {
|
||||
if !library_grid(app) {
|
||||
return false;
|
||||
}
|
||||
if let Ok(r) = app.run("album.toggleTarget", json!({})) {
|
||||
let n = app.session.selected().len();
|
||||
// `added` is null when nothing was left to toggle (the selected photos are gone): nothing to say
|
||||
if let Some(r) = app.run("album.toggleTarget", json!({})).ok().filter(|r| r["added"].is_boolean()) {
|
||||
let n = r["toggled"].as_u64().unwrap_or(0);
|
||||
let what = crate::i18n::tr_format!("{n} photo{}", if n == 1 { "" } else { "s" }, n = n);
|
||||
let name = r["name"].as_str().unwrap_or("Quick Collection").to_string();
|
||||
app.toast(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Lightroom parity checklist
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (LR-LIB-ALBUM: album commands leave out photos the library no longer has and record nothing when they add nothing; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
|
||||
The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we
|
||||
have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good
|
||||
@@ -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:<id>`); 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-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:<id>`); 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); album commands only take photos the library has (`album.create {addSelected}`, `album.addPhotos` and `album.toggleTarget` drop ids it lacks) and `album.addPhotos` with nothing to add (all photos already in, or none left) records no undo step and leaves redo alone; `album.toggleTarget` with none left changes nothing (`added` is null, no toast) |
|
||||
| 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:<id>`; 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`, `cmd:library.holdView`, `cmd:library.refresh`, `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; views and source counts with direct or nested relative-date rules expire on the session-clock minute; sidebar counts remain cached without building id lists; View ▸ Hold View Until Refreshed (`library.holdView`, off by default) keeps a photo that an edit or clock expiry made stop matching (a smart album, Picks, the filter bar) in the view until Refresh View, Reshuffle or another source, filter or sort, while newly matching photos enter at once |
|
||||
| LR-LIB-SHARED-ALBUM | Shared albums | P2 | ⬜ | | needs a sharing service |
|
||||
@@ -959,3 +959,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
| 2026-10-23 | patch | LR-BEHAV-ACCESS: the name field of New Album, New Smart Album, Rename and the other one-field dialogs takes the keyboard focus once when the dialog opens instead of every frame, so Tab and Shift+Tab reach the check box, Cancel and OK; status unchanged (🟡) |
|
||||
| 2026-10-22 | minor | LR-CROP-AUTO: Auto straighten turns by at most 10° and only on at least 3 agreeing lines, and says when it finds nothing to level by; measured on 68 photos straightened by hand and 300 left as shot, unwanted turns of more than 1° fell from 79 to 19 of the 300. Downgraded ✅ → 🟡: it still leaves most tilted photos alone |
|
||||
| 2026-10-23 | patch | LRC-LIB-FILTERBAR: showing an album (`library.source`) while the filter holds an album drops the filter's album, as showing a library folder already dropped its folder filter; before, the old album stayed in the filter unseen (the filter bar has no album chip) and narrowed the next source such as All Photos; the row's note says so; status unchanged (✅) |
|
||||
| 2026-10-24 | patch | LR-LIB-ALBUM: `album.create {addSelected}`, `album.addPhotos` and `album.toggleTarget` drop photo ids the catalog lacks before any album changes, and an add that adds nothing records no undo step and leaves redo alone; status unchanged (✅) |
|
||||
|
||||
@@ -19,6 +19,13 @@
|
||||
photo, and the line can be dragged: pressing it picks the region under the pointer (Highlights, Lights, Darks or
|
||||
Shadows) and moving up or down changes that slider, as one undo step. Hovering the line names the region first.
|
||||
|
||||
### Adding photos that are already in an album no longer costs an undo step
|
||||
- Add to Album and dragging photos to an album now leave the album, the undo history and Redo alone when every
|
||||
photo was in the album already. Before, they still recorded an undo step, so Redo was lost and Undo had a step
|
||||
that changed nothing. Photos the library no longer has (a selection kept after an Undo took them away) are also
|
||||
left out of a new album made from the selection, out of Add to Album, and out of the target album (B), which does
|
||||
nothing and says nothing when none of them is left, and counts only the photos it changed.
|
||||
|
||||
## 2026-10-23
|
||||
|
||||
### Tab leaves the name field in New Album, New Smart Album and the name prompts
|
||||
|
||||
Reference in New Issue
Block a user