Export to an album instead of a folder in the settings
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.
This commit is contained in:
@@ -211,6 +211,21 @@ fn wire_presets(window: &AppWindow, w: &DevelopWiring) {
|
||||
presets::render(window, &clipboard, &settings);
|
||||
}
|
||||
|
||||
/// An export that cannot start, said where the button that asked for it is,
|
||||
/// and the button given back.
|
||||
fn refuse(window: &AppWindow, to: export::Reporting, message: &str) {
|
||||
match to {
|
||||
export::Reporting::Develop => {
|
||||
window.set_export_status(message.into());
|
||||
window.set_export_busy(false);
|
||||
}
|
||||
export::Reporting::Library => {
|
||||
window.global::<Library>().set_library_status(message.into());
|
||||
window.global::<Library>().set_library_exporting(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// --- export (FR-EXP-6, FR-EXP-7, FR-EXP-9) ---------------------------
|
||||
///
|
||||
/// Two buttons, one worker. The develop view exports the image on screen and
|
||||
@@ -229,6 +244,7 @@ fn wire_export(window: &AppWindow, w: &DevelopWiring) {
|
||||
let collections = w.collections.clone();
|
||||
let activity = w.activity.clone();
|
||||
let gpu = w.gpu.clone();
|
||||
let open_image = w.open_image.clone();
|
||||
|
||||
// Replaced at each start, so the button always cancels the run it is
|
||||
// sitting on and a stale token cancels nothing.
|
||||
@@ -244,10 +260,37 @@ fn wire_export(window: &AppWindow, w: &DevelopWiring) {
|
||||
let gpu = gpu.clone();
|
||||
let cancel = cancel.clone();
|
||||
let drain = drain.clone();
|
||||
let collections = collections.clone();
|
||||
Rc::new(
|
||||
move |window: &AppWindow, sources: Vec<export::Source>, to: export::Reporting| {
|
||||
move |window: &AppWindow,
|
||||
sources: Vec<export::Source>,
|
||||
images: Vec<Option<dr_types::ImageId>>,
|
||||
to: export::Reporting| {
|
||||
let total = sources.len();
|
||||
let request = batch_request(sources, &settings, &library, gpu.as_ref());
|
||||
|
||||
// TRACES: FR-EXP-10
|
||||
// Where it goes is an album, resolved now rather than when
|
||||
// the sheet was last touched: the album may have been
|
||||
// deleted, merged away, or have no folder on this device.
|
||||
// Refused before anything is rendered, in the words the
|
||||
// photographer needs to fix it.
|
||||
let albums = collections.albums.borrow().clone();
|
||||
let resolved = match albums.as_ref() {
|
||||
Some(a) => a.export_destination(),
|
||||
None => Err("Albums are not ready yet.".into()),
|
||||
};
|
||||
let (album, target, folder) = match resolved {
|
||||
Ok(found) => found,
|
||||
Err(e) => {
|
||||
refuse(window, to, &e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut request = batch_request(sources, &settings, &library, gpu.as_ref());
|
||||
request.settings.target = target;
|
||||
request.settings.destination = folder;
|
||||
request.images = images;
|
||||
|
||||
let token = export::Cancel::default();
|
||||
*cancel.borrow_mut() = token.clone();
|
||||
@@ -261,6 +304,7 @@ fn wire_export(window: &AppWindow, w: &DevelopWiring) {
|
||||
// "Queued for Exports" sitting unchanged until somebody
|
||||
// presses Sync reads as an export that did not upload.
|
||||
let library_for_drain = library.clone();
|
||||
let weak = window.as_weak();
|
||||
export::drain_batch(
|
||||
window.as_weak(),
|
||||
&activity,
|
||||
@@ -268,7 +312,14 @@ fn wire_export(window: &AppWindow, w: &DevelopWiring) {
|
||||
rx,
|
||||
total,
|
||||
to,
|
||||
move || drain_outbox(&library_for_drain),
|
||||
move |written| {
|
||||
drain_outbox(&library_for_drain);
|
||||
// What the album now holds, and which photograph
|
||||
// each file came from.
|
||||
if let (Some(albums), Some(w)) = (albums.as_ref(), weak.upgrade()) {
|
||||
albums.record(&w, album, written);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
@@ -278,6 +329,8 @@ fn wire_export(window: &AppWindow, w: &DevelopWiring) {
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let settings = settings.clone();
|
||||
let library = library.clone();
|
||||
let open_image = open_image.clone();
|
||||
let start = start.clone();
|
||||
window.on_export_image(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
@@ -295,14 +348,20 @@ fn wire_export(window: &AppWindow, w: &DevelopWiring) {
|
||||
// would never be drawn in the "Rendering…" state at all.
|
||||
w.set_export_status("Rendering…".into());
|
||||
|
||||
// The photograph in the catalog, so the album can link its file
|
||||
// back to it. A file opened from the command line has none.
|
||||
let image = match &*open_image.borrow() {
|
||||
presets::Stored::Remote { path, .. } => library.image_id_for_path(path),
|
||||
_ => None,
|
||||
};
|
||||
match render_open_frame(&w, &session, settings.snapshot().export.colour_space) {
|
||||
Ok(source) => start(&w, vec![source], export::Reporting::Develop),
|
||||
Ok(source) => start(&w, vec![source], vec![image], export::Reporting::Develop),
|
||||
Err(e) => {
|
||||
w.set_export_status(format!("Export failed: {e}").into());
|
||||
w.set_export_busy(false);
|
||||
}
|
||||
}
|
||||
refresh_export_label(&w, &settings);
|
||||
refresh_export_label(&w);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -329,7 +388,7 @@ fn wire_export(window: &AppWindow, w: &DevelopWiring) {
|
||||
return;
|
||||
}
|
||||
|
||||
let sources = library.export_sources(&collections.selected());
|
||||
let (sources, images) = library.export_sources_with_ids(&collections.selected());
|
||||
if sources.is_empty() {
|
||||
// Said out loud rather than ignored, matching what a paste
|
||||
// or a judgement keystroke does with an empty selection.
|
||||
@@ -341,7 +400,7 @@ fn wire_export(window: &AppWindow, w: &DevelopWiring) {
|
||||
w.global::<Library>().set_library_exporting(true);
|
||||
w.global::<Library>()
|
||||
.set_library_status(format!("Exporting {} images…", sources.len()).into());
|
||||
start(&w, sources, export::Reporting::Library);
|
||||
start(&w, sources, images, export::Reporting::Library);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user