mod ai_tag_filter; mod captioner; mod color; 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| { // Fresh installs open at the fixed config size (1280×800) because the // window-state plugin has nothing saved yet — too tall for laptops // like 1366×768. Clamp the window to the monitor's work area (which // already excludes the taskbar) and re-center it there, but only when // it actually overflows, so a restored size/position on a roomier // display is left untouched. Sizes are physical pixels on both sides, // so this stays correct across display scaling. if let Some(window) = app.get_webview_window("main") { if let Ok(Some(monitor)) = window.current_monitor() { let area = monitor.work_area(); let max_w = area.size.width.saturating_sub(32); let max_h = area.size.height.saturating_sub(32); if let Ok(size) = window.outer_size() { if size.width > max_w || size.height > max_h { let new_w = size.width.min(max_w); let new_h = size.height.min(max_h); let _ = window.set_size(tauri::PhysicalSize::new(new_w, new_h)); let _ = window.set_position(tauri::PhysicalPosition::new( area.position.x + (area.size.width as i32 - new_w as i32) / 2, area.position.y + (area.size.height as i32 - new_h as i32) / 2, )); } } } } 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/AVIF 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 repaired_deferred = db::repair_deferred_embedding_jobs(&conn) .expect("Failed to repair deferred embedding jobs"); if repaired_deferred > 0 { log::info!("Requeued {repaired_deferred} deferred embedding jobs."); } let repaired_avif = db::repair_avif_jobs(&conn).expect("Failed to repair AVIF jobs"); if repaired_avif > 0 { log::info!("Requeued {repaired_avif} AVIF 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"); commands::restore_persisted_worker_pauses(&app_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()); // Backfill color palettes for images indexed before color search existed. indexer::start_color_backfill(app.handle().clone(), pool.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::add_folders, commands::list_directories, commands::get_folders, commands::reorder_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_worker_pauses_persist, commands::set_worker_pauses_persist, commands::get_visual_clusters, commands::get_explore_tags, commands::get_related_tags, commands::get_images_by_ids, commands::get_failed_embedding_images, commands::get_failed_tagging_images, commands::get_tagger_model_status, commands::get_tagger_acceleration, commands::set_tagger_acceleration, commands::get_tagger_model, commands::set_tagger_model, 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::rename_tag, commands::delete_tag, commands::get_image_exif, commands::list_albums, commands::create_album, commands::rename_album, commands::delete_album, commands::delete_albums, commands::reorder_albums, commands::add_images_to_album, commands::remove_images_from_album, commands::get_album_images, commands::bulk_update_details, commands::bulk_add_tags, commands::bulk_remove_tag, commands::get_build_variant, 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::open_map_location, commands::open_changelog_url, commands::get_database_info, commands::vacuum_database, commands::rebuild_semantic_index, 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_last_seen_version, commands::set_last_seen_version, commands::get_notifications_paused, commands::set_notifications_paused, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }