Merge pull request #73 from storytold/fix/issue-30-import-entry-point
CI: clean under clippy 1.99 (as_chunks for constant chunk sizes, ? in export resize parsing)
This commit is contained in:
commit
c02b39a15d
|
|
@ -732,7 +732,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
),
|
||||
cmd!(
|
||||
"library.import",
|
||||
"Add Photos…",
|
||||
"Import Photos",
|
||||
["File"],
|
||||
Some("Cmd+Shift+I"),
|
||||
"{paths: [file or folder (recursive)], mode?: add|copy (add = reference the files in place; copy = into the library's Originals/YYYY/YYYY-MM-DD/), destination?: folder for copies, organize?: date (YYYY/YYYY-MM-DD) | month (YYYY/YYYY-MM) | flat | a folder template, e.g. `{date:%Y}/{date:%Y%m%d}` → 2026/20260114 (the template's `/` make the folders, each level expanded with the rename tokens and made a safe folder name: never outside the destination; must be relative, no `..`; a level with missing metadata is `unknown`) — dated by capture time, else the import time, rename?: file-name template for copies, original extension added (tokens: {name} {num} {seq} {seq:N} {date} {date:%Y%m%d} {folder} {camera} {lens} {iso} {rating} {title} {creator} {ext}; photo.renameTokens explains each; blank = keep names), renameStart?: 1, metadataPreset?: name, dng?: bool (copy raws as DNG), local?: bool (browsing: the photos stay out of the library, like library.browse), album?: albumId, albumName?: new album, preset?: presetId, keywords?: [..]} → {imported, duplicates, failed, album?}",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! Cameras and memory cards: mounted volumes with a `DCIM` folder (the DCF layout every camera
|
||||
//! writes). Listed by `library.devices`; the app offers them under File → Add from Device, and
|
||||
//! writes). Listed by `library.devices`; the app offers them under File → Import from Device, and
|
||||
//! the import review copies from them into the library.
|
||||
//!
|
||||
//! Menus are rebuilt every frame (even with their popups closed), so [`devices`] never touches the
|
||||
|
|
|
|||
|
|
@ -942,7 +942,7 @@ mod tests {
|
|||
assert!(h.quit_requested());
|
||||
}
|
||||
|
||||
/// File → Add Folder… opens the import review for a folder (searched recursively).
|
||||
/// File → Import from Folder… opens the import review for a folder (searched recursively).
|
||||
#[test]
|
||||
fn add_folder_opens_the_import_review() {
|
||||
let mut h = demo([1200.0, 900.0]);
|
||||
|
|
@ -964,6 +964,13 @@ mod tests {
|
|||
let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review: {:?}", h.app.ui.dialog) };
|
||||
assert_eq!(opts.candidates.len(), 2, "both files, the subfolder's too");
|
||||
assert!(!opts.copy, "a folder is added in place by default");
|
||||
// the review names its source; scanning doesn't save a Local location
|
||||
assert_eq!(opts.sources, vec![dir.to_string_lossy().to_string()]);
|
||||
let name = dir.file_name().unwrap().to_string_lossy().to_string();
|
||||
assert_eq!(crate::import::source_summary(&opts.sources), format!("Folder “{name}” (and its subfolders)"));
|
||||
let r = h.request("ui.widgets", json!({}), t);
|
||||
assert!(r.to_string().contains("label:importSource"), "source shown");
|
||||
assert!(h.app.ui.local_roots.is_empty(), "no Local shortcut saved");
|
||||
// a camera / card folder: copied into the library by default
|
||||
h.app.ui.dialog = None;
|
||||
let r = h.request("engine.execute", json!({"command": "file.addFromDevice", "params": {"path": sub.to_string_lossy()}}), t);
|
||||
|
|
@ -1157,7 +1164,7 @@ mod tests {
|
|||
h.settle(SETTLE);
|
||||
}
|
||||
|
||||
/// File → Add Photos… opens the import review: candidates with thumbnails, the duplicate is
|
||||
/// File → Import Photos… opens the import review: candidates with thumbnails, the duplicate is
|
||||
/// unchecked; unchecking a cell and confirming imports the rest in batches, into a new album
|
||||
/// with keywords, as one undo step.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
//! The import review dialog (File → Add Photos…): the files found under the chosen files and
|
||||
//! folders as a grid of thumbnails with checkboxes (duplicates marked and unchecked), the
|
||||
//! destination (add in place / copy into the library's `Originals/` or a chosen folder, filed by day, by month,
|
||||
//! into one folder or by a custom folder template, optionally renamed), an album
|
||||
//! The import review dialog (File → Import Photos… / Import from Folder… / Import from Device):
|
||||
//! the source it scanned (a scanned folder is *not* added to Local), the files found under it as
|
||||
//! a grid of thumbnails with checkboxes (duplicates marked and unchecked), the destination (add in
|
||||
//! place / copy into the library's `Originals/` or a chosen folder, filed by day, by month, into
|
||||
//! one folder or by a custom folder template, optionally renamed), an album
|
||||
//! (existing or new), a preset and keywords to apply. Importing runs in small batches, one per
|
||||
//! frame, with a progress window; the whole import is one undo step.
|
||||
|
||||
|
|
@ -22,6 +23,8 @@ pub(crate) const BATCH: usize = 8;
|
|||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct ImportDialog {
|
||||
/// The files / folders that were scanned (shown as the source).
|
||||
pub sources: Vec<String>,
|
||||
pub candidates: Vec<ImportCandidate>,
|
||||
pub checked: Vec<bool>,
|
||||
/// Copy into the library (else add in place).
|
||||
|
|
@ -111,6 +114,8 @@ pub struct ScanTask {
|
|||
pub copy: bool,
|
||||
/// Browsing a folder (Local): the photos are read in place instead of opening the review.
|
||||
browse: bool,
|
||||
/// What is being scanned (the review's source).
|
||||
sources: Vec<String>,
|
||||
}
|
||||
|
||||
/// Scan `paths` in the background, then open the review dialog (see [`poll_scan`]).
|
||||
|
|
@ -118,6 +123,7 @@ pub fn open(app: &mut LightcraftApp, paths: Vec<String>) -> Result<Value, String
|
|||
if app.scan.is_some() {
|
||||
return Err("a scan is already running".into());
|
||||
}
|
||||
let sources = paths.clone();
|
||||
let (input, paths) = ScanInput::new(&mut app.session, &paths);
|
||||
let progress = std::sync::Arc::new(ScanProgress::default());
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
|
@ -129,7 +135,7 @@ pub fn open(app: &mut LightcraftApp, paths: Vec<String>) -> Result<Value, String
|
|||
std::thread::spawn(job);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
job();
|
||||
app.scan = Some(ScanTask { progress, rx, copy: false, browse: false });
|
||||
app.scan = Some(ScanTask { progress, rx, copy: false, browse: false, sources });
|
||||
Ok(json!({"scanning": true}))
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +196,7 @@ pub fn browse(app: &mut LightcraftApp, path: &str, subfolders: Option<bool>) ->
|
|||
std::thread::spawn(job);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
job();
|
||||
app.scan = Some(ScanTask { progress, rx, copy: false, browse: true });
|
||||
app.scan = Some(ScanTask { progress, rx, copy: false, browse: true, sources: Vec::new() });
|
||||
app.renderer.forget_imports();
|
||||
Ok(json!({"path": dir_s, "subfolders": subfolders, "scanning": true}))
|
||||
}
|
||||
|
|
@ -231,6 +237,7 @@ pub fn poll_scan(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
app.renderer.forget_imports();
|
||||
let mut d = ImportDialog::new(out.candidates);
|
||||
d.copy = task.copy;
|
||||
d.sources = task.sources;
|
||||
app.ui.dialog = Some(crate::state::Dialog::Import { opts: Box::new(d) });
|
||||
}
|
||||
|
||||
|
|
@ -360,7 +367,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
if let Some(f) = task.first {
|
||||
let _ = app.run("library.select", json!({"ids": [f]}));
|
||||
}
|
||||
let mut msg = format!("Added {} photo{}", task.imported, if task.imported == 1 { "" } else { "s" });
|
||||
let mut msg = format!("Imported {} photo{}", task.imported, if task.imported == 1 { "" } else { "s" });
|
||||
if task.duplicates > 0 {
|
||||
msg.push_str(&format!(" · {} duplicate{} skipped", task.duplicates, if task.duplicates == 1 { "" } else { "s" }));
|
||||
}
|
||||
|
|
@ -426,7 +433,18 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
|||
});
|
||||
ui.add_space(4.0);
|
||||
// options
|
||||
field(ui, "Destination", |ui| {
|
||||
if !d.sources.is_empty() {
|
||||
field(ui, "Source", |ui| {
|
||||
let r = ui.label(egui::RichText::new(source_summary(&d.sources)).color(t.text_label)).on_hover_text(d.sources.join("\n"));
|
||||
register(ui.ctx(), "label:importSource", r.rect);
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new("Importing scans the source for photos; it doesn't add the folder to Local (use Local → Browse Folder… to work in a folder without importing).")
|
||||
.color(t.text_dim)
|
||||
.small(),
|
||||
);
|
||||
}
|
||||
field(ui, "Transfer", |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
if crate::widgets::text_button(ui, "importAdd", "Add in place", !d.copy).on_hover_text("Reference the files where they are").clicked() {
|
||||
d.copy = false;
|
||||
|
|
@ -437,6 +455,15 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
|||
d.copy = true;
|
||||
}
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new(if d.copy {
|
||||
"Copy: the files are copied to the folder below and the library uses the copies; the originals are left as they are."
|
||||
} else {
|
||||
"Add in place: the library references the files where they are; nothing is copied or moved."
|
||||
})
|
||||
.color(t.text_dim)
|
||||
.small(),
|
||||
);
|
||||
if d.copy {
|
||||
field(ui, "Copy to", |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
|
|
@ -775,6 +802,23 @@ pub fn example_destination(app: &LightcraftApp, d: &ImportDialog) -> Option<Stri
|
|||
Some(parts.join(sep))
|
||||
}
|
||||
|
||||
/// The import source in a few words: a folder's name, a file's name, or "N files and folders".
|
||||
pub fn source_summary(sources: &[String]) -> String {
|
||||
let name = |p: &str| std::path::Path::new(p).file_name().map_or_else(|| p.to_string(), |n| n.to_string_lossy().to_string());
|
||||
match sources {
|
||||
[one] if std::path::Path::new(one).is_dir() => format!("Folder “{}” (and its subfolders)", name(one)),
|
||||
[one] => name(one),
|
||||
many => {
|
||||
let folders = many.iter().filter(|p| std::path::Path::new(p.as_str()).is_dir()).count();
|
||||
match folders {
|
||||
0 => format!("{} files", many.len()),
|
||||
f if f == many.len() => format!("{f} folders (and their subfolders)"),
|
||||
f => format!("{} files and {f} folder{}", many.len() - f, if f == 1 { "" } else { "s" }),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A labelled row (fixed label column).
|
||||
fn field<R>(ui: &mut egui::Ui, label: &str, add: impl FnOnce(&mut egui::Ui) -> R) -> R {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ const LAYOUT: &[(&str, &[&str])] = &[
|
|||
&[
|
||||
"file.addPhotos",
|
||||
"file.addFolder",
|
||||
"@Add from Device",
|
||||
"@Import from Device",
|
||||
"---",
|
||||
"app.openLibrary",
|
||||
"---",
|
||||
|
|
@ -433,7 +433,7 @@ fn expanded(app: &LightcraftApp, name: &str) -> Option<Vec<MenuNode>> {
|
|||
v.extend(groups.into_iter().map(|(label, g, k)| item("library.sort", json!({"group": k}), label, None, true, Some(cur.group == g))));
|
||||
v
|
||||
}
|
||||
"Add from Device" => {
|
||||
"Import from Device" => {
|
||||
let devices = lightcraft_engine::devices::devices();
|
||||
if devices.is_empty() {
|
||||
vec![item("file.addFromDevice", Value::Null, "No Camera or Card Found", None, false, None)]
|
||||
|
|
@ -733,6 +733,28 @@ mod tests {
|
|||
})
|
||||
}
|
||||
|
||||
/// File opens with the import entry points, worded as importing (not as adding a sidebar
|
||||
/// folder): Import Photos… (⇧⌘I), Import from Folder…, Import from Device ▸.
|
||||
#[test]
|
||||
fn file_menu_starts_with_the_import_entry_points() {
|
||||
let bar = menu_bar(&app());
|
||||
let file = &bar.iter().find(|(t, _)| t == "File").expect("File menu").1;
|
||||
let labels: Vec<String> = file
|
||||
.iter()
|
||||
.take(3)
|
||||
.map(|n| match n {
|
||||
MenuNode::Item { label, shortcut, .. } => format!("{label}{}", shortcut.as_deref().map(|s| format!(" [{s}]")).unwrap_or_default()),
|
||||
MenuNode::Submenu { label, .. } => format!("{label} ▸"),
|
||||
MenuNode::Separator => "---".into(),
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(labels[0], "Import Photos… [Cmd+Shift+I]");
|
||||
assert_eq!(labels[1..], ["Import from Folder…".to_string(), "Import from Device ▸".to_string()]);
|
||||
let all: Vec<MenuNode> = bar.iter().flat_map(|(_, v)| v.clone()).collect();
|
||||
let text = serde_json::to_string(&all).unwrap();
|
||||
assert!(!text.contains("Add Folder") && !text.contains("\"Add Photos"), "no add-folder wording left in the menus");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar_follows_the_registry_and_state() {
|
||||
let mut app = app();
|
||||
|
|
|
|||
|
|
@ -110,9 +110,9 @@ pub const UI_COMMANDS: &[UiCommand] = &[
|
|||
("merge.hdrLast", "HDR with Last Settings", Some("Ctrl+Shift+H"), "Photo>Photo Merge"),
|
||||
("merge.panoramaLast", "Panorama with Last Settings", Some("Ctrl+Shift+M"), "Photo>Photo Merge"),
|
||||
("merge.hdrPanoramaLast", "HDR Panorama with Last Settings", None, "Photo>Photo Merge"),
|
||||
("file.addPhotos", "Add Photos…", Some("Cmd+Shift+I"), "File"),
|
||||
("file.addFolder", "Add Folder…", None, "File"),
|
||||
("file.addFromDevice", "Add from Device", None, ""),
|
||||
("file.addPhotos", "Import Photos…", Some("Cmd+Shift+I"), "File"),
|
||||
("file.addFolder", "Import from Folder…", None, "File"),
|
||||
("file.addFromDevice", "Import from Device", None, ""),
|
||||
("file.findMissing", "Find Missing Photos…", None, "File"),
|
||||
("photo.locate", "Locate Missing File…", None, ""),
|
||||
("dialog.saveMetadataPreset", "Save Metadata Preset…", None, ""),
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
Dialog::NewAlbum { .. } => "Create Album",
|
||||
Dialog::RenameAlbum { .. } => "Rename Album",
|
||||
Dialog::Rename { .. } => "Rename Photos",
|
||||
Dialog::Import { .. } => "Add Photos",
|
||||
Dialog::Import { .. } => "Import Photos",
|
||||
Dialog::LabelNames { .. } => "Edit Color Label Names",
|
||||
Dialog::CaptureTime { .. } => "Edit Capture Time",
|
||||
Dialog::RenameKeyword { .. } => "Rename Keyword",
|
||||
|
|
@ -720,7 +720,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
let ok = match &dlg {
|
||||
Dialog::Import { opts } => {
|
||||
let n = opts.selected_paths().len();
|
||||
add_label = format!("Add {n} Photo{}", if n == 1 { "" } else { "s" });
|
||||
add_label = format!("Import {n} Photo{}", if n == 1 { "" } else { "s" });
|
||||
add_label.as_str()
|
||||
}
|
||||
Dialog::Merge { .. } => "Merge",
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
} else if app.session.source == lightcraft_engine::LibrarySource::Folder {
|
||||
super::empty_message(ui, ui.max_rect(), "No photos in this folder", "Turn on Include subfolders, or pick another folder under Local");
|
||||
} else {
|
||||
super::empty_message(ui, ui.max_rect(), "No photos", "Add photos with File → Add Photos (Cmd+Shift+I), or drop them here");
|
||||
super::empty_message(ui, ui.max_rect(), "No photos", "Import photos with File → Import Photos… (Cmd+Shift+I), or drop them here");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -403,7 +403,7 @@ pub enum Dialog {
|
|||
minutes: i32,
|
||||
zone: f32,
|
||||
},
|
||||
/// The import review (File → Add Photos…).
|
||||
/// The import review (File → Import Photos…).
|
||||
Import {
|
||||
opts: Box<crate::import::ImportDialog>,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -99,10 +99,10 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
|||
|
||||
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| LR-IMP-ADD-DIALOG | Add photos/folders | P0 | ✅ | `cmd:file.addPhotos`, `cmd:library.importPreview`, `cmd:library.import` (`mode`, `album`, `albumName`, `preset`, `keywords`), `crates/ui-egui/src/import.rs` | review dialog: candidate grid with thumbnails and checkboxes (duplicates by path/content marked and unchecked), destination, album (existing/new), preset, keywords; the folder scan runs on a background thread with a progress window and Cancel (a NAS folder no longer freezes the window); batched import with a progress window; one undo step. Drag-and-drop still imports directly |
|
||||
| LR-IMP-ADD-DIALOG | Add photos/folders | P0 | ✅ | `cmd:file.addPhotos`, `cmd:library.importPreview`, `cmd:library.import` (`mode`, `album`, `albumName`, `preset`, `keywords`), `crates/ui-egui/src/import.rs` | review dialog (Import Photos): the scanned source (a scanned folder is not added to Local), candidate grid with thumbnails and checkboxes (duplicates by path/content marked and unchecked), transfer mode (add in place / copy, each explained), album (existing/new), preset, keywords; the folder scan runs on a background thread with a progress window and Cancel (a NAS folder no longer freezes the window); batched import with a progress window; one undo step. Drag-and-drop still imports directly |
|
||||
| LR-IMP-DRAGDROP | Drop files/folders to import | P0 | ✅ | `crates/ui-egui/src/lib.rs` (dropped files → `cmd:library.import`) | dropping onto a specific album not supported |
|
||||
| LR-IMP-DUPES | Skip duplicates by content | P1 | ✅ | `crates/engine/src/import.rs`, `crates/engine/src/tests_import.rs` | |
|
||||
| LR-IMP-DEVICE | Import from camera/card | P1 | ✅ | `cmd:library.devices`, `cmd:file.addFromDevice`, `crates/engine/src/devices.rs` | mounted volumes with a DCIM folder (macOS /Volumes, Linux /media and /run/media, Windows drive letters); File → Add from Device → the import review, copying into the library by default; menus serve the last scan and rescan in the background (never blocking a frame; hot-plugs repaint); no PTP/MTP (cameras that don't mount as a disk) |
|
||||
| LR-IMP-DEVICE | Import from camera/card | P1 | ✅ | `cmd:library.devices`, `cmd:file.addFromDevice`, `crates/engine/src/devices.rs` | mounted volumes with a DCIM folder (macOS /Volumes, Linux /media and /run/media, Windows drive letters); File → Import from Device → the import review, copying into the library by default; menus serve the last scan and rescan in the background (never blocking a frame; hot-plugs repaint); no PTP/MTP (cameras that don't mount as a disk) |
|
||||
| LR-IMP-AUTO | Watched-folder auto import | P2 | ✅ | `cmd:library.autoImport`, `cmd:library.autoImportScan`, `crates/ui-egui/src/panels/settings.rs` | Settings ▸ Import ▸ Auto Import: a watched folder whose new photos are added (in place or copied) once complete, into an optional album; scanned every 3 s |
|
||||
| LR-IMP-PRESET | Preset on import | P2 | ✅ | `cmd:library.import` (`preset`) | chosen in the import review; one History entry |
|
||||
| LR-IMP-RAWDEFAULT | Raw defaults | P1 | ✅ | `cmd:library.preferences`, `crates/engine/src/import.rs`, `crates/ui-egui/src/panels/settings.rs` | LightCraft default / a preset / per camera (make + model); non-raw default too; the preset look counts as unedited and Reset returns to it |
|
||||
|
|
@ -506,8 +506,8 @@ visible menu bar yet: the menu model is only exposed through the control channel
|
|||
| MENU-APP-SIGNOUT | Sign out | OOS | 🚫 | | |
|
||||
| MENU-APP-HIDE | Hide / hide others / show all | P1 | ✅ | `apps/lightcraft/src/native_menu.rs` | the system's own items in the app menu (⌘H, ⌥⌘H) |
|
||||
| MENU-APP-QUIT | Quit | P0 | ✅ | `cmd:app.quit`, `apps/lightcraft/src/native_menu.rs` | macOS: app menu (native); elsewhere: File → Quit LightCraft, always the last item in its own group (after commands the File layout does not list) |
|
||||
| MENU-FILE-ADDPHOTOS | Add Photos… | P0 | ✅ | `cmd:file.addPhotos` | |
|
||||
| MENU-FILE-ADDFOLDER | Add Folder… | P0 | ✅ | `cmd:file.addFolder`, `cmd:library.importPreview` | folder picker (desktop) → the import review, subfolders included; `path` param for agents |
|
||||
| MENU-FILE-ADDPHOTOS | Import Photos… (was Add Photos…) | P0 | ✅ | `cmd:file.addPhotos`, `crates/ui-egui/src/menus.rs` | first item of File, ⇧⌘I; file picker → the import review |
|
||||
| MENU-FILE-ADDFOLDER | Import from Folder… (was Add Folder…) | P0 | ✅ | `cmd:file.addFolder`, `cmd:library.importPreview` | folder picker (desktop) → the import review, subfolders included; `path` param for agents. Worded as importing: it doesn't save a Local location (that is Local → Browse Folder…) |
|
||||
| MENU-FILE-MIGRATE | Migrate photos | OOS | 🚫 | | |
|
||||
| MENU-FILE-NEWALBUM | New Album… | P0 | ✅ | `cmd:dialog.newAlbum` | |
|
||||
| MENU-FILE-NEWFOLDER | New Folder… | P0 | ✅ | `cmd:dialog.newFolder` | ⇧⌘N |
|
||||
|
|
@ -639,7 +639,7 @@ From `06-shortcuts.md` part 1. Evidence is our binding; conflicts are explained
|
|||
| KEY-CREATEVERSION | Create version — ⇧M | P1 | ✅ | `cmd:version.create` | secondary binding (primary ⌘⇧S) |
|
||||
| KEY-CLOSEWINDOW | Close window — ⌘W | P1 | ✅ | `apps/lightcraft/src/native_menu.rs` | native File ▸ Close Window |
|
||||
| KEY-DELETE | Delete photo — ⌫ | P0 | ✅ | `cmd:photo.delete` | |
|
||||
| KEY-ADDPHOTOS | Add photos — ⇧⌘I | P0 | ✅ | `cmd:file.addPhotos` | |
|
||||
| KEY-ADDPHOTOS | Import photos — ⇧⌘I | P0 | ✅ | `cmd:file.addPhotos` | |
|
||||
| KEY-VERSIONS | Versions panel — ⇧V | P1 | ✅ | `cmd:panel.versions` | |
|
||||
| KEY-SECTIONS | Expand/collapse edit sections — ⌘1…⌘6 | P1 | ✅ | `cmd:section.light`, `cmd:section.color`, `cmd:section.effects`, `cmd:section.detail`, `cmd:section.optics` | ⌘1–⌘5 (Light, Color, Effects, Detail, Optics) as observed; our Edit panel has no Lens Blur / Geometry section for ⌘6 / ⌘7; Zoom 100% moved to ⌥⌘0 |
|
||||
| KEY-PRESETS | Presets panel — ⇧P | P0 | ✅ | `cmd:panel.presets` | |
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ same-origin or send CORP/CORS headers (the bundle has no third-party resources).
|
|||
(`navigator.storage.persist()`); without it the browser may evict the data under storage
|
||||
pressure. URL options: `?store=idb` forces IndexedDB, `?store=memory` keeps nothing, `?reset`
|
||||
deletes the stored library first.
|
||||
- **Importing photos with no filesystem.** *File ▸ Add Photos…* (<kbd>⌘⇧I</kbd>) opens the
|
||||
- **Importing photos with no filesystem.** *File ▸ Import Photos…* (<kbd>⌘⇧I</kbd>) opens the
|
||||
browser's file picker. You can also drop files anywhere on the page. The bytes are written to
|
||||
storage, then imported and decoded by the same engine code as the desktop app
|
||||
(`lightcraft_engine::files::{probe_bytes, load_bytes}`): JPEG/PNG/TIFF/WebP and the supported
|
||||
|
|
|
|||
Loading…
Reference in New Issue