mod auth; mod commands; mod connectivity; mod credentials; mod download; mod jellyfin; mod playback_mode; mod playback_reporting; mod player; mod repository; mod session_poller; pub mod settings; mod storage; mod thumbnail; pub mod utils; use std::sync::{Arc, Mutex}; use tokio::sync::Mutex as TokioMutex; use tauri::{Emitter, Manager}; use tauri_specta::Builder; use log::{error, info}; #[cfg(target_os = "android")] use log::warn; use commands::{ cancel_download, clear_stale_downloads, delete_album_downloads, delete_all_downloads, delete_download, download_album, download_item, download_item_and_start, download_video, download_series, download_season, get_download_storage_stats, get_downloads, get_download_manager_stats, set_max_concurrent_downloads, get_smart_cache_stats, update_smart_cache_config, get_smart_cache_config, get_album_recommendations, get_album_affinity_status, mark_download_completed, mark_download_failed, start_download, pin_item, unpin_item, is_item_pinned, offline_get_items, offline_is_available, offline_search, pause_download, resume_download, player_cycle_repeat, player_get_audio_settings, player_get_queue, player_get_status, player_get_video_settings, player_next, player_pause, player_play, player_play_album_track, player_play_item, player_play_queue, player_play_tracks, player_previous, player_seek, player_seek_video, player_set_audio_settings, player_set_audio_track, player_switch_audio_track, player_set_subtitle_track, player_set_video_settings, player_set_volume, player_toggle_mute, player_stop, player_toggle, player_toggle_shuffle, // Sleep timer and autoplay commands player_set_sleep_timer, player_cancel_sleep_timer, player_get_sleep_timer, player_get_autoplay_settings, player_set_autoplay_settings, player_cancel_autoplay_countdown, player_play_next_episode, player_on_playback_ended, // Queue manipulation commands player_add_to_queue, player_add_track_by_id, player_add_tracks_by_ids, player_remove_from_queue, player_move_in_queue, player_skip_to, // Preload commands player_preload_upcoming, player_set_cache_config, player_get_cache_config, // Jellyfin reporting commands player_configure_jellyfin, player_disable_jellyfin, // Session management commands player_get_session, player_dismiss_session, // Remote session control commands remote_play_on_session, remote_send_command, remote_session_seek, remote_session_set_volume, remote_session_toggle_mute, // Session polling commands sessions_set_polling_hint, sessions_poll_now, SessionPollerWrapper, // Playback mode commands playback_mode_get_current, playback_mode_set, playback_mode_is_transferring, playback_mode_transfer_to_remote, playback_mode_transfer_to_local, playback_mode_get_remote_status, // Playback reporting commands playback_reporter_init, playback_reporter_destroy, playback_report_start, playback_report_progress, playback_report_stopped, playback_mark_played, PlaybackReporterWrapper, // Auth commands auth_initialize, auth_connect_to_server, auth_login, auth_verify_session, auth_logout, auth_get_session, auth_set_session, auth_start_verification, auth_stop_verification, auth_reauthenticate, // Device commands device_get_id, device_set_id, // Connectivity commands connectivity_check_server, connectivity_set_server_url, connectivity_get_status, connectivity_start_monitoring, connectivity_stop_monitoring, connectivity_mark_reachable, connectivity_mark_unreachable, // Storage commands storage_delete_server, storage_delete_user, storage_get_access_token, storage_get_active_session, storage_get_active_user, storage_get_path, storage_get_playback_progress, storage_get_security_status, storage_get_servers, storage_get_size, storage_get_users, storage_init, storage_mark_played, storage_mark_synced, storage_save_server, storage_save_user, storage_set_active_user, storage_toggle_favorite, storage_update_playback_progress, storage_update_playback_context, // Offline cache commands storage_get_libraries, storage_get_items, storage_get_item, storage_search_items, storage_save_library, storage_save_item, storage_get_pending_sync_count, // Sync queue commands sync_queue_mutation, sync_get_pending, sync_mark_processing, sync_mark_completed, sync_mark_failed, sync_get_pending_count, sync_cleanup_completed, sync_clear_user, // Thumbnail cache and image commands thumbnail_get_cached, thumbnail_save, thumbnail_get_stats, thumbnail_set_limit, thumbnail_clear_cache, thumbnail_delete_item, image_get_url, // People cache commands storage_save_person, storage_get_person, storage_save_item_people, storage_get_item_people, // Series audio preferences storage_save_series_audio_preference, storage_get_series_audio_preference, // Repository commands repository_create, repository_destroy, repository_get_libraries, repository_get_items, repository_get_item, repository_get_latest_items, repository_get_resume_items, repository_get_next_up_episodes, repository_get_recently_played_audio, repository_get_resume_movies, repository_get_genres, repository_search, repository_get_playback_info, repository_get_video_stream_url, repository_get_audio_stream_url, repository_report_playback_start, repository_report_playback_progress, repository_report_playback_stopped, repository_get_image_url, repository_mark_favorite, repository_unmark_favorite, repository_get_person, repository_get_items_by_person, repository_get_similar_items, // Playlist commands playlist_create, playlist_delete, playlist_rename, playlist_get_items, playlist_add_items, playlist_remove_items, playlist_move_item, // Conversion commands format_time_seconds, format_time_seconds_long, convert_ticks_to_seconds, calc_progress, convert_percent_to_volume, AuthManagerWrapper, SessionVerifierWrapper, ConnectivityMonitorWrapper, CredentialStoreWrapper, DatabaseWrapper, PlayerStateWrapper, MediaSessionManagerWrapper, VideoSettingsWrapper, ThumbnailCacheWrapper, SmartCacheWrapper, PlaybackModeManagerWrapper, RepositoryManagerWrapper, DownloadManagerWrapper, }; #[cfg(target_os = "android")] use playback_mode::PlaybackModeManager; use auth::AuthManager; use connectivity::ConnectivityMonitor; use credentials::CredentialStore; use download::cache::{CacheConfig as SmartCacheConfig, SmartCache}; use download::DownloadManager; use jellyfin::{HttpClient, HttpConfig}; use player::{MediaSessionManager, PlayerBackend, PlayerController, TauriEventEmitter}; // NullBackend is used both for platforms without a native backend AND as a graceful // fallback when a native backend (MPV/ExoPlayer) fails to initialize, so the app can // still launch (browse library, manage downloads, see an error) instead of crashing. use player::NullBackend; #[cfg(target_os = "linux")] use player::MpvBackend; use settings::VideoSettings; use storage::Database; use thumbnail::{ThumbnailCache, CacheConfig as ThumbnailCacheConfig}; #[cfg(target_os = "android")] use credentials::initialize_secure_storage; #[cfg(target_os = "android")] use player::ExoPlayerBackend; #[cfg(target_os = "android")] use player::{MediaCommandHandler, RemoteVolumeHandler, set_media_command_handler, set_remote_volume_handler}; /// Handler for media commands from Android MediaSession (lockscreen/notification controls). /// /// Routes commands from the system media controls back to the PlayerController. #[cfg(target_os = "android")] struct MediaSessionHandler { player: Arc>, } #[cfg(target_os = "android")] impl MediaCommandHandler for MediaSessionHandler { fn on_command(&self, command: &str) { // Use blocking_lock since this is called from a non-async JNI callback let controller = self.player.blocking_lock(); match command { "play" => { if let Err(e) = controller.play() { error!("[MediaSession] Play failed: {}", e); } } "pause" => { if let Err(e) = controller.pause() { error!("[MediaSession] Pause failed: {}", e); } } "next" => { if let Err(e) = controller.next() { error!("[MediaSession] Next failed: {}", e); } } "previous" => { if let Err(e) = controller.previous() { error!("[MediaSession] Previous failed: {}", e); } } "stop" => { if let Err(e) = controller.stop() { error!("[MediaSession] Stop failed: {}", e); } } cmd if cmd.starts_with("seek:") => { if let Ok(pos) = cmd[5..].parse::() { if let Err(e) = controller.seek(pos) { error!("[MediaSession] Seek failed: {}", e); } } } _ => { warn!("[MediaSession] Unknown command: {}", command); } } } } /// Handler for remote volume changes from Android volume buttons when in remote playback mode. /// /// Routes volume commands to the Jellyfin session via the playback mode manager. #[cfg(target_os = "android")] struct RemoteVolumeSessionHandler { playback_mode: Arc, } #[cfg(target_os = "android")] impl RemoteVolumeHandler for RemoteVolumeSessionHandler { fn on_remote_volume_change(&self, command: &str, volume: i32) { log::info!("[RemoteVolume] Command: {}, Volume: {}", command, volume); // Send the volume command to the remote session asynchronously let playback_mode = Arc::clone(&self.playback_mode); let command_str = command.to_string(); // Use tauri::async_runtime::spawn instead of tokio::spawn // JNI callbacks happen on arbitrary threads without a Tokio runtime log::info!("[RemoteVolume] Spawning async task to send volume command..."); tauri::async_runtime::spawn(async move { log::info!("[RemoteVolume] Async task started, calling send_remote_volume_command..."); match playback_mode.send_remote_volume_command(&command_str, volume).await { Ok(_) => log::info!("[RemoteVolume] Volume command completed successfully"), Err(e) => log::error!("[RemoteVolume] Failed to send volume command: {}", e), } log::info!("[RemoteVolume] Async task completed"); }); log::info!("[RemoteVolume] Async task spawned, returning from JNI callback"); } } /// Payload emitted to the frontend when a native player backend fails to /// initialize and the app falls back to a no-op backend. #[derive(Clone, serde::Serialize)] struct BackendInitError { platform: &'static str, backend: &'static str, message: String, } /// Log a backend-initialization failure and notify the frontend, so the UI can /// surface "playback unavailable" instead of the app hard-crashing. fn emit_backend_init_failed(app_handle: &tauri::AppHandle, backend: &'static str, message: String) { error!( "[INIT] Player backend '{}' failed to initialize: {}. Falling back to NullBackend (playback disabled).", backend, message ); let _ = app_handle.emit( "backend-init-failed", BackendInitError { platform: std::env::consts::OS, backend, message, }, ); } /// Create the appropriate player backend for the current platform. fn create_player_backend( app_handle: tauri::AppHandle, playback_reporter: Arc>>, position_throttler: Arc, ) -> Box { let _event_emitter = Arc::new(TauriEventEmitter::new(app_handle.clone())); #[cfg(target_os = "android")] { info!("Android platform detected - initializing ExoPlayer backend"); // Get the Android context via ndk-context let ctx = ndk_context::android_context(); // Get JavaVM and create JNI environment let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }; match vm { Ok(java_vm) => { match java_vm.attach_current_thread() { Ok(mut env) => { let context_obj = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) }; match ExoPlayerBackend::new(&mut env, &context_obj, _event_emitter.clone(), playback_reporter.clone(), position_throttler.clone()) { Ok(backend) => { info!("Successfully initialized ExoPlayer backend for Android"); return Box::new(backend); } Err(e) => { // Degrade gracefully instead of crashing the app. emit_backend_init_failed(&app_handle, "exoplayer", e.to_string()); return Box::new(NullBackend::new()); } } } Err(e) => { emit_backend_init_failed(&app_handle, "exoplayer", format!("attach JNI thread failed: {}", e)); return Box::new(NullBackend::new()); } } } Err(e) => { emit_backend_init_failed(&app_handle, "exoplayer", format!("create JavaVM failed: {}", e)); return Box::new(NullBackend::new()); } } } // For Linux, use MPV backend for audio playback #[cfg(target_os = "linux")] { info!("Linux platform detected - initializing MPV backend for audio"); match MpvBackend::new(Some(_event_emitter), playback_reporter, position_throttler) { Ok(backend) => { info!("Successfully initialized MPV backend for Linux"); return Box::new(backend); } Err(e) => { error!("\n========================================"); error!("FATAL ERROR: Failed to initialize MPV backend"); error!("========================================"); error!("Error: {}", e); error!("\nCommon causes:"); error!(" 1. MPV is not installed"); error!(" Solution: Install MPV using your package manager"); error!(" - Arch/CachyOS: sudo pacman -S mpv"); error!(" - Ubuntu/Debian: sudo apt install mpv libmpv-dev"); error!(" - Fedora: sudo dnf install mpv mpv-libs-devel"); error!("\n 2. MPV version mismatch (app was built with different libmpv version)"); error!(" Solution: Rebuild the application"); error!(" - cd src-tauri && cargo clean && cargo build --release"); error!("\n 3. Audio system not working"); error!(" Solution: Verify audio works with: pactl info"); error!("\nAudio playback will NOT work until this is fixed."); error!("========================================\n"); // Degrade gracefully: launch with a no-op backend so the user can // still browse the library and manage downloads, and the frontend // can show a "playback unavailable" notice via this event. emit_backend_init_failed(&app_handle, "mpv", e.to_string()); return Box::new(NullBackend::new()); } } } // Fallback for other platforms #[cfg(not(any(target_os = "linux", target_os = "android")))] { warn!("WARNING: No audio backend available for this platform"); Box::new(NullBackend::new()) } } /// Construct the tauri-specta command builder. Shared by `run()` and the /// bindings-export test so the TypeScript bindings always match the handler. fn specta_builder() -> Builder { Builder::::new() .commands(tauri_specta::collect_commands![ // Player commands player_play_item, player_play_queue, player_play_album_track, player_play_tracks, player_play, player_pause, player_toggle, player_stop, player_next, player_previous, player_seek, player_seek_video, player_set_volume, player_toggle_mute, player_set_audio_track, player_switch_audio_track, player_set_subtitle_track, player_toggle_shuffle, player_cycle_repeat, player_get_status, player_get_queue, player_add_to_queue, player_add_track_by_id, player_add_tracks_by_ids, player_remove_from_queue, player_move_in_queue, player_skip_to, player_set_audio_settings, player_get_audio_settings, player_set_video_settings, player_get_video_settings, // Sleep timer and autoplay commands player_set_sleep_timer, player_cancel_sleep_timer, player_get_sleep_timer, player_get_autoplay_settings, player_set_autoplay_settings, player_cancel_autoplay_countdown, player_play_next_episode, player_on_playback_ended, // Preload commands player_preload_upcoming, player_set_cache_config, player_get_cache_config, // Jellyfin reporting commands player_configure_jellyfin, player_disable_jellyfin, // Session management commands player_get_session, player_dismiss_session, // Remote session control commands remote_play_on_session, remote_send_command, remote_session_seek, remote_session_set_volume, remote_session_toggle_mute, // Session polling commands sessions_set_polling_hint, sessions_poll_now, // Playback mode commands playback_mode_get_current, playback_mode_set, playback_mode_is_transferring, playback_mode_transfer_to_remote, playback_mode_get_remote_status, playback_mode_transfer_to_local, // Playback reporting commands playback_reporter_init, playback_reporter_destroy, playback_report_start, playback_report_progress, playback_report_stopped, playback_mark_played, // Auth commands auth_initialize, auth_connect_to_server, auth_login, auth_verify_session, auth_logout, auth_get_session, auth_set_session, auth_start_verification, auth_stop_verification, auth_reauthenticate, // Device commands device_get_id, device_set_id, // Connectivity commands connectivity_check_server, connectivity_set_server_url, connectivity_get_status, connectivity_start_monitoring, connectivity_stop_monitoring, connectivity_mark_reachable, connectivity_mark_unreachable, // Storage commands storage_init, storage_get_path, storage_get_size, storage_get_security_status, storage_save_server, storage_get_servers, storage_delete_server, storage_save_user, storage_get_users, storage_set_active_user, storage_get_active_user, storage_get_active_session, storage_get_access_token, storage_delete_user, // Playback progress commands storage_update_playback_progress, storage_update_playback_context, storage_mark_played, storage_get_playback_progress, storage_mark_synced, storage_toggle_favorite, // Download commands download_item, download_item_and_start, download_album, download_video, download_series, download_season, get_downloads, pause_download, resume_download, cancel_download, delete_download, delete_all_downloads, delete_album_downloads, clear_stale_downloads, get_download_storage_stats, mark_download_completed, mark_download_failed, start_download, get_download_manager_stats, set_max_concurrent_downloads, get_smart_cache_stats, update_smart_cache_config, get_smart_cache_config, get_album_recommendations, get_album_affinity_status, // Pinning commands pin_item, unpin_item, is_item_pinned, // Offline commands offline_is_available, offline_get_items, offline_search, // Offline cache commands storage_get_libraries, storage_get_items, storage_get_item, storage_search_items, storage_save_library, storage_save_item, storage_get_pending_sync_count, // Sync queue commands sync_queue_mutation, sync_get_pending, sync_mark_processing, sync_mark_completed, sync_mark_failed, sync_get_pending_count, sync_cleanup_completed, sync_clear_user, // Thumbnail cache and image commands thumbnail_get_cached, thumbnail_save, thumbnail_get_stats, thumbnail_set_limit, thumbnail_clear_cache, thumbnail_delete_item, image_get_url, // People cache commands storage_save_person, storage_get_person, storage_save_item_people, storage_get_item_people, // Series audio preferences storage_save_series_audio_preference, storage_get_series_audio_preference, // Repository commands repository_create, repository_destroy, repository_get_libraries, repository_get_items, repository_get_item, repository_get_latest_items, repository_get_resume_items, repository_get_next_up_episodes, repository_get_recently_played_audio, repository_get_resume_movies, repository_get_genres, repository_search, repository_get_playback_info, repository_get_video_stream_url, repository_get_audio_stream_url, repository_report_playback_start, repository_report_playback_progress, repository_report_playback_stopped, repository_get_image_url, repository_mark_favorite, repository_unmark_favorite, repository_get_person, repository_get_items_by_person, repository_get_similar_items, // Playlist commands playlist_create, playlist_delete, playlist_rename, playlist_get_items, playlist_add_items, playlist_remove_items, playlist_move_item, // Conversion commands format_time_seconds, format_time_seconds_long, convert_ticks_to_seconds, calc_progress, convert_percent_to_volume, ]) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { // Initialize logger env_logger::Builder::from_default_env() .filter_level(log::LevelFilter::Info) .init(); // NOTE: TypeScript bindings are generated by the `export_typescript_bindings` // test (`cargo test export_typescript_bindings`), NOT at runtime. Calling // `.export()` here would try to write `../src/lib/api/bindings.ts` at app // startup, which panics on devices (e.g. Android) where that path doesn't exist. let builder = specta_builder(); tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_os::init()) .setup(|app| { // Initialize database with proper app data directory // Check for test mode environment variable first let db_path = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") { let test_path = std::path::PathBuf::from(test_data_dir); info!("[INIT] Using test data directory: {:?}", test_path); test_path.join("jellytau.db") } else { app .path() .app_data_dir() .expect("Failed to get app data directory") .join("jellytau.db") }; info!("[INIT] Initializing database at: {:?}", db_path); // Create the directory if it doesn't exist if let Some(parent) = db_path.parent() { info!("[INIT] Creating database directory: {:?}", parent); match std::fs::create_dir_all(parent) { Ok(_) => info!("[INIT] Database directory ready"), Err(e) => { error!("[INIT ERROR] Failed to create database directory: {}", e); panic!("Failed to create database directory: {}", e); } } } info!("[INIT] Opening database..."); let database = match Database::open(&db_path) { Ok(db) => { info!("[INIT] Database initialized successfully"); db } Err(e) => { error!("[INIT ERROR] Failed to initialize database: {}", e); panic!("Failed to initialize database: {}", e); } }; let db_wrapper = DatabaseWrapper(Mutex::new(database)); app.manage(db_wrapper); // On Android, initialize SecureStorage BEFORE creating CredentialStore #[cfg(target_os = "android")] { info!("[INIT] Initializing Android SecureStorage for credentials..."); let ctx = ndk_context::android_context(); let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }; match vm { Ok(java_vm) => { match java_vm.attach_current_thread() { Ok(mut env) => { let context_obj = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) }; match initialize_secure_storage(&mut env, &context_obj) { Ok(()) => { info!("[INIT] Android SecureStorage initialized successfully"); } Err(e) => { warn!("[INIT WARNING] Failed to initialize SecureStorage: {}. Credentials will use encrypted file fallback.", e); } } } Err(e) => { warn!("[INIT WARNING] Failed to attach JNI thread: {}. Credentials will use encrypted file fallback.", e); } } } Err(e) => { warn!("[INIT WARNING] Failed to create JavaVM: {}. Credentials will use encrypted file fallback.", e); } } } // Initialize credential store (keyring with encrypted file fallback) info!("[INIT] Initializing credential store..."); let credential_store = CredentialStore::new(); let creds_wrapper = CredentialStoreWrapper(Mutex::new(credential_store)); app.manage(creds_wrapper); // Create shared reporter and throttler Arc wrappers before backend/controller info!("[INIT] Creating shared playback reporting infrastructure..."); let playback_reporter = Arc::new(tokio::sync::Mutex::new(None)); let position_throttler = Arc::new(playback_reporting::EventThrottler::new()); // Create player backend with access to AppHandle for event emission info!("[INIT] Creating player backend..."); let backend = create_player_backend( app.handle().clone(), playback_reporter.clone(), position_throttler.clone(), ); let player_controller = PlayerController::new( backend, playback_reporter.clone(), position_throttler.clone(), ); // Wire up event emitter for sleep timer and autoplay notifications let event_emitter = Arc::new(TauriEventEmitter::new(app.handle().clone())); player_controller.set_event_emitter(event_emitter.clone()); let player_arc = Arc::new(TokioMutex::new(player_controller)); // On Android, set up the MediaSession handler for lockscreen controls #[cfg(target_os = "android")] { info!("[INIT] Setting up MediaSession handler for lockscreen controls..."); let handler = Arc::new(MediaSessionHandler { player: player_arc.clone(), }); set_media_command_handler(handler); // Register player controller for autoplay decisions player::android::set_player_controller(player_arc.clone()); } let player_state = PlayerStateWrapper(player_arc.clone()); app.manage(player_state); // Initialize media session manager info!("[INIT] Initializing media session manager..."); let session_manager = MediaSessionManager::new(); let session_wrapper = MediaSessionManagerWrapper(Mutex::new(session_manager)); app.manage(session_wrapper); // Initialize playback mode manager info!("[INIT] Initializing playback mode manager..."); let jellyfin_client = { let player = player_arc.blocking_lock(); player.jellyfin_client() }; let playback_mode_manager = playback_mode::PlaybackModeManager::new( jellyfin_client.clone(), player_arc.clone(), ); let playback_mode_arc = Arc::new(playback_mode_manager); let playback_mode_wrapper = PlaybackModeManagerWrapper(playback_mode_arc.clone()); app.manage(playback_mode_wrapper); // Initialize session poller manager for remote session polling info!("[INIT] Initializing session poller manager..."); let session_poller = session_poller::SessionPollerManager::new( jellyfin_client, playback_mode_arc.clone(), ); session_poller.set_event_emitter(event_emitter.clone()); session_poller.start(); let session_poller_arc = Arc::new(session_poller); let session_poller_wrapper = SessionPollerWrapper(session_poller_arc); app.manage(session_poller_wrapper); // On Android, set up remote volume handler for volume button intercept in remote mode #[cfg(target_os = "android")] { info!("[INIT] Setting up remote volume handler for Android..."); let handler = Arc::new(RemoteVolumeSessionHandler { playback_mode: playback_mode_arc.clone(), }); set_remote_volume_handler(handler); } // Initialize video settings with defaults let video_settings = VideoSettingsWrapper(Mutex::new(VideoSettings::default())); app.manage(video_settings); // Initialize thumbnail cache info!("[INIT] Initializing thumbnail cache..."); let app_data_dir = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") { std::path::PathBuf::from(test_data_dir) } else { app .path() .app_data_dir() .expect("Failed to get app data directory") }; let thumbnail_cache = ThumbnailCache::new(app_data_dir.clone(), ThumbnailCacheConfig::default()); let thumbnail_wrapper = ThumbnailCacheWrapper(Arc::new(thumbnail_cache)); app.manage(thumbnail_wrapper); // Initialize smart cache for preloading info!("[INIT] Initializing smart cache..."); let smart_cache = SmartCache::new(SmartCacheConfig::default()); let smart_cache_wrapper = SmartCacheWrapper(Mutex::new(smart_cache)); app.manage(smart_cache_wrapper); // Initialize download manager info!("[INIT] Initializing download manager..."); let download_dir = app_data_dir.join("downloads"); let download_manager = DownloadManager::new(download_dir); let download_manager_wrapper = DownloadManagerWrapper(Mutex::new(download_manager)); app.manage(download_manager_wrapper); // Initialize connectivity monitor info!("[INIT] Initializing connectivity monitor..."); let http_config = HttpConfig::default(); let http_client = HttpClient::new(http_config) .expect("Failed to create HTTP client"); let mut connectivity_monitor = ConnectivityMonitor::new(http_client); connectivity_monitor.set_app_handle(app.handle().clone()); // Wrap in Arc for sharing with AuthManager let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor)); let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone()); app.manage(connectivity_wrapper); // Initialize auth manager info!("[INIT] Initializing auth manager..."); let auth_http_config = HttpConfig::default(); let auth_http_client = HttpClient::new(auth_http_config) .expect("Failed to create HTTP client for auth"); let mut auth_manager = AuthManager::new(auth_http_client); // Give auth manager a reference to connectivity monitor auth_manager.set_connectivity_monitor(connectivity_arc.clone()); let auth_manager_wrapper = AuthManagerWrapper(Arc::new(auth_manager)); app.manage(auth_manager_wrapper); // Initialize session verifier wrapper (initially empty) info!("[INIT] Initializing session verifier wrapper..."); let session_verifier_wrapper = SessionVerifierWrapper(Arc::new(tokio::sync::Mutex::new(None))); app.manage(session_verifier_wrapper); // Initialize repository manager info!("[INIT] Initializing repository manager..."); let repository_manager = commands::RepositoryManager::new(); let repository_manager_wrapper = RepositoryManagerWrapper(repository_manager); app.manage(repository_manager_wrapper); // Initialize playback reporter wrapper (initially empty, set on login) info!("[INIT] Initializing playback reporter wrapper..."); let playback_reporter_wrapper = PlaybackReporterWrapper(Arc::new(tokio::sync::Mutex::new(None))); app.manage(playback_reporter_wrapper); info!("[INIT] Application setup completed successfully"); Ok(()) }) .invoke_handler(builder.invoke_handler()) .run(tauri::generate_context!()) .expect("error while running tauri application"); } #[cfg(test)] mod specta_bindings { /// Generates `src/lib/api/bindings.ts`. Run with `cargo test export_typescript_bindings`. #[test] fn export_typescript_bindings() { super::specta_builder() .export( specta_typescript::Typescript::default().bigint(specta_typescript::BigIntExportBehavior::Number), "../src/lib/api/bindings.ts", ) .expect("failed to export typescript bindings"); } }