diff --git a/src-tauri/src/commands/app_data.rs b/src-tauri/src/commands/app_data.rs index bfec56c..fe3e0ce 100644 --- a/src-tauri/src/commands/app_data.rs +++ b/src-tauri/src/commands/app_data.rs @@ -7,12 +7,12 @@ use crate::{ #[tauri::command] pub fn get_app_data() -> Result { let guard = lock_r!(FDOLL); - Ok(guard.app_data.clone()) + Ok(guard.ui.app_data.clone()) } #[tauri::command] pub async fn refresh_app_data() -> Result { init_app_data().await; let guard = lock_r!(FDOLL); - Ok(guard.app_data.clone()) + Ok(guard.ui.app_data.clone()) } diff --git a/src-tauri/src/commands/dolls.rs b/src-tauri/src/commands/dolls.rs index 75ea234..1b9f291 100644 --- a/src-tauri/src/commands/dolls.rs +++ b/src-tauri/src/commands/dolls.rs @@ -51,6 +51,7 @@ pub async fn update_doll(id: String, dto: UpdateDollDto) -> Result Result<(), String> { let is_active_doll = { let guard = lock_r!(FDOLL); guard + .ui .app_data .user .as_ref() diff --git a/src-tauri/src/remotes/dolls.rs b/src-tauri/src/remotes/dolls.rs index 664a65f..61ecbe4 100644 --- a/src-tauri/src/remotes/dolls.rs +++ b/src-tauri/src/remotes/dolls.rs @@ -18,7 +18,7 @@ impl DollsRemote { .expect("App configuration error") .clone(), client: guard - .clients + .network.clients .as_ref() .expect("App configuration error") .http_client diff --git a/src-tauri/src/remotes/friends.rs b/src-tauri/src/remotes/friends.rs index 2c46fc6..46b574b 100644 --- a/src-tauri/src/remotes/friends.rs +++ b/src-tauri/src/remotes/friends.rs @@ -18,7 +18,7 @@ impl FriendRemote { .expect("App configuration error") .clone(), client: guard - .clients + .network.clients .as_ref() .expect("App configuration error") .http_client diff --git a/src-tauri/src/remotes/health.rs b/src-tauri/src/remotes/health.rs index 9599d35..73c9fd3 100644 --- a/src-tauri/src/remotes/health.rs +++ b/src-tauri/src/remotes/health.rs @@ -18,7 +18,7 @@ impl HealthRemote { .ok_or(HealthError::ConfigMissing("api_base_url"))?; let client = guard - .clients + .network.clients .as_ref() .map(|c| c.http_client.clone()) .ok_or(HealthError::ConfigMissing("http_client"))?; diff --git a/src-tauri/src/remotes/session.rs b/src-tauri/src/remotes/session.rs index c248281..d2b1540 100644 --- a/src-tauri/src/remotes/session.rs +++ b/src-tauri/src/remotes/session.rs @@ -20,7 +20,7 @@ impl SessionRemote { .expect("App configuration error") .clone(), client: guard - .clients + .network.clients .as_ref() .expect("App configuration error") .http_client diff --git a/src-tauri/src/remotes/user.rs b/src-tauri/src/remotes/user.rs index 0d2e02c..2dbf0cd 100644 --- a/src-tauri/src/remotes/user.rs +++ b/src-tauri/src/remotes/user.rs @@ -18,7 +18,7 @@ impl UserRemote { .expect("App configuration error") .clone(), client: guard - .clients + .network.clients .as_ref() .expect("App configuration error") .http_client diff --git a/src-tauri/src/services/auth.rs b/src-tauri/src/services/auth.rs index 2ac03e4..edf4f4b 100644 --- a/src-tauri/src/services/auth.rs +++ b/src-tauri/src/services/auth.rs @@ -25,10 +25,10 @@ const SERVICE_NAME: &str = "friendolls"; pub fn cancel_auth_flow() { let mut guard = lock_w!(FDOLL); - if let Some(token) = guard.oauth_flow.cancel_token.take() { + if let Some(token) = guard.auth.oauth_flow.cancel_token.take() { token.cancel(); } - guard.oauth_flow = Default::default(); + guard.auth.oauth_flow = Default::default(); } /// Errors that can occur during OAuth authentication flow. @@ -116,13 +116,13 @@ fn generate_code_challenge(code_verifier: &str) -> String { /// Automatically refreshes if expired. pub async fn get_tokens() -> Option { info!("Retrieving tokens"); - let Some(auth_pass) = ({ lock_r!(FDOLL).auth_pass.clone() }) else { + let Some(auth_pass) = ({ lock_r!(FDOLL).auth.auth_pass.clone() }) else { return None; }; let Some(issued_at) = auth_pass.issued_at else { warn!("Auth pass missing issued_at timestamp, clearing"); - lock_w!(FDOLL).auth_pass = None; + lock_w!(FDOLL).auth.auth_pass = None; return None; }; @@ -137,7 +137,7 @@ pub async fn get_tokens() -> Option { if refresh_expired { info!("Refresh token expired, clearing auth state"); - lock_w!(FDOLL).auth_pass = None; + lock_w!(FDOLL).auth.auth_pass = None; if let Err(e) = clear_auth_pass() { error!("Failed to clear expired auth pass: {}", e); } @@ -148,7 +148,7 @@ pub async fn get_tokens() -> Option { let _guard = REFRESH_LOCK.lock().await; // Double-check after acquiring lock - let auth_pass = lock_r!(FDOLL).auth_pass.clone()?; + let auth_pass = lock_r!(FDOLL).auth.auth_pass.clone()?; let current_time = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs(); let expired = current_time - auth_pass.issued_at? >= auth_pass.expires_in; @@ -162,7 +162,7 @@ pub async fn get_tokens() -> Option { Ok(new_pass) => Some(new_pass), Err(e) => { error!("Failed to refresh token: {}", e); - lock_w!(FDOLL).auth_pass = None; + lock_w!(FDOLL).auth.auth_pass = None; if let Err(e) = clear_auth_pass() { error!("Failed to clear auth pass after refresh failure: {}", e); } @@ -367,11 +367,11 @@ pub fn clear_auth_pass() -> Result<(), OAuthError> { /// ``` pub fn logout() -> Result<(), OAuthError> { info!("Logging out user"); - lock_w!(FDOLL).auth_pass = None; + lock_w!(FDOLL).auth.auth_pass = None; clear_auth_pass()?; // Clear OAuth flow state as well - lock_w!(FDOLL).oauth_flow = Default::default(); + lock_w!(FDOLL).auth.oauth_flow = Default::default(); // TODO: Call OAuth provider's revocation endpoint // This would require adding a revoke_token() function that calls: @@ -386,8 +386,8 @@ pub async fn logout_and_restart() -> Result<(), OAuthError> { let (refresh_token, session_state, base_url) = { let guard = lock_r!(FDOLL); ( - guard.auth_pass.as_ref().map(|p| p.refresh_token.clone()), - guard.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 @@ -460,7 +460,7 @@ pub async fn exchange_code_for_auth_pass( ) -> Result { let (app_config, http_client) = { let guard = lock_r!(FDOLL); - let clients = guard.clients.as_ref(); + let clients = guard.network.clients.as_ref(); if clients.is_none() { error!("Clients not initialized yet!"); return Err(OAuthError::InvalidConfig); @@ -581,10 +581,10 @@ where { let mut guard = lock_w!(FDOLL); - guard.oauth_flow.state = Some(state.clone()); - guard.oauth_flow.code_verifier = Some(code_verifier.clone()); - guard.oauth_flow.initiated_at = Some(current_time); - guard.oauth_flow.cancel_token = Some(cancel_token.clone()); + guard.auth.oauth_flow.state = Some(state.clone()); + guard.auth.oauth_flow.code_verifier = Some(code_verifier.clone()); + guard.auth.oauth_flow.initiated_at = Some(current_time); + guard.auth.oauth_flow.cancel_token = Some(cancel_token.clone()); } let mut url = match url::Url::parse(&format!("{}/auth", &app_config.auth.auth_url)) { @@ -655,8 +655,8 @@ where let (stored_state, stored_verifier) = { let guard = lock_r!(FDOLL); ( - guard.oauth_flow.state.clone(), - guard.oauth_flow.code_verifier.clone(), + guard.auth.oauth_flow.state.clone(), + guard.auth.oauth_flow.code_verifier.clone(), ) }; @@ -675,8 +675,8 @@ where Ok(auth_pass) => { { let mut guard = lock_w!(FDOLL); - guard.auth_pass = Some(auth_pass.clone()); - guard.oauth_flow = Default::default(); + guard.auth.auth_pass = Some(auth_pass.clone()); + guard.auth.oauth_flow = Default::default(); } if let Err(e) = save_auth_pass(&auth_pass) { error!("Failed to save auth pass: {}", e); @@ -720,7 +720,7 @@ pub async fn refresh_token(refresh_token: &str) -> Result ( guard.app_config.clone(), guard - .clients + .network.clients .as_ref() .expect("clients present") .http_client @@ -765,7 +765,7 @@ pub async fn refresh_token(refresh_token: &str) -> Result ); // Update state and storage - lock_w!(FDOLL).auth_pass = Some(auth_pass.clone()); + lock_w!(FDOLL).auth.auth_pass = Some(auth_pass.clone()); if let Err(e) = save_auth_pass(&auth_pass) { error!("Failed to save refreshed auth pass: {}", e); } else { diff --git a/src-tauri/src/services/cursor.rs b/src-tauri/src/services/cursor.rs index c227642..a3857c4 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.app_data.scene.display.screen_width as f64; - let screen_h = guard.app_data.scene.display.screen_height as f64; + 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; 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.app_data.scene.display.screen_width as f64; - let screen_h = guard.app_data.scene.display.screen_height as f64; + 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; CursorPosition { x: (normalized.x * screen_w).round(), @@ -124,7 +124,7 @@ async fn init_cursor_tracking() -> Result<(), String> { #[cfg(target_os = "windows")] let scale_factor = { let guard = lock_r!(FDOLL); - guard.app_data.scene.display.monitor_scale_factor + guard.ui.app_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 84c848c..650b487 100644 --- a/src-tauri/src/services/health_manager.rs +++ b/src-tauri/src/services/health_manager.rs @@ -117,7 +117,7 @@ pub fn close_health_manager_window() { } else { info!("Health manager window closed"); let guard = lock_r!(FDOLL); - let is_logged_in = guard.app_data.user.is_some(); + let is_logged_in = guard.ui.app_data.user.is_some(); drop(guard); update_system_tray(is_logged_in); } diff --git a/src-tauri/src/services/interaction.rs b/src-tauri/src/services/interaction.rs index 3b1ce95..2363f4c 100644 --- a/src-tauri/src/services/interaction.rs +++ b/src-tauri/src/services/interaction.rs @@ -9,7 +9,7 @@ pub async fn send_interaction(dto: SendInteractionDto) -> Result<(), String> { // Check if WS is initialized let client = { let guard = lock_r!(FDOLL); - if let Some(clients) = &guard.clients { + if let Some(clients) = &guard.network.clients { if clients.is_ws_initialized { clients.ws_client.clone() } else { diff --git a/src-tauri/src/services/ws/client.rs b/src-tauri/src/services/ws/client.rs index 6a99574..bc256bf 100644 --- a/src-tauri/src/services/ws/client.rs +++ b/src-tauri/src/services/ws/client.rs @@ -33,7 +33,7 @@ fn on_initialized(payload: Payload, _socket: RawClient) { // Mark WebSocket as initialized and reset backoff timer let mut guard = lock_w!(FDOLL); - if let Some(clients) = guard.clients.as_mut() { + if let Some(clients) = guard.network.clients.as_mut() { clients.is_ws_initialized = true; } @@ -57,7 +57,7 @@ pub async fn init_ws_client() { match build_ws_client(&app_config).await { Ok(ws_client) => { let mut guard = lock_w!(FDOLL); - if let Some(clients) = guard.clients.as_mut() { + if let Some(clients) = guard.network.clients.as_mut() { clients.ws_client = Some(ws_client); clients.is_ws_initialized = false; // wait for initialized event } @@ -66,7 +66,7 @@ pub async fn init_ws_client() { 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.clients.as_mut() { + if let Some(clients) = guard.network.clients.as_mut() { clients.ws_client = None; clients.is_ws_initialized = false; } diff --git a/src-tauri/src/services/ws/cursor.rs b/src-tauri/src/services/ws/cursor.rs index 9cf776e..d49d50b 100644 --- a/src-tauri/src/services/ws/cursor.rs +++ b/src-tauri/src/services/ws/cursor.rs @@ -15,7 +15,7 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) { // and if clients are actually initialized. let (client_opt, is_initialized) = { let guard = lock_r!(FDOLL); - if let Some(clients) = &guard.clients { + if let Some(clients) = &guard.network.clients { ( clients.ws_client.as_ref().cloned(), clients.is_ws_initialized, diff --git a/src-tauri/src/services/ws/doll.rs b/src-tauri/src/services/ws/doll.rs index 85b9729..da73937 100644 --- a/src-tauri/src/services/ws/doll.rs +++ b/src-tauri/src/services/ws/doll.rs @@ -36,6 +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 .as_ref() @@ -74,6 +75,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 .as_ref() diff --git a/src-tauri/src/state/auth.rs b/src-tauri/src/state/auth.rs new file mode 100644 index 0000000..a5fe5d1 --- /dev/null +++ b/src-tauri/src/state/auth.rs @@ -0,0 +1,40 @@ +use crate::services::auth::{load_auth_pass, AuthPass}; +use tracing::{info, warn}; + +#[derive(Default, Clone)] +pub struct OAuthFlowTracker { + pub state: Option, + pub code_verifier: Option, + pub initiated_at: Option, + pub cancel_token: Option, +} + +pub struct AuthState { + pub auth_pass: Option, + pub oauth_flow: OAuthFlowTracker, +} + +impl Default for AuthState { + fn default() -> Self { + Self { + auth_pass: None, + oauth_flow: OAuthFlowTracker::default(), + } + } +} + +pub fn init_auth_state() -> AuthState { + let auth_pass = match load_auth_pass() { + Ok(pass) => pass, + Err(e) => { + warn!("Failed to load auth pass from keyring: {e}"); + None + } + }; + info!("Loaded auth pass"); + + AuthState { + auth_pass, + oauth_flow: OAuthFlowTracker::default(), + } +} \ No newline at end of file diff --git a/src-tauri/src/state/mod.rs b/src-tauri/src/state/mod.rs new file mode 100644 index 0000000..c6783b4 --- /dev/null +++ b/src-tauri/src/state/mod.rs @@ -0,0 +1,44 @@ +// in app-core/src/state.rs +use crate::{lock_w}; +use std::{ + sync::{Arc, LazyLock, RwLock}, +}; +use tauri::tray::TrayIcon; +use tracing::info; + +mod network; +mod auth; +mod ui; + +pub use network::*; +pub use auth::*; +pub use ui::*; + +#[derive(Default)] +pub struct AppState { + pub app_config: crate::services::client_config_manager::AppConfig, + pub network: NetworkState, + pub auth: AuthState, + pub ui: UiState, + pub tracing_guard: Option, + pub tray: Option, +} + +// Global application state +// Read / write this state via the `lock_r!` / `lock_w!` macros from `fdoll-core::utilities` +pub static FDOLL: LazyLock>> = + LazyLock::new(|| Arc::new(RwLock::new(AppState::default()))); + +pub fn init_fdoll_state(tracing_guard: Option) { + { + let mut guard = lock_w!(FDOLL); + dotenvy::dotenv().ok(); + guard.tracing_guard = tracing_guard; + 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(); + } + + info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)"); +} diff --git a/src-tauri/src/state/network.rs b/src-tauri/src/state/network.rs new file mode 100644 index 0000000..94b6be7 --- /dev/null +++ b/src-tauri/src/state/network.rs @@ -0,0 +1,35 @@ + + +#[derive(Clone)] +pub struct Clients { + pub http_client: reqwest::Client, + pub ws_client: Option, + pub is_ws_initialized: bool, +} + +pub struct NetworkState { + pub clients: Option, +} + +impl Default for NetworkState { + fn default() -> Self { + Self { clients: None } + } +} + +pub fn init_network_state() -> NetworkState { + let http_client = reqwest::ClientBuilder::new() + .timeout(std::time::Duration::from_secs(30)) + .connect_timeout(std::time::Duration::from_secs(10)) + .user_agent("friendolls-desktop/0.1.0") + .build() + .expect("Client should build"); + + NetworkState { + clients: Some(Clients { + http_client, + ws_client: None, + is_ws_initialized: false, + }), + } +} \ No newline at end of file diff --git a/src-tauri/src/state.rs b/src-tauri/src/state/ui.rs similarity index 54% rename from src-tauri/src/state.rs rename to src-tauri/src/state/ui.rs index f2a087d..525c93c 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state/ui.rs @@ -1,153 +1,99 @@ -// in app-core/src/state.rs use crate::{ get_app_handle, lock_r, lock_w, models::app_data::AppData, remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, - services::{ - auth::{load_auth_pass, AuthPass}, - client_config_manager::{load_app_config, AppConfig}, - }, }; use std::{ collections::HashSet, - sync::{Arc, LazyLock, RwLock}, + sync::{LazyLock}, }; use tauri::Emitter; use tokio::sync::Mutex; use tracing::{info, warn}; -#[derive(Default, Clone)] -pub struct OAuthFlowTracker { - pub state: Option, - pub code_verifier: Option, - pub initiated_at: Option, - pub cancel_token: Option, -} - -pub struct Clients { - pub http_client: reqwest::Client, - pub ws_client: Option, - pub is_ws_initialized: bool, -} - -#[derive(Default)] -pub struct AppState { - pub app_config: AppConfig, - pub clients: Option, - pub auth_pass: Option, - pub oauth_flow: OAuthFlowTracker, - pub tracing_guard: Option, - pub tray: Option, - - // exposed to the frontend +pub struct UiState { pub app_data: AppData, } -// Global application state -// Read / write this state via the `lock_r!` / `lock_w!` macros from `fdoll-core::utilities` -pub static FDOLL: LazyLock>> = - LazyLock::new(|| Arc::new(RwLock::new(AppState::default()))); - -pub fn init_fdoll_state(tracing_guard: Option) { - { - let mut guard = lock_w!(FDOLL); - dotenvy::dotenv().ok(); - guard.tracing_guard = tracing_guard; - guard.app_config = load_app_config(); - guard.auth_pass = match load_auth_pass() { - Ok(pass) => pass, - Err(e) => { - warn!("Failed to load auth pass from keyring: {e}"); - None - } - }; - info!("Loaded auth pass"); - - // Initialize HTTP client immediately (non-blocking) - let http_client = reqwest::ClientBuilder::new() - .timeout(std::time::Duration::from_secs(30)) - .connect_timeout(std::time::Duration::from_secs(10)) - .user_agent("friendolls-desktop/0.1.0") - .build() - .expect("Client should build"); - - // Store HTTP client immediately - WebSocket client will be added later - guard.clients = Some(Clients { - http_client, - ws_client: None, - is_ws_initialized: false, - }); - info!("Initialized HTTP client"); - - // Initialize screen dimensions - let app_handle = get_app_handle(); - - // 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; - loop { - match app_handle.primary_monitor() { - Ok(Some(monitor)) => { - info!("Primary monitor acquired for state initialization"); - break Some(monitor); - } - Ok(None) => { - retry_count += 1; - if retry_count >= max_retries { - warn!( - "No primary monitor found after {} retries during state init", - max_retries - ); - break None; - } - warn!( - "Primary monitor not available during state init, retrying... ({}/{})", - retry_count, max_retries - ); - std::thread::sleep(std::time::Duration::from_millis(100)); - } - Err(e) => { - retry_count += 1; - if retry_count >= max_retries { - warn!("Failed to get primary monitor during state init: {}", e); - break None; - } - warn!( - "Error getting primary monitor during state init, retrying... ({}/{}): {}", - retry_count, max_retries, e - ); - std::thread::sleep(std::time::Duration::from_millis(100)); - } - } - } - }; - - if let Some(monitor) = primary_monitor { - let monitor_dimensions = monitor.size(); - let monitor_scale_factor = monitor.scale_factor(); - let logical_monitor_dimensions: tauri::LogicalSize = - monitor_dimensions.to_logical(monitor_scale_factor); - - guard.app_data.scene.display.screen_width = logical_monitor_dimensions.width; - guard.app_data.scene.display.screen_height = logical_monitor_dimensions.height; - guard.app_data.scene.display.monitor_scale_factor = monitor_scale_factor; - guard.app_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, - guard.app_data.scene.grid_size - ); - } else { - warn!("Could not initialize screen dimensions in global state - no monitor found"); +impl Default for UiState { + fn default() -> Self { + Self { + app_data: AppData::default(), } } +} - info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)"); +pub fn init_ui_state() -> UiState { + let mut ui_state = UiState::default(); + + // Initialize screen dimensions + let app_handle = get_app_handle(); + + // 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; + loop { + match app_handle.primary_monitor() { + Ok(Some(monitor)) => { + info!("Primary monitor acquired for state initialization"); + break Some(monitor); + } + Ok(None) => { + retry_count += 1; + if retry_count >= max_retries { + warn!( + "No primary monitor found after {} retries during state init", + max_retries + ); + break None; + } + warn!( + "Primary monitor not available during state init, retrying... ({}/{})", + retry_count, max_retries + ); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + Err(e) => { + retry_count += 1; + if retry_count >= max_retries { + warn!("Failed to get primary monitor during state init: {}", e); + break None; + } + warn!( + "Error getting primary monitor during state init, retrying... ({}/{}): {}", + retry_count, max_retries, e + ); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + } + } + }; + + if let Some(monitor) = primary_monitor { + let monitor_dimensions = monitor.size(); + let monitor_scale_factor = monitor.scale_factor(); + 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 + + 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 + ); + } 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 @@ -172,6 +118,11 @@ 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>> = + LazyLock::new(|| Mutex::new(HashSet::new())); + /// Populate specific parts of app data from the server based on the scope. /// /// # Arguments @@ -185,11 +136,6 @@ pub async fn init_app_data() { /// // Refresh only user profile when updating user settings /// init_app_data_scoped(AppDataRefreshScope::User).await; /// ``` -static REFRESH_IN_FLIGHT: LazyLock>> = - LazyLock::new(|| Mutex::new(HashSet::new())); -static REFRESH_PENDING: LazyLock>> = - LazyLock::new(|| Mutex::new(HashSet::new())); - pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { loop { // Deduplicate concurrent refreshes for the same scope @@ -212,8 +158,8 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::User) { match user_remote.get_user(None).await { Ok(user) => { - let mut guard = lock_w!(FDOLL); - guard.app_data.user = Some(user); + let mut guard = lock_w!(crate::state::FDOLL); + guard.ui.app_data.user = Some(user); } Err(e) => { warn!("Failed to fetch user profile: {}", e); @@ -240,8 +186,8 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { ) { match friend_remote.get_friends().await { Ok(friends) => { - let mut guard = lock_w!(FDOLL); - guard.app_data.friends = Some(friends); + let mut guard = lock_w!(crate::state::FDOLL); + guard.ui.app_data.friends = Some(friends); } Err(e) => { warn!("Failed to fetch friends list: {}", e); @@ -265,8 +211,8 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::Dolls) { match dolls_remote.get_dolls().await { Ok(dolls) => { - let mut guard = lock_w!(FDOLL); - guard.app_data.dolls = Some(dolls); + let mut guard = lock_w!(crate::state::FDOLL); + guard.ui.app_data.dolls = Some(dolls); } Err(e) => { warn!("Failed to fetch dolls list: {}", e); @@ -288,8 +234,8 @@ 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!(FDOLL); // Use read lock to get data - let app_data_clone = guard.app_data.clone(); + let guard = lock_r!(crate::state::FDOLL); // Use read lock to get data + let app_data_clone = guard.ui.app_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) { @@ -334,4 +280,4 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { break; } -} +} \ No newline at end of file