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
, paths: Vec, 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::>() { m.set_vec(paths.into_iter().map(SharedString::from).collect::>()); } w.set_status(status.into()); }); } /// Set only the status line (never touches the model). fn set_status(weak: &slint::Weak
, 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::::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 = 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(); }