Satisfy rustfmt and clippy on the album and folder picker changes

rustfmt over the files the albums work touched, and the album merge's
incoming row as a named struct rather than an eight-field tuple, which
clippy's type_complexity refused.
This commit is contained in:
2026-09-26 14:13:54 -04:00
parent 1abb18d972
commit 9b580c3720
18 changed files with 487 additions and 303 deletions
+56 -43
View File
@@ -128,7 +128,11 @@ impl AlbumsController {
let borrow = catalog.borrow();
let Some(cat) = borrow.as_ref() else { return };
if let Err(e) = albums::record_exports(cat.connection(), album, &files) {
log::warn!("recording {} exports in album {}: {e}", files.len(), album.0);
log::warn!(
"recording {} exports in album {}: {e}",
files.len(),
album.0
);
return;
}
}
@@ -188,11 +192,12 @@ fn adopt_old_destination(ctl: &AlbumsController, cat: &dr_catalog::Catalog) {
return;
}
let place = Place::Local(export.destination.trim().to_string());
match albums::create(conn, "Exports", &place)
.and_then(|id| albums::get(conn, id))
{
match albums::create(conn, "Exports", &place).and_then(|id| albums::get(conn, id)) {
Ok(Some(album)) => {
log::info!("the export folder {} is now the album “Exports”", export.destination);
log::info!(
"the export folder {} is now the album “Exports”",
export.destination
);
ctl.settings.edit(|s| s.export.album = album.uuid);
}
Ok(None) => {}
@@ -252,7 +257,10 @@ fn describe(place: Option<&Place>) -> String {
fn display_folder(folder: &str) -> String {
if folder.starts_with("content://") {
let tail = folder.rsplit('/').next().unwrap_or(folder);
let decoded = tail.replace("%3A", ":").replace("%2F", "/").replace("%20", " ");
let decoded = tail
.replace("%3A", ":")
.replace("%2F", "/")
.replace("%20", " ");
return decoded
.split_once(':')
.map(|(_, p)| p.to_string())
@@ -268,7 +276,10 @@ fn render_sheet(window: &AppWindow, ctl: &AlbumsController) {
albums.set_sheet_open(false);
return;
};
let server = ctl.library.credentials().is_some_and(|c| !c.account.login.is_empty());
let server = ctl
.library
.credentials()
.is_some_and(|c| !c.account.login.is_empty());
let refusal = if s.at == Where::Server {
refusal(&s.browser.path, ctl)
} else {
@@ -329,7 +340,9 @@ fn inside(path: &str, root: &str) -> bool {
/// List the sheet's server folder, and redraw when the answer comes.
fn browse(weak: slint::Weak<AppWindow>, ctl: Rc<AlbumsController>, path: String) {
let Some(conn) = ctl.library.credentials() else { return };
let Some(conn) = ctl.library.credentials() else {
return;
};
if let Some(s) = ctl.sheet.borrow_mut().as_mut() {
s.browser.path = path.clone();
s.browser.entries.clear();
@@ -375,7 +388,10 @@ fn settle(
/// Open the sheet — on a new album, or on `editing`.
fn open_sheet(window: &AppWindow, ctl: &Rc<AlbumsController>, editing: Option<AlbumId>) {
let album = editing.and_then(|id| ctl.rows.borrow().iter().find(|a| a.id == id).cloned());
let server = ctl.library.credentials().is_some_and(|c| !c.account.login.is_empty());
let server = ctl
.library
.credentials()
.is_some_and(|c| !c.account.login.is_empty());
let (at, device_folder, server_path) = match album.as_ref().and_then(|a| a.place.clone()) {
Some(Place::Local(f)) => (Where::Device, f, String::new()),
Some(Place::Server(p)) => (Where::Server, String::new(), p),
@@ -417,14 +433,13 @@ fn save(window: &AppWindow, ctl: &Rc<AlbumsController>) {
let Some(cat) = borrow.as_ref() else { return };
let conn = cat.connection();
match editing {
Some(id) => albums::rename(conn, id, &name)
.and_then(|()| {
let current = albums::get(conn, id)?.and_then(|a| a.place);
if current.as_ref() != Some(&place) {
albums::set_place(conn, id, &place)?;
}
Ok(id)
}),
Some(id) => albums::rename(conn, id, &name).and_then(|()| {
let current = albums::get(conn, id)?.and_then(|a| a.place);
if current.as_ref() != Some(&place) {
albums::set_place(conn, id, &place)?;
}
Ok(id)
}),
None => albums::create(conn, &name, &place),
}
.and_then(|id| Ok((id, albums::get(conn, id)?)))
@@ -686,7 +701,9 @@ pub fn wire(
let ctl = ctl.clone();
albums.on_browse_make(move |name| {
let Some(w) = weak.upgrade() else { return };
let Some(conn) = ctl.library.credentials() else { return };
let Some(conn) = ctl.library.credentials() else {
return;
};
let parent = {
let mut sheet = ctl.sheet.borrow_mut();
let Some(s) = sheet.as_mut() else { return };
@@ -700,18 +717,16 @@ pub fn wire(
let name = name.trim().to_string();
let (weak, ctl2) = (weak.clone(), ctl.clone());
let child = name.clone();
crate::remote_folders::make(conn, parent.clone(), name, move |result| {
match result {
Ok(_) => {
let path = if parent.is_empty() {
child
} else {
format!("{parent}/{child}")
};
browse(weak, ctl2, path);
}
Err(e) => settle(&weak, &ctl2, &parent, Err(e)),
crate::remote_folders::make(conn, parent.clone(), name, move |result| match result {
Ok(_) => {
let path = if parent.is_empty() {
child
} else {
format!("{parent}/{child}")
};
browse(weak, ctl2, path);
}
Err(e) => settle(&weak, &ctl2, &parent, Err(e)),
});
});
}
@@ -720,20 +735,18 @@ pub fn wire(
{
let weak = window.as_weak();
let ctl = ctl.clone();
window
.global::<ExportOptions>()
.on_album_picked(move |i| {
let Some(w) = weak.upgrade() else { return };
let uuid = ctl
.rows
.borrow()
.get(i.max(0) as usize)
.map(|a| a.uuid.clone());
if let Some(uuid) = uuid {
ctl.settings.edit(|s| s.export.album = uuid);
}
render(&w, &ctl);
});
window.global::<ExportOptions>().on_album_picked(move |i| {
let Some(w) = weak.upgrade() else { return };
let uuid = ctl
.rows
.borrow()
.get(i.max(0) as usize)
.map(|a| a.uuid.clone());
if let Some(uuid) = uuid {
ctl.settings.edit(|s| s.export.album = uuid);
}
render(&w, &ctl);
});
}
{
let weak = window.as_weak();
+3 -1
View File
@@ -220,7 +220,9 @@ fn refuse(window: &AppWindow, to: export::Reporting, message: &str) {
window.set_export_busy(false);
}
export::Reporting::Library => {
window.global::<Library>().set_library_status(message.into());
window
.global::<Library>()
.set_library_status(message.into());
window.global::<Library>().set_library_exporting(false);
}
}
+8 -3
View File
@@ -313,8 +313,7 @@ fn stage(encoded: &Encoded, remote_dir: &str, outbox: &Path) -> Result<PathBuf,
Some(dir) => format!("{dir}\n{}\naccount\n", encoded.name),
None => format!("{remote_dir}\n{}\n", encoded.name),
};
std::fs::write(&record, text)
.map_err(|e| format!("{}: {e}", record.display()))?;
std::fs::write(&record, text).map_err(|e| format!("{}: {e}", record.display()))?;
Ok(candidate)
}
@@ -1503,7 +1502,13 @@ mod tests {
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
place(&encoded("a.jpg", b"x"), ExportTarget::Remote, "/Shared/Web", &dir).unwrap();
place(
&encoded("a.jpg", b"x"),
ExportTarget::Remote,
"/Shared/Web",
&dir,
)
.unwrap();
let entry = pending(&dir).pop().expect("one staged export");
assert!(entry.account);
+2 -2
View File
@@ -33,10 +33,10 @@
use std::path::PathBuf;
#[cfg(not(target_os = "android"))]
use slint::ComponentHandle;
#[cfg(not(target_os = "android"))]
use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _};
#[cfg(not(target_os = "android"))]
use slint::ComponentHandle;
use crate::AppWindow;
+8 -1
View File
@@ -506,7 +506,14 @@ fn wire_options(window: &AppWindow, ctl: &Rc<ImportController>) {
let ctl = ctl.clone();
window.on_import_choose_backup(move || {
let Some(w) = weak.upgrade() else { return };
let start = ctl.settings.settings.borrow().import.backup.trim().to_string();
let start = ctl
.settings
.settings
.borrow()
.import
.backup
.trim()
.to_string();
let (weak, ctl) = (weak.clone(), ctl.clone());
crate::folder_dialog::ask(
&w,
+3 -1
View File
@@ -350,7 +350,9 @@ fn wire_folder_picker_navigation(window: &AppWindow, controller: &Rc<LaunchContr
let (account, parent) = {
let m = ctl.model.borrow();
let Some(b) = m.browser.as_ref() else { return };
let Some(account) = m.session().cloned() else { return };
let Some(account) = m.session().cloned() else {
return;
};
(account, b.path.clone())
};
let conn = match ctl
+2 -2
View File
@@ -34,8 +34,8 @@ mod display_ui;
mod duplicates;
mod duplicates_ui;
mod export;
mod folder_dialog;
pub mod faces;
mod folder_dialog;
pub use library::render_native;
// Generated from the `GESTURE:` comments beside the code that implements each
// one — see `tools/traceability`. Regenerate with
@@ -68,9 +68,9 @@ mod recovery_ui;
mod refine;
mod remote;
mod remote_folders;
pub mod repairs;
#[cfg(target_os = "android")]
mod saf;
pub mod repairs;
mod segmentation;
mod settings_store;
mod settings_ui;
+18 -3
View File
@@ -661,18 +661,33 @@ mod tests {
.unwrap();
let filter = RatingFilter::default();
let cells = read_cells_scoped(&catalog, Some(Scope::Collection(coll)), &filter, 0, 100).unwrap();
let cells =
read_cells_scoped(&catalog, Some(Scope::Collection(coll)), &filter, 0, 100).unwrap();
assert_eq!(cells.len(), 2);
for (i, cell) in cells.iter().enumerate() {
assert_eq!(
ordinal_of_path(&catalog, Some(Scope::Collection(coll)), &filter, false, &cell.remote_path).unwrap(),
ordinal_of_path(
&catalog,
Some(Scope::Collection(coll)),
&filter,
false,
&cell.remote_path
)
.unwrap(),
Some(i),
"{} is drawn at row {i} of the collection",
cell.remote_path
);
}
assert_eq!(
ordinal_of_path(&catalog, Some(Scope::Collection(coll)), &filter, false, "2019/a.CR2").unwrap(),
ordinal_of_path(
&catalog,
Some(Scope::Collection(coll)),
&filter,
false,
"2019/a.CR2"
)
.unwrap(),
None,
"not a member"
);
+92 -22
View File
@@ -415,8 +415,8 @@ mod tests {
let catalog = with_images(10);
let ids = image_ids(&catalog);
let album = albums::create(catalog.connection(), "Web", &Place::Local("/out".into()))
.unwrap();
let album =
albums::create(catalog.connection(), "Web", &Place::Local("/out".into())).unwrap();
// Two files from one photograph, one from another: two cells.
albums::record_exports(
catalog.connection(),
@@ -460,10 +460,22 @@ mod tests {
.unwrap();
coll::add_images(catalog.connection(), c, &ids[2..5]).unwrap();
let cells = read_cells_scoped(&catalog, Some(Scope::Collection(c)), &RatingFilter::default(), 0, 120).unwrap();
let cells = read_cells_scoped(
&catalog,
Some(Scope::Collection(c)),
&RatingFilter::default(),
0,
120,
)
.unwrap();
assert_eq!(cells.len(), 3);
assert_eq!(
total_images_scoped(&catalog, Some(Scope::Collection(c)), &RatingFilter::default()).unwrap(),
total_images_scoped(
&catalog,
Some(Scope::Collection(c)),
&RatingFilter::default()
)
.unwrap(),
3
);
// Unscoped is still the whole library.
@@ -493,11 +505,22 @@ mod tests {
coll::add_images(catalog.connection(), iceland, &ids[0..4]).unwrap();
// The parent itself has no direct members at all.
let cells =
read_cells_scoped(&catalog, Some(Scope::Collection(trips)), &RatingFilter::default(), 0, 120).unwrap();
let cells = read_cells_scoped(
&catalog,
Some(Scope::Collection(trips)),
&RatingFilter::default(),
0,
120,
)
.unwrap();
assert_eq!(cells.len(), 4, "the set shows what its children hold");
assert_eq!(
total_images_scoped(&catalog, Some(Scope::Collection(trips)), &RatingFilter::default()).unwrap(),
total_images_scoped(
&catalog,
Some(Scope::Collection(trips)),
&RatingFilter::default()
)
.unwrap(),
4
);
}
@@ -522,11 +545,22 @@ mod tests {
coll::add_images(catalog.connection(), trips, &ids[0..2]).unwrap();
coll::add_images(catalog.connection(), iceland, &ids[0..3]).unwrap();
let cells =
read_cells_scoped(&catalog, Some(Scope::Collection(trips)), &RatingFilter::default(), 0, 120).unwrap();
let cells = read_cells_scoped(
&catalog,
Some(Scope::Collection(trips)),
&RatingFilter::default(),
0,
120,
)
.unwrap();
assert_eq!(cells.len(), 3, "images 0..3, each once");
assert_eq!(
total_images_scoped(&catalog, Some(Scope::Collection(trips)), &RatingFilter::default()).unwrap(),
total_images_scoped(
&catalog,
Some(Scope::Collection(trips)),
&RatingFilter::default()
)
.unwrap(),
3
);
}
@@ -542,7 +576,14 @@ mod tests {
let c = coll::create(catalog.connection(), "Big", None, CollectionKind::Manual).unwrap();
coll::add_images(catalog.connection(), c, &ids).unwrap();
let page = read_cells_scoped(&catalog, Some(Scope::Collection(c)), &RatingFilter::default(), 10, 5).unwrap();
let page = read_cells_scoped(
&catalog,
Some(Scope::Collection(c)),
&RatingFilter::default(),
10,
5,
)
.unwrap();
assert_eq!(page.len(), 5);
assert_eq!(page[0].name, "img010.CR2");
}
@@ -556,13 +597,22 @@ mod tests {
let catalog = with_images(10);
let c = coll::create(catalog.connection(), "Empty", None, CollectionKind::Manual).unwrap();
assert!(
read_cells_scoped(&catalog, Some(Scope::Collection(c)), &RatingFilter::default(), 0, 120)
.unwrap()
.is_empty()
);
assert!(read_cells_scoped(
&catalog,
Some(Scope::Collection(c)),
&RatingFilter::default(),
0,
120
)
.unwrap()
.is_empty());
assert_eq!(
total_images_scoped(&catalog, Some(Scope::Collection(c)), &RatingFilter::default()).unwrap(),
total_images_scoped(
&catalog,
Some(Scope::Collection(c)),
&RatingFilter::default()
)
.unwrap(),
0
);
}
@@ -671,15 +721,29 @@ mod tests {
let wanted: Vec<_> = all.iter().rev().copied().collect();
dr_catalog::collections::set_order(catalog.connection(), id, &wanted).unwrap();
let cells = read_cells_scoped(&catalog, Some(Scope::Collection(id)), &RatingFilter::default(), 0, 50).unwrap();
let cells = read_cells_scoped(
&catalog,
Some(Scope::Collection(id)),
&RatingFilter::default(),
0,
50,
)
.unwrap();
let drawn: Vec<_> = cells
.iter()
.map(|c| dr_types::ImageId(c.image_id as u64))
.collect();
assert_eq!(drawn, wanted, "the grid draws the order that was written");
let spanned =
read_ids_span(&catalog, Some(Scope::Collection(id)), &RatingFilter::default(), false, 0, 4).unwrap();
let spanned = read_ids_span(
&catalog,
Some(Scope::Collection(id)),
&RatingFilter::default(),
false,
0,
4,
)
.unwrap();
assert_eq!(spanned, wanted, "and a range resolves through the same one");
assert_eq!(
@@ -715,8 +779,14 @@ mod tests {
let reversed: Vec<_> = all.iter().rev().copied().collect();
dr_catalog::collections::set_order(catalog.connection(), parent, &reversed).unwrap();
let cells =
read_cells_scoped(&catalog, Some(Scope::Collection(parent)), &RatingFilter::default(), 0, 50).unwrap();
let cells = read_cells_scoped(
&catalog,
Some(Scope::Collection(parent)),
&RatingFilter::default(),
0,
50,
)
.unwrap();
let drawn: Vec<_> = cells
.iter()
.map(|c| dr_types::ImageId(c.image_id as u64))
+1 -1
View File
@@ -342,7 +342,7 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
.set_library_scope_pinned(match scope {
Some(library::Scope::Collection(id)) => {
dr_catalog::collections::descendants(catalog.connection(), id)
.map(|ids| collection_images(catalog, &ids))
.map(|ids| collection_images(catalog, &ids))
.map(|images| scope_is_pinned(catalog, &images))
.unwrap_or(false)
}
+8 -2
View File
@@ -1100,9 +1100,15 @@ pub fn wire_named(
window.on_browse_presets(move |file| {
let Some(w) = weak.upgrade() else { return };
let (title, pick) = if file {
("Import a Lightroom preset", crate::folder_dialog::Pick::File(&["xmp"]))
(
"Import a Lightroom preset",
crate::folder_dialog::Pick::File(&["xmp"]),
)
} else {
("Import a folder of Lightroom presets", crate::folder_dialog::Pick::Folder)
(
"Import a folder of Lightroom presets",
crate::folder_dialog::Pick::Folder,
)
};
let (weak, named) = (weak.clone(), named.clone());
crate::folder_dialog::ask(&w, title, pick, None, move |path| {
+33 -23
View File
@@ -18,7 +18,11 @@ use dr_sync::{Connection, EntryKind, RemotePath};
/// Child folder names at `path` (empty is the account root), sorted as a
/// person reads them.
pub fn list(conn: Connection, path: String, done: impl FnOnce(Result<Vec<String>, String>) + 'static) {
pub fn list(
conn: Connection,
path: String,
done: impl FnOnce(Result<Vec<String>, String>) + 'static,
) {
run(conn, done, move |backend| {
Box::pin(async move { children(backend.as_ref(), &path).await })
});
@@ -129,28 +133,34 @@ where
let mut done = Some(done);
let timer = slint::Timer::default();
let held = slot.clone();
timer.start(slint::TimerMode::Repeated, Duration::from_millis(100), move || {
let outcome = match rx.try_recv() {
Ok(result) => result,
Err(TryRecvError::Empty) => return,
// A worker that panicked drops the sender without sending, and
// treating that as "still loading" is how a picker sits on
// "Loading…" for ever.
Err(TryRecvError::Disconnected) => Err("the folder request failed unexpectedly".into()),
};
if let Some(done) = done.take() {
done(outcome);
}
// Stopped here, released a turn later: a timer dropped inside its
// own callback is freed while Slint is still calling it.
if let Some(t) = held.borrow().as_ref() {
t.stop();
}
let held = held.clone();
slint::Timer::single_shot(Duration::ZERO, move || {
held.borrow_mut().take();
});
});
timer.start(
slint::TimerMode::Repeated,
Duration::from_millis(100),
move || {
let outcome = match rx.try_recv() {
Ok(result) => result,
Err(TryRecvError::Empty) => return,
// A worker that panicked drops the sender without sending, and
// treating that as "still loading" is how a picker sits on
// "Loading…" for ever.
Err(TryRecvError::Disconnected) => {
Err("the folder request failed unexpectedly".into())
}
};
if let Some(done) = done.take() {
done(outcome);
}
// Stopped here, released a turn later: a timer dropped inside its
// own callback is freed while Slint is still calling it.
if let Some(t) = held.borrow().as_ref() {
t.stop();
}
let held = held.clone();
slint::Timer::single_shot(Duration::ZERO, move || {
held.borrow_mut().take();
});
},
);
*slot.borrow_mut() = Some(timer);
}
+57 -39
View File
@@ -91,7 +91,11 @@ fn text(env: &mut jni::Env, value: JObject) -> jni::errors::Result<Option<String
/// activity, and this one's event loop keeps running under it.
pub fn pick_tree(chosen: impl FnOnce(String) + 'static) {
let started = call("opening the folder picker", |env, activity| {
let cls = class(env, activity, jni::jni_str!("paris.tourolle.darkroom.FolderPicker"))?;
let cls = class(
env,
activity,
jni::jni_str!("paris.tourolle.darkroom.FolderPicker"),
)?;
env.call_static_method(
&cls,
jni::jni_str!("start"),
@@ -112,44 +116,52 @@ pub fn pick_tree(chosen: impl FnOnce(String) + 'static) {
let mut chosen = Some(chosen);
let since = Instant::now();
let timer = slint::Timer::default();
timer.start(slint::TimerMode::Repeated, Duration::from_millis(250), move || {
let answer = call("reading the folder picker", |env, activity| {
let cls = class(env, activity, jni::jni_str!("paris.tourolle.darkroom.FolderPicker"))?;
let value = env
.call_static_method(
&cls,
jni::jni_str!("poll"),
jni::jni_sig!("()Ljava/lang/String;"),
&[],
)?
.l()?;
text(env, value)
});
let finished = match answer {
Ok(None) => since.elapsed() > Duration::from_secs(600),
Ok(Some(uri)) => {
if !uri.is_empty() {
if let Some(chosen) = chosen.take() {
chosen(uri);
}
}
true
}
Err(e) => {
log::warn!("{e}");
true
}
};
if finished {
if let Some(t) = held.borrow().as_ref() {
t.stop();
}
let held = held.clone();
slint::Timer::single_shot(Duration::ZERO, move || {
held.borrow_mut().take();
timer.start(
slint::TimerMode::Repeated,
Duration::from_millis(250),
move || {
let answer = call("reading the folder picker", |env, activity| {
let cls = class(
env,
activity,
jni::jni_str!("paris.tourolle.darkroom.FolderPicker"),
)?;
let value = env
.call_static_method(
&cls,
jni::jni_str!("poll"),
jni::jni_sig!("()Ljava/lang/String;"),
&[],
)?
.l()?;
text(env, value)
});
}
});
let finished = match answer {
Ok(None) => since.elapsed() > Duration::from_secs(600),
Ok(Some(uri)) => {
if !uri.is_empty() {
if let Some(chosen) = chosen.take() {
chosen(uri);
}
}
true
}
Err(e) => {
log::warn!("{e}");
true
}
};
if finished {
if let Some(t) = held.borrow().as_ref() {
t.stop();
}
let held = held.clone();
slint::Timer::single_shot(Duration::ZERO, move || {
held.borrow_mut().take();
});
}
},
);
*slot.borrow_mut() = Some(timer);
}
@@ -176,7 +188,13 @@ pub fn exists(tree: &str, name: &str) -> bool {
/// Write an export into the folder. Returns the name it has there, which the
/// provider may have changed on a collision.
pub fn write(tree: &str, name: &str, mime: &str, bytes: &[u8], replace: bool) -> Result<String, String> {
pub fn write(
tree: &str,
name: &str,
mime: &str,
bytes: &[u8],
replace: bool,
) -> Result<String, String> {
call("writing to the album folder", |env, activity| {
let cls = class(env, activity, jni::jni_str!("paris.tourolle.darkroom.Saf"))?;
let jtree = env.new_string(tree)?;
+2 -2
View File
@@ -26,8 +26,8 @@ use std::rc::Rc;
use dr_types::settings::budget;
use dr_types::{
CacheSettings, CollisionPolicy, ColourSpace, ExportFormat, FaceDetector,
GroupNavigation, LibrarySettings, OutputSharpening, ScreenSize, Settings, SizingMode,
CacheSettings, CollisionPolicy, ColourSpace, ExportFormat, FaceDetector, GroupNavigation,
LibrarySettings, OutputSharpening, ScreenSize, Settings, SizingMode,
};
use slint::ComponentHandle;