diff --git a/src-tauri/src/commands/app_data.rs b/src-tauri/src/commands/app_data.rs index fe3e0ce..1d29adc 100644 --- a/src-tauri/src/commands/app_data.rs +++ b/src-tauri/src/commands/app_data.rs @@ -1,18 +1,18 @@ use crate::{ lock_r, models::app_data::AppData, - state::{init_app_data, FDOLL}, + state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}, }; #[tauri::command] pub fn get_app_data() -> Result { let guard = lock_r!(FDOLL); - Ok(guard.ui.app_data.clone()) + Ok(guard.user_data.clone()) } #[tauri::command] pub async fn refresh_app_data() -> Result { - init_app_data().await; + init_app_data_scoped(AppDataRefreshScope::All).await; let guard = lock_r!(FDOLL); - Ok(guard.ui.app_data.clone()) + Ok(guard.user_data.clone()) } diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs index f724bad..006b67f 100644 --- a/src-tauri/src/commands/auth.rs +++ b/src-tauri/src/commands/auth.rs @@ -1,7 +1,7 @@ use tauri; use tracing; -use crate::init::lifecycle; +use crate::{init::lifecycle::construct_user_session, services::scene::close_splash_window}; #[tauri::command] pub async fn logout_and_restart() -> Result<(), String> { @@ -16,11 +16,11 @@ pub fn start_auth_flow() -> Result<(), String> { crate::services::auth::cancel_auth_flow(); crate::services::auth::init_auth_code_retrieval(|| { - tracing::info!("Authentication successful, creating scene..."); - // Close welcome window if it's still open + tracing::info!("Authentication successful, constructing user session..."); crate::services::welcome::close_welcome_window(); tauri::async_runtime::spawn(async { - lifecycle::handle_authentication_flow().await; + construct_user_session().await; + close_splash_window(); }); }) .map_err(|e| e.to_string()) diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 8d75da1..8e238b5 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -8,8 +8,8 @@ pub mod interaction; pub mod sprite; pub mod user_status; -use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}; use crate::lock_r; +use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}; use tauri::async_runtime; /// Helper to execute a mutation operation and refresh app data scopes in the background. @@ -33,18 +33,21 @@ pub async fn refresh_app_data(scopes: &[AppDataRefreshScope]) { } /// Helper to execute a mutation operation with conditional refresh. -/// +/// /// # Example /// ```ignore /// pub async fn delete_doll(id: String) -> Result<(), String> { /// let result = DollsRemote::new().delete_doll(&id).await.map_err(|e| e.to_string())?; /// let is_active = is_active_doll(&id); -/// refresh_app_data_conditionally(&[AppDataRefreshScope::Dolls], +/// refresh_app_data_conditionally(&[AppDataRefreshScope::Dolls], /// is_active.then_some(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends])); /// Ok(result) /// } /// ``` -pub async fn refresh_app_data_conditionally(base_scopes: &[AppDataRefreshScope], conditional_scopes: Option<&[AppDataRefreshScope]>) { +pub async fn refresh_app_data_conditionally( + base_scopes: &[AppDataRefreshScope], + conditional_scopes: Option<&[AppDataRefreshScope]>, +) { let mut all_scopes = base_scopes.to_vec(); if let Some(extra_scopes) = conditional_scopes { all_scopes.extend_from_slice(extra_scopes); @@ -57,8 +60,7 @@ pub async fn refresh_app_data_conditionally(base_scopes: &[AppDataRefreshScope], pub fn is_active_doll(doll_id: &str) -> bool { let guard = lock_r!(FDOLL); guard - .ui - .app_data + .user_data .user .as_ref() .and_then(|u| u.active_doll_id.as_ref()) diff --git a/src-tauri/src/init/lifecycle.rs b/src-tauri/src/init/lifecycle.rs index 7bbf301..78f1925 100644 --- a/src-tauri/src/init/lifecycle.rs +++ b/src-tauri/src/init/lifecycle.rs @@ -1,59 +1,58 @@ use reqwest::StatusCode; use std::time::Duration; use tokio::time::sleep; -use tracing::{info, warn}; +use tracing::warn; use crate::{ - init::startup::{initialize_app_data_and_connections, transition_to_main_interface}, - lock_w, models::health::HealthError, remotes::health::HealthRemote, services::{ - active_app::init_active_app_changes_listener, - auth::get_tokens, - health_manager::show_health_manager_with_error, - scene::close_splash_window, - welcome::open_welcome_window, + close_all_windows, + health_manager::open_health_manager_window, + scene::open_scene_window, + ws::client::{clear_ws_client, establish_websocket_connection}, }, - state::FDOLL, - system_tray::{init_system_tray, update_system_tray}, + state::{clear_app_data, init_app_data_scoped, AppDataRefreshScope}, + system_tray::update_system_tray, }; -/// Initializes and starts the core app lifecycle after initial setup. -/// -/// This function handles: -/// - System tray initialization and storage in app state -/// - Active app change listener setup -/// - Startup sequence execution with error handling -/// -/// # Errors -/// If the startup sequence fails, displays a health manager dialog -/// with the error details. -/// -/// # Example -/// ``` -/// // Called automatically during app setup in initialize_app_environment() -/// lifecycle::launch_core_services().await; -/// ``` -pub async fn launch_core_services() { - let tray = init_system_tray(); - { - let mut guard = lock_w!(FDOLL); - guard.tray = Some(tray); - } - - // Begin listening for foreground app changes - init_active_app_changes_listener(); - - if let Err(err) = validate_environment_and_start_app().await { - tracing::warn!("Startup sequence encountered an error: {}", err); - show_health_manager_with_error(Some(err.to_string())); - } +/// Connects the user profile and opens the scene window. +pub async fn construct_user_session() { + connect_user_profile().await; + open_scene_window(); + update_system_tray(true); } -/// Perform checks for environment, network condition -/// and handle situations where startup would not be appropriate. -pub async fn validate_environment_and_start_app() -> Result<(), HealthError> { +/// Disconnects the user profile and closes the scene window. +pub async fn destruct_user_session() { + disconnect_user_profile().await; + close_all_windows(); + update_system_tray(false); +} + +/// Initializes the user profile and establishes a WebSocket connection. +async fn connect_user_profile() { + init_app_data_scoped(AppDataRefreshScope::All).await; + establish_websocket_connection().await; +} + +/// Clears the user profile and WebSocket connection. +async fn disconnect_user_profile() { + clear_app_data(); + clear_ws_client().await; +} + +/// Destructs the user session and show health manager window +/// with error message, offering troubleshooting options. +pub async fn handle_disasterous_failure(error_message: Option) { + destruct_user_session().await; + open_health_manager_window(error_message); +} + +/// Pings the server's health endpoint a maximum of +/// three times with a backoff of 500ms between +/// attempts. Return health error if no success. +pub async fn validate_server_health() -> Result<(), HealthError> { let health_remote = HealthRemote::try_new()?; // simple retry loop to smooth transient network issues @@ -63,7 +62,6 @@ pub async fn validate_environment_and_start_app() -> Result<(), HealthError> { for attempt in 1..=MAX_ATTEMPTS { match health_remote.get_health().await { Ok(_) => { - handle_authentication_flow().await; return Ok(()); } Err(HealthError::NonOkStatus(status)) => { @@ -87,25 +85,9 @@ pub async fn validate_environment_and_start_app() -> Result<(), HealthError> { } } + warn!("Server is unavailable!"); + Err(HealthError::UnexpectedStatus( StatusCode::SERVICE_UNAVAILABLE, )) } - -/// Handles authentication flow: checks for tokens and either restores session or shows welcome. -pub async fn handle_authentication_flow() { - match get_tokens().await { - Some(_tokens) => { - info!("Tokens found in keyring - restoring user session"); - let start = initialize_app_data_and_connections().await; - transition_to_main_interface(start).await; - update_system_tray(true); - } - None => { - info!("No active session found - showing welcome first"); - open_welcome_window(); - close_splash_window(); - update_system_tray(false); - } - } -} diff --git a/src-tauri/src/init/mod.rs b/src-tauri/src/init/mod.rs index ef73da1..4c6e78a 100644 --- a/src-tauri/src/init/mod.rs +++ b/src-tauri/src/init/mod.rs @@ -1,3 +1,41 @@ +use crate::{ + init::{ + lifecycle::{construct_user_session, handle_disasterous_failure, validate_server_health}, + tracing::init_logging, + }, + services::{ + active_app::init_foreground_app_change_listener, + auth::get_session_token, + cursor::init_cursor_tracking, + scene::{close_splash_window, open_splash_window}, + welcome::open_welcome_window, + }, + state::init_app_state, + system_tray::init_system_tray, +}; + pub mod lifecycle; -pub mod startup; pub mod tracing; + +/// The very function that handles +/// init and startup of everything. +pub async fn launch_app() { + init_logging(); + open_splash_window(); + init_app_state(); + init_system_tray(); + init_cursor_tracking().await; + init_foreground_app_change_listener(); + + if let Err(err) = validate_server_health().await { + handle_disasterous_failure(Some(err.to_string())).await; + return; + } + + match get_session_token().await { + Some(_tokens) => construct_user_session().await, + None => open_welcome_window(), + } + + close_splash_window(); +} diff --git a/src-tauri/src/init/startup.rs b/src-tauri/src/init/startup.rs deleted file mode 100644 index 4dd49d5..0000000 --- a/src-tauri/src/init/startup.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::time::Duration; -use tokio::time::{sleep, Instant}; - -use crate::{ - services::{ - auth::get_access_token, - scene::{close_splash_window, open_scene_window, open_splash_window}, - ws::init_ws_client, - }, - state::init_app_data, -}; - -async fn establish_websocket_connection() { - const MAX_ATTEMPTS: u8 = 5; - const BACKOFF: Duration = Duration::from_millis(300); - - for _attempt in 1..=MAX_ATTEMPTS { - if get_access_token().await.is_some() { - init_ws_client().await; - return; - } - - sleep(BACKOFF).await; - } -} - -pub async fn initialize_app_data_and_connections() -> Instant { - open_splash_window(); - - // Record start time for minimum splash duration - let start = Instant::now(); - - // Initialize app data first so we only start WebSocket after auth is fully available - init_app_data().await; - - // Initialize WebSocket client after we know auth is present - establish_websocket_connection().await; - - start -} - -pub async fn transition_to_main_interface(start: Instant) { - // Ensure splash stays visible for at least 3 seconds - let elapsed = start.elapsed(); - if elapsed < Duration::from_secs(3) { - sleep(Duration::from_secs(3) - elapsed).await; - } - - // Close splash and open main scene - close_splash_window(); - open_scene_window(); -} \ No newline at end of file diff --git a/src-tauri/src/init/tracing.rs b/src-tauri/src/init/tracing.rs index c73a18e..9ff5414 100644 --- a/src-tauri/src/init/tracing.rs +++ b/src-tauri/src/init/tracing.rs @@ -4,7 +4,7 @@ use tracing_subscriber::util::SubscriberInitExt; use crate::get_app_handle; /// Initialize `tracing_subscriber` for logging to file & console -pub fn setup_logging() { +pub fn init_logging() { // Set up file appender let app_handle = get_app_handle(); let app_log_dir = app_handle diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c73647d..004d6fa 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,10 +1,6 @@ -use crate::{ - init::tracing::setup_logging, - services::{ - cursor::start_cursor_tracking, - doll_editor::open_doll_editor_window, - scene::{open_splash_window, set_pet_menu_state, set_scene_interactive}, - }, +use crate::services::{ + doll_editor::open_doll_editor_window, + scene::{set_pet_menu_state, set_scene_interactive}, }; use commands::app::{quit_app, restart_app}; use commands::app_data::{get_app_data, refresh_app_data}; @@ -40,14 +36,6 @@ pub fn get_app_handle<'a>() -> &'a tauri::AppHandle { .expect("get_app_handle called but app is still not initialized") } -fn initialize_app_environment() -> Result<(), tauri::Error> { - setup_logging(); - open_splash_window(); - state::init_fdoll_state(); - async_runtime::spawn(async move { init::lifecycle::launch_core_services().await }); - Ok(()) -} - fn register_app_events(event: tauri::RunEvent) { if let tauri::RunEvent::ExitRequested { api, code, .. } = event { if code.is_none() { @@ -66,7 +54,6 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_process::init()) .invoke_handler(tauri::generate_handler![ - start_cursor_tracking, get_app_data, refresh_app_data, list_friends, @@ -102,7 +89,7 @@ pub fn run() { APP_HANDLE .set(app.handle().to_owned()) .expect("Failed to init app handle."); - initialize_app_environment().expect("Failed to setup app."); + async_runtime::spawn(async move { init::launch_app().await }); Ok(()) }) .build(tauri::generate_context!()) diff --git a/src-tauri/src/models/app_data.rs b/src-tauri/src/models/app_data.rs index c7d1535..14192d7 100644 --- a/src-tauri/src/models/app_data.rs +++ b/src-tauri/src/models/app_data.rs @@ -43,5 +43,5 @@ pub struct AppData { pub user: Option, pub friends: Option>, pub dolls: Option>, - pub scene: SceneData, + pub scene: SceneData, // TODO: move this out of app data } diff --git a/src-tauri/src/services/active_app.rs b/src-tauri/src/services/active_app.rs index e9687fa..e3f6367 100644 --- a/src-tauri/src/services/active_app.rs +++ b/src-tauri/src/services/active_app.rs @@ -756,8 +756,10 @@ mod windows_impl { pub static ACTIVE_APP_CHANGED: &str = "active-app-changed"; -/// Initializes the active app change listener and emits events to the Tauri app on changes. -pub fn init_active_app_changes_listener() { +/// Initializes the foreground app change listener +/// and emits events to the Tauri app on changes. +/// Used for app to emit user foreground app to peers. +pub fn init_foreground_app_change_listener() { let app_handle = get_app_handle(); listen_for_active_app_changes(|app_names: AppMetadata| { if let Err(e) = app_handle.emit(ACTIVE_APP_CHANGED, app_names) { diff --git a/src-tauri/src/services/auth.rs b/src-tauri/src/services/auth.rs index edf4f4b..ff0cb0e 100644 --- a/src-tauri/src/services/auth.rs +++ b/src-tauri/src/services/auth.rs @@ -114,7 +114,7 @@ fn generate_code_challenge(code_verifier: &str) -> String { /// Returns the auth pass object, including /// access token, refresh token, expire time etc. /// Automatically refreshes if expired. -pub async fn get_tokens() -> Option { +pub async fn get_session_token() -> Option { info!("Retrieving tokens"); let Some(auth_pass) = ({ lock_r!(FDOLL).auth.auth_pass.clone() }) else { return None; @@ -173,7 +173,7 @@ pub async fn get_tokens() -> Option { /// Helper function to get the current access token. pub async fn get_access_token() -> Option { - get_tokens().await.map(|pass| pass.access_token) + get_session_token().await.map(|pass| pass.access_token) } /// Save auth_pass to secure storage (keyring) and update app state. @@ -367,7 +367,7 @@ pub fn clear_auth_pass() -> Result<(), OAuthError> { /// ``` pub fn logout() -> Result<(), OAuthError> { info!("Logging out user"); - lock_w!(FDOLL).auth.auth_pass = None; + lock_w!(FDOLL).auth.auth_pass = None; clear_auth_pass()?; // Clear OAuth flow state as well @@ -386,8 +386,16 @@ pub async fn logout_and_restart() -> Result<(), OAuthError> { let (refresh_token, session_state, base_url) = { let guard = lock_r!(FDOLL); ( - guard.auth.auth_pass.as_ref().map(|p| p.refresh_token.clone()), - guard.auth.auth_pass.as_ref().map(|p| p.session_state.clone()), + guard + .auth + .auth_pass + .as_ref() + .map(|p| p.refresh_token.clone()), + guard + .auth + .auth_pass + .as_ref() + .map(|p| p.session_state.clone()), guard .app_config .api_base_url @@ -676,7 +684,7 @@ where { let mut guard = lock_w!(FDOLL); guard.auth.auth_pass = Some(auth_pass.clone()); - guard.auth.oauth_flow = Default::default(); + guard.auth.oauth_flow = Default::default(); } if let Err(e) = save_auth_pass(&auth_pass) { error!("Failed to save auth pass: {}", e); @@ -720,7 +728,8 @@ pub async fn refresh_token(refresh_token: &str) -> Result ( guard.app_config.clone(), guard - .network.clients + .network + .clients .as_ref() .expect("clients present") .http_client diff --git a/src-tauri/src/services/cursor.rs b/src-tauri/src/services/cursor.rs index a3857c4..33ec00e 100644 --- a/src-tauri/src/services/cursor.rs +++ b/src-tauri/src/services/cursor.rs @@ -40,8 +40,8 @@ pub fn get_latest_cursor_position() -> Option { /// Convert absolute screen coordinates to normalized coordinates (0.0 - 1.0) pub fn absolute_to_normalized(pos: &CursorPosition) -> CursorPosition { let guard = lock_r!(FDOLL); - let screen_w = guard.ui.app_data.scene.display.screen_width as f64; - let screen_h = guard.ui.app_data.scene.display.screen_height as f64; + let screen_w = guard.user_data.scene.display.screen_width as f64; + let screen_h = guard.user_data.scene.display.screen_height as f64; CursorPosition { x: (pos.x / screen_w).clamp(0.0, 1.0), @@ -52,8 +52,8 @@ pub fn absolute_to_normalized(pos: &CursorPosition) -> CursorPosition { /// Convert normalized coordinates to absolute screen coordinates pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition { let guard = lock_r!(FDOLL); - let screen_w = guard.ui.app_data.scene.display.screen_width as f64; - let screen_h = guard.ui.app_data.scene.display.screen_height as f64; + let screen_w = guard.user_data.scene.display.screen_width as f64; + let screen_h = guard.user_data.scene.display.screen_height as f64; CursorPosition { x: (normalized.x * screen_w).round(), @@ -61,10 +61,9 @@ pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition { } } -/// Initialize cursor tracking - can be called multiple times safely from any window -/// Only the first call will actually start tracking, subsequent calls are no-ops -#[tauri::command] -pub async fn start_cursor_tracking() -> Result<(), String> { +/// Initialize cursor tracking. Broadcasts cursor +/// position changes via `cursor-position` event. +pub async fn init_cursor_tracking() { info!("start_cursor_tracking called"); // Use OnceCell to ensure this only runs once, even if called from multiple windows @@ -74,17 +73,16 @@ pub async fn start_cursor_tracking() -> Result<(), String> { info!("First call to start_cursor_tracking - spawning cursor tracking task"); tauri::async_runtime::spawn(async { - if let Err(e) = init_cursor_tracking().await { + if let Err(e) = init_cursor_tracking_i().await { error!("Failed to initialize cursor tracking: {}", e); } }); }); info!("Cursor tracking initialization registered"); - Ok(()) } -async fn init_cursor_tracking() -> Result<(), String> { +async fn init_cursor_tracking_i() -> Result<(), String> { info!("Initializing cursor tracking..."); // Create a channel to decouple event generation (producer) from processing (consumer). @@ -124,7 +122,7 @@ async fn init_cursor_tracking() -> Result<(), String> { #[cfg(target_os = "windows")] let scale_factor = { let guard = lock_r!(FDOLL); - guard.ui.app_data.scene.display.monitor_scale_factor + guard.user_data.scene.display.monitor_scale_factor }; // The producer closure moves `tx` into it. diff --git a/src-tauri/src/services/health_manager.rs b/src-tauri/src/services/health_manager.rs index 650b487..3a1166a 100644 --- a/src-tauri/src/services/health_manager.rs +++ b/src-tauri/src/services/health_manager.rs @@ -1,36 +1,15 @@ use crate::get_app_handle; use crate::{lock_r, state::FDOLL, system_tray::update_system_tray}; -use tauri::{Emitter, Manager}; +use tauri::Manager; use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder, MessageDialogKind}; use tauri_plugin_positioner::WindowExt; use tracing::{error, info}; pub static HEALTH_MANAGER_WINDOW_LABEL: &str = "health_manager"; -pub static HEALTH_MANAGER_EVENT: &str = "health-error"; - -fn close_window_if_exists(label: &str) { - let app_handle = get_app_handle(); - if let Some(window) = app_handle.get_window(label) { - info!("Closing window with label: {}", label); - if let Err(e) = window.close() { - error!("Failed to close {} window: {}", label, e); - } else { - info!("Closed window with label: {}", label); - } - } else { - info!("No window found with label: {}", label); - } -} /// Closes primary UI windows and shows the health manager with an optional error message. -pub fn show_health_manager_with_error(error_message: Option) { +pub fn open_health_manager_window(error_message: Option) { let app_handle = get_app_handle(); - // Ensure other windows are closed before showing health manager - close_window_if_exists(crate::services::scene::SPLASH_WINDOW_LABEL); - close_window_if_exists(crate::services::scene::SCENE_WINDOW_LABEL); - close_window_if_exists(crate::services::app_menu::APP_MENU_WINDOW_LABEL); - - update_system_tray(false); let existing_webview_window = app_handle.get_window(HEALTH_MANAGER_WINDOW_LABEL); @@ -45,12 +24,6 @@ pub fn show_health_manager_with_error(error_message: Option) { .kind(MessageDialogKind::Error) .show(|_| {}); } - - if let Some(message) = error_message { - if let Err(e) = window.emit(HEALTH_MANAGER_EVENT, message.clone()) { - error!("Failed to emit health error event: {}", e); - } - } return; } @@ -58,7 +31,13 @@ pub fn show_health_manager_with_error(error_message: Option) { let webview_window = match tauri::WebviewWindowBuilder::new( app_handle, HEALTH_MANAGER_WINDOW_LABEL, - tauri::WebviewUrl::App("/health-manager".into()), + tauri::WebviewUrl::App( + format!( + "/health-manager?err={}", + error_message.unwrap_or(String::from("Something went wrong!")) + ) + .into(), + ), ) .title("Health Manager") .inner_size(420.0, 420.0) @@ -89,12 +68,6 @@ pub fn show_health_manager_with_error(error_message: Option) { error!("Failed to move health manager window to center: {}", e); } - if let Some(message) = error_message { - if let Err(e) = webview_window.emit(HEALTH_MANAGER_EVENT, message.clone()) { - error!("Failed to emit health error event: {}", e); - } - } - if let Err(e) = webview_window.show() { error!("Failed to show health manager window: {}", e); MessageDialogBuilder::new( @@ -117,7 +90,7 @@ pub fn close_health_manager_window() { } else { info!("Health manager window closed"); let guard = lock_r!(FDOLL); - let is_logged_in = guard.ui.app_data.user.is_some(); + let is_logged_in = guard.user_data.user.is_some(); drop(guard); update_system_tray(is_logged_in); } diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 9535e80..4775760 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1,3 +1,7 @@ +use tauri::Manager; + +use crate::get_app_handle; + pub mod active_app; pub mod app_menu; pub mod auth; @@ -10,3 +14,11 @@ pub mod scene; pub mod sprite_recolor; pub mod welcome; pub mod ws; + +pub fn close_all_windows() { + let app_handle = get_app_handle(); + let webview_windows = app_handle.webview_windows(); + for window in webview_windows { + window.1.close().unwrap(); + } +} diff --git a/src-tauri/src/services/ws/client.rs b/src-tauri/src/services/ws/client.rs index 91deb09..cbe1961 100644 --- a/src-tauri/src/services/ws/client.rs +++ b/src-tauri/src/services/ws/client.rs @@ -1,15 +1,32 @@ +use std::time::Duration; + use rust_socketio::ClientBuilder; use tauri::async_runtime; +use tokio::time::sleep; use tracing::{error, info}; use crate::{ lock_r, lock_w, - services::client_config_manager::AppConfig, + services::{auth::get_access_token, client_config_manager::AppConfig}, state::FDOLL, }; use super::handlers; +pub async fn establish_websocket_connection() { + const MAX_ATTEMPTS: u8 = 5; + const BACKOFF: Duration = Duration::from_millis(300); + + for _attempt in 1..=MAX_ATTEMPTS { + if get_access_token().await.is_some() { + init_ws_client().await; + return; + } + + sleep(BACKOFF).await; + } +} + pub async fn init_ws_client() { let app_config = { let guard = lock_r!(FDOLL); @@ -26,16 +43,19 @@ pub async fn init_ws_client() { } Err(e) => { error!("Failed to initialize WebSocket client: {}", e); - // If we failed because no token, clear the WS client to avoid stale retries - let mut guard = lock_w!(FDOLL); - if let Some(clients) = guard.network.clients.as_mut() { - clients.ws_client = None; - clients.is_ws_initialized = false; - } + clear_ws_client().await; } } } +pub async fn clear_ws_client() { + let mut guard = lock_w!(FDOLL); + if let Some(clients) = guard.network.clients.as_mut() { + clients.ws_client = None; + clients.is_ws_initialized = false; + } +} + pub async fn build_ws_client( app_config: &AppConfig, ) -> Result { diff --git a/src-tauri/src/services/ws/cursor.rs b/src-tauri/src/services/ws/cursor.rs index d49d50b..053679f 100644 --- a/src-tauri/src/services/ws/cursor.rs +++ b/src-tauri/src/services/ws/cursor.rs @@ -3,8 +3,7 @@ use tauri::async_runtime; use tracing::error; use crate::{ - lock_r, - services::{cursor::CursorPosition, health_manager::show_health_manager_with_error}, + init::lifecycle::handle_disasterous_failure, lock_r, services::cursor::CursorPosition, state::FDOLL, }; @@ -41,11 +40,11 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) { Ok(Ok(_)) => (), Ok(Err(e)) => { error!("Failed to emit cursor report: {}", e); - show_health_manager_with_error(Some(format!("WebSocket emit failed: {}", e))); + handle_disasterous_failure(Some(format!("WebSocket emit failed: {}", e))).await; } Err(e) => { error!("Failed to execute blocking task for cursor report: {}", e); - show_health_manager_with_error(Some(format!("WebSocket task failed: {}", e))); + handle_disasterous_failure(Some(format!("WebSocket task failed: {}", e))).await; } } } diff --git a/src-tauri/src/services/ws/doll.rs b/src-tauri/src/services/ws/doll.rs index da73937..cfff7e7 100644 --- a/src-tauri/src/services/ws/doll.rs +++ b/src-tauri/src/services/ws/doll.rs @@ -36,8 +36,7 @@ pub fn on_doll_updated(payload: Payload, _socket: RawClient) { let is_active_doll = if let Some(id) = doll_id { let guard = lock_r!(FDOLL); guard - .ui - .app_data + .user_data .user .as_ref() .and_then(|u| u.active_doll_id.as_ref()) @@ -75,8 +74,7 @@ pub fn on_doll_deleted(payload: Payload, _socket: RawClient) { let is_active_doll = if let Some(id) = doll_id { let guard = lock_r!(FDOLL); guard - .ui - .app_data + .user_data .user .as_ref() .and_then(|u| u.active_doll_id.as_ref()) diff --git a/src-tauri/src/services/ws/mod.rs b/src-tauri/src/services/ws/mod.rs index 98e7e44..a5a0273 100644 --- a/src-tauri/src/services/ws/mod.rs +++ b/src-tauri/src/services/ws/mod.rs @@ -41,7 +41,7 @@ impl WS_EVENT { pub const CLIENT_SEND_INTERACTION: &str = "client-send-interaction"; } -mod client; +pub mod client; mod connection; mod cursor; mod doll; @@ -50,6 +50,5 @@ mod handlers; mod interaction; mod user_status; -pub use client::init_ws_client; pub use cursor::report_cursor_data; pub use user_status::{report_user_status, UserStatusPayload}; diff --git a/src-tauri/src/services/ws/user_status.rs b/src-tauri/src/services/ws/user_status.rs index 3d6c387..70249f9 100644 --- a/src-tauri/src/services/ws/user_status.rs +++ b/src-tauri/src/services/ws/user_status.rs @@ -1,12 +1,12 @@ use once_cell::sync::Lazy; use rust_socketio::Payload; -use tauri::async_runtime; +use tauri::async_runtime::{self}; use tokio::sync::Mutex; use tokio::task::JoinHandle; use tokio::time::Duration; use tracing::error; -use crate::{lock_r, services::health_manager::show_health_manager_with_error, state::FDOLL}; +use crate::{init::lifecycle::handle_disasterous_failure, lock_r, state::FDOLL}; use super::WS_EVENT; @@ -66,20 +66,16 @@ pub async fn report_user_status(status: UserStatusPayload) { Ok(Ok(_)) => (), Ok(Err(e)) => { error!("Failed to emit user status report: {}", e); - show_health_manager_with_error(Some(format!( - "WebSocket emit failed: {}", - e - ))); + handle_disasterous_failure(Some(format!("WebSocket emit failed: {}", e))) + .await; } Err(e) => { error!( "Failed to execute blocking task for user status report: {}", e ); - show_health_manager_with_error(Some(format!( - "WebSocket task failed: {}", - e - ))); + handle_disasterous_failure(Some(format!("WebSocket task failed: {}", e))) + .await; } } } diff --git a/src-tauri/src/state/mod.rs b/src-tauri/src/state/mod.rs index 9279fba..2c71244 100644 --- a/src-tauri/src/state/mod.rs +++ b/src-tauri/src/state/mod.rs @@ -1,5 +1,5 @@ // in app-core/src/state.rs -use crate::lock_w; +use crate::{lock_w, models::app_data::AppData}; use std::sync::{Arc, LazyLock, RwLock}; use tauri::tray::TrayIcon; use tracing::info; @@ -17,7 +17,7 @@ pub struct AppState { pub app_config: crate::services::client_config_manager::AppConfig, pub network: NetworkState, pub auth: AuthState, - pub ui: UiState, + pub user_data: AppData, pub tray: Option, } @@ -26,15 +26,17 @@ pub struct AppState { pub static FDOLL: LazyLock>> = LazyLock::new(|| Arc::new(RwLock::new(AppState::default()))); -pub fn init_fdoll_state() { +/// Populate app state with initial +/// values and necesary client instances. +pub fn init_app_state() { + dotenvy::dotenv().ok(); { let mut guard = lock_w!(FDOLL); - dotenvy::dotenv().ok(); guard.app_config = crate::services::client_config_manager::load_app_config(); guard.network = init_network_state(); guard.auth = init_auth_state(); - guard.ui = init_ui_state(); + guard.user_data = AppData::default(); } - + update_display_dimensions_for_scene_state(); info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)"); } diff --git a/src-tauri/src/state/ui.rs b/src-tauri/src/state/ui.rs index 525c93c..c48ba94 100644 --- a/src-tauri/src/state/ui.rs +++ b/src-tauri/src/state/ui.rs @@ -1,36 +1,19 @@ use crate::{ get_app_handle, lock_r, lock_w, - models::app_data::AppData, remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, + state::FDOLL, }; -use std::{ - collections::HashSet, - sync::{LazyLock}, -}; +use std::{collections::HashSet, sync::LazyLock}; use tauri::Emitter; use tokio::sync::Mutex; use tracing::{info, warn}; -pub struct UiState { - pub app_data: AppData, -} - -impl Default for UiState { - fn default() -> Self { - Self { - app_data: AppData::default(), - } - } -} - -pub fn init_ui_state() -> UiState { - let mut ui_state = UiState::default(); - - // Initialize screen dimensions +pub fn update_display_dimensions_for_scene_state() { let app_handle = get_app_handle(); + let mut guard = lock_w!(FDOLL); + // Get primary monitor with retries - // Note: This duplicates logic from init_cursor_tracking, but we need it here for global state let primary_monitor = { let mut retry_count = 0; let max_retries = 3; @@ -77,23 +60,21 @@ pub fn init_ui_state() -> UiState { let logical_monitor_dimensions: tauri::LogicalSize = monitor_dimensions.to_logical(monitor_scale_factor); - ui_state.app_data.scene.display.screen_width = logical_monitor_dimensions.width; - ui_state.app_data.scene.display.screen_height = logical_monitor_dimensions.height; - ui_state.app_data.scene.display.monitor_scale_factor = monitor_scale_factor; - ui_state.app_data.scene.grid_size = 600; // Hardcoded grid size + guard.user_data.scene.display.screen_width = logical_monitor_dimensions.width; + guard.user_data.scene.display.screen_height = logical_monitor_dimensions.height; + guard.user_data.scene.display.monitor_scale_factor = monitor_scale_factor; + guard.user_data.scene.grid_size = 600; // Hardcoded grid size info!( "Initialized global AppData with screen dimensions: {}x{}, scale: {}, grid: {}", logical_monitor_dimensions.width, logical_monitor_dimensions.height, monitor_scale_factor, - ui_state.app_data.scene.grid_size + guard.user_data.scene.grid_size ); } else { warn!("Could not initialize screen dimensions in global state - no monitor found"); } - - ui_state } /// Defines which parts of AppData should be refreshed from the server @@ -109,15 +90,6 @@ pub enum AppDataRefreshScope { Dolls, } -/// To be called in init state or need to refresh data. -/// Populate user data in app state from the server. -/// -/// This is a convenience wrapper that refreshes all data. -/// For more control, use `init_app_data_scoped`. -pub async fn init_app_data() { - init_app_data_scoped(AppDataRefreshScope::All).await; -} - static REFRESH_IN_FLIGHT: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); static REFRESH_PENDING: LazyLock>> = @@ -159,7 +131,7 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { match user_remote.get_user(None).await { Ok(user) => { let mut guard = lock_w!(crate::state::FDOLL); - guard.ui.app_data.user = Some(user); + guard.user_data.user = Some(user); } Err(e) => { warn!("Failed to fetch user profile: {}", e); @@ -187,7 +159,7 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { match friend_remote.get_friends().await { Ok(friends) => { let mut guard = lock_w!(crate::state::FDOLL); - guard.ui.app_data.friends = Some(friends); + guard.user_data.friends = Some(friends); } Err(e) => { warn!("Failed to fetch friends list: {}", e); @@ -212,7 +184,7 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { match dolls_remote.get_dolls().await { Ok(dolls) => { let mut guard = lock_w!(crate::state::FDOLL); - guard.ui.app_data.dolls = Some(dolls); + guard.user_data.dolls = Some(dolls); } Err(e) => { warn!("Failed to fetch dolls list: {}", e); @@ -235,7 +207,7 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { // Emit event regardless of partial success, frontend should handle nulls/empty states { let guard = lock_r!(crate::state::FDOLL); // Use read lock to get data - let app_data_clone = guard.ui.app_data.clone(); + let app_data_clone = guard.user_data.clone(); drop(guard); // Drop lock before emitting to prevent potential deadlocks if let Err(e) = get_app_handle().emit("app-data-refreshed", &app_data_clone) { @@ -280,4 +252,11 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { break; } -} \ No newline at end of file +} + +pub fn clear_app_data() { + let mut guard = lock_w!(FDOLL); + guard.user_data.dolls = None; + guard.user_data.user = None; + guard.user_data.friends = None; +} diff --git a/src-tauri/src/system_tray.rs b/src-tauri/src/system_tray.rs index c93abdf..4a6242f 100644 --- a/src-tauri/src/system_tray.rs +++ b/src-tauri/src/system_tray.rs @@ -1,11 +1,15 @@ -use crate::{get_app_handle, lock_r, services::app_menu::open_app_menu_window, state::FDOLL}; +use crate::{ + get_app_handle, lock_r, lock_w, services::app_menu::open_app_menu_window, state::FDOLL, +}; use tauri::{ menu::{Menu, MenuItem}, tray::TrayIconBuilder, }; use tracing::error; -pub fn init_system_tray() -> tauri::tray::TrayIcon { +/// Constructs app system tray. +/// Uses Tauri. +pub fn init_system_tray() { let app = get_app_handle(); let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap(); @@ -17,7 +21,7 @@ pub fn init_system_tray() -> tauri::tray::TrayIcon { Err(err) => todo!("Handle error: {}", err), }; - TrayIconBuilder::new() + let tray = TrayIconBuilder::new() .menu(&menu) .on_menu_event(|app, event| match event.id.as_ref() { "quit" => { @@ -32,9 +36,17 @@ pub fn init_system_tray() -> tauri::tray::TrayIcon { }) .icon(app.default_window_icon().unwrap().clone()) .build(app) - .unwrap_or_else(|err| panic!("Failed to build tray: {}", err)) + .unwrap_or_else(|err| panic!("Failed to build tray: {}", err)); + { + let mut guard = lock_w!(FDOLL); + guard.tray = Some(tray); + }; + + update_system_tray(false); } +/// Toggle the "Open App Menu" item in the system tray. +/// Used for when user is signed in vs not signed in. pub fn update_system_tray(is_logged_in: bool) { let app = get_app_handle(); let guard = lock_r!(FDOLL); diff --git a/src/events/cursor.ts b/src/events/cursor.ts index 76e41ce..fc69bc4 100644 --- a/src/events/cursor.ts +++ b/src/events/cursor.ts @@ -50,9 +50,6 @@ export async function initCursorTracking() { } try { - // Start tracking - await invoke("start_cursor_tracking"); - // Listen to cursor position events (each window subscribes independently) unlistenCursor = await listen( "cursor-position", diff --git a/src/routes/health-manager/+page.svelte b/src/routes/health-manager/+page.svelte index ca4cafc..eb5610a 100644 --- a/src/routes/health-manager/+page.svelte +++ b/src/routes/health-manager/+page.svelte @@ -1,22 +1,13 @@