Make the CI checks say what they mean, and format the workspace
Build and test / Desktop (Linux) (push) Successful in 1h23m26s
Build and test / Android (aarch64) (push) Failing after 2s
Build and test / Layer separation (push) Successful in 50s
Traceability / Requirement traces (push) Failing after 1m9s

The Android job's "Verify minimum API level" step has never verified the
minimum API level. It took the first `*.so` anywhere under the target
directory, which is a host proc-macro from debug/deps — an x86-64 object
built by the runner's gcc, whose .comment section cannot mention Android
and so can never contradict the expected value. It now reads the artifact
under the target triple, compares against MIN_API parsed from the
Dockerfile rather than a second copy of the number, and fails on a
mismatch. Both sides are checked non-empty first: two failed parses would
otherwise compare equal and pass, which is the same silent success in a
new costume.

The Android image installs one SDK package per layer and keeps the
output. sdkmanager is a JVM program that aborts when it cannot get memory,
and the single `> /dev/null` step reported that as a bare "exit code 134"
while a retry re-downloaded everything that had already succeeded.

tools/ci-local.sh runs all four jobs — desktop, android, layering,
traceability — against the host toolchain, which is pinned to the same
1.92.0 CI installs. Its matrix check compares regeneration against the
working tree rather than against HEAD: CI starts from a clean checkout, so
git's answer is the right one there and reports every local run stale here.

The rest is rustfmt across the workspace, and the clippy findings that
surfaced once it did: manual_contains in dr-thumbs and collections_ui, a
map iterated as pairs for its keys, an index loop over a slice, and two
runtime assertions on a constant now made at compile time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 12:02:51 +02:00
co-authored by Claude Opus 5
parent 94a2686dcb
commit 03326242a1
31 changed files with 927 additions and 387 deletions
+43 -41
View File
@@ -268,7 +268,9 @@ impl LibraryController {
// applies to every fetch from now on.
let Some(dir) = self.cache_dir() else { return };
let borrow = self.catalog.borrow();
let Some(catalog) = borrow.as_ref() else { return };
let Some(catalog) = borrow.as_ref() else {
return;
};
match dr_catalog::Cache::open(&dir, budget)
.and_then(|cache| cache.enforce(catalog.connection()))
{
@@ -568,9 +570,7 @@ fn drain_scan(
// off, and hiding them makes an incremental rescan look
// identical to a full one.
let status = if pruned > 0 {
format!(
"{directories} folders · {pruned} unchanged · {images} images"
)
format!("{directories} folders · {pruned} unchanged · {images} images")
} else {
format!("{directories} folders · {images} images")
};
@@ -628,9 +628,7 @@ fn drain_scan(
// than the part that was scrolled past.
start_sweep(&w, ctl);
}
Err(e) => {
w.set_library_error(format!("opening catalog: {e}").into())
}
Err(e) => w.set_library_error(format!("opening catalog: {e}").into()),
}
stop(&ctl.scan_timer);
return;
@@ -1166,8 +1164,7 @@ fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
*ctl.file_ids.borrow_mut() = cells.iter().map(|c| c.file_id).collect();
*ctl.sizes.borrow_mut() = cells.iter().map(|c| c.size).collect();
*ctl.image_ids.borrow_mut() = cells.iter().map(|c| c.image_id).collect();
*ctl.needs_metadata.borrow_mut() =
cells.iter().map(|c| c.metadata_state < 2).collect();
*ctl.needs_metadata.borrow_mut() = cells.iter().map(|c| c.metadata_state < 2).collect();
ctl.requested.borrow_mut().clear();
// The model is about to be replaced, so every thumbnail still in flight
// addresses a window that no longer exists. Bumping here — before the swap,
@@ -1320,11 +1317,7 @@ fn apply_judgement(
}
/// What the status line says about a judgement that just landed.
fn judgement_summary(
n: usize,
rating: Option<u8>,
flag: Option<dr_types::FlagState>,
) -> String {
fn judgement_summary(n: usize, rating: Option<u8>, flag: Option<dr_types::FlagState>) -> String {
let what = match (rating, flag) {
(Some(0), _) => "unrated".to_string(),
(Some(r), _) => format!("{r} star{}", if r == 1 { "" } else { "s" }),
@@ -1613,9 +1606,7 @@ fn drain_thumbnails(
// Date reads produce no cell, so they are counted into
// the bar's denominator or it finishes while work is
// still running.
w.set_library_thumbs_total(
w.get_library_thumbs_total() + dating as i32,
);
w.set_library_thumbs_total(w.get_library_thumbs_total() + dating as i32);
let mut parts = Vec::new();
if cached > 0 {
@@ -1820,7 +1811,11 @@ fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>) {
report.shards_uploaded,
report.shards_downloaded,
report.thumbnails_adopted,
if report.catalog_uploaded { "pushed" } else { "not pushed" },
if report.catalog_uploaded {
"pushed"
} else {
"not pushed"
},
if report.collections_gained > 0 {
format!(", {} collection(s) gained", report.collections_gained)
} else {
@@ -2045,9 +2040,8 @@ fn refresh_timeline(window: &AppWindow, catalog: &Catalog, ctl: &Rc<LibraryContr
window.set_library_current_bucket(current.unwrap_or(0) as i32);
window.set_library_current_fraction(fraction);
window.set_library_timeline_anchored(current.is_some());
window.set_library_timeline_label(
format!("{} – {}", format_date(from), format_date(to)).into(),
);
window
.set_library_timeline_label(format!("{} – {}", format_date(from), format_date(to)).into());
window.set_library_timeline(slint::ModelRc::new(slint::VecModel::from(bars)));
}
@@ -2264,8 +2258,7 @@ pub fn wire<F>(
ctl: Rc<LibraryController>,
coll_ctl: Rc<crate::collections_ui::CollectionsController>,
on_open_image: F,
)
where
) where
F: Fn(String) + 'static,
{
{
@@ -2435,8 +2428,8 @@ where
// Reacting to every scroll event would re-query and re-fetch
// continuously during a drag; this fires a few times per screenful.
let margin = window_size / 4;
let inside = first_visible >= current + margin
&& first_visible + margin < current + window_size;
let inside =
first_visible >= current + margin && first_visible + margin < current + window_size;
if inside {
return;
}
@@ -2453,8 +2446,12 @@ where
window.on_library_timeline_pan(move |fraction| {
let Some(w) = weak.upgrade() else { return };
let borrow = ctl.catalog.borrow();
let Some(catalog) = borrow.as_ref() else { return };
let Some(full) = catalog_span(catalog) else { return };
let Some(catalog) = borrow.as_ref() else {
return;
};
let Some(full) = catalog_span(catalog) else {
return;
};
// A fraction of the *visible* span, so dragging half the axis
// moves half a span's worth of time whatever the zoom — and a
@@ -2466,11 +2463,7 @@ where
return;
}
let centre = ctl
.timeline_centre
.borrow()
.unwrap_or((from + to) / 2)
+ shift;
let centre = ctl.timeline_centre.borrow().unwrap_or((from + to) / 2) + shift;
*ctl.timeline_centre.borrow_mut() = Some(centre.clamp(full.0, full.1));
refresh_timeline(&w, catalog, &ctl);
});
@@ -2501,8 +2494,12 @@ where
window.on_library_scrub_fraction(move |f| {
let Some(w) = weak.upgrade() else { return };
let borrow = ctl.catalog.borrow();
let Some(catalog) = borrow.as_ref() else { return };
let Some(full) = catalog_span(catalog) else { return };
let Some(catalog) = borrow.as_ref() else {
return;
};
let Some(full) = catalog_span(catalog) else {
return;
};
let (from, to) = zoomed_span(
full,
@@ -2784,8 +2781,6 @@ fn stop(slot: &RefCell<Option<slint::Timer>>) {
mod tests {
use super::*;
/// The instant a scrub fraction names within a span, as the handler
/// computes it.
fn instant_at(span: (i64, i64), f: f32) -> i64 {
@@ -2903,7 +2898,6 @@ mod tests {
assert_eq!(pinch_step(&mut accum, 0.72), 0, "back roughly to 1.0");
}
/// One zoom step, as the handler applies it.
fn zoom_cell(current: f32, delta: i32) -> f32 {
let next = if delta > 0 {
@@ -2920,7 +2914,10 @@ mod tests {
// step is imperceptible at 400px and violent at 90px.
let a = zoom_cell(180.0, 1);
assert!((a - 225.0).abs() < 0.01);
assert!((zoom_cell(a, -1) - 180.0).abs() < 0.01, "in then out returns");
assert!(
(zoom_cell(a, -1) - 180.0).abs() < 0.01,
"in then out returns"
);
}
#[test]
@@ -2943,12 +2940,17 @@ mod tests {
// The point of the second class: past 256px a grid thumbnail is being
// upscaled, and the softness shows.
assert_eq!(ThumbSize::for_cell(180), ThumbSize::Grid);
assert_eq!(ThumbSize::for_cell(zoom_cell(225.0, 1) as u32), ThumbSize::Large);
assert_eq!(
ThumbSize::for_cell(zoom_cell(225.0, 1) as u32),
ThumbSize::Large
);
// And zooming back down does not keep paying for it.
assert_eq!(ThumbSize::for_cell(zoom_cell(281.0, -1) as u32), ThumbSize::Grid);
assert_eq!(
ThumbSize::for_cell(zoom_cell(281.0, -1) as u32),
ThumbSize::Grid
);
}
#[test]
fn a_request_key_survives_the_window_moving() {
use dr_thumbs::ThumbSize;