Export took a path typed into the settings page, or a folder inside the library on the server. The first is how exports end up somewhere nobody looks; the second put JPEGs into the tree a scan catalogues, where they came back as photographs beside the RAWs they were made from. The destination is now an album (FR-EXP-10), chosen by name in the export sheet. Albums are listed under the collections in the sidebar; "+" there, or "New album…" in the sheet, opens a sheet for its name and its folder — on this device through the platform's dialogue, or on the server through the browser with "New folder". A server folder inside the library is refused, and the sheet says why. Selecting an album narrows the grid to the photographs behind its files: library::Scope is Collection or Album, and scope_clause is the one place the two are spelled, which also retires the two copies of the collection predicate total_images_scoped and read_cells_scoped had inlined. A batch resolves the album when it starts, and refuses in words when none is chosen, it has gone, or its folder is local to another device. Each item reports the image it came from, and the files written are recorded against the album in one transaction when the batch ends. A server album lives outside the library, so its queued uploads are relative to the account root. That is a third line in the outbox's .dest record rather than a leading slash, because a record written before albums may carry a stray slash and must keep the meaning it was written with. An export folder set before albums becomes an album called "Exports" on first open, so upgrading does not lose where exports were going. The old destination fields stay in ExportSettings so older settings files still read.
138 lines
4.8 KiB
Rust
138 lines
4.8 KiB
Rust
//! TRACES: FR-CAT-1 | FR-CAT-4 | FR-NC-3 | NFR-P9
|
|
//! Opening a remote library: scan → catalog → grid.
|
|
//!
|
|
//! This is the wire between three pieces that already worked separately —
|
|
//! `dr_sync::scan` walks the tree, `dr_catalog` indexes it, and
|
|
//! `dr_decode::preview` turns bytes into pixels. Until now the "Open library"
|
|
//! button logged its intent and stopped.
|
|
//!
|
|
//! # Threading
|
|
//!
|
|
//! Slint's event loop is single-threaded and must never block (NFR-P9), so
|
|
//! every network and decode operation runs on a worker thread and results
|
|
//! return through an mpsc channel drained by a Slint timer. That is the same
|
|
//! shape the login flow uses; it is repeated rather than shared because the
|
|
//! message types differ and a generic version would obscure both.
|
|
//!
|
|
//! # Why thumbnails are fetched, not derived from the scan
|
|
//!
|
|
//! A scan yields paths and sizes, nothing visual. Each thumbnail costs its own
|
|
//! range request, so they are fetched **only for cells the grid actually
|
|
//! wants** — never for the whole library up front. On the reference library
|
|
//! that is the difference between a few MB and ~370 GB (ARCH §6.7).
|
|
//!
|
|
//! Split into one module per area (docs/dev/code-health.md CH-1): filtering,
|
|
//! sidecar writes, on-disk paths, scanning, XMP, thumbnail fetch and
|
|
//! generation, the background sweeps, and reading the grid's cells and spans.
|
|
//! `mod.rs` re-exports every module's public items so nothing outside
|
|
//! `library` needs to change.
|
|
|
|
mod cells;
|
|
mod filters;
|
|
mod paths;
|
|
mod scan;
|
|
mod sidecar;
|
|
mod spans;
|
|
mod sweep;
|
|
mod thumbnails_fetch;
|
|
mod thumbnails_gen;
|
|
mod xmp;
|
|
|
|
pub use cells::*;
|
|
pub use filters::*;
|
|
pub use paths::*;
|
|
pub use scan::*;
|
|
pub use sidecar::*;
|
|
pub use spans::*;
|
|
pub use sweep::*;
|
|
pub use thumbnails_fetch::*;
|
|
pub use thumbnails_gen::*;
|
|
pub use xmp::*;
|
|
|
|
/// What narrows the grid, when something does.
|
|
///
|
|
/// A collection is a set of originals the photographer files; an album is the
|
|
/// photographs whose exports went into one folder. The grid reads either the
|
|
/// same way — see `spans::scope_clause`, the one place the difference is
|
|
/// spelled — and the whole library is `None`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Scope {
|
|
Collection(dr_types::CollectionId),
|
|
Album(dr_catalog::AlbumId),
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) mod test_support {
|
|
//! Fixtures shared with the modules that build on this one's catalog
|
|
//! layout -- `repairs`, whose work lists are read over the same `images`
|
|
//! and `remote` rows a scan writes.
|
|
use dr_catalog::Catalog;
|
|
use dr_sync::{RemoteEntry, RemoteId, RemotePath};
|
|
|
|
use super::scan::persist;
|
|
|
|
pub fn entry(path: &str, file_id: u64, size: u64) -> RemoteEntry {
|
|
RemoteEntry {
|
|
id: RemoteId::Stable(file_id),
|
|
path: RemotePath::new(path),
|
|
kind: dr_sync::EntryKind::File,
|
|
validator: dr_sync::Validator::new("v"),
|
|
size,
|
|
modified: None,
|
|
has_preview: false,
|
|
materialised: true,
|
|
}
|
|
}
|
|
|
|
/// A catalog with `n` images, ready to file into collections.
|
|
pub fn with_images(n: usize) -> Catalog {
|
|
let catalog = Catalog::in_memory().unwrap();
|
|
let images: Vec<RemoteEntry> = (0..n)
|
|
.map(|i| entry(&format!("PhotosRaw/img{i:03}.CR2"), i as u64, 1000))
|
|
.collect();
|
|
let result = dr_sync::ScanResult {
|
|
images,
|
|
directories: vec![(RemotePath::new("PhotosRaw"), dr_sync::Validator::new("e"))],
|
|
progress: Default::default(),
|
|
sidecars: Vec::new(),
|
|
};
|
|
persist(&catalog, "PhotosRaw", &result).unwrap();
|
|
catalog
|
|
}
|
|
|
|
/// A catalog with `n` scanned images, none trashed.
|
|
///
|
|
/// Shared with `spans`, whose collection-membership tests build on the
|
|
/// same "n images, no folders beyond the root" catalog `cells`'s trash
|
|
/// tests do.
|
|
pub fn scanned(n: u64) -> Catalog {
|
|
let catalog = Catalog::in_memory().unwrap();
|
|
let images = (1..=n)
|
|
.map(|i| entry(&format!("PhotosRaw/IMG_{i:04}.CR2"), 1000 + i, 30_000_000))
|
|
.collect();
|
|
persist(
|
|
&catalog,
|
|
"PhotosRaw",
|
|
&dr_sync::ScanResult {
|
|
images,
|
|
directories: vec![(RemotePath::new("PhotosRaw"), dr_sync::Validator::new("e1"))],
|
|
progress: Default::default(),
|
|
sidecars: Vec::new(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
catalog
|
|
}
|
|
|
|
pub fn image_ids(catalog: &Catalog) -> Vec<dr_types::ImageId> {
|
|
let mut stmt = catalog
|
|
.connection()
|
|
.prepare("SELECT id FROM images ORDER BY source_ref")
|
|
.unwrap();
|
|
stmt.query_map([], |r| Ok(dr_types::ImageId(r.get::<_, i64>(0)? as u64)))
|
|
.unwrap()
|
|
.map(Result::unwrap)
|
|
.collect()
|
|
}
|
|
}
|