Rust service refactor: app data, session windows & client config pt 2

This commit is contained in:
2026-03-10 15:46:49 +08:00
parent 341dd48132
commit 858858ab48
23 changed files with 308 additions and 347 deletions

View File

@@ -2,9 +2,10 @@ use crate::{
lock_r, lock_r,
models::app_data::UserData, models::app_data::UserData,
services::{ services::{
app_data::{init_app_data_scoped, AppDataRefreshScope},
friends, presence_modules::models::ModuleMetadata, sprite, friends, presence_modules::models::ModuleMetadata, sprite,
}, },
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}, state::FDOLL,
}; };
#[tauri::command] #[tauri::command]

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
lock_w, lock_w,
services::client_config_manager::{ services::client_config::{
load_app_config, open_config_manager_window, save_app_config, AppConfig, load_app_config, open_config_window, save_app_config, AppConfig,
}, },
state::FDOLL, state::FDOLL,
}; };
@@ -29,6 +29,6 @@ pub fn save_client_config(config: AppConfig) -> Result<(), String> {
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn open_client_config_manager() -> Result<(), String> { pub async fn open_client_config() -> Result<(), String> {
open_config_manager_window().map_err(|e| e.to_string()) open_config_window().map_err(|e| e.to_string())
} }

View File

@@ -1,11 +1,11 @@
use crate::{ use crate::{
commands::{is_active_doll, refresh_app_data, refresh_app_data_conditionally},
models::dolls::{CreateDollDto, DollDto, UpdateDollDto}, models::dolls::{CreateDollDto, DollDto, UpdateDollDto},
remotes::{ remotes::{
dolls::DollsRemote, dolls::DollsRemote,
user::UserRemote, user::UserRemote,
}, },
state::AppDataRefreshScope, services::app_data::AppDataRefreshScope,
commands::{refresh_app_data, refresh_app_data_conditionally, is_active_doll},
}; };
#[tauri::command] #[tauri::command]

View File

@@ -2,8 +2,8 @@ use crate::remotes::friends::FriendRemote;
use crate::models::friends::{ use crate::models::friends::{
FriendRequestResponseDto, FriendshipResponseDto, SendFriendRequestDto, UserBasicDto, FriendRequestResponseDto, FriendshipResponseDto, SendFriendRequestDto, UserBasicDto,
}; };
use crate::state::AppDataRefreshScope;
use crate::commands::refresh_app_data; use crate::commands::refresh_app_data;
use crate::services::app_data::AppDataRefreshScope;
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]

View File

@@ -9,7 +9,10 @@ pub mod petpet;
pub mod sprite; pub mod sprite;
use crate::lock_r; use crate::lock_r;
use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}; use crate::{
services::app_data::{init_app_data_scoped, AppDataRefreshScope},
state::FDOLL,
};
use tauri::async_runtime; use tauri::async_runtime;
/// Helper to execute a mutation operation and refresh app data scopes in the background. /// Helper to execute a mutation operation and refresh app data scopes in the background.

View File

@@ -7,16 +7,14 @@ use crate::{
models::health::HealthError, models::health::HealthError,
remotes::health::HealthRemote, remotes::health::HealthRemote,
services::{ services::{
close_all_windows, app_data::{clear_app_data, init_app_data_scoped, AppDataRefreshScope},
health_manager::open_health_manager_window, health_manager::open_health_manager_window,
health_monitor::{start_health_monitor, stop_health_monitor}, health_monitor::{start_health_monitor, stop_health_monitor},
scene::open_scene_window, scene::open_scene_window,
session_windows::close_all_windows,
ws::client::{clear_ws_client, establish_websocket_connection}, ws::client::{clear_ws_client, establish_websocket_connection},
}, },
state::{ state::auth::{start_background_token_refresh, stop_background_token_refresh},
auth::{start_background_token_refresh, stop_background_token_refresh},
clear_app_data, init_app_data_scoped, AppDataRefreshScope,
},
system_tray::update_system_tray, system_tray::update_system_tray,
}; };

View File

@@ -11,7 +11,7 @@ use commands::app_state::{
refresh_app_data, refresh_app_data,
}; };
use commands::auth::{change_password, login, logout_and_restart, register, reset_password}; use commands::auth::{change_password, login, logout_and_restart, register, reset_password};
use commands::config::{get_client_config, open_client_config_manager, save_client_config}; use commands::config::{get_client_config, open_client_config, save_client_config};
use commands::dolls::{ use commands::dolls::{
create_doll, delete_doll, get_doll, get_dolls, remove_active_doll, set_active_doll, update_doll, create_doll, delete_doll, get_doll, get_dolls, remove_active_doll, set_active_doll, update_doll,
}; };
@@ -94,7 +94,7 @@ pub fn run() {
retry_connection, retry_connection,
get_client_config, get_client_config,
save_client_config, save_client_config,
open_client_config_manager, open_client_config,
open_doll_editor_window, open_doll_editor_window,
get_scene_interactive, get_scene_interactive,
set_scene_interactive, set_scene_interactive,

View File

@@ -0,0 +1,70 @@
use crate::{get_app_handle, lock_w, state::FDOLL};
use tracing::{info, warn};
pub fn update_display_dimensions_for_scene_state() {
let app_handle = get_app_handle();
let mut guard = lock_w!(FDOLL);
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(error) => {
retry_count += 1;
if retry_count >= max_retries {
warn!("Failed to get primary monitor during state init: {}", error);
break None;
}
warn!(
"Error getting primary monitor during state init, retrying... ({}/{}): {}",
retry_count, max_retries, error
);
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.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;
info!(
"Initialized global AppData with screen dimensions: {}x{}, scale: {}, grid: {}",
logical_monitor_dimensions.width,
logical_monitor_dimensions.height,
monitor_scale_factor,
guard.user_data.scene.grid_size
);
} else {
warn!("Could not initialize screen dimensions in global state - no monitor found");
}
}

View File

@@ -0,0 +1,5 @@
mod display;
mod refresh;
pub use display::update_display_dimensions_for_scene_state;
pub use refresh::{clear_app_data, init_app_data_scoped, AppDataRefreshScope};

View File

@@ -0,0 +1,174 @@
use std::{collections::HashSet, sync::LazyLock};
use tauri_plugin_dialog::MessageDialogBuilder;
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
use tauri_specta::Event as _;
use tokio::sync::Mutex;
use tracing::warn;
use crate::{
get_app_handle, lock_r, lock_w,
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
services::{
app_events::{ActiveDollSpriteChanged, AppDataRefreshed},
friends, sprite,
},
state::FDOLL,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AppDataRefreshScope {
All,
User,
Friends,
Dolls,
}
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 {
{
let mut in_flight = REFRESH_IN_FLIGHT.lock().await;
if in_flight.contains(&scope) {
let mut pending = REFRESH_PENDING.lock().await;
pending.insert(scope);
return;
}
in_flight.insert(scope);
}
let result: Result<(), ()> = async {
let user_remote = UserRemote::new();
let friend_remote = FriendRemote::new();
let dolls_remote = DollsRemote::new();
if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::User) {
match user_remote.get_user(None).await {
Ok(user) => {
let mut guard = lock_w!(FDOLL);
guard.user_data.user = Some(user);
}
Err(error) => {
warn!("Failed to fetch user profile: {}", error);
show_refresh_error_dialog(
"Network Error",
"Failed to fetch user profile. You may be offline.",
);
return Err(());
}
}
}
if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::Friends) {
match friend_remote.get_friends().await {
Ok(friends_list) => {
let mut guard = lock_w!(FDOLL);
guard.user_data.friends = Some(friends_list);
drop(guard);
friends::sync_from_app_data();
}
Err(error) => {
warn!("Failed to fetch friends list: {}", error);
show_refresh_error_dialog(
"Network Error",
"Failed to fetch friends list. You may be offline.",
);
return Err(());
}
}
}
if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::Dolls) {
match dolls_remote.get_dolls().await {
Ok(dolls) => {
let mut guard = lock_w!(FDOLL);
guard.user_data.dolls = Some(dolls);
}
Err(error) => {
warn!("Failed to fetch dolls list: {}", error);
show_refresh_error_dialog(
"Network Error",
"Failed to fetch dolls list. You may be offline.",
);
return Err(());
}
}
}
emit_refresh_events(scope);
Ok(())
}
.await;
{
let mut in_flight = REFRESH_IN_FLIGHT.lock().await;
in_flight.remove(&scope);
}
let rerun = {
let mut pending = REFRESH_PENDING.lock().await;
pending.remove(&scope)
};
if rerun {
continue;
}
if result.is_err() {
return;
}
break;
}
}
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;
drop(guard);
friends::clear();
}
fn emit_refresh_events(scope: AppDataRefreshScope) {
let guard = lock_r!(FDOLL);
let app_data = guard.user_data.clone();
drop(guard);
if let Err(error) = AppDataRefreshed(app_data).emit(get_app_handle()) {
warn!("Failed to emit app-data-refreshed event: {}", error);
show_refresh_error_dialog(
"Sync Error",
"Could not broadcast refreshed data to the UI. Some data may be stale.",
);
}
if matches!(
scope,
AppDataRefreshScope::All | AppDataRefreshScope::User | AppDataRefreshScope::Dolls
) {
match sprite::get_active_doll_sprite_base64() {
Ok(sprite_b64) => {
if let Err(error) = ActiveDollSpriteChanged(sprite_b64).emit(get_app_handle()) {
warn!("Failed to emit active-doll-sprite-changed event: {}", error);
}
}
Err(error) => {
warn!("Failed to generate active doll sprite: {}", error);
}
}
}
}
fn show_refresh_error_dialog(title: &str, message: &str) {
let handle = get_app_handle();
MessageDialogBuilder::new(handle.dialog().clone(), title, message)
.kind(MessageDialogKind::Error)
.show(|_| {});
}

View File

@@ -6,7 +6,7 @@ use specta::Type;
use thiserror::Error; use thiserror::Error;
pub use store::{load_app_config, save_app_config}; pub use store::{load_app_config, save_app_config};
pub use window::open_config_manager_window; pub use window::open_config_window;
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
pub struct AppConfig { pub struct AppConfig {
@@ -23,10 +23,10 @@ pub enum ClientConfigError {
Parse(#[from] serde_json::Error), Parse(#[from] serde_json::Error),
#[error("failed to run on main thread: {0}")] #[error("failed to run on main thread: {0}")]
Dispatch(#[from] tauri::Error), Dispatch(#[from] tauri::Error),
#[error("failed to build client config manager window: {0}")] #[error("failed to build client config window: {0}")]
Window(tauri::Error), Window(tauri::Error),
#[error("failed to show client config manager window: {0}")] #[error("failed to show client config window: {0}")]
ShowWindow(tauri::Error), ShowWindow(tauri::Error),
} }
pub static CLIENT_CONFIG_MANAGER_WINDOW_LABEL: &str = "client_config_manager"; pub static CLIENT_CONFIG_WINDOW_LABEL: &str = "client_config";

View File

@@ -3,28 +3,28 @@ use tracing::error;
use crate::get_app_handle; use crate::get_app_handle;
use super::{ClientConfigError, CLIENT_CONFIG_MANAGER_WINDOW_LABEL}; use super::{ClientConfigError, CLIENT_CONFIG_WINDOW_LABEL};
#[tauri::command] #[tauri::command]
pub fn open_config_manager_window() -> Result<(), ClientConfigError> { pub fn open_config_window() -> Result<(), ClientConfigError> {
let app_handle = get_app_handle(); let app_handle = get_app_handle();
let existing_webview_window = app_handle.get_window(CLIENT_CONFIG_MANAGER_WINDOW_LABEL); let existing_webview_window = app_handle.get_window(CLIENT_CONFIG_WINDOW_LABEL);
if let Some(window) = existing_webview_window { if let Some(window) = existing_webview_window {
if let Err(e) = window.show() { if let Err(e) = window.show() {
error!("Failed to show client config manager window: {e}"); error!("Failed to show client config window: {e}");
return Err(ClientConfigError::ShowWindow(e)); return Err(ClientConfigError::ShowWindow(e));
} }
if let Err(e) = window.set_focus() { if let Err(e) = window.set_focus() {
error!("Failed to focus client config manager window: {e}"); error!("Failed to focus client config window: {e}");
} }
return Ok(()); return Ok(());
} }
match tauri::WebviewWindowBuilder::new( match tauri::WebviewWindowBuilder::new(
app_handle, app_handle,
CLIENT_CONFIG_MANAGER_WINDOW_LABEL, CLIENT_CONFIG_WINDOW_LABEL,
tauri::WebviewUrl::App("/client-config-manager".into()), tauri::WebviewUrl::App("/client-config".into()),
) )
.title("Advanced Configuration") .title("Advanced Configuration")
.inner_size(300.0, 420.0) .inner_size(300.0, 420.0)
@@ -35,16 +35,16 @@ pub fn open_config_manager_window() -> Result<(), ClientConfigError> {
{ {
Ok(window) => { Ok(window) => {
if let Err(e) = window.show() { if let Err(e) = window.show() {
error!("Failed to show client config manager window: {}", e); error!("Failed to show client config window: {}", e);
return Err(ClientConfigError::ShowWindow(e)); return Err(ClientConfigError::ShowWindow(e));
} }
if let Err(e) = window.set_focus() { if let Err(e) = window.set_focus() {
error!("Failed to focus client config manager window: {e}"); error!("Failed to focus client config window: {e}");
} }
Ok(()) Ok(())
} }
Err(e) => { Err(e) => {
error!("Failed to build client config manager window: {}", e); error!("Failed to build client config window: {}", e);
Err(ClientConfigError::Window(e)) Err(ClientConfigError::Window(e))
} }
} }

View File

@@ -1,12 +1,8 @@
use tauri::Manager; pub mod app_data;
use crate::get_app_handle;
use tracing::warn;
pub mod app_events; pub mod app_events;
pub mod app_menu; pub mod app_menu;
pub mod auth; pub mod auth;
pub mod client_config_manager; pub mod client_config;
pub mod cursor; pub mod cursor;
pub mod doll_editor; pub mod doll_editor;
pub mod friends; pub mod friends;
@@ -16,17 +12,8 @@ pub mod interaction;
pub mod petpet; pub mod petpet;
pub mod presence_modules; pub mod presence_modules;
pub mod scene; pub mod scene;
pub mod session_windows;
pub mod sprite; pub mod sprite;
pub mod sprite_recolor; pub mod sprite_recolor;
pub mod welcome; pub mod welcome;
pub mod ws; 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 {
if let Err(err) = window.1.close() {
warn!("Failed to close window '{}': {}", window.0, err);
}
}
}

View File

@@ -0,0 +1,13 @@
use tauri::Manager;
use tracing::warn;
use crate::get_app_handle;
pub fn close_all_windows() {
let app_handle = get_app_handle();
for (label, window) in app_handle.webview_windows() {
if let Err(error) = window.close() {
warn!("Failed to close window '{}': {}", label, error);
}
}
}

View File

@@ -7,7 +7,7 @@ use tracing::{error, info};
use crate::{ use crate::{
lock_r, lock_w, lock_r, lock_w,
services::{auth::get_access_token, client_config_manager::AppConfig}, services::{auth::get_access_token, client_config::AppConfig},
state::FDOLL, state::FDOLL,
}; };

View File

@@ -5,7 +5,7 @@ use super::{refresh, utils};
/// Handler for doll.created event /// Handler for doll.created event
pub fn on_doll_created(payload: Payload, _socket: RawClient) { pub fn on_doll_created(payload: Payload, _socket: RawClient) {
if utils::extract_text_value(payload, "doll.created").is_ok() { if utils::extract_text_value(payload, "doll.created").is_ok() {
refresh::refresh_app_data(crate::state::AppDataRefreshScope::Dolls); refresh::refresh_app_data(crate::services::app_data::AppDataRefreshScope::Dolls);
} }
} }

View File

@@ -6,6 +6,7 @@ use crate::models::event_payloads::{
FriendRequestDeniedPayload, FriendRequestReceivedPayload, FriendUserStatusPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload, FriendUserStatusPayload,
UnfriendedPayload, UnfriendedPayload,
}; };
use crate::services::app_data::AppDataRefreshScope;
use crate::services::app_events::{ use crate::services::app_events::{
FriendActiveDollChanged, FriendDisconnected, FriendRequestAccepted, FriendRequestDenied, FriendActiveDollChanged, FriendDisconnected, FriendRequestAccepted, FriendRequestDenied,
FriendRequestReceived, FriendUserStatusChanged, Unfriended, FriendRequestReceived, FriendUserStatusChanged, Unfriended,
@@ -14,7 +15,6 @@ use crate::services::{
cursor::{normalized_to_absolute, CursorPositions}, cursor::{normalized_to_absolute, CursorPositions},
friends, friends,
}; };
use crate::state::AppDataRefreshScope;
use super::{emitter, refresh, types::IncomingFriendCursorPayload, utils}; use super::{emitter, refresh, types::IncomingFriendCursorPayload, utils};

View File

@@ -1,6 +1,6 @@
use tauri::async_runtime; use tauri::async_runtime;
use crate::state::{init_app_data_scoped, AppDataRefreshScope}; use crate::services::app_data::{init_app_data_scoped, AppDataRefreshScope};
/// Refresh app data with the given scope /// Refresh app data with the given scope
pub fn refresh_app_data(scope: AppDataRefreshScope) { pub fn refresh_app_data(scope: AppDataRefreshScope) {

View File

@@ -1,6 +1,8 @@
// in app-core/src/state.rs // in app-core/src/state.rs
use crate::{ use crate::{
lock_w, models::app_data::UserData, services::presence_modules::models::ModuleMetadata, lock_w,
models::app_data::UserData,
services::{app_data::update_display_dimensions_for_scene_state, presence_modules::models::ModuleMetadata},
}; };
use std::sync::{Arc, LazyLock, RwLock}; use std::sync::{Arc, LazyLock, RwLock};
use tauri::tray::TrayIcon; use tauri::tray::TrayIcon;
@@ -8,11 +10,9 @@ use tracing::info;
pub mod auth; pub mod auth;
mod network; mod network;
mod ui;
pub use auth::*; pub use auth::*;
pub use network::*; pub use network::*;
pub use ui::*;
#[derive(Default)] #[derive(Default)]
pub struct Modules { pub struct Modules {
@@ -22,7 +22,7 @@ pub struct Modules {
#[derive(Default)] #[derive(Default)]
pub struct AppState { pub struct AppState {
pub app_config: crate::services::client_config_manager::AppConfig, pub app_config: crate::services::client_config::AppConfig,
pub network: NetworkState, pub network: NetworkState,
pub auth: AuthState, pub auth: AuthState,
pub user_data: UserData, pub user_data: UserData,
@@ -41,7 +41,7 @@ pub fn init_app_state() {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
{ {
let mut guard = lock_w!(FDOLL); let mut guard = lock_w!(FDOLL);
guard.app_config = crate::services::client_config_manager::load_app_config(); guard.app_config = crate::services::client_config::load_app_config();
guard.network = init_network_state(); guard.network = init_network_state();
guard.auth = init_auth_state(); guard.auth = init_auth_state();
guard.user_data = UserData::default(); guard.user_data = UserData::default();

View File

@@ -1,290 +0,0 @@
use crate::{
get_app_handle, lock_r, lock_w,
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
services::{
app_events::{ActiveDollSpriteChanged, AppDataRefreshed},
friends, sprite,
},
state::FDOLL,
};
use std::{collections::HashSet, sync::LazyLock};
use tauri_specta::Event as _;
use tokio::sync::Mutex;
use tracing::{info, warn};
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
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.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,
guard.user_data.scene.grid_size
);
} else {
warn!("Could not initialize screen dimensions in global state - no monitor found");
}
}
/// Defines which parts of AppData should be refreshed from the server
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AppDataRefreshScope {
/// Refresh all data (user profile + friends list + dolls list)
All,
/// Refresh only user profile
User,
/// Refresh only friends list
Friends,
/// Refresh only dolls list
Dolls,
}
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
/// * `scope` - Determines which data to refresh (All, User, or Friends)
///
/// # Examples
/// ```
/// // Refresh only friends list when a friend request is accepted
/// init_app_data_scoped(AppDataRefreshScope::Friends).await;
///
/// // Refresh only user profile when updating user settings
/// init_app_data_scoped(AppDataRefreshScope::User).await;
/// ```
pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
loop {
// Deduplicate concurrent refreshes for the same scope
{
let mut in_flight = REFRESH_IN_FLIGHT.lock().await;
if in_flight.contains(&scope) {
let mut pending = REFRESH_PENDING.lock().await;
pending.insert(scope);
return;
}
in_flight.insert(scope);
}
let result: Result<(), ()> = async {
let user_remote = UserRemote::new();
let friend_remote = FriendRemote::new();
let dolls_remote = DollsRemote::new();
// Fetch user profile if needed
if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::User) {
match user_remote.get_user(None).await {
Ok(user) => {
let mut guard = lock_w!(crate::state::FDOLL);
guard.user_data.user = Some(user);
}
Err(e) => {
warn!("Failed to fetch user profile: {}", e);
use tauri_plugin_dialog::MessageDialogBuilder;
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
let handle = get_app_handle();
MessageDialogBuilder::new(
handle.dialog().clone(),
"Network Error",
"Failed to fetch user profile. You may be offline.",
)
.kind(MessageDialogKind::Error)
.show(|_| {});
return Err(());
}
}
}
// Fetch friends list if needed
if matches!(
scope,
AppDataRefreshScope::All | AppDataRefreshScope::Friends
) {
match friend_remote.get_friends().await {
Ok(friends) => {
let mut guard = lock_w!(crate::state::FDOLL);
guard.user_data.friends = Some(friends);
drop(guard);
friends::sync_from_app_data();
}
Err(e) => {
warn!("Failed to fetch friends list: {}", e);
use tauri_plugin_dialog::MessageDialogBuilder;
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
let handle = get_app_handle();
MessageDialogBuilder::new(
handle.dialog().clone(),
"Network Error",
"Failed to fetch friends list. You may be offline.",
)
.kind(MessageDialogKind::Error)
.show(|_| {});
return Err(());
}
}
}
// Fetch dolls list if needed
if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::Dolls) {
match dolls_remote.get_dolls().await {
Ok(dolls) => {
let mut guard = lock_w!(crate::state::FDOLL);
guard.user_data.dolls = Some(dolls);
}
Err(e) => {
warn!("Failed to fetch dolls list: {}", e);
use tauri_plugin_dialog::MessageDialogBuilder;
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
let handle = get_app_handle();
MessageDialogBuilder::new(
handle.dialog().clone(),
"Network Error",
"Failed to fetch dolls list. You may be offline.",
)
.kind(MessageDialogKind::Error)
.show(|_| {});
return Err(());
}
}
}
// 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.user_data.clone();
drop(guard); // Drop lock before emitting to prevent potential deadlocks
if let Err(e) = AppDataRefreshed(app_data_clone).emit(get_app_handle()) {
warn!("Failed to emit app-data-refreshed event: {}", e);
use tauri_plugin_dialog::MessageDialogBuilder;
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
let handle = get_app_handle();
MessageDialogBuilder::new(
handle.dialog().clone(),
"Sync Error",
"Could not broadcast refreshed data to the UI. Some data may be stale.",
)
.kind(MessageDialogKind::Error)
.show(|_| {});
}
if matches!(
scope,
AppDataRefreshScope::All
| AppDataRefreshScope::User
| AppDataRefreshScope::Dolls
) {
match sprite::get_active_doll_sprite_base64() {
Ok(sprite_b64) => {
if let Err(e) =
ActiveDollSpriteChanged(sprite_b64).emit(get_app_handle())
{
warn!("Failed to emit active-doll-sprite-changed event: {}", e);
}
}
Err(e) => {
warn!("Failed to generate active doll sprite: {}", e);
}
}
}
}
Ok(())
}
.await;
// Clear in-flight marker even on early exit
{
let mut in_flight = REFRESH_IN_FLIGHT.lock().await;
in_flight.remove(&scope);
}
// If a refresh was queued while this one was running, run again
let rerun = {
let mut pending = REFRESH_PENDING.lock().await;
pending.remove(&scope)
};
if rerun {
continue;
}
if result.is_err() {
return;
}
break;
}
}
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;
drop(guard);
friends::clear();
}

View File

@@ -89,8 +89,8 @@ async getClientConfig() : Promise<AppConfig> {
async saveClientConfig(config: AppConfig) : Promise<null> { async saveClientConfig(config: AppConfig) : Promise<null> {
return await TAURI_INVOKE("save_client_config", { config }); return await TAURI_INVOKE("save_client_config", { config });
}, },
async openClientConfigManager() : Promise<null> { async openClientConfig() : Promise<null> {
return await TAURI_INVOKE("open_client_config_manager"); return await TAURI_INVOKE("open_client_config");
}, },
async openDollEditorWindow(dollId: string | null) : Promise<void> { async openDollEditorWindow(dollId: string | null) : Promise<void> {
await TAURI_INVOKE("open_doll_editor_window", { dollId }); await TAURI_INVOKE("open_doll_editor_window", { dollId });