mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-26 19:17:02 -06:00
LR-PRE-PANEL + LR-PRE-CREATE: preset hover preview in the loupe, optional preset thumbnails, Create Preset group checklist
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
62a2860c7e
commit
32dd3a9588
@@ -385,6 +385,56 @@ mod tests {
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
|
||||
/// Presets column: resting on a preset previews it in the loupe without a history entry;
|
||||
/// thumbnails are variant renders; Create Preset includes only the checked groups.
|
||||
#[test]
|
||||
fn preset_hover_previews_and_create_preset_groups() {
|
||||
let mut h = demo([1400.0, 900.0]);
|
||||
let t = Duration::from_secs(10);
|
||||
h.request("ui.set", json!({"view": "detail"}), t);
|
||||
h.request("engine.execute", json!({"command": "panel.presets"}), t);
|
||||
h.settle(SETTLE);
|
||||
let id = h.app.session.active().unwrap();
|
||||
let photo = |h: &Headless| h.app.session.catalog.photo(id).unwrap().clone();
|
||||
let before = photo(&h);
|
||||
h.request("ui.hoverWidget", json!({"id": "preset:lc.bw-high-contrast"}), t);
|
||||
h.settle(SETTLE);
|
||||
h.step();
|
||||
assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Preset: High Contrast B&W"));
|
||||
assert_eq!(h.app.loupe_shown.map(|l| l.1), Some("hover"));
|
||||
let after = photo(&h);
|
||||
assert_eq!(after.develop, before.develop);
|
||||
assert_eq!(after.history.len(), before.history.len(), "no history entry while hovering");
|
||||
assert!(h.app.session.undo.is_empty());
|
||||
// thumbnails: variant textures for the visible presets
|
||||
h.app.ui.preset_thumbs = true;
|
||||
h.request("ui.move", json!({"x": 5, "y": 500}), t);
|
||||
h.settle(SETTLE);
|
||||
h.step();
|
||||
assert!(h.app.hover_preview.is_none());
|
||||
assert!(h.app.renderer.variant_textures() >= 5, "{}", h.app.renderer.variant_textures());
|
||||
// Create Preset with a group checklist: untick Light, keep Effects
|
||||
h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), t);
|
||||
h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "effects.clarity", "value": 25}}), t);
|
||||
h.app.ui.dialog = Some(crate::state::Dialog::create_preset());
|
||||
h.step();
|
||||
h.step();
|
||||
let r = h.request("ui.clickWidget", json!({"id": "presetInclude:light"}), t);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
match &h.app.ui.dialog {
|
||||
Some(crate::state::Dialog::CreatePreset { groups, .. }) => {
|
||||
assert!(!groups.iter().any(|g| g == "light") && groups.iter().any(|g| g == "effects"), "{groups:?}");
|
||||
assert!(!groups.iter().any(|g| g == "crop" || g == "masks"), "crop and masks off by default");
|
||||
}
|
||||
d => panic!("dialog: {d:?}"),
|
||||
}
|
||||
let r = h.request("ui.dialog.confirm", json!({}), t);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
let p = h.app.session.presets.iter().find(|p| !p.builtin).expect("created").clone();
|
||||
assert!(p.settings.get("light").is_none() && p.settings["effects"]["clarity"] == 25.0, "{}", p.settings);
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
|
||||
/// The filter bar drives `library.filter` and saves the view as a smart album.
|
||||
#[test]
|
||||
fn filter_bar_filters_and_saves_a_smart_album() {
|
||||
|
||||
@@ -339,7 +339,7 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"dialog.createPreset" => {
|
||||
app.ui.dialog = Some(Dialog::CreatePreset { name: String::new(), group: "User Presets".into() });
|
||||
app.ui.dialog = Some(Dialog::create_preset());
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"dialog.copySettings" => {
|
||||
|
||||
@@ -77,26 +77,13 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
confirm = true;
|
||||
}
|
||||
}
|
||||
Dialog::CreatePreset { name, group } => {
|
||||
Dialog::CreatePreset { name, group, groups } => {
|
||||
ui.add(egui::TextEdit::singleline(name).hint_text("Preset name").desired_width(f32::INFINITY));
|
||||
ui.add(egui::TextEdit::singleline(group).hint_text("Group").desired_width(f32::INFINITY));
|
||||
ui.label(egui::RichText::new("Includes the current settings except crop, masks and remove.").color(t.text_dim));
|
||||
}
|
||||
Dialog::CopySettings { groups } => {
|
||||
ui.columns(2, |cols| {
|
||||
for (i, g) in SettingsGroup::ALL.iter().enumerate() {
|
||||
let key = serde_json::to_value(g).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default();
|
||||
let mut on = groups.contains(&key);
|
||||
if cols[i % 2].checkbox(&mut on, g.label()).changed() {
|
||||
if on {
|
||||
groups.push(key);
|
||||
} else {
|
||||
groups.retain(|x| *x != key);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.label(egui::RichText::new("Settings to include").color(t.text_dim));
|
||||
group_checklist(ui, "presetInclude", groups);
|
||||
}
|
||||
Dialog::CopySettings { groups } => group_checklist(ui, "copyGroup", groups),
|
||||
Dialog::Export { opts, long_edge, limit_kb, dir } => {
|
||||
use lightcraft_engine::export::{Anchor as P, ExportFormat as F, MetadataPolicy as M, SharpenAmount as A, SharpenFor as S};
|
||||
let n = app.session.selection.ids.len().max(1);
|
||||
@@ -282,9 +269,14 @@ pub fn confirm_dialog(app: &mut LightcraftApp, dlg: &Dialog) -> Result<serde_jso
|
||||
Dialog::RenameAlbum { id, name } => app.run("album.rename", json!({"id": id, "name": name})),
|
||||
Dialog::AutoStack { gap } => app.run("stack.auto", json!({"gap": gap})),
|
||||
Dialog::NewSmartAlbum { name } => app.run("album.createSmart", json!({"name": if name.trim().is_empty() { "Smart Album" } else { name }})),
|
||||
Dialog::CreatePreset { name, group } => {
|
||||
app.run("preset.create", json!({"name": if name.is_empty() { "My Preset" } else { name }, "group": group}))
|
||||
}
|
||||
Dialog::CreatePreset { name, group, groups } => app.run(
|
||||
"preset.create",
|
||||
json!({
|
||||
"name": if name.trim().is_empty() { "My Preset" } else { name },
|
||||
"group": if group.trim().is_empty() { "User Presets" } else { group },
|
||||
"groups": groups,
|
||||
}),
|
||||
),
|
||||
Dialog::CopySettings { groups } => app.run("develop.copy", json!({"groups": groups})),
|
||||
Dialog::Export { opts, long_edge, limit_kb, dir } => app.run(
|
||||
"app.export",
|
||||
@@ -301,6 +293,38 @@ pub fn confirm_dialog(app: &mut LightcraftApp, dlg: &Dialog) -> Result<serde_jso
|
||||
|
||||
// ------------------------------------------------------------------------------------------ dialog widgets
|
||||
|
||||
/// Two columns of settings-group checkboxes (Copy Settings, Create Preset) plus All / None.
|
||||
fn group_checklist(ui: &mut egui::Ui, tag: &str, groups: &mut Vec<String>) {
|
||||
let key_of = |g: &SettingsGroup| serde_json::to_value(g).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default();
|
||||
ui.columns(2, |cols| {
|
||||
for (i, g) in SettingsGroup::ALL.iter().enumerate() {
|
||||
let key = key_of(g);
|
||||
let mut on = groups.contains(&key);
|
||||
let r = cols[i % 2].checkbox(&mut on, g.label());
|
||||
crate::widgets::register(&r.ctx, format!("{tag}:{key}"), r.rect);
|
||||
if r.changed() {
|
||||
if on {
|
||||
groups.push(key);
|
||||
} else {
|
||||
groups.retain(|x| *x != key);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
let all = ui.small_button("All");
|
||||
crate::widgets::register(ui.ctx(), format!("{tag}:all"), all.rect);
|
||||
if all.clicked() {
|
||||
*groups = SettingsGroup::ALL.iter().map(key_of).collect();
|
||||
}
|
||||
let none = ui.small_button("None");
|
||||
crate::widgets::register(ui.ctx(), format!("{tag}:none"), none.rect);
|
||||
if none.clicked() {
|
||||
groups.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Width of the label column in dialogs.
|
||||
const LABEL_W: f32 = 78.0;
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
//! The Presets column (opens to the left of the Edit panel): grouped presets with hover preview,
|
||||
//! amount slider, create/favourite, import/export of preset files.
|
||||
//! The Presets column (opens to the left of the Edit panel): grouped presets with hover preview
|
||||
//! (the loupe shows the photo with the preset while the pointer rests on it; nothing is
|
||||
//! committed), optional live thumbnails, amount slider, create/favourite, import/export of preset
|
||||
//! files.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use egui::{Align2, Rect, Sense, pos2, vec2};
|
||||
use lightcraft_develop::{ControlSpec, Section, Track};
|
||||
use egui::{Align2, Color32, Rect, Sense, pos2, vec2};
|
||||
use lightcraft_develop::{ControlSpec, Preset, Section, Track};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::LightcraftApp;
|
||||
use crate::icons::{Icon, paint};
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets::{divider, icon_button, register, slider};
|
||||
use crate::{HoverPreview, LightcraftApp};
|
||||
|
||||
/// Long edge of preset thumbnails (px).
|
||||
const THUMB_EDGE: usize = 128;
|
||||
|
||||
pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
@@ -29,10 +34,13 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
);
|
||||
if icon_button(&mut hdr, "presetCreate", Icon::Plus, vec2(28.0, 28.0), false, app.session.active().is_some(), "Create Preset…").clicked()
|
||||
{
|
||||
app.ui.dialog = Some(crate::state::Dialog::CreatePreset { name: String::new(), group: "User Presets".into() });
|
||||
app.ui.dialog = Some(crate::state::Dialog::create_preset());
|
||||
}
|
||||
let more = icon_button(&mut hdr, "presetMore", Icon::More, vec2(28.0, 28.0), false, true, "More preset options");
|
||||
egui::Popup::menu(&more).show(|ui| {
|
||||
let r = ui.checkbox(&mut app.ui.preset_thumbs, "Show Thumbnails");
|
||||
register(ui.ctx(), "presetMenu:thumbnails", r.rect);
|
||||
ui.separator();
|
||||
if ui.button("Import Presets…").clicked() {
|
||||
let _ = app.run("file.importPresets", json!({}));
|
||||
}
|
||||
@@ -66,10 +74,14 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
divider(ui);
|
||||
}
|
||||
let active = app.session.active();
|
||||
let current = active.and_then(|id| app.session.develop_of(id)).map(|d| (*d).clone());
|
||||
let thumbs = app.ui.preset_thumbs && active.is_some();
|
||||
let row_h = if thumbs { 54.0 } else { 26.0 };
|
||||
egui::ScrollArea::vertical().id_salt("presets-scroll").auto_shrink([false, false]).show(ui, |ui| {
|
||||
let mut groups: BTreeMap<String, Vec<(String, String, bool)>> = BTreeMap::new();
|
||||
let mut groups: BTreeMap<String, Vec<Preset>> = BTreeMap::new();
|
||||
for p in &app.session.presets {
|
||||
groups.entry(p.group.clone()).or_default().push((p.id.clone(), p.name.clone(), p.favorite));
|
||||
groups.entry(p.group.clone()).or_default().push(p.clone());
|
||||
}
|
||||
for (g, items) in groups {
|
||||
let open_id = egui::Id::new(("preset-group", &g));
|
||||
@@ -94,16 +106,29 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
if !open {
|
||||
continue;
|
||||
}
|
||||
for (pid, name, fav) in items {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 26.0), Sense::click());
|
||||
for pr in items {
|
||||
let (pid, name, fav) = (pr.id.clone(), pr.name.clone(), pr.favorite);
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), row_h), Sense::click());
|
||||
register(ui.ctx(), format!("preset:{pid}"), r);
|
||||
let active = last.as_ref().is_some_and(|(l, _)| *l == pid);
|
||||
if active {
|
||||
let is_last = last.as_ref().is_some_and(|(l, _)| *l == pid);
|
||||
if is_last {
|
||||
ui.painter().rect_filled(r, 0.0, t.tool_active);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(r, 0.0, t.hover.gamma_multiply(0.7));
|
||||
}
|
||||
ui.painter().text(pos2(r.left() + 40.0, r.center().y), Align2::LEFT_CENTER, &name, t.font(13.0), t.text_label);
|
||||
let look = if thumbs || resp.hovered() { current.as_ref().map(|d| pr.apply(d, 1.0)) } else { None };
|
||||
let mut text_x = r.left() + 40.0;
|
||||
if thumbs && let (Some(id), Some(s)) = (active, look.as_ref()) {
|
||||
let tr = Rect::from_min_size(pos2(r.left() + 36.0, r.top() + 5.0), vec2(66.0, row_h - 10.0));
|
||||
ui.painter().rect_filled(tr, 2.0, t.canvas);
|
||||
if let Some(job) = app.session.variant_job(id, s, THUMB_EDGE)
|
||||
&& let Some(tex) = app.renderer.variant(job)
|
||||
{
|
||||
ui.painter().image(tex.tex.id(), tr, cover_uv(tr, tex.size), Color32::WHITE);
|
||||
}
|
||||
text_x = tr.right() + 10.0;
|
||||
}
|
||||
ui.painter().text(pos2(text_x, r.center().y), Align2::LEFT_CENTER, &name, t.font(13.0), t.text_label);
|
||||
if fav {
|
||||
paint(
|
||||
ui.painter(),
|
||||
@@ -112,6 +137,12 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
t.star,
|
||||
);
|
||||
}
|
||||
// resting on a preset previews it in the loupe (no history entry)
|
||||
if resp.hovered()
|
||||
&& let Some(s) = look
|
||||
{
|
||||
app.hover_preview = Some(HoverPreview { label: format!("Preset: {name}"), settings: s });
|
||||
}
|
||||
if resp.clicked() {
|
||||
let _ = app.run("preset.apply", json!({"id": pid, "amount": 100}));
|
||||
ui.data_mut(|d| d.insert_temp(amt_id, (pid.clone(), 100.0)));
|
||||
@@ -131,3 +162,16 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// The part of a `size` image that covers `r` (centre crop), as texture coordinates.
|
||||
pub fn cover_uv(r: Rect, size: [usize; 2]) -> Rect {
|
||||
let a = size[0] as f32 / size[1].max(1) as f32;
|
||||
let ra = r.width() / r.height().max(1.0);
|
||||
if a > ra {
|
||||
let f = ra / a;
|
||||
Rect::from_min_max(pos2((1.0 - f) / 2.0, 0.0), pos2((1.0 + f) / 2.0, 1.0))
|
||||
} else {
|
||||
let f = a / ra;
|
||||
Rect::from_min_max(pos2(0.0, (1.0 - f) / 2.0), pos2(1.0, (1.0 + f) / 2.0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use lightcraft_develop::DevelopSettings;
|
||||
use lightcraft_engine::presets::{PROFILES, ProfileInfo, profile, profile_groups};
|
||||
use serde_json::json;
|
||||
|
||||
use super::presets::cover_uv;
|
||||
use crate::icons::{Icon, paint};
|
||||
use crate::state::RightPanel;
|
||||
use crate::theme::Tokens;
|
||||
@@ -92,19 +93,6 @@ fn with_profile(d: &DevelopSettings, p: &ProfileInfo) -> DevelopSettings {
|
||||
s
|
||||
}
|
||||
|
||||
/// The part of a `size` image that covers `r` (centre crop), as texture coordinates.
|
||||
fn cover_uv(r: Rect, size: [usize; 2]) -> Rect {
|
||||
let a = size[0] as f32 / size[1].max(1) as f32;
|
||||
let ra = r.width() / r.height().max(1.0);
|
||||
if a > ra {
|
||||
let f = ra / a;
|
||||
Rect::from_min_max(pos2((1.0 - f) / 2.0, 0.0), pos2((1.0 + f) / 2.0, 1.0))
|
||||
} else {
|
||||
let f = a / ra;
|
||||
Rect::from_min_max(pos2(0.0, (1.0 - f) / 2.0), pos2(1.0, (1.0 + f) / 2.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn cell_ui(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, id: PhotoId, tag: &str, p: &'static ProfileInfo, cell: Rect, th: f32) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
|
||||
@@ -85,6 +85,8 @@ pub struct UiState {
|
||||
pub right: RightPanel,
|
||||
/// Presets column open (opens to the left of the Edit panel).
|
||||
pub presets: bool,
|
||||
/// Presets column: show a live thumbnail of the photo with each preset.
|
||||
pub preset_thumbs: bool,
|
||||
pub filmstrip: bool,
|
||||
pub zoom: Zoom,
|
||||
/// Pan offset of the loupe when zoomed (image-normalized centre).
|
||||
@@ -142,6 +144,19 @@ pub struct UiState {
|
||||
pub dialog: Option<Dialog>,
|
||||
}
|
||||
|
||||
/// Settings group ids (`SettingsGroup` serde names) a new preset includes by default: everything
|
||||
/// but crop, masks, spots and red eye.
|
||||
pub fn default_preset_groups() -> Vec<String> {
|
||||
lightcraft_develop::SettingsGroup::default_copy().iter().filter_map(|g| serde_json::to_value(g).ok()?.as_str().map(str::to_string)).collect()
|
||||
}
|
||||
|
||||
impl Dialog {
|
||||
/// A fresh Create Preset dialog.
|
||||
pub fn create_preset() -> Dialog {
|
||||
Dialog::CreatePreset { name: String::new(), group: "User Presets".into(), groups: default_preset_groups() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||
pub enum Dialog {
|
||||
@@ -161,9 +176,12 @@ pub enum Dialog {
|
||||
NewSmartAlbum {
|
||||
name: String,
|
||||
},
|
||||
/// `groups`: the settings groups the preset includes (`SettingsGroup` ids).
|
||||
CreatePreset {
|
||||
name: String,
|
||||
group: String,
|
||||
#[serde(default = "default_preset_groups")]
|
||||
groups: Vec<String>,
|
||||
},
|
||||
CopySettings {
|
||||
groups: Vec<String>,
|
||||
@@ -190,6 +208,7 @@ impl Default for UiState {
|
||||
left_panel: false,
|
||||
right: RightPanel::Edit,
|
||||
presets: false,
|
||||
preset_thumbs: false,
|
||||
filmstrip: true,
|
||||
zoom: Zoom::Fit,
|
||||
pan: (0.5, 0.5),
|
||||
|
||||
+12
-14
@@ -64,21 +64,19 @@ Ordered by tier, then user value, then (low) effort. Take the first one nobody i
|
||||
4. **LR-REM-SPOT-EDIT + LR-REM-BRUSH-PARAMS** (P0) — select a spot pin, move target/source, delete with ⌫,
|
||||
feather/opacity sliders, `[`/`]` size keys. Medium effort.
|
||||
5. **LR-VIEW-PHOTOGRID** (P0) — group the justified grid by capture date with headers. Low–medium effort.
|
||||
6. **LR-PRE-CREATE + LR-PRE-PANEL + LR-BEHAV-PREVIEW-HOVER** (P0/P1) — per-group checkboxes in Create Preset;
|
||||
live preview while hovering presets/profiles/versions. Medium effort.
|
||||
7. **LR-LIB-KEYWORD** (P0) — rename/delete a keyword library-wide; keyword list in the left panel for browsing.
|
||||
8. **LR-PREF-GENERAL + LR-IMP-RAWDEFAULT** (P0/P1) — a Settings dialog: raw defaults (preset or camera-specific),
|
||||
6. **LR-LIB-KEYWORD** (P0) — rename/delete a keyword library-wide; keyword list in the left panel for browsing.
|
||||
7. **LR-PREF-GENERAL + LR-IMP-RAWDEFAULT** (P0/P1) — a Settings dialog: raw defaults (preset or camera-specific),
|
||||
XMP prefs, cache, GPU. Medium effort.
|
||||
9. **LR-IMP-ADD-DIALOG + LR-IMP-LOCAL** (P0) — an import review grid with per-photo checkboxes and destination
|
||||
8. **LR-IMP-ADD-DIALOG + LR-IMP-LOCAL** (P0) — an import review grid with per-photo checkboxes and destination
|
||||
album; browse folders before adding. Medium effort.
|
||||
10. **LR-EXP-TYPE + LR-EXP-DIM** (P0) — Original (+XMP) and DNG export (writer exists in
|
||||
9. **LR-EXP-TYPE + LR-EXP-DIM** (P0) — Original (+XMP) and DNG export (writer exists in
|
||||
`crates/raw/src/dngwrite.rs`), short edge / width / height / megapixels, "don't enlarge", ppi.
|
||||
11. **LR-IMP-FORMATS** (P0) — CR3, compressed NEF/RAF/ORF, RW2 v4 (preview only today); HEIC/AVIF decode (no
|
||||
10. **LR-IMP-FORMATS** (P0) — CR3, compressed NEF/RAF/ORF, RW2 v4 (preview only today); HEIC/AVIF decode (no
|
||||
permissive pure-Rust decoder yet). High value, high effort (clean sources needed).
|
||||
12. **LR-VIEW-FULLSCREEN + LR-VIEW-NAVIGATOR + LR-VIEW-INFOOVERLAY** (P1) — view modes. Medium effort.
|
||||
13. **LR-LIB-RENAME + LR-LIB-CAPTURETIME + LR-LIB-LABEL UI** (P1) — batch rename, capture-time edit, label menu/names.
|
||||
14. **LR-EXP-BITDEPTH + LR-EXP-COMPRESSION** (P1) — 16-bit TIFF/PNG and TIFF compression choice.
|
||||
15. **LR-EDIT-OPTICS-PROFILE** (P1) — a lens-profile database of our own (embedded DNG/maker corrections work today).
|
||||
11. **LR-VIEW-FULLSCREEN + LR-VIEW-NAVIGATOR + LR-VIEW-INFOOVERLAY** (P1) — view modes. Medium effort.
|
||||
12. **LR-LIB-RENAME + LR-LIB-CAPTURETIME + LR-LIB-LABEL UI** (P1) — batch rename, capture-time edit, label menu/names.
|
||||
13. **LR-EXP-BITDEPTH + LR-EXP-COMPRESSION** (P1) — 16-bit TIFF/PNG and TIFF compression choice.
|
||||
14. **LR-EDIT-OPTICS-PROFILE** (P1) — a lens-profile database of our own (embedded DNG/maker corrections work today).
|
||||
|
||||
## Shortcuts: conflicts and missing bindings
|
||||
|
||||
@@ -357,8 +355,8 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
|
||||
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| LR-PRE-PANEL | Presets panel | P0 | 🟡 | `cmd:panel.presets`, `cmd:preset.apply`, `crates/ui-egui/src/panels/presets.rs` | grouped list with amount; no hover preview |
|
||||
| LR-PRE-CREATE | Create preset | P0 | 🟡 | `cmd:dialog.createPreset`, `cmd:preset.create` (`groups`) | dialog has no per-group checkboxes (fixed subset) |
|
||||
| LR-PRE-PANEL | Presets panel | P0 | ✅ | `cmd:panel.presets`, `cmd:preset.apply`, `crates/ui-egui/src/panels/presets.rs` | grouped list with amount; resting on a preset previews it in the loupe (no history entry); optional live thumbnails (⋯ → Show Thumbnails) |
|
||||
| LR-PRE-CREATE | Create preset | P0 | ✅ | `cmd:dialog.createPreset`, `cmd:preset.create` (`groups`) | name, group and a checklist of settings groups (crop, masks, remove, red eye off by default; All / None) |
|
||||
| LR-PRE-MANAGE | Manage presets | P1 | 🟡 | `cmd:preset.delete`, `cmd:preset.favorite`, `cmd:preset.import`, `cmd:preset.export` | no rename, update-with-current, move group, hide groups |
|
||||
| LR-PRE-AMOUNT | Preset amount | P1 | ✅ | `cmd:preset.apply` (`amount` 0–200) | |
|
||||
| LR-PRE-ADAPTIVE | Adaptive presets | P2 | ⬜ | | |
|
||||
@@ -495,7 +493,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| LR-BEHAV-UNDO | Global undo | P0 | ✅ | `cmd:edit.undo`, `crates/engine/src/tests.rs` (`rating_flag_undo_redo`) | covers ratings, albums, deletes, edits |
|
||||
| LR-BEHAV-MULTISELECT | Multi-selection | P0 | ✅ | `cmd:library.select` (`replace`/`add`/`toggle`/`range`), `cmd:library.selectAll` | |
|
||||
| LR-BEHAV-BATCH | Batch apply to selection | P0 | ✅ | `cmd:photo.rate`, `cmd:develop.paste`, `cmd:preset.apply`, `cmd:app.export` | |
|
||||
| LR-BEHAV-PREVIEW-HOVER | Hover previews | P1 | ⬜ | | presets, profiles, versions |
|
||||
| LR-BEHAV-PREVIEW-HOVER | Hover previews | P1 | 🟡 | `crates/ui-egui/src/panels/presets.rs`, `crates/ui-egui/src/panels/profiles.rs`, `crates/ui-egui/src/panels/detail.rs` | presets, profile menu and profile browser preview in the loupe; versions do not |
|
||||
| LR-BEHAV-PROGRESSIVE | Progressive rendering | P0 | ✅ | `crates/engine/src/media.rs`, `crates/preview/src/lib.rs` | |
|
||||
| LR-BEHAV-BG-TASKS | Background tasks | P0 | 🟡 | `crates/preview/src/lib.rs` (`JobPool`) | renders off the UI thread; no progress popover for import/export |
|
||||
| LR-BEHAV-OFFLINE | Offline editing | P1 | ✅ | | local-first: everything works offline |
|
||||
|
||||
Reference in New Issue
Block a user