gallery/src/lib.rs
Greg Pomerantz c72b00f7ec M1: instant list paint + streaming first scan
- relaunch: DB list paints immediately ('N files (cached)'), background
  walk refreshes ('Updating… N') — no more 5.5 s blank list on warm start
- first launch: walk_stream() feeds the list in 512-file batches while
  scanning; UI never waits for the full DB build
- skip hidden dirs (.thumbnails/.trash/…): -559 entries on the phone
- db.rs: cfg-gate ANDROID_PACKAGE (dead-code warning on desktop)

Phone (8.5k photos): cold scan 4.2 s with live list; warm relaunch
paints instantly, rescan 4.2 s in background.
2026-10-27 13:45:37 -04:00

133 lines
5.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

slint::include_modules!();
pub mod db;
pub mod scan;
use slint::{ComponentHandle, Model, SharedString, VecModel};
/// Fill the window's list model from `paths` and set the status line.
fn set_list(weak: &slint::Weak<Main>, paths: Vec<String>, status: String) {
let weak = weak.clone();
let _ = slint::invoke_from_event_loop(move || {
let Some(w) = weak.upgrade() else { return };
if let Some(m) = w.get_items().as_any().downcast_ref::<VecModel<SharedString>>() {
m.set_vec(paths.into_iter().map(SharedString::from).collect::<Vec<_>>());
}
w.set_status(status.into());
});
}
/// Set only the status line (never touches the model).
fn set_status(weak: &slint::Weak<Main>, status: String) {
let weak = weak.clone();
let _ = slint::invoke_from_event_loop(move || {
if let Some(w) = weak.upgrade() {
w.set_status(status.into());
}
});
}
/// M1: metadata from sqlite paints the list immediately (first frame < 100 ms
/// per HANDOFF §8); a background walk then refreshes the DB and the list.
/// UI updates are posted to the event loop — the scan thread never touches the
/// window directly. Called from both entry points after `Main::new()`.
pub fn start_scan(window: &Main) {
let model = slint::ModelRc::new(VecModel::<SharedString>::default());
window.set_items(model);
let weak = window.as_weak();
std::thread::spawn(move || {
let mut db = match db::open(&db::db_path()) {
Ok(db) => db,
Err(e) => {
eprintln!("gallery: db error: {e}");
set_list(&weak, Vec::new(), format!("db error: {e}"));
return;
}
};
// 1. Instant first paint from the cached DB (relaunch path).
let cached = db::list_paths(&mut db).unwrap_or_default();
let n0 = cached.len();
let db_warm = n0 > 0;
if db_warm {
set_list(&weak, cached, format!("{n0} files (cached)"));
}
// 2. Background rescan: detect new/changed/removed files.
// Cold start: stream results into the list as they're found, so the
// UI fills in while the scan runs (never waits for the full DB).
// Warm start: keep the cached list, only update the status line.
let t0 = std::time::Instant::now();
let roots = scan::roots();
let entries = scan::walk_stream(&roots, &|batch| {
let n = batch.len();
if db_warm {
set_status(&weak, format!("Updating… {n}"));
} else {
let mut sorted = batch.to_vec();
sorted.sort_by(|a, b| b.mtime.cmp(&a.mtime).then_with(|| a.path.cmp(&b.path)));
set_list(
&weak,
sorted.into_iter().map(|e| e.path).collect(),
format!("Scanning… {n}"),
);
}
});
let (ins, upd, del) = db::apply(&mut db, &entries).unwrap_or_else(|e| {
eprintln!("gallery: db apply error: {e}");
(0, 0, 0)
});
let paths = db::list_paths(&mut db).unwrap_or_default();
let n = paths.len();
let ms = t0.elapsed().as_millis();
log::info!("gallery: scan {n} files ({ins}+ {upd}~ {del}-) in {ms} ms");
set_list(&weak, paths, format!("{n} files ({ins}+ {upd}~ {del}–) in {ms} ms"));
});
}
#[cfg(target_os = "android")]
use std::sync::OnceLock;
/// A clone of `AndroidApp` kept for the app's whole lifetime so the Java side
/// stays reachable from ANY thread after Slint has consumed the original:
/// `run_on_java_main_thread`, `vm_as_ptr()` (JVM), `activity_as_ptr()` (Activity).
/// This is the substrate for the storage-permission flow (HANDOFF.md §7) and,
/// later, anything that needs JNI.
#[cfg(target_os = "android")]
static ANDROID_APP: OnceLock<slint::android::AndroidApp> = OnceLock::new();
#[cfg(target_os = "android")]
pub fn android_app() -> Option<&'static slint::android::AndroidApp> {
ANDROID_APP.get()
}
/// Android entry point.
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
fn android_main(app: slint::android::AndroidApp) {
android_logger::init_once(
android_logger::Config::default().with_max_level(log::LevelFilter::Info),
);
let _ = ANDROID_APP.set(app.clone());
// `init_with_event_listener` gives us the raw MainEvent stream
// (Start/Resume/Pause/Stop/SaveState/ConfigChanged/...) while Slint owns
// the event loop. This is Slint's one first-class Android lifecycle hook:
// M5 re-checks storage permission on Resume (user returned from Settings).
slint::android::init_with_event_listener(app, |event| {
use slint::android::android_activity::{MainEvent, PollEvent};
if let PollEvent::Main(MainEvent::Resume { .. } | MainEvent::Start { .. }) = event {
log::info!("gallery: app resumed");
// TODO(M5): storage::recheck_permission()
}
})
.unwrap();
let main_window = Main::new().unwrap();
start_scan(&main_window);
main_window.run().unwrap();
}