state management refactoring
This commit is contained in:
@@ -7,12 +7,12 @@ use crate::{
|
||||
#[tauri::command]
|
||||
pub fn get_app_data() -> Result<AppData, String> {
|
||||
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<AppData, String> {
|
||||
init_app_data().await;
|
||||
let guard = lock_r!(FDOLL);
|
||||
Ok(guard.app_data.clone())
|
||||
Ok(guard.ui.app_data.clone())
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ pub async fn update_doll(id: String, dto: UpdateDollDto) -> Result<DollDto, Stri
|
||||
let is_active_doll = {
|
||||
let guard = lock_r!(FDOLL);
|
||||
guard
|
||||
.ui
|
||||
.app_data
|
||||
.user
|
||||
.as_ref()
|
||||
@@ -82,6 +83,7 @@ pub async fn delete_doll(id: String) -> Result<(), String> {
|
||||
let is_active_doll = {
|
||||
let guard = lock_r!(FDOLL);
|
||||
guard
|
||||
.ui
|
||||
.app_data
|
||||
.user
|
||||
.as_ref()
|
||||
|
||||
@@ -18,7 +18,7 @@ impl DollsRemote {
|
||||
.expect("App configuration error")
|
||||
.clone(),
|
||||
client: guard
|
||||
.clients
|
||||
.network.clients
|
||||
.as_ref()
|
||||
.expect("App configuration error")
|
||||
.http_client
|
||||
|
||||
@@ -18,7 +18,7 @@ impl FriendRemote {
|
||||
.expect("App configuration error")
|
||||
.clone(),
|
||||
client: guard
|
||||
.clients
|
||||
.network.clients
|
||||
.as_ref()
|
||||
.expect("App configuration error")
|
||||
.http_client
|
||||
|
||||
@@ -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"))?;
|
||||
|
||||
@@ -20,7 +20,7 @@ impl SessionRemote {
|
||||
.expect("App configuration error")
|
||||
.clone(),
|
||||
client: guard
|
||||
.clients
|
||||
.network.clients
|
||||
.as_ref()
|
||||
.expect("App configuration error")
|
||||
.http_client
|
||||
|
||||
@@ -18,7 +18,7 @@ impl UserRemote {
|
||||
.expect("App configuration error")
|
||||
.clone(),
|
||||
client: guard
|
||||
.clients
|
||||
.network.clients
|
||||
.as_ref()
|
||||
.expect("App configuration error")
|
||||
.http_client
|
||||
|
||||
@@ -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<AuthPass> {
|
||||
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<AuthPass> {
|
||||
|
||||
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<AuthPass> {
|
||||
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<AuthPass> {
|
||||
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<AuthPass, OAuthError> {
|
||||
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<AuthPass, OAuthError>
|
||||
(
|
||||
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<AuthPass, OAuthError>
|
||||
);
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -40,8 +40,8 @@ pub fn get_latest_cursor_position() -> Option<CursorPosition> {
|
||||
/// 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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
40
src-tauri/src/state/auth.rs
Normal file
40
src-tauri/src/state/auth.rs
Normal file
@@ -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<String>,
|
||||
pub code_verifier: Option<String>,
|
||||
pub initiated_at: Option<u64>,
|
||||
pub cancel_token: Option<tokio_util::sync::CancellationToken>,
|
||||
}
|
||||
|
||||
pub struct AuthState {
|
||||
pub auth_pass: Option<AuthPass>,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
44
src-tauri/src/state/mod.rs
Normal file
44
src-tauri/src/state/mod.rs
Normal file
@@ -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<tracing_appender::non_blocking::WorkerGuard>,
|
||||
pub tray: Option<TrayIcon>,
|
||||
}
|
||||
|
||||
// Global application state
|
||||
// Read / write this state via the `lock_r!` / `lock_w!` macros from `fdoll-core::utilities`
|
||||
pub static FDOLL: LazyLock<Arc<RwLock<AppState>>> =
|
||||
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
|
||||
|
||||
pub fn init_fdoll_state(tracing_guard: Option<tracing_appender::non_blocking::WorkerGuard>) {
|
||||
{
|
||||
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)");
|
||||
}
|
||||
35
src-tauri/src/state/network.rs
Normal file
35
src-tauri/src/state/network.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Clients {
|
||||
pub http_client: reqwest::Client,
|
||||
pub ws_client: Option<rust_socketio::client::Client>,
|
||||
pub is_ws_initialized: bool,
|
||||
}
|
||||
|
||||
pub struct NetworkState {
|
||||
pub clients: Option<Clients>,
|
||||
}
|
||||
|
||||
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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
pub code_verifier: Option<String>,
|
||||
pub initiated_at: Option<u64>,
|
||||
pub cancel_token: Option<tokio_util::sync::CancellationToken>,
|
||||
}
|
||||
|
||||
pub struct Clients {
|
||||
pub http_client: reqwest::Client,
|
||||
pub ws_client: Option<rust_socketio::client::Client>,
|
||||
pub is_ws_initialized: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AppState {
|
||||
pub app_config: AppConfig,
|
||||
pub clients: Option<Clients>,
|
||||
pub auth_pass: Option<AuthPass>,
|
||||
pub oauth_flow: OAuthFlowTracker,
|
||||
pub tracing_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
|
||||
pub tray: Option<tauri::tray::TrayIcon>,
|
||||
|
||||
// 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<Arc<RwLock<AppState>>> =
|
||||
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
|
||||
|
||||
pub fn init_fdoll_state(tracing_guard: Option<tracing_appender::non_blocking::WorkerGuard>) {
|
||||
{
|
||||
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<i32> =
|
||||
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<i32> =
|
||||
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<Mutex<HashSet<AppDataRefreshScope>>> =
|
||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
||||
static REFRESH_PENDING: LazyLock<Mutex<HashSet<AppDataRefreshScope>>> =
|
||||
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<Mutex<HashSet<AppDataRefreshScope>>> =
|
||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
||||
static REFRESH_PENDING: LazyLock<Mutex<HashSet<AppDataRefreshScope>>> =
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user