mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 11:37:13 -06:00
Merge PR #1601
# Conflicts: # crates/ui-egui/src/headless.rs # crates/ui-egui/src/lib.rs # crates/ui-egui/src/state.rs # docs/parity-checklist.md # docs/whats-new.md
This commit is contained in:
@@ -43,6 +43,9 @@ fn prefs(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = i.get("perCamera").and_then(Value::as_bool) {
|
||||
d.per_camera = v;
|
||||
}
|
||||
if let Some(v) = i.get("showOnCard").and_then(Value::as_bool) {
|
||||
d.show_on_card = v;
|
||||
}
|
||||
if let Some(v) = i.get("copyright").and_then(Value::as_str) {
|
||||
d.copyright = v.trim().to_string();
|
||||
}
|
||||
@@ -107,7 +110,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Library Preferences",
|
||||
[],
|
||||
None,
|
||||
"{import?: {rawPreset?: presetId|\"default\", otherPreset?: presetId|\"default\", perCamera?: bool, cameras?: [{camera: \"Make Model\", preset: presetId|null}], copyright?: text, creator?: text (given to imported photos without one)}, camera?: {camera, preset?, remove?: bool}, cacheMb?: n (0 = default), forgetLocalDays?: n (forget untouched Local photos of folders not browsed for n days; 0 = never)} — develop defaults applied on import (raws / other images / per camera) and the thumbnail cache size, saved with the library → {xmp, import, cacheMb, forgetLocalDays, persistent}",
|
||||
"{import?: {rawPreset?: presetId|\"default\", otherPreset?: presetId|\"default\", perCamera?: bool, cameras?: [{camera: \"Make Model\", preset: presetId|null}], copyright?: text, creator?: text (given to imported photos without one), showOnCard?: bool (open the import review when a camera or card is connected)}, camera?: {camera, preset?, remove?: bool}, cacheMb?: n (0 = default), forgetLocalDays?: n (forget untouched Local photos of folders not browsed for n days; 0 = never)} — develop defaults applied on import (raws / other images / per camera) and the thumbnail cache size, saved with the library → {xmp, import, cacheMb, forgetLocalDays, persistent}",
|
||||
always,
|
||||
prefs
|
||||
)]
|
||||
|
||||
@@ -104,6 +104,18 @@ impl DeviceWatch {
|
||||
s.list.clone()
|
||||
}
|
||||
|
||||
/// Like [`DeviceWatch::get`], but `None` until the first scan has landed, so an empty list
|
||||
/// means no cards rather than not looked yet.
|
||||
pub fn get_scanned(self: &Arc<Self>) -> Option<Vec<Device>> {
|
||||
let _ = self.get();
|
||||
let s = self.state();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let scanned = s.scanned.is_some();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let scanned = false;
|
||||
scanned.then(|| s.list.clone())
|
||||
}
|
||||
|
||||
/// Scan now on this thread, and serve the result to later [`DeviceWatch::get`] calls.
|
||||
pub fn scan_now(&self) -> Vec<Device> {
|
||||
let list = (self.scanner)();
|
||||
@@ -148,6 +160,12 @@ pub fn devices() -> Vec<Device> {
|
||||
global().get()
|
||||
}
|
||||
|
||||
/// [`devices`], but `None` until the first scan lands. The import prompt uses it so a card that was
|
||||
/// already in when LightCraft started isn't taken for a new one.
|
||||
pub fn devices_scanned() -> Option<Vec<Device>> {
|
||||
global().get_scanned()
|
||||
}
|
||||
|
||||
/// Scan the mounted volumes now (blocking), for explicit requests.
|
||||
pub fn devices_now() -> Vec<Device> {
|
||||
global().scan_now()
|
||||
@@ -285,4 +303,24 @@ mod tests {
|
||||
assert!(watch.scan_now().is_empty());
|
||||
assert_eq!(changes.load(Ordering::SeqCst), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_scanned_waits_for_the_first_scan() {
|
||||
let gate = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let g = gate.clone();
|
||||
let watch = DeviceWatch::new(
|
||||
move || {
|
||||
while !g.load(Ordering::SeqCst) {
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
vec![card("NIKON Z 8")]
|
||||
},
|
||||
Duration::from_secs(60),
|
||||
);
|
||||
assert_eq!(watch.get_scanned(), None, "nothing scanned yet");
|
||||
assert_eq!(watch.get_scanned(), None);
|
||||
gate.store(true, Ordering::SeqCst);
|
||||
assert!(until(|| watch.get_scanned().is_some()));
|
||||
assert_eq!(watch.get_scanned(), Some(vec![card("NIKON Z 8")]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +310,8 @@ pub struct ImportDefaults {
|
||||
pub auto_folder: Option<String>,
|
||||
pub auto_copy: bool,
|
||||
pub auto_album: Option<String>,
|
||||
/// Open the import review when a camera or memory card is connected.
|
||||
pub show_on_card: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -108,7 +108,7 @@ fn preferences_persist_with_the_library() {
|
||||
s.execute("library.xmpPreferences", &json!({"autoWrite": true})).unwrap();
|
||||
s.execute(
|
||||
"library.preferences",
|
||||
&json!({"import": {"rawPreset": "user.rawlook", "perCamera": true}, "camera": {"camera": "Canon EOS R5", "preset": "user.canonlook"}, "cacheMb": 512}),
|
||||
&json!({"import": {"rawPreset": "user.rawlook", "perCamera": true, "showOnCard": true}, "camera": {"camera": "Canon EOS R5", "preset": "user.canonlook"}, "cacheMb": 512}),
|
||||
)
|
||||
.unwrap();
|
||||
s.close_library().unwrap();
|
||||
@@ -118,6 +118,7 @@ fn preferences_persist_with_the_library() {
|
||||
assert!(s.xmp.auto_write);
|
||||
assert_eq!(s.import_defaults.raw_preset.as_deref(), Some("user.rawlook"));
|
||||
assert!(s.import_defaults.per_camera);
|
||||
assert!(s.import_defaults.show_on_card);
|
||||
assert_eq!(s.import_defaults.cameras.len(), 1);
|
||||
assert_eq!(s.import_defaults.preset_for(true, "Canon EOS R5"), Some("user.canonlook"));
|
||||
assert_eq!(s.cache_mb, 512);
|
||||
|
||||
@@ -1405,6 +1405,7 @@
|
||||
"Show overlay (O)": "Überlagerung anzeigen (O)",
|
||||
"Show ratings, flags and edit badges": "Bewertungen, Markierungen und Bearbeitungssymbole anzeigen",
|
||||
"Show the Navigator while zoomed in": "Navigator beim Hineinzoomen anzeigen",
|
||||
"Show the import review when a camera or card is connected": "Importfenster öffnen, wenn eine Kamera oder Karte angeschlossen wird",
|
||||
"Show the template tags; click one to insert it": "Vorlagenplatzhalter anzeigen; zum Einfügen anklicken",
|
||||
"Show/Hide Mask": "Maske ein-/ausblenden",
|
||||
"Show/hide My Photos panel": "Bereich „Meine Fotos“ ein-/ausblenden",
|
||||
|
||||
@@ -1418,6 +1418,7 @@
|
||||
"Show overlay (O)": "Mostrar superposición (O)",
|
||||
"Show ratings, flags and edit badges": "Mostrar valoraciones, marcas e indicadores de edición",
|
||||
"Show the Navigator while zoomed in": "Mostrar el Navegador al ampliar",
|
||||
"Show the import review when a camera or card is connected": "Abrir la importación al conectar una cámara o tarjeta",
|
||||
"Show the template tags; click one to insert it": "Mostrar las etiquetas de plantilla; hacer clic en una para insertarla",
|
||||
"Show/Hide Mask": "Mostrar/ocultar máscara",
|
||||
"Show/hide My Photos panel": "Mostrar/ocultar panel Mis fotos",
|
||||
|
||||
@@ -1341,6 +1341,7 @@
|
||||
"Show overlay (O)": "Afficher l'incrustation (O)",
|
||||
"Show ratings, flags and edit badges": "Afficher les notes, les drapeaux et les pastilles de retouche",
|
||||
"Show the Navigator while zoomed in": "Afficher le Navigateur lors du zoom",
|
||||
"Show the import review when a camera or card is connected": "Ouvrir l'importation lorsqu'un appareil photo ou une carte est connecté",
|
||||
"Show the template tags; click one to insert it": "Afficher les balises du modèle ; cliquez sur l'une d'elles pour l'insérer",
|
||||
"Show/Hide Mask": "Afficher/masquer le masque",
|
||||
"Show/hide My Photos panel": "Afficher/masquer le panneau Mes photos",
|
||||
|
||||
@@ -1287,6 +1287,7 @@
|
||||
"Show overlay (O)": "重ね表示(O)",
|
||||
"Show ratings, flags and edit badges": "評価・フラグ・編集済みマークを表示",
|
||||
"Show the Navigator while zoomed in": "拡大時にナビゲーターを表示",
|
||||
"Show the import review when a camera or card is connected": "カメラやメモリーカードを接続したときに読み込み画面を表示",
|
||||
"Show the template tags; click one to insert it": "テンプレートのタグを表示(クリックで挿入)",
|
||||
"Show/Hide Mask": "マスクの表示/非表示",
|
||||
"Show/hide My Photos panel": "マイフォトパネルを表示/非表示",
|
||||
|
||||
@@ -1364,6 +1364,7 @@
|
||||
"Show overlay (O)": "Mostrar sobreposição (O)",
|
||||
"Show ratings, flags and edit badges": "Mostrar avaliações, marcações e indicadores de edição",
|
||||
"Show the Navigator while zoomed in": "Mostrar o Navegador quando ampliado",
|
||||
"Show the import review when a camera or card is connected": "Abrir a importação ao conectar uma câmera ou cartão",
|
||||
"Show the template tags; click one to insert it": "Mostrar os marcadores do modelo; clique em um para inseri-lo",
|
||||
"Show/Hide Mask": "Mostrar/ocultar máscara",
|
||||
"Show/hide My Photos panel": "Mostrar/ocultar painel Minhas fotos",
|
||||
|
||||
@@ -1132,6 +1132,7 @@
|
||||
"Show overlay (O)": "Показать наложение (O)",
|
||||
"Show ratings, flags and edit badges": "Показывать оценки, флаги и значки правок",
|
||||
"Show the Navigator while zoomed in": "Показывать навигатор при увеличении",
|
||||
"Show the import review when a camera or card is connected": "Открывать импорт при подключении камеры или карты",
|
||||
"Show the template tags; click one to insert it": "Показать теги шаблона; щёлкните по тегу, чтобы вставить его",
|
||||
"Show/Hide Mask": "Показать/скрыть маску",
|
||||
"Show/hide My Photos panel": "Показать/скрыть панель «Мои фото»",
|
||||
|
||||
@@ -1355,6 +1355,7 @@
|
||||
"Show overlay (O)": "Показати накладання (O)",
|
||||
"Show ratings, flags and edit badges": "Показати оцінки, прапорці й позначки змін",
|
||||
"Show the Navigator while zoomed in": "Показувати навігатор під час збільшення",
|
||||
"Show the import review when a camera or card is connected": "Відкривати імпорт під час підключення камери або карти",
|
||||
"Show the template tags; click one to insert it": "Показати теги шаблону; клацніть тег, щоб вставити його",
|
||||
"Show/Hide Mask": "Показати/приховати маску",
|
||||
"Show/hide My Photos panel": "Показати/приховати панель моїх фото",
|
||||
|
||||
@@ -1349,6 +1349,7 @@
|
||||
"Show overlay (O)": "显示叠加 (O)",
|
||||
"Show ratings, flags and edit badges": "显示星级、旗标和编辑标记",
|
||||
"Show the Navigator while zoomed in": "放大时显示导航器",
|
||||
"Show the import review when a camera or card is connected": "连接相机或存储卡时显示导入窗口",
|
||||
"Show the template tags; click one to insert it": "显示模板标签;单击即可插入",
|
||||
"Show/Hide Mask": "显示/隐藏蒙版",
|
||||
"Show/hide My Photos panel": "显示/隐藏我的照片面板",
|
||||
|
||||
@@ -1349,6 +1349,7 @@
|
||||
"Show overlay (O)": "顯示疊加(O)",
|
||||
"Show ratings, flags and edit badges": "顯示評分、旗標與編輯標記",
|
||||
"Show the Navigator while zoomed in": "放大時顯示導覽器",
|
||||
"Show the import review when a camera or card is connected": "連接相機或記憶卡時顯示匯入視窗",
|
||||
"Show the template tags; click one to insert it": "顯示範本標籤;點擊即可插入",
|
||||
"Show/Hide Mask": "顯示/隱藏遮色片",
|
||||
"Show/hide My Photos panel": "顯示/隱藏我的照片面板",
|
||||
|
||||
@@ -3101,6 +3101,48 @@ mod tests {
|
||||
assert!(heights.iter().any(|h| (h - first.height()).abs() > 20.0), "the tabs differ in height: {heights:?}");
|
||||
}
|
||||
|
||||
/// Settings → Import → "Show the import review when a camera or card is connected": a card that
|
||||
/// was already in doesn't ask, a new one opens Import from Device (copying), and a busy window
|
||||
/// asks once it's free.
|
||||
#[test]
|
||||
fn a_connected_card_opens_the_import_review() {
|
||||
let mut h = demo([1200.0, 900.0]);
|
||||
let t = Duration::from_secs(10);
|
||||
let root = std::env::temp_dir().join(format!("lc-card-{}", std::process::id()));
|
||||
let dcim = root.join("DCIM/100NIKON");
|
||||
std::fs::create_dir_all(&dcim).unwrap();
|
||||
let img = lightcraft_raster::Rgba8::from_fn(8, 8, |x, y| [(x * 9) as u8, (y * 12) as u8, 80, 255]);
|
||||
let o = lightcraft_engine::export::ExportOptions { format: lightcraft_engine::export::ExportFormat::Png, ..Default::default() };
|
||||
std::fs::write(dcim.join("DSC_0001.png"), lightcraft_engine::export::encode_image(&img, &o).unwrap()).unwrap();
|
||||
let card = lightcraft_engine::devices::Device {
|
||||
name: "NIKON Z 8".into(),
|
||||
path: root.join("DCIM").to_string_lossy().to_string(),
|
||||
root: root.to_string_lossy().to_string(),
|
||||
};
|
||||
h.app.ui.dialog = None;
|
||||
// first look: the card was already in
|
||||
crate::import::cards_seen(&mut h.app, vec![card.clone()]);
|
||||
assert!(h.app.scan.is_none() && h.app.ui.dialog.is_none(), "a card that was already in doesn't ask");
|
||||
crate::import::cards_seen(&mut h.app, vec![card.clone()]);
|
||||
assert!(h.app.scan.is_none());
|
||||
// taken out, then put back while another dialog is open: it waits
|
||||
crate::import::cards_seen(&mut h.app, Vec::new());
|
||||
h.app.ui.dialog = Some(crate::state::Dialog::About);
|
||||
crate::import::cards_seen(&mut h.app, vec![card.clone()]);
|
||||
assert!(h.app.scan.is_none(), "a busy window isn't interrupted");
|
||||
h.app.ui.dialog = None;
|
||||
crate::import::cards_seen(&mut h.app, vec![card.clone()]);
|
||||
assert!(h.app.scan.is_some(), "the card is scanned once the window is free");
|
||||
assert!(h.step_until(t, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. }))));
|
||||
let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review") };
|
||||
assert!(opts.copy, "a card is copied into the library");
|
||||
// with the option off nothing is remembered, so turning it on again starts with a first look
|
||||
assert!(!h.app.session.import_defaults.show_on_card);
|
||||
h.step();
|
||||
assert!(h.app.ui.known_cards.is_none());
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
/// The folder scan runs in the background: the request returns at once and the review opens
|
||||
/// when the scan finishes (a folder on a network share must not freeze the window).
|
||||
/// Clicking the folder being read again keeps the running read (and its progress).
|
||||
|
||||
@@ -444,6 +444,28 @@ pub(crate) fn busy_synchronizing(app: &LightcraftApp) -> Result<(), String> {
|
||||
if app.sync_run.is_some() { Err(crate::i18n::tr("A folder is being synchronized").to_string()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
/// Settings → Import → "Show the import review when a camera or card is connected", called with
|
||||
/// each device scan while it's on. The first look only notes the cards already in. A card that
|
||||
/// shows up after that opens Import from Device once nothing else is open, and a card that is
|
||||
/// taken out and put back in asks again.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn cards_seen(app: &mut LightcraftApp, now: Vec<lightcraft_engine::devices::Device>) {
|
||||
let Some(known) = app.ui.known_cards.as_mut() else {
|
||||
app.ui.known_cards = Some(now.into_iter().map(|d| d.root).collect());
|
||||
return;
|
||||
};
|
||||
known.retain(|root| now.iter().any(|d| d.root == *root));
|
||||
let Some(card) = now.into_iter().find(|d| !known.contains(&d.root)) else { return };
|
||||
// busy: ask when the window is free (the card isn't noted until then)
|
||||
if app.import.is_some() || app.scan.is_some() || app.sync_run.is_some() || app.lightroom.is_some() || app.ui.dialog.is_some() {
|
||||
return;
|
||||
}
|
||||
known.push(card.root);
|
||||
if let Err(e) = app.run("file.addFromDevice", json!({"path": card.path})) {
|
||||
log::warn!("import from {}: {e}", card.name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan `paths` in the background, then open the review dialog (see [`poll_scan`]).
|
||||
pub fn open(app: &mut LightcraftApp, paths: Vec<String>) -> Result<Value, String> {
|
||||
if app.scan.is_some() {
|
||||
|
||||
@@ -1138,6 +1138,16 @@ impl LightcraftApp {
|
||||
}
|
||||
self.ui.sidecars_waiting = waiting;
|
||||
}
|
||||
// Settings → Import: open the import review when a camera or card is connected
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.session.import_defaults.show_on_card {
|
||||
if let Some(now) = lightcraft_engine::devices::devices_scanned() {
|
||||
import::cards_seen(self, now);
|
||||
}
|
||||
ctx.request_repaint_after(lightcraft_engine::devices::MAX_AGE);
|
||||
} else {
|
||||
self.ui.known_cards = None;
|
||||
}
|
||||
self.session.persist_if_dirty();
|
||||
panels::denoise::pump(self, ctx);
|
||||
model_setup::pump(self, ctx);
|
||||
|
||||
@@ -365,6 +365,11 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
}
|
||||
});
|
||||
if !cfg!(target_arch = "wasm32") {
|
||||
heading(ui, t, crate::i18n::tr("Cameras and Cards"));
|
||||
let mut on_card = d.show_on_card;
|
||||
if check(ui, "settings.importOnCard", &mut on_card, "Show the import review when a camera or card is connected") {
|
||||
let _ = app.run("library.preferences", json!({"import": {"showOnCard": on_card}}));
|
||||
}
|
||||
heading(ui, t, crate::i18n::tr("Auto Import"));
|
||||
hint(ui, t, crate::i18n::tr("Photos that arrive in this folder (tethering, a scanner, a sync app) are added as soon as they're complete."));
|
||||
row(ui, t, crate::i18n::tr("Watched folder"), |ui| {
|
||||
|
||||
@@ -734,6 +734,10 @@ pub struct UiState {
|
||||
pub sidecar_retry_at: f64,
|
||||
#[serde(skip)]
|
||||
pub sidecars_waiting: usize,
|
||||
/// The cards and cameras already seen (volume roots) while Settings → Import → "Show the
|
||||
/// import review when a camera or card is connected" is on; `None` = not looked yet.
|
||||
#[serde(skip)]
|
||||
pub known_cards: Option<Vec<String>>,
|
||||
/// The develop control whose slider is being dragged (geometry sliders show a grid).
|
||||
#[serde(skip)]
|
||||
pub dragging_control: Option<String>,
|
||||
@@ -1273,6 +1277,7 @@ impl Default for UiState {
|
||||
auto_import_at: 0.0,
|
||||
sidecar_retry_at: 0.0,
|
||||
sidecars_waiting: 0,
|
||||
known_cards: None,
|
||||
search: String::new(),
|
||||
focus_search: false,
|
||||
renaming_mask: None,
|
||||
|
||||
@@ -76,7 +76,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
| 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; with several file types, a toggle per type checks or unchecks all of its files, e.g. the raws without the JPEGs beside them, issue #344) filling a resizable window (issue #337), transfer mode (add in place / copy / move, each explained), album (existing/new), preset, keywords; the folder scan runs on a background thread, shown in the activity stack with ✕ to cancel (a NAS folder no longer freezes the window); the import itself (probing, sidecars, copying / moving files) runs on a worker thread and its batches join the library between frames, shown in the activity stack with ✕ to cancel; one undo step. Drag-and-drop goes through the same background import. File dialogs list the importable extensions (`lightcraft_engine::import::EXTENSIONS`) in any letter case on Linux, where GTK and the desktop portal match globs case-sensitively (`apps/lightcraft/src/dialog_filter.rs`, issue #342; 0.4.0 hid `.JPG` / `.NEF`, issue #705). The folder scan, Browse Folder, drag-and-drop and the sidecar lookup match extensions in any case (`crates/engine/src/tests_import.rs`) |
|
||||
| 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 → 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); copies are verified (a new file, synced, checked against the content hash the scan computed — byte compare with the source when there is none, issue #134; a bad copy is removed and reported as failed, never counted as imported; taken names get -1, -2…, never replaced — issue #96, `crates/engine/src/import_move.rs`); 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); copies are verified (a new file, synced, checked against the content hash the scan computed — byte compare with the source when there is none, issue #134; a bad copy is removed and reported as failed, never counted as imported; taken names get -1, -2…, never replaced — issue #96, `crates/engine/src/import_move.rs`); no PTP/MTP (cameras that don't mount as a disk); Settings → Import → Cameras and Cards can open the import review when a camera or card is connected (off by default; a card that was already in doesn't ask, and an open dialog or running import is finished first; `crates/ui-egui/src/import.rs`) |
|
||||
| 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 |
|
||||
@@ -814,6 +814,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
| 2026-10-13 | minor | LR-EDIT-DETAIL-NR, LR-EDIT-DETAIL-CNR: noise-reduction radii in the original's pixels, so previews at any size and smaller exports look like the full-size render scaled down (issue #960); status unchanged |
|
||||
| 2026-10-13 | minor | KEYC-RATING: backtick cycles the flag (`photo.cycleFlag`), ⌘↑ / ⌘↓ raise / lower it (`photo.increaseFlag`, `photo.decreaseFlag`); status unchanged (🟡) |
|
||||
| 2026-10-13 | patch | LR-MASK-LUMRANGE / LR-MASK-COLORRANGE: range drags keep the grabbed endpoint; colour samples use the render's enabled sections and HDR/SDR settings; regression coverage, status unchanged |
|
||||
| 2026-10-13 | minor | LR-IMP-DEVICE: Settings → Import → Cameras and Cards can open the import review when a camera or card is connected; status unchanged (✅) |
|
||||
| 2026-10-11 | minor | LR-MERGE-HDR: HDR and panorama merges of raws without colour matrices (CR2, NEF, ARW…) write the colour they open with as DNG tags (issue #965); status unchanged |
|
||||
| 2026-10-12 | minor | LR-META-XMP: Read Metadata from File no longer auto-writes the sidecar it has just read, which added `lc:settings` and hid another app's later `crs:` edits (#934); status unchanged (✅) |
|
||||
| 2026-10-12 | patch | LR-IMP-CAMERA-COVERAGE: CR2 white level from the active area, not the saturated masked rows above it (issue #1272); status unchanged (🟡) |
|
||||
|
||||
@@ -81,6 +81,11 @@
|
||||
search by lens. LightCraft now reads the lens range Nikon keeps in its own maker note, the way Lightroom shows a
|
||||
lens it doesn't have a name for. Newly imported photos show it.
|
||||
|
||||
### Import when a card is connected
|
||||
- Settings → Import → Cameras and Cards has an option to open the import review as soon as a camera or memory card
|
||||
is connected, the same review File → Import from Device opens. It's off by default. A card that's already in when
|
||||
LightCraft starts doesn't ask, and if a dialog is open or an import is running it waits until that's done.
|
||||
|
||||
### Hard-edged Heal and Clone spots
|
||||
- A spot with Feather at 0 no longer draws black pixels exactly on its radius. This also fixes spots at 0% Opacity
|
||||
changing pixels along their edge. The hard edge now leaves pixels on the radius unchanged.
|
||||
@@ -198,6 +203,8 @@
|
||||
"LightCraft stopped unexpectedly" and why, instead of "could not open its window", and the next start renders
|
||||
on the CPU and explains how to turn the GPU back on.
|
||||
|
||||
## 2026-10-13
|
||||
|
||||
### Slider drags on large raws
|
||||
- Dragging a slider no longer re-renders the edited photo's filmstrip thumbnail on every frame; it follows once you
|
||||
let go. On a 64 MP raw the first of those renders decoded the whole file again, which held up the loupe's
|
||||
|
||||
Reference in New Issue
Block a user