state management refactoring
This commit is contained in:
@@ -7,12 +7,12 @@ use crate::{
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_app_data() -> Result<AppData, String> {
|
pub fn get_app_data() -> Result<AppData, String> {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
Ok(guard.app_data.clone())
|
Ok(guard.ui.app_data.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn refresh_app_data() -> Result<AppData, String> {
|
pub async fn refresh_app_data() -> Result<AppData, String> {
|
||||||
init_app_data().await;
|
init_app_data().await;
|
||||||
let guard = lock_r!(FDOLL);
|
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 is_active_doll = {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
guard
|
guard
|
||||||
|
.ui
|
||||||
.app_data
|
.app_data
|
||||||
.user
|
.user
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -82,6 +83,7 @@ pub async fn delete_doll(id: String) -> Result<(), String> {
|
|||||||
let is_active_doll = {
|
let is_active_doll = {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
guard
|
guard
|
||||||
|
.ui
|
||||||
.app_data
|
.app_data
|
||||||
.user
|
.user
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ impl DollsRemote {
|
|||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.clone(),
|
.clone(),
|
||||||
client: guard
|
client: guard
|
||||||
.clients
|
.network.clients
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.http_client
|
.http_client
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ impl FriendRemote {
|
|||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.clone(),
|
.clone(),
|
||||||
client: guard
|
client: guard
|
||||||
.clients
|
.network.clients
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.http_client
|
.http_client
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ impl HealthRemote {
|
|||||||
.ok_or(HealthError::ConfigMissing("api_base_url"))?;
|
.ok_or(HealthError::ConfigMissing("api_base_url"))?;
|
||||||
|
|
||||||
let client = guard
|
let client = guard
|
||||||
.clients
|
.network.clients
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.http_client.clone())
|
.map(|c| c.http_client.clone())
|
||||||
.ok_or(HealthError::ConfigMissing("http_client"))?;
|
.ok_or(HealthError::ConfigMissing("http_client"))?;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ impl SessionRemote {
|
|||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.clone(),
|
.clone(),
|
||||||
client: guard
|
client: guard
|
||||||
.clients
|
.network.clients
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.http_client
|
.http_client
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ impl UserRemote {
|
|||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.clone(),
|
.clone(),
|
||||||
client: guard
|
client: guard
|
||||||
.clients
|
.network.clients
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.http_client
|
.http_client
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ const SERVICE_NAME: &str = "friendolls";
|
|||||||
|
|
||||||
pub fn cancel_auth_flow() {
|
pub fn cancel_auth_flow() {
|
||||||
let mut guard = lock_w!(FDOLL);
|
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();
|
token.cancel();
|
||||||
}
|
}
|
||||||
guard.oauth_flow = Default::default();
|
guard.auth.oauth_flow = Default::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors that can occur during OAuth authentication flow.
|
/// Errors that can occur during OAuth authentication flow.
|
||||||
@@ -116,13 +116,13 @@ fn generate_code_challenge(code_verifier: &str) -> String {
|
|||||||
/// Automatically refreshes if expired.
|
/// Automatically refreshes if expired.
|
||||||
pub async fn get_tokens() -> Option<AuthPass> {
|
pub async fn get_tokens() -> Option<AuthPass> {
|
||||||
info!("Retrieving tokens");
|
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;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(issued_at) = auth_pass.issued_at else {
|
let Some(issued_at) = auth_pass.issued_at else {
|
||||||
warn!("Auth pass missing issued_at timestamp, clearing");
|
warn!("Auth pass missing issued_at timestamp, clearing");
|
||||||
lock_w!(FDOLL).auth_pass = None;
|
lock_w!(FDOLL).auth.auth_pass = None;
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ pub async fn get_tokens() -> Option<AuthPass> {
|
|||||||
|
|
||||||
if refresh_expired {
|
if refresh_expired {
|
||||||
info!("Refresh token expired, clearing auth state");
|
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() {
|
if let Err(e) = clear_auth_pass() {
|
||||||
error!("Failed to clear expired auth pass: {}", e);
|
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;
|
let _guard = REFRESH_LOCK.lock().await;
|
||||||
|
|
||||||
// Double-check after acquiring lock
|
// 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 current_time = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
|
||||||
let expired = current_time - auth_pass.issued_at? >= auth_pass.expires_in;
|
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),
|
Ok(new_pass) => Some(new_pass),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to refresh token: {}", 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() {
|
if let Err(e) = clear_auth_pass() {
|
||||||
error!("Failed to clear auth pass after refresh failure: {}", e);
|
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> {
|
pub fn logout() -> Result<(), OAuthError> {
|
||||||
info!("Logging out user");
|
info!("Logging out user");
|
||||||
lock_w!(FDOLL).auth_pass = None;
|
lock_w!(FDOLL).auth.auth_pass = None;
|
||||||
clear_auth_pass()?;
|
clear_auth_pass()?;
|
||||||
|
|
||||||
// Clear OAuth flow state as well
|
// 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
|
// TODO: Call OAuth provider's revocation endpoint
|
||||||
// This would require adding a revoke_token() function that calls:
|
// 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 (refresh_token, session_state, base_url) = {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
(
|
(
|
||||||
guard.auth_pass.as_ref().map(|p| p.refresh_token.clone()),
|
guard.auth.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.session_state.clone()),
|
||||||
guard
|
guard
|
||||||
.app_config
|
.app_config
|
||||||
.api_base_url
|
.api_base_url
|
||||||
@@ -460,7 +460,7 @@ pub async fn exchange_code_for_auth_pass(
|
|||||||
) -> Result<AuthPass, OAuthError> {
|
) -> Result<AuthPass, OAuthError> {
|
||||||
let (app_config, http_client) = {
|
let (app_config, http_client) = {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
let clients = guard.clients.as_ref();
|
let clients = guard.network.clients.as_ref();
|
||||||
if clients.is_none() {
|
if clients.is_none() {
|
||||||
error!("Clients not initialized yet!");
|
error!("Clients not initialized yet!");
|
||||||
return Err(OAuthError::InvalidConfig);
|
return Err(OAuthError::InvalidConfig);
|
||||||
@@ -581,10 +581,10 @@ where
|
|||||||
|
|
||||||
{
|
{
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
guard.oauth_flow.state = Some(state.clone());
|
guard.auth.oauth_flow.state = Some(state.clone());
|
||||||
guard.oauth_flow.code_verifier = Some(code_verifier.clone());
|
guard.auth.oauth_flow.code_verifier = Some(code_verifier.clone());
|
||||||
guard.oauth_flow.initiated_at = Some(current_time);
|
guard.auth.oauth_flow.initiated_at = Some(current_time);
|
||||||
guard.oauth_flow.cancel_token = Some(cancel_token.clone());
|
guard.auth.oauth_flow.cancel_token = Some(cancel_token.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut url = match url::Url::parse(&format!("{}/auth", &app_config.auth.auth_url)) {
|
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 (stored_state, stored_verifier) = {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
(
|
(
|
||||||
guard.oauth_flow.state.clone(),
|
guard.auth.oauth_flow.state.clone(),
|
||||||
guard.oauth_flow.code_verifier.clone(),
|
guard.auth.oauth_flow.code_verifier.clone(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -675,8 +675,8 @@ where
|
|||||||
Ok(auth_pass) => {
|
Ok(auth_pass) => {
|
||||||
{
|
{
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
guard.auth_pass = Some(auth_pass.clone());
|
guard.auth.auth_pass = Some(auth_pass.clone());
|
||||||
guard.oauth_flow = Default::default();
|
guard.auth.oauth_flow = Default::default();
|
||||||
}
|
}
|
||||||
if let Err(e) = save_auth_pass(&auth_pass) {
|
if let Err(e) = save_auth_pass(&auth_pass) {
|
||||||
error!("Failed to save auth pass: {}", e);
|
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.app_config.clone(),
|
||||||
guard
|
guard
|
||||||
.clients
|
.network.clients
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("clients present")
|
.expect("clients present")
|
||||||
.http_client
|
.http_client
|
||||||
@@ -765,7 +765,7 @@ pub async fn refresh_token(refresh_token: &str) -> Result<AuthPass, OAuthError>
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Update state and storage
|
// 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) {
|
if let Err(e) = save_auth_pass(&auth_pass) {
|
||||||
error!("Failed to save refreshed auth pass: {}", e);
|
error!("Failed to save refreshed auth pass: {}", e);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ pub fn get_latest_cursor_position() -> Option<CursorPosition> {
|
|||||||
/// Convert absolute screen coordinates to normalized coordinates (0.0 - 1.0)
|
/// Convert absolute screen coordinates to normalized coordinates (0.0 - 1.0)
|
||||||
pub fn absolute_to_normalized(pos: &CursorPosition) -> CursorPosition {
|
pub fn absolute_to_normalized(pos: &CursorPosition) -> CursorPosition {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
let screen_w = guard.app_data.scene.display.screen_width as f64;
|
let screen_w = guard.ui.app_data.scene.display.screen_width as f64;
|
||||||
let screen_h = guard.app_data.scene.display.screen_height as f64;
|
let screen_h = guard.ui.app_data.scene.display.screen_height as f64;
|
||||||
|
|
||||||
CursorPosition {
|
CursorPosition {
|
||||||
x: (pos.x / screen_w).clamp(0.0, 1.0),
|
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
|
/// Convert normalized coordinates to absolute screen coordinates
|
||||||
pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition {
|
pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
let screen_w = guard.app_data.scene.display.screen_width as f64;
|
let screen_w = guard.ui.app_data.scene.display.screen_width as f64;
|
||||||
let screen_h = guard.app_data.scene.display.screen_height as f64;
|
let screen_h = guard.ui.app_data.scene.display.screen_height as f64;
|
||||||
|
|
||||||
CursorPosition {
|
CursorPosition {
|
||||||
x: (normalized.x * screen_w).round(),
|
x: (normalized.x * screen_w).round(),
|
||||||
@@ -124,7 +124,7 @@ async fn init_cursor_tracking() -> Result<(), String> {
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let scale_factor = {
|
let scale_factor = {
|
||||||
let guard = lock_r!(FDOLL);
|
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.
|
// The producer closure moves `tx` into it.
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ pub fn close_health_manager_window() {
|
|||||||
} else {
|
} else {
|
||||||
info!("Health manager window closed");
|
info!("Health manager window closed");
|
||||||
let guard = lock_r!(FDOLL);
|
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);
|
drop(guard);
|
||||||
update_system_tray(is_logged_in);
|
update_system_tray(is_logged_in);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub async fn send_interaction(dto: SendInteractionDto) -> Result<(), String> {
|
|||||||
// Check if WS is initialized
|
// Check if WS is initialized
|
||||||
let client = {
|
let client = {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
if let Some(clients) = &guard.clients {
|
if let Some(clients) = &guard.network.clients {
|
||||||
if clients.is_ws_initialized {
|
if clients.is_ws_initialized {
|
||||||
clients.ws_client.clone()
|
clients.ws_client.clone()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ fn on_initialized(payload: Payload, _socket: RawClient) {
|
|||||||
|
|
||||||
// Mark WebSocket as initialized and reset backoff timer
|
// Mark WebSocket as initialized and reset backoff timer
|
||||||
let mut guard = lock_w!(FDOLL);
|
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;
|
clients.is_ws_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ pub async fn init_ws_client() {
|
|||||||
match build_ws_client(&app_config).await {
|
match build_ws_client(&app_config).await {
|
||||||
Ok(ws_client) => {
|
Ok(ws_client) => {
|
||||||
let mut guard = lock_w!(FDOLL);
|
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.ws_client = Some(ws_client);
|
||||||
clients.is_ws_initialized = false; // wait for initialized event
|
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);
|
error!("Failed to initialize WebSocket client: {}", e);
|
||||||
// If we failed because no token, clear the WS client to avoid stale retries
|
// If we failed because no token, clear the WS client to avoid stale retries
|
||||||
let mut guard = lock_w!(FDOLL);
|
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.ws_client = None;
|
||||||
clients.is_ws_initialized = false;
|
clients.is_ws_initialized = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
|||||||
// and if clients are actually initialized.
|
// and if clients are actually initialized.
|
||||||
let (client_opt, is_initialized) = {
|
let (client_opt, is_initialized) = {
|
||||||
let guard = lock_r!(FDOLL);
|
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.ws_client.as_ref().cloned(),
|
||||||
clients.is_ws_initialized,
|
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 is_active_doll = if let Some(id) = doll_id {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
guard
|
guard
|
||||||
|
.ui
|
||||||
.app_data
|
.app_data
|
||||||
.user
|
.user
|
||||||
.as_ref()
|
.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 is_active_doll = if let Some(id) = doll_id {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
guard
|
guard
|
||||||
|
.ui
|
||||||
.app_data
|
.app_data
|
||||||
.user
|
.user
|
||||||
.as_ref()
|
.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::{
|
use crate::{
|
||||||
get_app_handle, lock_r, lock_w,
|
get_app_handle, lock_r, lock_w,
|
||||||
models::app_data::AppData,
|
models::app_data::AppData,
|
||||||
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
||||||
services::{
|
|
||||||
auth::{load_auth_pass, AuthPass},
|
|
||||||
client_config_manager::{load_app_config, AppConfig},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
sync::{Arc, LazyLock, RwLock},
|
sync::{LazyLock},
|
||||||
};
|
};
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
pub struct UiState {
|
||||||
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 app_data: AppData,
|
pub app_data: AppData,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global application state
|
impl Default for UiState {
|
||||||
// Read / write this state via the `lock_r!` / `lock_w!` macros from `fdoll-core::utilities`
|
fn default() -> Self {
|
||||||
pub static FDOLL: LazyLock<Arc<RwLock<AppState>>> =
|
Self {
|
||||||
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
|
app_data: AppData::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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
/// 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;
|
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.
|
/// Populate specific parts of app data from the server based on the scope.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -185,11 +136,6 @@ pub async fn init_app_data() {
|
|||||||
/// // Refresh only user profile when updating user settings
|
/// // Refresh only user profile when updating user settings
|
||||||
/// init_app_data_scoped(AppDataRefreshScope::User).await;
|
/// 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) {
|
pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
|
||||||
loop {
|
loop {
|
||||||
// Deduplicate concurrent refreshes for the same scope
|
// 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) {
|
if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::User) {
|
||||||
match user_remote.get_user(None).await {
|
match user_remote.get_user(None).await {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(crate::state::FDOLL);
|
||||||
guard.app_data.user = Some(user);
|
guard.ui.app_data.user = Some(user);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to fetch user profile: {}", 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 {
|
match friend_remote.get_friends().await {
|
||||||
Ok(friends) => {
|
Ok(friends) => {
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(crate::state::FDOLL);
|
||||||
guard.app_data.friends = Some(friends);
|
guard.ui.app_data.friends = Some(friends);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to fetch friends list: {}", 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) {
|
if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::Dolls) {
|
||||||
match dolls_remote.get_dolls().await {
|
match dolls_remote.get_dolls().await {
|
||||||
Ok(dolls) => {
|
Ok(dolls) => {
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(crate::state::FDOLL);
|
||||||
guard.app_data.dolls = Some(dolls);
|
guard.ui.app_data.dolls = Some(dolls);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to fetch dolls list: {}", 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
|
// Emit event regardless of partial success, frontend should handle nulls/empty states
|
||||||
{
|
{
|
||||||
let guard = lock_r!(FDOLL); // Use read lock to get data
|
let guard = lock_r!(crate::state::FDOLL); // Use read lock to get data
|
||||||
let app_data_clone = guard.app_data.clone();
|
let app_data_clone = guard.ui.app_data.clone();
|
||||||
drop(guard); // Drop lock before emitting to prevent potential deadlocks
|
drop(guard); // Drop lock before emitting to prevent potential deadlocks
|
||||||
|
|
||||||
if let Err(e) = get_app_handle().emit("app-data-refreshed", &app_data_clone) {
|
if let Err(e) = get_app_handle().emit("app-data-refreshed", &app_data_clone) {
|
||||||
Reference in New Issue
Block a user