7403f0cfeb
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
202 lines
8.6 KiB
Rust
202 lines
8.6 KiB
Rust
mod captioner;
|
|
mod commands;
|
|
mod db;
|
|
mod embedder;
|
|
mod hnsw_index;
|
|
mod indexer;
|
|
mod media;
|
|
mod storage;
|
|
mod tagger;
|
|
mod thumbnail;
|
|
mod vector;
|
|
|
|
use crate::storage::StorageProfile;
|
|
use tauri::Manager;
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
tauri::Builder::default()
|
|
// Must be the first plugin: a second launch hands its args to the
|
|
// running instance and exits before anything else initializes.
|
|
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.unminimize();
|
|
let _ = window.set_focus();
|
|
}
|
|
}))
|
|
.plugin(
|
|
tauri_plugin_log::Builder::new()
|
|
.targets([
|
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
|
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir {
|
|
file_name: Some("phokus".into()),
|
|
}),
|
|
])
|
|
.level(log::LevelFilter::Info)
|
|
.max_file_size(5 * 1024 * 1024)
|
|
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne)
|
|
.build(),
|
|
)
|
|
.plugin(tauri_plugin_window_state::Builder::new().build())
|
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
.plugin(tauri_plugin_process::init())
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.plugin(tauri_plugin_fs::init())
|
|
.plugin(tauri_plugin_notification::init())
|
|
.setup(|app| {
|
|
let app_dir = app
|
|
.path()
|
|
.app_data_dir()
|
|
.expect("Failed to get app data dir");
|
|
|
|
std::fs::create_dir_all(&app_dir).expect("Failed to create app data dir");
|
|
|
|
// FFmpeg provisioning happens in the background so the window
|
|
// appears immediately; workers gate video jobs on readiness and
|
|
// the onboarding/Settings UI shows progress and retry.
|
|
media::spawn_ffmpeg_provision(app.handle().clone());
|
|
|
|
let db_path = app_dir.join("gallery.db");
|
|
let pool = db::create_pool(&db_path).expect("Failed to create database pool");
|
|
let media_tools = media::MediaTools::resolve();
|
|
|
|
{
|
|
let conn = pool.get().expect("Failed to get connection for migration");
|
|
db::migrate(&conn).expect("Failed to run migrations");
|
|
db::reset_inflight_jobs(&conn).expect("Failed to reset inflight jobs");
|
|
let backfilled =
|
|
db::backfill_embedding_jobs(&conn).expect("Failed to backfill embedding jobs");
|
|
if backfilled > 0 {
|
|
log::info!("Backfilled {backfilled} embedding jobs.");
|
|
}
|
|
let (orphaned_vectors, missing_vectors) = db::repair_embedding_consistency(&conn)
|
|
.expect("Failed to repair embedding consistency");
|
|
if orphaned_vectors > 0 || missing_vectors > 0 {
|
|
log::info!(
|
|
"Repaired embedding consistency: removed {orphaned_vectors} orphaned vectors, requeued {missing_vectors} missing vectors."
|
|
);
|
|
}
|
|
}
|
|
|
|
let thumb_dir = app_dir.join("thumbnails");
|
|
std::fs::create_dir_all(&thumb_dir).expect("Failed to create thumbnail dir");
|
|
|
|
// The asset protocol scope is no longer a blanket "**": thumbnails
|
|
// are allowed statically in tauri.conf.json, and each indexed
|
|
// folder is allowed here (and in add_folder/update_folder_path).
|
|
{
|
|
let scope = app.asset_protocol_scope();
|
|
let conn = pool.get().expect("Failed to get connection for asset scope");
|
|
for folder in db::get_folders(&conn).unwrap_or_default() {
|
|
if let Err(error) = scope.allow_directory(&folder.path, true) {
|
|
log::error!("Failed to allow asset scope for {}: {}", folder.path, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
let thumbnail_worker_count = std::thread::available_parallelism()
|
|
.map(|parallelism| StorageProfile::Balanced.thumbnail_workers(parallelism.get()))
|
|
.unwrap_or(2);
|
|
|
|
for _ in 0..thumbnail_worker_count {
|
|
indexer::start_thumbnail_worker(
|
|
app.handle().clone(),
|
|
pool.clone(),
|
|
media_tools.clone(),
|
|
thumb_dir.clone(),
|
|
);
|
|
}
|
|
indexer::start_metadata_worker(app.handle().clone(), pool.clone(), media_tools.clone());
|
|
indexer::start_embedding_worker(app.handle().clone(), pool.clone());
|
|
// Caption worker disabled — UI removed; keeping backend code intact for future use.
|
|
// indexer::start_caption_worker(app.handle().clone(), pool.clone(), app_dir.clone());
|
|
indexer::start_tagging_worker(app.handle().clone(), pool.clone(), app_dir.clone());
|
|
|
|
let watcher_handle = indexer::start_watcher(app.handle().clone(), pool.clone(), thumb_dir.clone());
|
|
|
|
app.manage(pool);
|
|
app.manage(media_tools);
|
|
app.manage(watcher_handle);
|
|
|
|
Ok(())
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
commands::add_folder,
|
|
commands::get_folders,
|
|
commands::get_background_job_progress,
|
|
commands::remove_folder,
|
|
commands::get_images,
|
|
commands::reindex_folder,
|
|
commands::update_image_details,
|
|
commands::find_similar_images,
|
|
commands::find_similar_by_region,
|
|
commands::debug_similar_images,
|
|
commands::retry_failed_embeddings,
|
|
commands::semantic_search_images,
|
|
commands::search_images_by_tag,
|
|
commands::get_caption_model_status,
|
|
commands::get_caption_acceleration,
|
|
commands::set_caption_acceleration,
|
|
commands::get_caption_detail,
|
|
commands::set_caption_detail,
|
|
commands::prepare_caption_model,
|
|
commands::delete_caption_model,
|
|
commands::probe_caption_runtime,
|
|
commands::probe_caption_image,
|
|
commands::generate_caption_for_image,
|
|
commands::queue_caption_jobs,
|
|
commands::clear_caption_jobs,
|
|
commands::reset_generated_captions,
|
|
commands::set_generated_caption,
|
|
commands::suggest_image_tags,
|
|
commands::set_worker_paused,
|
|
commands::get_worker_states,
|
|
commands::get_tag_cloud,
|
|
commands::get_explore_tags,
|
|
commands::get_images_by_ids,
|
|
commands::get_failed_embedding_images,
|
|
commands::get_tagger_model_status,
|
|
commands::get_tagger_acceleration,
|
|
commands::set_tagger_acceleration,
|
|
commands::probe_tagger_runtime,
|
|
commands::get_tagger_threshold,
|
|
commands::set_tagger_threshold,
|
|
commands::get_tagger_batch_size,
|
|
commands::set_tagger_batch_size,
|
|
commands::prepare_tagger_model,
|
|
commands::delete_tagger_model,
|
|
commands::queue_tagging_jobs,
|
|
commands::clear_tagging_jobs,
|
|
commands::get_image_tags,
|
|
commands::add_user_tag,
|
|
commands::remove_tag,
|
|
commands::search_tags_autocomplete,
|
|
commands::find_duplicates,
|
|
commands::load_duplicate_scan_cache,
|
|
commands::invalidate_duplicate_scan_cache,
|
|
commands::delete_images_from_disk,
|
|
commands::rename_folder,
|
|
commands::update_folder_path,
|
|
commands::get_tagging_queue_scope,
|
|
commands::set_tagging_queue_scope,
|
|
commands::get_tagging_queue_folder_ids,
|
|
commands::set_tagging_queue_folder_ids,
|
|
commands::open_app_data_folder,
|
|
commands::get_database_info,
|
|
commands::vacuum_database,
|
|
commands::get_orphaned_thumbnails_info,
|
|
commands::cleanup_orphaned_thumbnails,
|
|
commands::get_muted_folder_ids,
|
|
commands::set_muted_folder_ids,
|
|
commands::get_ffmpeg_status,
|
|
commands::retry_ffmpeg_download,
|
|
commands::get_onboarding_completed,
|
|
commands::set_onboarding_completed,
|
|
commands::get_notifications_paused,
|
|
commands::set_notifications_paused,
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|