feat(onboarding): guided first-run tour with background FFmpeg provisioning

Phase 5 of the 0.1.0 release prep:
- FFmpeg no longer blocks startup (or crashes the app offline): provisioning
  runs in a background thread emitting throttled ffmpeg-progress events,
  with installed/downloading/failed state queryable via get_ffmpeg_status
  and a retry command
- video jobs are invisible to claiming and tier-priority checks until FFmpeg
  is ready, so they wait as pending without failing — and without starving
  image embeddings/tagging (include_videos gating in db.rs)
- 7-step show-don't-tell onboarding wizard: welcome + live FFmpeg progress,
  real first-folder picker, faked animating pipeline bar, placeholder
  gallery tiles, cycling search-syntax demo, views overview, and an AI
  features step with a real opt-in tagger download
- skippable at every point (Escape included), persisted via
  settings/onboarding_completed.txt, re-runnable from Settings > General,
  which also gains an FFmpeg status/retry row
This commit is contained in:
2026-06-12 23:09:05 +01:00
parent 1640e30330
commit 7403f0cfeb
17 changed files with 1139 additions and 30 deletions
+47
View File
@@ -2101,3 +2101,50 @@ pub async fn set_notifications_paused(app: AppHandle, paused: bool) -> Result<()
)
.map_err(|e| e.to_string())
}
#[derive(Serialize)]
pub struct FfmpegStatus {
pub installed: bool,
pub downloading: bool,
pub failed: bool,
}
#[tauri::command]
pub async fn get_ffmpeg_status() -> Result<FfmpegStatus, String> {
Ok(FfmpegStatus {
installed: crate::media::ffmpeg_ready(),
downloading: crate::media::ffmpeg_downloading(),
failed: crate::media::ffmpeg_failed(),
})
}
#[tauri::command]
pub async fn retry_ffmpeg_download(app: AppHandle) -> Result<(), String> {
crate::media::spawn_ffmpeg_provision(app);
Ok(())
}
const ONBOARDING_COMPLETED_FILE: &str = "settings/onboarding_completed.txt";
#[tauri::command]
pub async fn get_onboarding_completed(app: AppHandle) -> Result<bool, String> {
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
let path = app_dir.join(ONBOARDING_COMPLETED_FILE);
if !path.exists() {
return Ok(false);
}
let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
Ok(content.trim() == "true")
}
#[tauri::command]
pub async fn set_onboarding_completed(app: AppHandle, completed: bool) -> Result<(), String> {
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
let settings_dir = app_dir.join("settings");
std::fs::create_dir_all(&settings_dir).map_err(|e| e.to_string())?;
std::fs::write(
settings_dir.join("onboarding_completed.txt"),
if completed { "true" } else { "false" },
)
.map_err(|e| e.to_string())
}