Rust service refactor: app data, session windows & client config pt 2
This commit is contained in:
@@ -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]
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
70
src-tauri/src/services/app_data/display.rs
Normal file
70
src-tauri/src/services/app_data/display.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src-tauri/src/services/app_data/mod.rs
Normal file
5
src-tauri/src/services/app_data/mod.rs
Normal 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};
|
||||||
174
src-tauri/src/services/app_data/refresh.rs
Normal file
174
src-tauri/src/services/app_data/refresh.rs
Normal 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(|_| {});
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
13
src-tauri/src/services/session_windows.rs
Normal file
13
src-tauri/src/services/session_windows.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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 });
|
||||||
|
|||||||
Reference in New Issue
Block a user