# Conflicts:
#	crates/ui-egui/src/import.rs
This commit is contained in:
Claude 2026-10-08 17:57:23 +00:00
commit b5fcf58e55
No known key found for this signature in database
1 changed files with 74 additions and 7 deletions

View File

@ -1,11 +1,11 @@
//! 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 or move 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 on a worker thread (files are
//! probed, copied or moved there) and its batches join the catalog between frames, with a progress
//! window and Cancel; the whole import is one undo step.
//! a grid of thumbnails with checkboxes (duplicates marked and unchecked; Shift-click sets a
//! range), the destination (add in place / copy or move 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 on a worker thread (files are probed, copied or moved there) and its batches join the
//! catalog between frames, with a progress window and Cancel; the whole import is one undo step.
use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2};
use lightcraft_engine::import::{ImportCandidate, ScanInput, ScanOutput, ScanProgress, scan_with};
@ -82,6 +82,9 @@ pub struct ImportDialog {
pub trashed: Vec<bool>,
/// What to do with those: "" (leave them), `restore` or `fresh` (the `onDeleted` param).
pub on_deleted: String,
/// The candidate clicked last: where a Shift-click range starts ([`ImportDialog::click`]).
#[serde(skip)]
pub last_clicked: Option<usize>,
}
impl ImportDialog {
@ -110,6 +113,24 @@ impl ImportDialog {
}
}
}
/// A click on candidate `i` toggles its checkbox. With `shift`, every importable candidate
/// from the last clicked one to `i` takes `i`'s new state, as in a file manager: click the
/// first, Shift-click the last, and the whole range is checked (or unchecked).
pub fn click(&mut self, i: usize, shift: bool) {
if !self.importable(i) {
return;
}
let Some(on) = self.checked.get(i).map(|c| !c) else { return };
let from = if shift { self.last_clicked.filter(|a| *a < self.checked.len()).unwrap_or(i) } else { i };
for k in from.min(i)..=from.max(i) {
if self.importable(k)
&& let Some(c) = self.checked.get_mut(k)
{
*c = on;
}
}
self.last_clicked = Some(i);
}
pub fn selected_paths(&self) -> Vec<String> {
self.candidates
.iter()
@ -1134,7 +1155,8 @@ fn candidate_cell(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDial
);
let resp = resp.on_hover_text(tip);
if resp.clicked() && ok {
d.checked[i] = !d.checked[i];
let shift = ui.input(|input| input.modifiers.shift);
d.click(i, shift);
}
}
@ -1305,4 +1327,49 @@ mod tests {
assert_eq!(grid_height(700.0, f32::NAN, 142.0), 700.0);
assert_eq!(grid_height(f32::INFINITY, 300.0, 142.0), DIALOG_SIZE[1] - DEFAULT_BELOW_GRID);
}
/// Six candidates, all checked; candidate 3 is a duplicate (unchecked, not importable).
fn dialog() -> ImportDialog {
let mut candidates: Vec<ImportCandidate> = (0..6).map(|i| ImportCandidate { path: format!("img{i}.jpg"), ..Default::default() }).collect();
candidates[3].duplicate = Some("content".into());
ImportDialog::new(candidates)
}
#[test]
fn click_toggles_one_candidate() {
let mut d = dialog();
d.click(1, false);
assert_eq!(d.checked, [true, false, true, false, true, true]);
d.click(1, false);
assert_eq!(d.checked, [true, true, true, false, true, true]);
d.click(3, false);
assert_eq!(d.checked, [true, true, true, false, true, true], "a duplicate can't be checked");
}
#[test]
fn shift_click_sets_the_range_to_the_clicked_state() {
let mut d = dialog();
d.click(1, false);
d.click(4, true);
assert_eq!(d.checked, [true, false, false, false, false, true], "1..=4 unchecked, the duplicate left alone");
assert_eq!(d.selected_paths(), ["img0.jpg", "img5.jpg"]);
// backwards from the new anchor (4) re-checks 2..=4
d.click(2, true);
assert_eq!(d.checked, [true, false, true, false, true, true]);
}
#[test]
fn shift_click_without_an_earlier_click_toggles_one() {
let mut d = dialog();
d.click(2, true);
assert_eq!(d.checked, [true, true, false, false, true, true]);
}
#[test]
fn shift_click_with_a_stale_anchor_toggles_one() {
let mut d = dialog();
d.last_clicked = Some(99);
d.click(2, true);
assert_eq!(d.checked, [true, true, false, false, true, true]);
}
}