mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-30 13:07:43 -06:00
Merge PR #974
# Conflicts: # crates/engine/src/cmd/mod.rs # crates/ui-egui/locales/de-formats.json # crates/ui-egui/locales/de.json # crates/ui-egui/locales/es-formats.json # crates/ui-egui/locales/es.json # crates/ui-egui/locales/fr-formats.json # crates/ui-egui/locales/fr.json # crates/ui-egui/locales/ja-formats.json # crates/ui-egui/locales/ja.json # crates/ui-egui/locales/pt-br-formats.json # crates/ui-egui/locales/pt-br.json # crates/ui-egui/locales/ru-formats.json # crates/ui-egui/locales/ru.json # crates/ui-egui/locales/uk-formats.json # crates/ui-egui/locales/uk.json # crates/ui-egui/locales/zh-hans-formats.json # crates/ui-egui/locales/zh-hans.json # crates/ui-egui/locales/zh-hant-formats.json # crates/ui-egui/locales/zh-hant.json # docs/mcp.md # docs/parity-checklist.md
This commit is contained in:
@@ -201,12 +201,16 @@ fn edit_copy(s: &mut Session, p: &Value, id: lightcraft_catalog::PhotoId, opts:
|
||||
|
||||
/// The op that brings a photo up to date with what its file is now (`info`, a fresh probe), or
|
||||
/// `None` when nothing changed. Covers a raw that became decodable (or stopped being: a different
|
||||
/// file relinked), i.e. a change of [`lightcraft_catalog::Photo::preview_only`].
|
||||
/// file relinked), i.e. a change of [`lightcraft_catalog::Photo::preview_only`], and a
|
||||
/// preview-only photo this version hasn't checked yet: its stamp is refreshed so it isn't probed
|
||||
/// again (see [`lightcraft_catalog::Photo::preview_checked`]).
|
||||
pub(crate) fn content_op(id: lightcraft_catalog::PhotoId, ph: &lightcraft_catalog::Photo, info: crate::media::ProbeInfo) -> Option<Op> {
|
||||
let preview_checked = lightcraft_catalog::preview_stamp(&info.preview_only);
|
||||
let same = info.content_hash == ph.content_hash
|
||||
&& info.file_size == ph.file_size
|
||||
&& (info.width, info.height) == (ph.width, ph.height)
|
||||
&& info.preview_only == ph.preview_only;
|
||||
&& info.preview_only == ph.preview_only
|
||||
&& preview_checked == ph.preview_checked;
|
||||
(!same).then_some(Op::SetContent {
|
||||
id,
|
||||
width: info.width,
|
||||
@@ -214,6 +218,7 @@ pub(crate) fn content_op(id: lightcraft_catalog::PhotoId, ph: &lightcraft_catalo
|
||||
file_size: info.file_size,
|
||||
content_hash: info.content_hash,
|
||||
preview_only: info.preview_only,
|
||||
preview_checked,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -271,6 +276,37 @@ pub(crate) fn as_shot_wb_op(id: lightcraft_catalog::PhotoId, ph: &lightcraft_cat
|
||||
(ph.develops_raw() && !ph.relative_wb() && ph.as_shot_wb != Some(read)).then_some(Op::SetAsShotWb { id, wb: Some(read) })
|
||||
}
|
||||
|
||||
/// The ops that bring photo `id` and its virtual copies up to date with what their file is now
|
||||
/// (`info`, a fresh probe), appended to `ops`; the photos they change are appended to `changed`.
|
||||
/// Reload and the preview-only recheck (`library.recheckPreviewOnly`) share it.
|
||||
pub(crate) fn reload_ops(
|
||||
cat: &lightcraft_catalog::Catalog,
|
||||
id: lightcraft_catalog::PhotoId,
|
||||
info: &crate::media::ProbeInfo,
|
||||
ops: &mut Vec<Op>,
|
||||
changed: &mut Vec<lightcraft_catalog::PhotoId>,
|
||||
) {
|
||||
let Some(ph) = cat.photo(id) else { return };
|
||||
// camera fields the catalog lacks (e.g. a raw imported before its format was read) are filled in;
|
||||
// nothing already set is overwritten
|
||||
let meta_ops = fill_missing_meta(id, ph, info);
|
||||
let mut mine: Vec<Op> =
|
||||
content_op(id, ph, info.clone()).into_iter().chain(meta_ops).chain(lens_ops(id, ph, info)).chain(as_shot_wb_op(id, ph, info)).collect();
|
||||
if !mine.is_empty() {
|
||||
ops.append(&mut mine);
|
||||
changed.push(id);
|
||||
}
|
||||
// virtual copies share the file
|
||||
for c in cat.photos().filter(|c| c.copy_of == Some(id)) {
|
||||
let theirs: Vec<Op> =
|
||||
content_op(c.id, c, info.clone()).into_iter().chain(lens_ops(c.id, c, info)).chain(as_shot_wb_op(c.id, c, info)).collect();
|
||||
if !theirs.is_empty() {
|
||||
ops.extend(theirs);
|
||||
changed.push(c.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-read photos whose files changed on disk (an external editor saved them): new size,
|
||||
/// dimensions and content hash, cached sources dropped. → {reloaded: [ids]}
|
||||
fn reload(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -286,25 +322,8 @@ fn reload(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut ops = Vec::new();
|
||||
let mut reloaded = Vec::new();
|
||||
for ((id, _), info) in paths.into_iter().zip(probed) {
|
||||
let (Ok(info), Some(ph)) = (info, s.catalog.photo(id)) else { continue };
|
||||
// camera fields the catalog lacks (e.g. a raw imported before its format was read) are filled in;
|
||||
// nothing already set is overwritten
|
||||
let meta_ops = fill_missing_meta(id, ph, &info);
|
||||
let mut mine: Vec<Op> =
|
||||
content_op(id, ph, info.clone()).into_iter().chain(meta_ops).chain(lens_ops(id, ph, &info)).chain(as_shot_wb_op(id, ph, &info)).collect();
|
||||
if !mine.is_empty() {
|
||||
ops.append(&mut mine);
|
||||
reloaded.push(id);
|
||||
}
|
||||
// virtual copies share the file
|
||||
for c in s.catalog.photos().filter(|c| c.copy_of == Some(id)) {
|
||||
let theirs: Vec<Op> =
|
||||
content_op(c.id, c, info.clone()).into_iter().chain(lens_ops(c.id, c, &info)).chain(as_shot_wb_op(c.id, c, &info)).collect();
|
||||
if !theirs.is_empty() {
|
||||
ops.extend(theirs);
|
||||
reloaded.push(c.id);
|
||||
}
|
||||
}
|
||||
let Ok(info) = info else { continue };
|
||||
reload_ops(&s.catalog, id, &info, &mut ops, &mut reloaded);
|
||||
}
|
||||
for id in &reloaded {
|
||||
s.media.forget(*id);
|
||||
|
||||
@@ -34,6 +34,7 @@ mod preset_files;
|
||||
pub mod previews;
|
||||
mod query;
|
||||
mod slideshow;
|
||||
pub mod recheck;
|
||||
mod sync;
|
||||
mod xmp;
|
||||
|
||||
@@ -159,6 +160,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(before::specs());
|
||||
v.extend(browse::specs());
|
||||
v.extend(missing::specs());
|
||||
v.extend(recheck::specs());
|
||||
v.extend(sync::specs());
|
||||
v.extend(metadata::specs());
|
||||
v.extend(geocode::specs());
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//! Preview-only raws rechecked after an update (issue #662): a raw imported while its format or
|
||||
//! variant couldn't be decoded keeps [`Photo::preview_only`] until something re-reads its file.
|
||||
//! Each flag is stamped with the app version that set it ([`Photo::preview_checked`]); when a
|
||||
//! library opens in another version, its stale preview-only photos are probed again and get
|
||||
//! exactly what Reload from Disk would give them: a file that decodes now loses the flag, one that
|
||||
//! still doesn't gets the new reason and a fresh stamp, so it isn't probed again.
|
||||
//!
|
||||
//! The work is split so the app can probe on a worker thread: [`stale`] lists what to probe (no
|
||||
//! file-system calls), [`probe`] reads the files without the session, and [`apply`] applies the
|
||||
//! results as LightCraft's own bookkeeping (journaled, not an undo step). Agents and tests use
|
||||
//! `library.recheckPreviewOnly`, which does all three at once.
|
||||
|
||||
use lightcraft_catalog::{Catalog, Op, Photo, PhotoId};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, always, bad, cmd};
|
||||
use crate::media::{FileProbe, ProbeInfo};
|
||||
use crate::{Result, Session};
|
||||
|
||||
/// A preview-only photo to probe, and what the library knew about it when it was listed (so a
|
||||
/// result for a photo that changed meanwhile is dropped).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Stale {
|
||||
pub id: PhotoId,
|
||||
pub path: String,
|
||||
preview_only: Option<String>,
|
||||
preview_checked: String,
|
||||
}
|
||||
|
||||
impl Stale {
|
||||
fn of(p: &Photo, path: &str) -> Stale {
|
||||
Stale { id: p.id, path: path.to_string(), preview_only: p.preview_only.clone(), preview_checked: p.preview_checked.clone() }
|
||||
}
|
||||
|
||||
/// The photo is still as it was listed: same file, flag and stamp.
|
||||
fn current(&self, p: &Photo) -> bool {
|
||||
crate::cmd::missing::checked_path(p) == Some(self.path.as_str())
|
||||
&& p.preview_only == self.preview_only
|
||||
&& p.preview_checked == self.preview_checked
|
||||
}
|
||||
}
|
||||
|
||||
/// The library's preview-only photos whose flag another app version set (or every preview-only
|
||||
/// photo, with `all`): library photos with a file, not virtual copies (they follow the photo they
|
||||
/// were copied from). No file-system calls.
|
||||
pub fn stale(cat: &Catalog, all: bool) -> Vec<Stale> {
|
||||
cat.photos()
|
||||
.filter(|p| p.copy_of.is_none() && p.preview_only.is_some() && (all || p.preview_only_stale()))
|
||||
.filter_map(|p| crate::cmd::missing::checked_path(p).map(|path| Stale::of(p, path)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The session's file probe, to [`probe`] with on a worker thread; `None` when the session has no
|
||||
/// file access (a demo or browser session), where nothing can be rechecked.
|
||||
pub fn file_probe(s: &Session) -> Option<FileProbe> {
|
||||
s.media.file_probe.clone()
|
||||
}
|
||||
|
||||
/// Probe each listed photo's file as Reload would. Doesn't touch the session.
|
||||
pub fn probe(probe: &FileProbe, stale: Vec<Stale>) -> Vec<(Stale, std::result::Result<ProbeInfo, String>)> {
|
||||
let paths: Vec<String> = stale.iter().map(|x| x.path.clone()).collect();
|
||||
stale.into_iter().zip(crate::import::probe_with(probe, &paths)).collect()
|
||||
}
|
||||
|
||||
/// What a recheck changed.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Rechecked {
|
||||
/// Photos brought up to date (virtual copies included).
|
||||
pub updated: Vec<PhotoId>,
|
||||
/// Of those, the ones that develop from their raw data now.
|
||||
pub cleared: Vec<PhotoId>,
|
||||
}
|
||||
|
||||
impl Rechecked {
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({"updated": self.updated.iter().map(|i| i.0).collect::<Vec<_>>(), "cleared": self.cleared.iter().map(|i| i.0).collect::<Vec<_>>()})
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply [`probe`]'s results: each photo gets the ops Reload would give it, all in one batch that is
|
||||
/// journaled but isn't an undo step. A file that can't be read (missing, offline) changes nothing,
|
||||
/// so it is tried again next time; a photo that changed since it was listed (relinked, reloaded,
|
||||
/// removed) is skipped.
|
||||
pub fn apply(s: &mut Session, results: Vec<(Stale, std::result::Result<ProbeInfo, String>)>) -> Result<Rechecked> {
|
||||
let mut ops = Vec::new();
|
||||
let mut out = Rechecked::default();
|
||||
for (item, info) in results {
|
||||
let Ok(info) = info else { continue };
|
||||
if !s.catalog.photo(item.id).is_some_and(|p| item.current(p)) {
|
||||
continue;
|
||||
}
|
||||
let first = out.updated.len();
|
||||
crate::cmd::convert::reload_ops(&s.catalog, item.id, &info, &mut ops, &mut out.updated);
|
||||
if info.preview_only.is_none() {
|
||||
out.cleared.extend_from_slice(&out.updated[first..]);
|
||||
}
|
||||
}
|
||||
if !ops.is_empty() {
|
||||
s.apply_system(Op::Batch { ops })?;
|
||||
}
|
||||
for id in &out.updated {
|
||||
s.media.forget(*id);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn recheck(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "library.recheckPreviewOnly";
|
||||
let all = p.get("all").and_then(Value::as_bool).unwrap_or(false);
|
||||
// without file access every probe "succeeds" with nothing known, which would clear every flag
|
||||
let fp = file_probe(s).ok_or_else(|| bad(C, "this session has no file access"))?;
|
||||
let listed = stale(&s.catalog, all);
|
||||
let n = listed.len();
|
||||
let mut r = apply(s, probe(&fp, listed))?.to_json();
|
||||
r["checked"] = json!(n);
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!(
|
||||
"library.recheckPreviewOnly",
|
||||
"Recheck Preview-Only Raws",
|
||||
[],
|
||||
None,
|
||||
"{all?: bool} — probe again the raws shown from their embedded preview whose flag another LightCraft version set (the app does this in the background when a library opens; `all` rechecks every preview-only raw, e.g. when testing a decoder build of the same version), and bring each up to date as Reload from Disk would; not an undo step, develop settings and history are kept; files that can't be read are left for next time → {checked, updated: [ids], cleared: [ids] (develop from their raw data now)}",
|
||||
always,
|
||||
recheck
|
||||
)]
|
||||
}
|
||||
Reference in New Issue
Block a user