mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
LR-CLOUD-SMARTPREVIEW: look refresh only for per-file-look raws, once per LOOK_VERSION, serialised with builds
Only proxies of raws that take the per-file camera look (Photo::relative_wb) go stale: JPEG/PNG/DNG proxies are never rebuilt for the look. The opening scan runs once per LOOK_VERSION (a look-version marker in the smart previews folder), not at every library open; Build Smart Previews still refreshes stale proxies. Each proxy is checked and written under one lock, so the background refresh and library.smartPreviews never write the same file at once. LOOK_VERSION 2: main's DRO / ILCE-7CR camera-look changes (#528, #583, #568, #616). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b7e791a136
commit
7a65cc6340
6 files changed
+148
-40
No files matched your search
@@ -20,7 +20,10 @@ use lightcraft_raw::{RawFormat, RawImage, color::CameraTransform, profile::HsvTa
|
||||
/// **Bump this whenever a change makes the fit give a different result for the same file** (the
|
||||
/// fit, its gates, the fallbacks, the camera profiles' effect on it). Do not bump it for changes
|
||||
/// that leave the fitted look alone. `RENDER_CACHE_VERSION` covers renders; this covers proxies.
|
||||
pub const LOOK_VERSION: u32 = 1;
|
||||
///
|
||||
/// 1: the fit as of #499's follow-up; 2: Sony DRO (tone curve lowered to Sony's curve without DRO)
|
||||
/// and the ILCE-7CR profile (#528, #583, #568, #616).
|
||||
pub const LOOK_VERSION: u32 = 2;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct CameraLook {
|
||||
|
||||
@@ -108,9 +108,16 @@ fn build(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
/// One photo's smart preview to build or discard: (photo, its file in the smart previews folder,
|
||||
/// how to load the original at preview size).
|
||||
/// how to load the original at preview size, whether the photo takes the per-file camera look —
|
||||
/// `camera_preview::file_local_look`, [`lightcraft_catalog::Photo::relative_wb`]: only those
|
||||
/// proxies go stale when the fit changes).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
type SmartJob = (lightcraft_catalog::PhotoId, std::path::PathBuf, Option<crate::media::SourceRef>);
|
||||
type SmartJob = (lightcraft_catalog::PhotoId, std::path::PathBuf, Option<crate::media::SourceRef>, bool);
|
||||
|
||||
/// Held while one proxy is checked and (re)written, so Build Smart Previews and the opening look
|
||||
/// refresh ([`refresh_stale_smart_previews`]), on their own threads, never write the same file at once.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
static SMART_WRITE: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
/// What Build / Discard Smart Previews did.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -163,10 +170,11 @@ fn smart_run(
|
||||
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
}
|
||||
}
|
||||
for (id, path, source) in jobs {
|
||||
for (id, path, source, look) in jobs {
|
||||
if state.is_some_and(|st| st.cancel.load(Ordering::Relaxed)) {
|
||||
break;
|
||||
}
|
||||
let _one = SMART_WRITE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if discard {
|
||||
if std::fs::remove_file(&path).is_ok() {
|
||||
n.removed += 1;
|
||||
@@ -176,7 +184,7 @@ fn smart_run(
|
||||
// full drive) is (re)built, and so is one written by an older camera-look fit (see
|
||||
// `camera_preview::LOOK_VERSION`) when its original can be read
|
||||
let damaged = path.exists();
|
||||
let stale = damaged && crate::smart::is_stale(&path);
|
||||
let stale = look && damaged && crate::smart::is_stale(&path);
|
||||
if damaged && !stale && crate::smart::is_valid(&path) {
|
||||
n.built += 1;
|
||||
} else {
|
||||
@@ -246,7 +254,7 @@ fn smart(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let path = dir.join(crate::smart::file_name(&ph));
|
||||
let source = (!discard).then(|| s.media.source_ref(&ph, crate::media::SourceLevel::Preview));
|
||||
jobs.push((id, path, source));
|
||||
jobs.push((id, path, source, ph.relative_wb()));
|
||||
}
|
||||
let custom = s.smart_previews_dir.is_some();
|
||||
if background {
|
||||
@@ -272,18 +280,24 @@ fn smart(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Bring smart previews written by an older camera-look fit up to date, once per library open:
|
||||
/// on a worker thread (the UI never waits on the drives), each stale proxy is rebuilt from its
|
||||
/// original when that can be read and kept as it is when it can't (its header still says it is
|
||||
/// stale, so the next open with the original online rebuilds it). Only existing proxies are
|
||||
/// touched: none is built, and a missing folder is never created. Returns the thread, if started.
|
||||
/// Bring smart previews written by an older camera-look fit up to date, once per fit version: when
|
||||
/// the smart previews folder has not been checked at this `LOOK_VERSION` (its marker,
|
||||
/// [`crate::smart::look_checked`]), a worker thread (the UI never waits on the drives) looks at the
|
||||
/// proxies of the raws that take the per-file camera look, the only ones the fit reaches (JPEG,
|
||||
/// PNG… proxies are never rebuilt), and rebuilds the stale ones from their originals. One whose
|
||||
/// original can't be read (offline) is kept as it is, still marked stale in its header: Build
|
||||
/// Smart Previews rebuilds it once the original is back. Only existing proxies are touched: none
|
||||
/// is built, and a missing folder is never created. Returns the thread, if started.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn refresh_stale_smart_previews(s: &mut Session) -> Option<std::thread::JoinHandle<()>> {
|
||||
let dir = s.media.smart_dir.clone()?;
|
||||
if !dir.is_dir() || crate::smart::look_checked(&dir) {
|
||||
return None;
|
||||
}
|
||||
let max_edge = crate::media::SourceLevel::Preview.max_edge();
|
||||
let photos: Vec<_> = s.catalog.photos().filter(|p| matches!(p.source, lightcraft_catalog::Source::File { .. })).collect();
|
||||
let photos: Vec<_> = s.catalog.photos().filter(|p| matches!(p.source, lightcraft_catalog::Source::File { .. }) && p.relative_wb()).collect();
|
||||
let jobs: Vec<SmartJob> =
|
||||
photos.iter().map(|p| (p.id, dir.join(crate::smart::file_name(p)), Some(s.media.origin_ref(&p.source, max_edge)))).collect();
|
||||
photos.iter().map(|p| (p.id, dir.join(crate::smart::file_name(p)), Some(s.media.origin_ref(&p.source, max_edge)), true)).collect();
|
||||
std::thread::Builder::new()
|
||||
.name("lc-smart-look".into())
|
||||
.spawn(move || refresh_stale(&dir, jobs))
|
||||
@@ -294,14 +308,20 @@ pub(crate) fn refresh_stale_smart_previews(s: &mut Session) -> Option<std::threa
|
||||
/// The body of [`refresh_stale_smart_previews`]: rebuild the stale proxies among `jobs`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn refresh_stale(dir: &std::path::Path, jobs: Vec<SmartJob>) {
|
||||
let stale: Vec<SmartJob> = jobs.into_iter().filter(|(_, path, _)| crate::smart::is_stale(path)).collect();
|
||||
if stale.is_empty() {
|
||||
return;
|
||||
let stale: Vec<SmartJob> = jobs.into_iter().filter(|(_, path, _, _)| crate::smart::is_stale(path)).collect();
|
||||
if !stale.is_empty() {
|
||||
// `custom`: the folder is there already, and must not be recreated if it went away since
|
||||
match smart_run(dir, true, false, stale, None) {
|
||||
Ok(n) => log::info!("smart previews: {} refreshed to the current camera look, {} kept (original offline)", n.refreshed, n.stale_kept),
|
||||
Err(e) => {
|
||||
// not marked: the next opening tries again
|
||||
log::warn!("smart previews: look refresh: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// `custom`: the folder is there already, and must not be recreated if it went away since
|
||||
match smart_run(dir, true, false, stale, None) {
|
||||
Ok(n) => log::info!("smart previews: {} refreshed to the current camera look, {} kept (original offline)", n.refreshed, n.stale_kept),
|
||||
Err(e) => log::warn!("smart previews: look refresh: {e}"),
|
||||
if let Err(e) = crate::smart::mark_look_checked(dir) {
|
||||
log::warn!("smart previews: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -218,6 +218,29 @@ pub fn is_stale(path: &Path) -> bool {
|
||||
.is_some_and(|h| header_look_version(&h) < u64::from(crate::camera_preview::LOOK_VERSION))
|
||||
}
|
||||
|
||||
/// The marker file in a smart previews folder: the `LOOK_VERSION` its proxies were last checked
|
||||
/// at ([`look_checked`]).
|
||||
const LOOK_MARKER: &str = "look-version";
|
||||
|
||||
/// Whether the smart previews folder `dir` has been checked for stale proxies at this build's
|
||||
/// camera-look fit version (or a newer one). A missing or unreadable marker counts as not.
|
||||
pub fn look_checked(dir: &Path) -> bool {
|
||||
use std::io::Read;
|
||||
let Ok(f) = std::fs::File::open(dir.join(LOOK_MARKER)) else { return false };
|
||||
let mut text = String::new();
|
||||
if f.take(64).read_to_string(&mut text).is_err() {
|
||||
return false;
|
||||
}
|
||||
text.trim().parse::<u64>().is_ok_and(|v| v >= u64::from(crate::camera_preview::LOOK_VERSION))
|
||||
}
|
||||
|
||||
/// Record that the proxies in `dir` were checked at this build's camera-look fit version.
|
||||
pub fn mark_look_checked(dir: &Path) -> Result<(), String> {
|
||||
let path = dir.join(LOOK_MARKER);
|
||||
lightcraft_catalog::safe_file::write_atomic(&path, format!("{}\n", crate::camera_preview::LOOK_VERSION).as_bytes())
|
||||
.map_err(|e| format!("{}: {e}", path.display()))
|
||||
}
|
||||
|
||||
/// Load the proxy at `path`.
|
||||
pub fn load(path: &Path) -> Result<crate::media::DecodedSource, String> {
|
||||
let b = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
|
||||
@@ -1170,24 +1170,37 @@ fn look_loader(
|
||||
|
||||
/// The follow-up to the camera tone curve change (#499): a smart preview keeps the look the fit
|
||||
/// gave it, so one written by an older fit is rebuilt when its original is online, kept while the
|
||||
/// original is offline, and not rebuilt again once current.
|
||||
/// original is offline, and not rebuilt again once current. Only raws that take the per-file
|
||||
/// camera look go stale (a PNG's proxy never does), and the opening scan runs once per fit version.
|
||||
#[test]
|
||||
fn smart_previews_from_an_older_look_fit_are_refreshed() {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
let src = temp_dir("smartlook-src");
|
||||
let lib = temp_dir("smartlook-lib");
|
||||
write_png(&src.join("a.png"), 3);
|
||||
write_png(&src.join("b.png"), 9);
|
||||
let mut s = Session::new().with_fs();
|
||||
s.open_library(&lib, false).unwrap();
|
||||
s.execute("library.import", &json!({"paths": [src.join("a.png").to_string_lossy()]})).unwrap();
|
||||
let id = s.active().unwrap();
|
||||
let file = s.media.smart_dir.clone().unwrap().join(crate::smart::file_name(s.catalog.photo(id).unwrap()));
|
||||
s.execute("library.import", &json!({"paths": [src.join("a.png").to_string_lossy(), src.join("b.png").to_string_lossy()]})).unwrap();
|
||||
let find = |s: &Session, name: &str| s.catalog.photos().find(|p| p.file_name == name).unwrap().id;
|
||||
let (id, png) = (find(&s, "a.png"), find(&s, "b.png"));
|
||||
// `a` stands for a raw that takes the per-file camera look (the test loader decodes its pixels)
|
||||
let mut raw = (**s.catalog.photo(id).unwrap()).clone();
|
||||
raw.format = "ARW".into();
|
||||
raw.kind = lightcraft_catalog::MediaKind::Raw;
|
||||
s.catalog.apply(lightcraft_catalog::Op::RemovePhoto { id }).unwrap();
|
||||
s.catalog.apply(lightcraft_catalog::Op::AddPhoto { photo: Box::new(raw) }).unwrap();
|
||||
assert!(s.catalog.photo(id).unwrap().relative_wb() && !s.catalog.photo(png).unwrap().relative_wb());
|
||||
s.execute("library.select", &json!({"ids": [id.0, png.0], "active": id.0})).unwrap();
|
||||
let dir = s.media.smart_dir.clone().unwrap();
|
||||
let file = dir.join(crate::smart::file_name(s.catalog.photo(id).unwrap()));
|
||||
let png_file = dir.join(crate::smart::file_name(s.catalog.photo(png).unwrap()));
|
||||
let (old, new) = (test_tone(1.5), test_tone(3.5));
|
||||
let calls = std::sync::Arc::new(AtomicUsize::new(0));
|
||||
let plain = s.media.file_loader.clone().unwrap();
|
||||
s.media.file_loader = Some(look_loader(plain.clone(), old, calls.clone()));
|
||||
let r = s.execute("library.smartPreviews", &json!({})).unwrap();
|
||||
assert_eq!((r["built"].as_u64(), r["refreshed"].as_u64()), (Some(1), Some(0)), "{r}");
|
||||
assert_eq!((r["built"].as_u64(), r["refreshed"].as_u64()), (Some(2), Some(0)), "{r}");
|
||||
assert_eq!(smart_header(&file)["look_version"], json!(crate::camera_preview::LOOK_VERSION), "the writer stamps the fit version");
|
||||
assert!(!crate::smart::is_stale(&file));
|
||||
|
||||
@@ -1196,39 +1209,61 @@ fn smart_previews_from_an_older_look_fit_are_refreshed() {
|
||||
calls.store(0, Ordering::SeqCst);
|
||||
s.media.file_loader = Some(look_loader(plain.clone(), new, calls.clone()));
|
||||
let r = s.execute("library.smartPreviews", &json!({})).unwrap();
|
||||
assert_eq!((r["built"].as_u64(), r["refreshed"].as_u64()), (Some(1), Some(0)), "{r}");
|
||||
assert_eq!((r["built"].as_u64(), r["refreshed"].as_u64()), (Some(2), Some(0)), "{r}");
|
||||
assert!(std::fs::read(&file).unwrap() == before, "a current proxy is not rebuilt");
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 0);
|
||||
|
||||
// an old header (no field), original offline: kept unchanged, and still marked stale
|
||||
edit_smart_header(&file, |h| {
|
||||
h.as_object_mut().unwrap().remove("look_version");
|
||||
});
|
||||
// old headers (no field), originals offline: kept unchanged, and still marked stale
|
||||
for f in [&file, &png_file] {
|
||||
edit_smart_header(f, |h| {
|
||||
h.as_object_mut().unwrap().remove("look_version");
|
||||
});
|
||||
}
|
||||
assert!(crate::smart::is_stale(&file), "no field = version 0");
|
||||
let old_bytes = std::fs::read(&file).unwrap();
|
||||
let png_bytes = std::fs::read(&png_file).unwrap();
|
||||
std::fs::rename(&src, src.with_extension("offline")).unwrap();
|
||||
s.media.forget(id);
|
||||
s.media.forget(png);
|
||||
let r = s.execute("library.smartPreviews", &json!({})).unwrap();
|
||||
assert_eq!((r["built"].as_u64(), r["refreshed"].as_u64(), r["staleKept"].as_u64()), (Some(1), Some(0), Some(1)), "{r}");
|
||||
assert_eq!((r["built"].as_u64(), r["refreshed"].as_u64(), r["staleKept"].as_u64()), (Some(2), Some(0), Some(1)), "{r}");
|
||||
assert!(r["failed"].as_array().unwrap().is_empty(), "{r}");
|
||||
assert!(std::fs::read(&file).unwrap() == old_bytes, "offline: the stored preview and curve are untouched");
|
||||
assert!(crate::smart::is_stale(&file), "and it stays marked for the next time");
|
||||
assert_eq!(crate::smart::decode(&old_bytes).unwrap().1, Some(old), "the old look is still there");
|
||||
// ... and the opening refresh keeps it too
|
||||
// ... and the opening refresh keeps it too, then marks the folder checked
|
||||
assert!(!crate::smart::look_checked(&dir));
|
||||
crate::cmd::previews::refresh_stale_smart_previews(&mut s).unwrap().join().unwrap();
|
||||
assert!(std::fs::read(&file).unwrap() == old_bytes);
|
||||
assert!(crate::smart::look_checked(&dir));
|
||||
|
||||
// the original comes back: the library's next opening rebuilds it with the current look
|
||||
// the original comes back: the scan has run at this version, so the next opening leaves it;
|
||||
// Build Smart Previews rebuilds it with the current look
|
||||
std::fs::rename(src.with_extension("offline"), &src).unwrap();
|
||||
calls.store(0, Ordering::SeqCst);
|
||||
crate::cmd::previews::refresh_stale_smart_previews(&mut s).unwrap().join().unwrap();
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
assert!(crate::cmd::previews::refresh_stale_smart_previews(&mut s).is_none(), "checked once per version");
|
||||
assert!(std::fs::read(&file).unwrap() == old_bytes);
|
||||
let r = s.execute("library.smartPreviews", &json!({})).unwrap();
|
||||
assert_eq!((r["refreshed"].as_u64(), calls.load(Ordering::SeqCst)), (Some(1), 1), "{r}");
|
||||
assert_eq!(smart_header(&file)["look_version"], json!(crate::camera_preview::LOOK_VERSION));
|
||||
assert_eq!(crate::smart::decode(&std::fs::read(&file).unwrap()).unwrap().1, Some(new), "rebuilt with the current look");
|
||||
assert!(!crate::smart::is_stale(&file));
|
||||
assert!(std::fs::read(&png_file).unwrap() == png_bytes, "a PNG's proxy is never rebuilt for the look");
|
||||
|
||||
// a folder checked at an older version (or not at all) is scanned once at the next opening
|
||||
edit_smart_header(&file, |h| h["look_version"] = json!(0));
|
||||
std::fs::write(dir.join("look-version"), b"0\n").unwrap();
|
||||
assert!(!crate::smart::look_checked(&dir));
|
||||
s.media.forget(id);
|
||||
calls.store(0, Ordering::SeqCst);
|
||||
crate::cmd::previews::refresh_stale_smart_previews(&mut s).unwrap().join().unwrap();
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1, "only the raw's original is read");
|
||||
assert!(!crate::smart::is_stale(&file));
|
||||
assert!(std::fs::read(&png_file).unwrap() == png_bytes);
|
||||
assert!(crate::smart::look_checked(&dir));
|
||||
// once only
|
||||
let rebuilt = std::fs::read(&file).unwrap();
|
||||
crate::cmd::previews::refresh_stale_smart_previews(&mut s).unwrap().join().unwrap();
|
||||
assert!(crate::cmd::previews::refresh_stale_smart_previews(&mut s).is_none());
|
||||
let r = s.execute("library.smartPreviews", &json!({})).unwrap();
|
||||
assert_eq!((r["refreshed"].as_u64(), calls.load(Ordering::SeqCst)), (Some(0), 1), "{r}");
|
||||
assert!(std::fs::read(&file).unwrap() == rebuilt);
|
||||
@@ -1243,6 +1278,31 @@ fn smart_previews_from_an_older_look_fit_are_refreshed() {
|
||||
let _ = std::fs::remove_dir_all(&lib);
|
||||
}
|
||||
|
||||
/// The look marker: missing, damaged or older than this build's fit version counts as unchecked.
|
||||
#[test]
|
||||
fn smart_preview_look_marker_is_read_safely() {
|
||||
let dir = temp_dir("smartlook-marker");
|
||||
assert!(!crate::smart::look_checked(&dir), "no marker");
|
||||
let cur = crate::camera_preview::LOOK_VERSION;
|
||||
for (text, checked) in [
|
||||
(String::new(), false),
|
||||
("banana".into(), false),
|
||||
("-1".into(), false),
|
||||
(format!("{}", cur.saturating_sub(1)), false),
|
||||
("9".repeat(200), false),
|
||||
(format!("{cur}\n"), true),
|
||||
(format!(" {}\n", cur + 1), true),
|
||||
] {
|
||||
std::fs::write(dir.join("look-version"), &text).unwrap();
|
||||
assert_eq!(crate::smart::look_checked(&dir), checked, "{text:?}");
|
||||
}
|
||||
std::fs::remove_file(dir.join("look-version")).unwrap();
|
||||
crate::smart::mark_look_checked(&dir).unwrap();
|
||||
assert!(crate::smart::look_checked(&dir));
|
||||
assert!(crate::smart::mark_look_checked(&dir.join("missing")).is_err());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A hostile or damaged `look_version` never panics: anything that is not a non-negative integer
|
||||
/// counts as 0 (rebuilt when the original is online); a version from a newer build is left alone.
|
||||
#[test]
|
||||
|
||||
@@ -107,9 +107,11 @@ The fit therefore has a version, `LOOK_VERSION` in `crates/engine/src/camera_pre
|
||||
|
||||
A proxy older than the constant is brought up to date by the existing Build Smart Previews path (`smart_run`), which also repairs damaged proxies:
|
||||
|
||||
- when the library is opened, a background thread looks at the existing proxies and rebuilds the stale ones from their originals (no proxy is created, nothing is shown, the UI never waits on a drive);
|
||||
- Build Smart Previews does the same for the photos it is run on (`refreshed` in its result);
|
||||
- when the original can't be read (offline drive), the proxy and its curve are kept exactly as they are. The old version in its header is the mark: the next opening, or the next Build Smart Previews, with the original online rebuilds it. A rebuilt proxy carries the current version, so nothing is rebuilt twice. A proxy is only ever rebuilt from the original, never from another proxy.
|
||||
- only proxies of raws that take the per-file look (`file_local_look`: ARW, NEF, RW2, RAF, CR3, CR2, PEF, SRW, ORF…) go stale; the fit never reaches JPEG, PNG, TIFF or DNG proxies, which are never rebuilt for it;
|
||||
- when the library is opened and its smart previews folder has not been checked at this `LOOK_VERSION` (the `look-version` marker file in the folder), a background thread looks at those proxies, rebuilds the stale ones from their originals and then writes the marker, so the scan runs once per version bump, not at every opening (no proxy is created, nothing is shown, the UI never waits on a drive);
|
||||
- Build Smart Previews does the same for the photos it is run on (`refreshed` in its result), whatever the marker says;
|
||||
- when the original can't be read (offline drive), the proxy and its curve are kept exactly as they are (`staleKept`). The old version in its header is the mark: the next Build Smart Previews with the original online rebuilds it. A rebuilt proxy carries the current version, so nothing is rebuilt twice. A proxy is only ever rebuilt from the original, never from another proxy;
|
||||
- the two never write the same proxy at once: each proxy is checked and written under one lock.
|
||||
|
||||
## Nikon crop and preview colour metadata
|
||||
|
||||
|
||||
+1
-1
@@ -502,7 +502,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| LR-CLOUD-SYNC | Cloud sync | OOS | 🚫 | | |
|
||||
| LR-CLOUD-SMARTPREVIEW | Editable proxies | P2 | ✅ | `cmd:library.smartPreviews`, `cmd:library.smartPreviewsLocation`, `cmd:photo.smartPreview`, `crates/engine/src/smart.rs`, `crates/ui-egui/src/panels/settings.rs` | File ▸ Previews ▸ Build / Discard Smart Previews (on a worker thread, progress and Stop like Build Previews): ~1 MB proxies in the library; with the original offline the photo renders, edits and exports (at proxy size) from its proxy; Info shows the status; the proxy folder is chosen per library (Settings → Performance → Smart previews, saved in the library): the effective path, count and size are shown, changing it needs a choice for the proxies already built (move / leave / delete), and an unavailable or unwritable folder is an error, never a fallback to the library drive; proxies are written atomically and a damaged one (cut short by a crash or a full drive) doesn't count as built, is rebuilt (`repaired`) and is never moved over a good one (issue #106). Each proxy records the version of the camera-look fit that wrote it (`look_version`, `LOOK_VERSION` in `crates/engine/src/camera_preview.rs`, bumped whenever the fit changes); one from an older fit is rebuilt from its original when the library is opened or Build Smart Previews runs, and kept as it is while the original is offline (`docs/camera-preview-colour.md`). The thumbnail cache stays in the library |
|
||||
| LR-CLOUD-SMARTPREVIEW | Editable proxies | P2 | ✅ | `cmd:library.smartPreviews`, `cmd:library.smartPreviewsLocation`, `cmd:photo.smartPreview`, `crates/engine/src/smart.rs`, `crates/ui-egui/src/panels/settings.rs` | File ▸ Previews ▸ Build / Discard Smart Previews (on a worker thread, progress and Stop like Build Previews): ~1 MB proxies in the library; with the original offline the photo renders, edits and exports (at proxy size) from its proxy; Info shows the status; the proxy folder is chosen per library (Settings → Performance → Smart previews, saved in the library): the effective path, count and size are shown, changing it needs a choice for the proxies already built (move / leave / delete), and an unavailable or unwritable folder is an error, never a fallback to the library drive; proxies are written atomically and a damaged one (cut short by a crash or a full drive) doesn't count as built, is rebuilt (`repaired`) and is never moved over a good one (issue #106). Each proxy records the version of the camera-look fit that wrote it (`look_version`, `LOOK_VERSION` in `crates/engine/src/camera_preview.rs`, bumped whenever the fit changes); only raws with the per-file camera look go stale (JPEG / PNG / DNG proxies are never rebuilt for it); one from an older fit is rebuilt from its original by a background scan the first time the library is opened at a new version (a `look-version` marker in the proxy folder keeps it to once per version) and by Build Smart Previews, and kept as it is while the original is offline (`docs/camera-preview-colour.md`). The thumbnail cache stays in the library |
|
||||
| LR-AI-UPDATE-INDICATOR | AI-settings update indicator | P2 | ⬜ | | |
|
||||
| LR-AI-CREDITS | Generative credits | OOS | 🚫 | | |
|
||||
|
||||
|
||||
Reference in new issue
Block a user