minor refactoring of app startup sequence & some extra trivial matters
This commit is contained in:
@@ -1,18 +1,18 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
lock_r,
|
lock_r,
|
||||||
models::app_data::AppData,
|
models::app_data::AppData,
|
||||||
state::{init_app_data, FDOLL},
|
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[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.ui.app_data.clone())
|
Ok(guard.user_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_scoped(AppDataRefreshScope::All).await;
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
Ok(guard.ui.app_data.clone())
|
Ok(guard.user_data.clone())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use tauri;
|
use tauri;
|
||||||
use tracing;
|
use tracing;
|
||||||
|
|
||||||
use crate::init::lifecycle;
|
use crate::{init::lifecycle::construct_user_session, services::scene::close_splash_window};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn logout_and_restart() -> Result<(), String> {
|
pub async fn logout_and_restart() -> Result<(), String> {
|
||||||
@@ -16,11 +16,11 @@ pub fn start_auth_flow() -> Result<(), String> {
|
|||||||
crate::services::auth::cancel_auth_flow();
|
crate::services::auth::cancel_auth_flow();
|
||||||
|
|
||||||
crate::services::auth::init_auth_code_retrieval(|| {
|
crate::services::auth::init_auth_code_retrieval(|| {
|
||||||
tracing::info!("Authentication successful, creating scene...");
|
tracing::info!("Authentication successful, constructing user session...");
|
||||||
// Close welcome window if it's still open
|
|
||||||
crate::services::welcome::close_welcome_window();
|
crate::services::welcome::close_welcome_window();
|
||||||
tauri::async_runtime::spawn(async {
|
tauri::async_runtime::spawn(async {
|
||||||
lifecycle::handle_authentication_flow().await;
|
construct_user_session().await;
|
||||||
|
close_splash_window();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ pub mod interaction;
|
|||||||
pub mod sprite;
|
pub mod sprite;
|
||||||
pub mod user_status;
|
pub mod user_status;
|
||||||
|
|
||||||
use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL};
|
|
||||||
use crate::lock_r;
|
use crate::lock_r;
|
||||||
|
use crate::state::{init_app_data_scoped, AppDataRefreshScope, 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.
|
||||||
@@ -44,7 +44,10 @@ pub async fn refresh_app_data(scopes: &[AppDataRefreshScope]) {
|
|||||||
/// Ok(result)
|
/// Ok(result)
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn refresh_app_data_conditionally(base_scopes: &[AppDataRefreshScope], conditional_scopes: Option<&[AppDataRefreshScope]>) {
|
pub async fn refresh_app_data_conditionally(
|
||||||
|
base_scopes: &[AppDataRefreshScope],
|
||||||
|
conditional_scopes: Option<&[AppDataRefreshScope]>,
|
||||||
|
) {
|
||||||
let mut all_scopes = base_scopes.to_vec();
|
let mut all_scopes = base_scopes.to_vec();
|
||||||
if let Some(extra_scopes) = conditional_scopes {
|
if let Some(extra_scopes) = conditional_scopes {
|
||||||
all_scopes.extend_from_slice(extra_scopes);
|
all_scopes.extend_from_slice(extra_scopes);
|
||||||
@@ -57,8 +60,7 @@ pub async fn refresh_app_data_conditionally(base_scopes: &[AppDataRefreshScope],
|
|||||||
pub fn is_active_doll(doll_id: &str) -> bool {
|
pub fn is_active_doll(doll_id: &str) -> bool {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
guard
|
guard
|
||||||
.ui
|
.user_data
|
||||||
.app_data
|
|
||||||
.user
|
.user
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|u| u.active_doll_id.as_ref())
|
.and_then(|u| u.active_doll_id.as_ref())
|
||||||
|
|||||||
@@ -1,59 +1,58 @@
|
|||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tracing::{info, warn};
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
init::startup::{initialize_app_data_and_connections, transition_to_main_interface},
|
|
||||||
lock_w,
|
|
||||||
models::health::HealthError,
|
models::health::HealthError,
|
||||||
remotes::health::HealthRemote,
|
remotes::health::HealthRemote,
|
||||||
services::{
|
services::{
|
||||||
active_app::init_active_app_changes_listener,
|
close_all_windows,
|
||||||
auth::get_tokens,
|
health_manager::open_health_manager_window,
|
||||||
health_manager::show_health_manager_with_error,
|
scene::open_scene_window,
|
||||||
scene::close_splash_window,
|
ws::client::{clear_ws_client, establish_websocket_connection},
|
||||||
welcome::open_welcome_window,
|
|
||||||
},
|
},
|
||||||
state::FDOLL,
|
state::{clear_app_data, init_app_data_scoped, AppDataRefreshScope},
|
||||||
system_tray::{init_system_tray, update_system_tray},
|
system_tray::update_system_tray,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Initializes and starts the core app lifecycle after initial setup.
|
/// Connects the user profile and opens the scene window.
|
||||||
///
|
pub async fn construct_user_session() {
|
||||||
/// This function handles:
|
connect_user_profile().await;
|
||||||
/// - System tray initialization and storage in app state
|
open_scene_window();
|
||||||
/// - Active app change listener setup
|
update_system_tray(true);
|
||||||
/// - Startup sequence execution with error handling
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// If the startup sequence fails, displays a health manager dialog
|
|
||||||
/// with the error details.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// // Called automatically during app setup in initialize_app_environment()
|
|
||||||
/// lifecycle::launch_core_services().await;
|
|
||||||
/// ```
|
|
||||||
pub async fn launch_core_services() {
|
|
||||||
let tray = init_system_tray();
|
|
||||||
{
|
|
||||||
let mut guard = lock_w!(FDOLL);
|
|
||||||
guard.tray = Some(tray);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin listening for foreground app changes
|
/// Disconnects the user profile and closes the scene window.
|
||||||
init_active_app_changes_listener();
|
pub async fn destruct_user_session() {
|
||||||
|
disconnect_user_profile().await;
|
||||||
if let Err(err) = validate_environment_and_start_app().await {
|
close_all_windows();
|
||||||
tracing::warn!("Startup sequence encountered an error: {}", err);
|
update_system_tray(false);
|
||||||
show_health_manager_with_error(Some(err.to_string()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform checks for environment, network condition
|
/// Initializes the user profile and establishes a WebSocket connection.
|
||||||
/// and handle situations where startup would not be appropriate.
|
async fn connect_user_profile() {
|
||||||
pub async fn validate_environment_and_start_app() -> Result<(), HealthError> {
|
init_app_data_scoped(AppDataRefreshScope::All).await;
|
||||||
|
establish_websocket_connection().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the user profile and WebSocket connection.
|
||||||
|
async fn disconnect_user_profile() {
|
||||||
|
clear_app_data();
|
||||||
|
clear_ws_client().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destructs the user session and show health manager window
|
||||||
|
/// with error message, offering troubleshooting options.
|
||||||
|
pub async fn handle_disasterous_failure(error_message: Option<String>) {
|
||||||
|
destruct_user_session().await;
|
||||||
|
open_health_manager_window(error_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pings the server's health endpoint a maximum of
|
||||||
|
/// three times with a backoff of 500ms between
|
||||||
|
/// attempts. Return health error if no success.
|
||||||
|
pub async fn validate_server_health() -> Result<(), HealthError> {
|
||||||
let health_remote = HealthRemote::try_new()?;
|
let health_remote = HealthRemote::try_new()?;
|
||||||
|
|
||||||
// simple retry loop to smooth transient network issues
|
// simple retry loop to smooth transient network issues
|
||||||
@@ -63,7 +62,6 @@ pub async fn validate_environment_and_start_app() -> Result<(), HealthError> {
|
|||||||
for attempt in 1..=MAX_ATTEMPTS {
|
for attempt in 1..=MAX_ATTEMPTS {
|
||||||
match health_remote.get_health().await {
|
match health_remote.get_health().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
handle_authentication_flow().await;
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(HealthError::NonOkStatus(status)) => {
|
Err(HealthError::NonOkStatus(status)) => {
|
||||||
@@ -87,25 +85,9 @@ pub async fn validate_environment_and_start_app() -> Result<(), HealthError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
warn!("Server is unavailable!");
|
||||||
|
|
||||||
Err(HealthError::UnexpectedStatus(
|
Err(HealthError::UnexpectedStatus(
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles authentication flow: checks for tokens and either restores session or shows welcome.
|
|
||||||
pub async fn handle_authentication_flow() {
|
|
||||||
match get_tokens().await {
|
|
||||||
Some(_tokens) => {
|
|
||||||
info!("Tokens found in keyring - restoring user session");
|
|
||||||
let start = initialize_app_data_and_connections().await;
|
|
||||||
transition_to_main_interface(start).await;
|
|
||||||
update_system_tray(true);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
info!("No active session found - showing welcome first");
|
|
||||||
open_welcome_window();
|
|
||||||
close_splash_window();
|
|
||||||
update_system_tray(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,41 @@
|
|||||||
|
use crate::{
|
||||||
|
init::{
|
||||||
|
lifecycle::{construct_user_session, handle_disasterous_failure, validate_server_health},
|
||||||
|
tracing::init_logging,
|
||||||
|
},
|
||||||
|
services::{
|
||||||
|
active_app::init_foreground_app_change_listener,
|
||||||
|
auth::get_session_token,
|
||||||
|
cursor::init_cursor_tracking,
|
||||||
|
scene::{close_splash_window, open_splash_window},
|
||||||
|
welcome::open_welcome_window,
|
||||||
|
},
|
||||||
|
state::init_app_state,
|
||||||
|
system_tray::init_system_tray,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod lifecycle;
|
pub mod lifecycle;
|
||||||
pub mod startup;
|
|
||||||
pub mod tracing;
|
pub mod tracing;
|
||||||
|
|
||||||
|
/// The very function that handles
|
||||||
|
/// init and startup of everything.
|
||||||
|
pub async fn launch_app() {
|
||||||
|
init_logging();
|
||||||
|
open_splash_window();
|
||||||
|
init_app_state();
|
||||||
|
init_system_tray();
|
||||||
|
init_cursor_tracking().await;
|
||||||
|
init_foreground_app_change_listener();
|
||||||
|
|
||||||
|
if let Err(err) = validate_server_health().await {
|
||||||
|
handle_disasterous_failure(Some(err.to_string())).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match get_session_token().await {
|
||||||
|
Some(_tokens) => construct_user_session().await,
|
||||||
|
None => open_welcome_window(),
|
||||||
|
}
|
||||||
|
|
||||||
|
close_splash_window();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
use tokio::time::{sleep, Instant};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
services::{
|
|
||||||
auth::get_access_token,
|
|
||||||
scene::{close_splash_window, open_scene_window, open_splash_window},
|
|
||||||
ws::init_ws_client,
|
|
||||||
},
|
|
||||||
state::init_app_data,
|
|
||||||
};
|
|
||||||
|
|
||||||
async fn establish_websocket_connection() {
|
|
||||||
const MAX_ATTEMPTS: u8 = 5;
|
|
||||||
const BACKOFF: Duration = Duration::from_millis(300);
|
|
||||||
|
|
||||||
for _attempt in 1..=MAX_ATTEMPTS {
|
|
||||||
if get_access_token().await.is_some() {
|
|
||||||
init_ws_client().await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep(BACKOFF).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn initialize_app_data_and_connections() -> Instant {
|
|
||||||
open_splash_window();
|
|
||||||
|
|
||||||
// Record start time for minimum splash duration
|
|
||||||
let start = Instant::now();
|
|
||||||
|
|
||||||
// Initialize app data first so we only start WebSocket after auth is fully available
|
|
||||||
init_app_data().await;
|
|
||||||
|
|
||||||
// Initialize WebSocket client after we know auth is present
|
|
||||||
establish_websocket_connection().await;
|
|
||||||
|
|
||||||
start
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn transition_to_main_interface(start: Instant) {
|
|
||||||
// Ensure splash stays visible for at least 3 seconds
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
if elapsed < Duration::from_secs(3) {
|
|
||||||
sleep(Duration::from_secs(3) - elapsed).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close splash and open main scene
|
|
||||||
close_splash_window();
|
|
||||||
open_scene_window();
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ use tracing_subscriber::util::SubscriberInitExt;
|
|||||||
use crate::get_app_handle;
|
use crate::get_app_handle;
|
||||||
|
|
||||||
/// Initialize `tracing_subscriber` for logging to file & console
|
/// Initialize `tracing_subscriber` for logging to file & console
|
||||||
pub fn setup_logging() {
|
pub fn init_logging() {
|
||||||
// Set up file appender
|
// Set up file appender
|
||||||
let app_handle = get_app_handle();
|
let app_handle = get_app_handle();
|
||||||
let app_log_dir = app_handle
|
let app_log_dir = app_handle
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
use crate::{
|
use crate::services::{
|
||||||
init::tracing::setup_logging,
|
|
||||||
services::{
|
|
||||||
cursor::start_cursor_tracking,
|
|
||||||
doll_editor::open_doll_editor_window,
|
doll_editor::open_doll_editor_window,
|
||||||
scene::{open_splash_window, set_pet_menu_state, set_scene_interactive},
|
scene::{set_pet_menu_state, set_scene_interactive},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use commands::app::{quit_app, restart_app};
|
use commands::app::{quit_app, restart_app};
|
||||||
use commands::app_data::{get_app_data, refresh_app_data};
|
use commands::app_data::{get_app_data, refresh_app_data};
|
||||||
@@ -40,14 +36,6 @@ pub fn get_app_handle<'a>() -> &'a tauri::AppHandle<tauri::Wry> {
|
|||||||
.expect("get_app_handle called but app is still not initialized")
|
.expect("get_app_handle called but app is still not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize_app_environment() -> Result<(), tauri::Error> {
|
|
||||||
setup_logging();
|
|
||||||
open_splash_window();
|
|
||||||
state::init_fdoll_state();
|
|
||||||
async_runtime::spawn(async move { init::lifecycle::launch_core_services().await });
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_app_events(event: tauri::RunEvent) {
|
fn register_app_events(event: tauri::RunEvent) {
|
||||||
if let tauri::RunEvent::ExitRequested { api, code, .. } = event {
|
if let tauri::RunEvent::ExitRequested { api, code, .. } = event {
|
||||||
if code.is_none() {
|
if code.is_none() {
|
||||||
@@ -66,7 +54,6 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
start_cursor_tracking,
|
|
||||||
get_app_data,
|
get_app_data,
|
||||||
refresh_app_data,
|
refresh_app_data,
|
||||||
list_friends,
|
list_friends,
|
||||||
@@ -102,7 +89,7 @@ pub fn run() {
|
|||||||
APP_HANDLE
|
APP_HANDLE
|
||||||
.set(app.handle().to_owned())
|
.set(app.handle().to_owned())
|
||||||
.expect("Failed to init app handle.");
|
.expect("Failed to init app handle.");
|
||||||
initialize_app_environment().expect("Failed to setup app.");
|
async_runtime::spawn(async move { init::launch_app().await });
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
|
|||||||
@@ -43,5 +43,5 @@ pub struct AppData {
|
|||||||
pub user: Option<UserProfile>,
|
pub user: Option<UserProfile>,
|
||||||
pub friends: Option<Vec<FriendshipResponseDto>>,
|
pub friends: Option<Vec<FriendshipResponseDto>>,
|
||||||
pub dolls: Option<Vec<DollDto>>,
|
pub dolls: Option<Vec<DollDto>>,
|
||||||
pub scene: SceneData,
|
pub scene: SceneData, // TODO: move this out of app data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -756,8 +756,10 @@ mod windows_impl {
|
|||||||
|
|
||||||
pub static ACTIVE_APP_CHANGED: &str = "active-app-changed";
|
pub static ACTIVE_APP_CHANGED: &str = "active-app-changed";
|
||||||
|
|
||||||
/// Initializes the active app change listener and emits events to the Tauri app on changes.
|
/// Initializes the foreground app change listener
|
||||||
pub fn init_active_app_changes_listener() {
|
/// and emits events to the Tauri app on changes.
|
||||||
|
/// Used for app to emit user foreground app to peers.
|
||||||
|
pub fn init_foreground_app_change_listener() {
|
||||||
let app_handle = get_app_handle();
|
let app_handle = get_app_handle();
|
||||||
listen_for_active_app_changes(|app_names: AppMetadata| {
|
listen_for_active_app_changes(|app_names: AppMetadata| {
|
||||||
if let Err(e) = app_handle.emit(ACTIVE_APP_CHANGED, app_names) {
|
if let Err(e) = app_handle.emit(ACTIVE_APP_CHANGED, app_names) {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ fn generate_code_challenge(code_verifier: &str) -> String {
|
|||||||
/// Returns the auth pass object, including
|
/// Returns the auth pass object, including
|
||||||
/// access token, refresh token, expire time etc.
|
/// access token, refresh token, expire time etc.
|
||||||
/// Automatically refreshes if expired.
|
/// Automatically refreshes if expired.
|
||||||
pub async fn get_tokens() -> Option<AuthPass> {
|
pub async fn get_session_token() -> Option<AuthPass> {
|
||||||
info!("Retrieving tokens");
|
info!("Retrieving tokens");
|
||||||
let Some(auth_pass) = ({ lock_r!(FDOLL).auth.auth_pass.clone() }) else {
|
let Some(auth_pass) = ({ lock_r!(FDOLL).auth.auth_pass.clone() }) else {
|
||||||
return None;
|
return None;
|
||||||
@@ -173,7 +173,7 @@ pub async fn get_tokens() -> Option<AuthPass> {
|
|||||||
|
|
||||||
/// Helper function to get the current access token.
|
/// Helper function to get the current access token.
|
||||||
pub async fn get_access_token() -> Option<String> {
|
pub async fn get_access_token() -> Option<String> {
|
||||||
get_tokens().await.map(|pass| pass.access_token)
|
get_session_token().await.map(|pass| pass.access_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save auth_pass to secure storage (keyring) and update app state.
|
/// Save auth_pass to secure storage (keyring) and update app state.
|
||||||
@@ -386,8 +386,16 @@ 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.auth_pass.as_ref().map(|p| p.refresh_token.clone()),
|
guard
|
||||||
guard.auth.auth_pass.as_ref().map(|p| p.session_state.clone()),
|
.auth
|
||||||
|
.auth_pass
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.refresh_token.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
|
||||||
@@ -720,7 +728,8 @@ pub async fn refresh_token(refresh_token: &str) -> Result<AuthPass, OAuthError>
|
|||||||
(
|
(
|
||||||
guard.app_config.clone(),
|
guard.app_config.clone(),
|
||||||
guard
|
guard
|
||||||
.network.clients
|
.network
|
||||||
|
.clients
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("clients present")
|
.expect("clients present")
|
||||||
.http_client
|
.http_client
|
||||||
|
|||||||
@@ -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.ui.app_data.scene.display.screen_width as f64;
|
let screen_w = guard.user_data.scene.display.screen_width as f64;
|
||||||
let screen_h = guard.ui.app_data.scene.display.screen_height as f64;
|
let screen_h = guard.user_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.ui.app_data.scene.display.screen_width as f64;
|
let screen_w = guard.user_data.scene.display.screen_width as f64;
|
||||||
let screen_h = guard.ui.app_data.scene.display.screen_height as f64;
|
let screen_h = guard.user_data.scene.display.screen_height as f64;
|
||||||
|
|
||||||
CursorPosition {
|
CursorPosition {
|
||||||
x: (normalized.x * screen_w).round(),
|
x: (normalized.x * screen_w).round(),
|
||||||
@@ -61,10 +61,9 @@ pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize cursor tracking - can be called multiple times safely from any window
|
/// Initialize cursor tracking. Broadcasts cursor
|
||||||
/// Only the first call will actually start tracking, subsequent calls are no-ops
|
/// position changes via `cursor-position` event.
|
||||||
#[tauri::command]
|
pub async fn init_cursor_tracking() {
|
||||||
pub async fn start_cursor_tracking() -> Result<(), String> {
|
|
||||||
info!("start_cursor_tracking called");
|
info!("start_cursor_tracking called");
|
||||||
|
|
||||||
// Use OnceCell to ensure this only runs once, even if called from multiple windows
|
// Use OnceCell to ensure this only runs once, even if called from multiple windows
|
||||||
@@ -74,17 +73,16 @@ pub async fn start_cursor_tracking() -> Result<(), String> {
|
|||||||
|
|
||||||
info!("First call to start_cursor_tracking - spawning cursor tracking task");
|
info!("First call to start_cursor_tracking - spawning cursor tracking task");
|
||||||
tauri::async_runtime::spawn(async {
|
tauri::async_runtime::spawn(async {
|
||||||
if let Err(e) = init_cursor_tracking().await {
|
if let Err(e) = init_cursor_tracking_i().await {
|
||||||
error!("Failed to initialize cursor tracking: {}", e);
|
error!("Failed to initialize cursor tracking: {}", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("Cursor tracking initialization registered");
|
info!("Cursor tracking initialization registered");
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init_cursor_tracking() -> Result<(), String> {
|
async fn init_cursor_tracking_i() -> Result<(), String> {
|
||||||
info!("Initializing cursor tracking...");
|
info!("Initializing cursor tracking...");
|
||||||
|
|
||||||
// Create a channel to decouple event generation (producer) from processing (consumer).
|
// Create a channel to decouple event generation (producer) from processing (consumer).
|
||||||
@@ -124,7 +122,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.ui.app_data.scene.display.monitor_scale_factor
|
guard.user_data.scene.display.monitor_scale_factor
|
||||||
};
|
};
|
||||||
|
|
||||||
// The producer closure moves `tx` into it.
|
// The producer closure moves `tx` into it.
|
||||||
|
|||||||
@@ -1,36 +1,15 @@
|
|||||||
use crate::get_app_handle;
|
use crate::get_app_handle;
|
||||||
use crate::{lock_r, state::FDOLL, system_tray::update_system_tray};
|
use crate::{lock_r, state::FDOLL, system_tray::update_system_tray};
|
||||||
use tauri::{Emitter, Manager};
|
use tauri::Manager;
|
||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder, MessageDialogKind};
|
use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder, MessageDialogKind};
|
||||||
use tauri_plugin_positioner::WindowExt;
|
use tauri_plugin_positioner::WindowExt;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
pub static HEALTH_MANAGER_WINDOW_LABEL: &str = "health_manager";
|
pub static HEALTH_MANAGER_WINDOW_LABEL: &str = "health_manager";
|
||||||
pub static HEALTH_MANAGER_EVENT: &str = "health-error";
|
|
||||||
|
|
||||||
fn close_window_if_exists(label: &str) {
|
|
||||||
let app_handle = get_app_handle();
|
|
||||||
if let Some(window) = app_handle.get_window(label) {
|
|
||||||
info!("Closing window with label: {}", label);
|
|
||||||
if let Err(e) = window.close() {
|
|
||||||
error!("Failed to close {} window: {}", label, e);
|
|
||||||
} else {
|
|
||||||
info!("Closed window with label: {}", label);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!("No window found with label: {}", label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Closes primary UI windows and shows the health manager with an optional error message.
|
/// Closes primary UI windows and shows the health manager with an optional error message.
|
||||||
pub fn show_health_manager_with_error(error_message: Option<String>) {
|
pub fn open_health_manager_window(error_message: Option<String>) {
|
||||||
let app_handle = get_app_handle();
|
let app_handle = get_app_handle();
|
||||||
// Ensure other windows are closed before showing health manager
|
|
||||||
close_window_if_exists(crate::services::scene::SPLASH_WINDOW_LABEL);
|
|
||||||
close_window_if_exists(crate::services::scene::SCENE_WINDOW_LABEL);
|
|
||||||
close_window_if_exists(crate::services::app_menu::APP_MENU_WINDOW_LABEL);
|
|
||||||
|
|
||||||
update_system_tray(false);
|
|
||||||
|
|
||||||
let existing_webview_window = app_handle.get_window(HEALTH_MANAGER_WINDOW_LABEL);
|
let existing_webview_window = app_handle.get_window(HEALTH_MANAGER_WINDOW_LABEL);
|
||||||
|
|
||||||
@@ -45,12 +24,6 @@ pub fn show_health_manager_with_error(error_message: Option<String>) {
|
|||||||
.kind(MessageDialogKind::Error)
|
.kind(MessageDialogKind::Error)
|
||||||
.show(|_| {});
|
.show(|_| {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = error_message {
|
|
||||||
if let Err(e) = window.emit(HEALTH_MANAGER_EVENT, message.clone()) {
|
|
||||||
error!("Failed to emit health error event: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +31,13 @@ pub fn show_health_manager_with_error(error_message: Option<String>) {
|
|||||||
let webview_window = match tauri::WebviewWindowBuilder::new(
|
let webview_window = match tauri::WebviewWindowBuilder::new(
|
||||||
app_handle,
|
app_handle,
|
||||||
HEALTH_MANAGER_WINDOW_LABEL,
|
HEALTH_MANAGER_WINDOW_LABEL,
|
||||||
tauri::WebviewUrl::App("/health-manager".into()),
|
tauri::WebviewUrl::App(
|
||||||
|
format!(
|
||||||
|
"/health-manager?err={}",
|
||||||
|
error_message.unwrap_or(String::from("Something went wrong!"))
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.title("Health Manager")
|
.title("Health Manager")
|
||||||
.inner_size(420.0, 420.0)
|
.inner_size(420.0, 420.0)
|
||||||
@@ -89,12 +68,6 @@ pub fn show_health_manager_with_error(error_message: Option<String>) {
|
|||||||
error!("Failed to move health manager window to center: {}", e);
|
error!("Failed to move health manager window to center: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = error_message {
|
|
||||||
if let Err(e) = webview_window.emit(HEALTH_MANAGER_EVENT, message.clone()) {
|
|
||||||
error!("Failed to emit health error event: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = webview_window.show() {
|
if let Err(e) = webview_window.show() {
|
||||||
error!("Failed to show health manager window: {}", e);
|
error!("Failed to show health manager window: {}", e);
|
||||||
MessageDialogBuilder::new(
|
MessageDialogBuilder::new(
|
||||||
@@ -117,7 +90,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.ui.app_data.user.is_some();
|
let is_logged_in = guard.user_data.user.is_some();
|
||||||
drop(guard);
|
drop(guard);
|
||||||
update_system_tray(is_logged_in);
|
update_system_tray(is_logged_in);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
use crate::get_app_handle;
|
||||||
|
|
||||||
pub mod active_app;
|
pub mod active_app;
|
||||||
pub mod app_menu;
|
pub mod app_menu;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
@@ -10,3 +14,11 @@ pub mod scene;
|
|||||||
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 {
|
||||||
|
window.1.close().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,32 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use rust_socketio::ClientBuilder;
|
use rust_socketio::ClientBuilder;
|
||||||
use tauri::async_runtime;
|
use tauri::async_runtime;
|
||||||
|
use tokio::time::sleep;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
lock_r, lock_w,
|
lock_r, lock_w,
|
||||||
services::client_config_manager::AppConfig,
|
services::{auth::get_access_token, client_config_manager::AppConfig},
|
||||||
state::FDOLL,
|
state::FDOLL,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::handlers;
|
use super::handlers;
|
||||||
|
|
||||||
|
pub async fn establish_websocket_connection() {
|
||||||
|
const MAX_ATTEMPTS: u8 = 5;
|
||||||
|
const BACKOFF: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
|
for _attempt in 1..=MAX_ATTEMPTS {
|
||||||
|
if get_access_token().await.is_some() {
|
||||||
|
init_ws_client().await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(BACKOFF).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn init_ws_client() {
|
pub async fn init_ws_client() {
|
||||||
let app_config = {
|
let app_config = {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
@@ -26,15 +43,18 @@ pub async fn init_ws_client() {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
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
|
clear_ws_client().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_ws_client() {
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
if let Some(clients) = guard.network.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn build_ws_client(
|
pub async fn build_ws_client(
|
||||||
app_config: &AppConfig,
|
app_config: &AppConfig,
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ use tauri::async_runtime;
|
|||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
lock_r,
|
init::lifecycle::handle_disasterous_failure, lock_r, services::cursor::CursorPosition,
|
||||||
services::{cursor::CursorPosition, health_manager::show_health_manager_with_error},
|
|
||||||
state::FDOLL,
|
state::FDOLL,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,11 +40,11 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
|||||||
Ok(Ok(_)) => (),
|
Ok(Ok(_)) => (),
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
error!("Failed to emit cursor report: {}", e);
|
error!("Failed to emit cursor report: {}", e);
|
||||||
show_health_manager_with_error(Some(format!("WebSocket emit failed: {}", e)));
|
handle_disasterous_failure(Some(format!("WebSocket emit failed: {}", e))).await;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to execute blocking task for cursor report: {}", e);
|
error!("Failed to execute blocking task for cursor report: {}", e);
|
||||||
show_health_manager_with_error(Some(format!("WebSocket task failed: {}", e)));
|
handle_disasterous_failure(Some(format!("WebSocket task failed: {}", e))).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +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
|
.user_data
|
||||||
.app_data
|
|
||||||
.user
|
.user
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|u| u.active_doll_id.as_ref())
|
.and_then(|u| u.active_doll_id.as_ref())
|
||||||
@@ -75,8 +74,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
|
.user_data
|
||||||
.app_data
|
|
||||||
.user
|
.user
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|u| u.active_doll_id.as_ref())
|
.and_then(|u| u.active_doll_id.as_ref())
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ impl WS_EVENT {
|
|||||||
pub const CLIENT_SEND_INTERACTION: &str = "client-send-interaction";
|
pub const CLIENT_SEND_INTERACTION: &str = "client-send-interaction";
|
||||||
}
|
}
|
||||||
|
|
||||||
mod client;
|
pub mod client;
|
||||||
mod connection;
|
mod connection;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
mod doll;
|
mod doll;
|
||||||
@@ -50,6 +50,5 @@ mod handlers;
|
|||||||
mod interaction;
|
mod interaction;
|
||||||
mod user_status;
|
mod user_status;
|
||||||
|
|
||||||
pub use client::init_ws_client;
|
|
||||||
pub use cursor::report_cursor_data;
|
pub use cursor::report_cursor_data;
|
||||||
pub use user_status::{report_user_status, UserStatusPayload};
|
pub use user_status::{report_user_status, UserStatusPayload};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rust_socketio::Payload;
|
use rust_socketio::Payload;
|
||||||
use tauri::async_runtime;
|
use tauri::async_runtime::{self};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{lock_r, services::health_manager::show_health_manager_with_error, state::FDOLL};
|
use crate::{init::lifecycle::handle_disasterous_failure, lock_r, state::FDOLL};
|
||||||
|
|
||||||
use super::WS_EVENT;
|
use super::WS_EVENT;
|
||||||
|
|
||||||
@@ -66,20 +66,16 @@ pub async fn report_user_status(status: UserStatusPayload) {
|
|||||||
Ok(Ok(_)) => (),
|
Ok(Ok(_)) => (),
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
error!("Failed to emit user status report: {}", e);
|
error!("Failed to emit user status report: {}", e);
|
||||||
show_health_manager_with_error(Some(format!(
|
handle_disasterous_failure(Some(format!("WebSocket emit failed: {}", e)))
|
||||||
"WebSocket emit failed: {}",
|
.await;
|
||||||
e
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"Failed to execute blocking task for user status report: {}",
|
"Failed to execute blocking task for user status report: {}",
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
show_health_manager_with_error(Some(format!(
|
handle_disasterous_failure(Some(format!("WebSocket task failed: {}", e)))
|
||||||
"WebSocket task failed: {}",
|
.await;
|
||||||
e
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// in app-core/src/state.rs
|
// in app-core/src/state.rs
|
||||||
use crate::lock_w;
|
use crate::{lock_w, models::app_data::AppData};
|
||||||
use std::sync::{Arc, LazyLock, RwLock};
|
use std::sync::{Arc, LazyLock, RwLock};
|
||||||
use tauri::tray::TrayIcon;
|
use tauri::tray::TrayIcon;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -17,7 +17,7 @@ pub struct AppState {
|
|||||||
pub app_config: crate::services::client_config_manager::AppConfig,
|
pub app_config: crate::services::client_config_manager::AppConfig,
|
||||||
pub network: NetworkState,
|
pub network: NetworkState,
|
||||||
pub auth: AuthState,
|
pub auth: AuthState,
|
||||||
pub ui: UiState,
|
pub user_data: AppData,
|
||||||
pub tray: Option<TrayIcon>,
|
pub tray: Option<TrayIcon>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,15 +26,17 @@ pub struct AppState {
|
|||||||
pub static FDOLL: LazyLock<Arc<RwLock<AppState>>> =
|
pub static FDOLL: LazyLock<Arc<RwLock<AppState>>> =
|
||||||
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
|
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
|
||||||
|
|
||||||
pub fn init_fdoll_state() {
|
/// Populate app state with initial
|
||||||
|
/// values and necesary client instances.
|
||||||
|
pub fn init_app_state() {
|
||||||
|
dotenvy::dotenv().ok();
|
||||||
{
|
{
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
dotenvy::dotenv().ok();
|
|
||||||
guard.app_config = crate::services::client_config_manager::load_app_config();
|
guard.app_config = crate::services::client_config_manager::load_app_config();
|
||||||
guard.network = init_network_state();
|
guard.network = init_network_state();
|
||||||
guard.auth = init_auth_state();
|
guard.auth = init_auth_state();
|
||||||
guard.ui = init_ui_state();
|
guard.user_data = AppData::default();
|
||||||
}
|
}
|
||||||
|
update_display_dimensions_for_scene_state();
|
||||||
info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)");
|
info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,19 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle, lock_r, lock_w,
|
get_app_handle, lock_r, lock_w,
|
||||||
models::app_data::AppData,
|
|
||||||
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
||||||
|
state::FDOLL,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{collections::HashSet, sync::LazyLock};
|
||||||
collections::HashSet,
|
|
||||||
sync::{LazyLock},
|
|
||||||
};
|
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
pub struct UiState {
|
pub fn update_display_dimensions_for_scene_state() {
|
||||||
pub app_data: AppData,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for UiState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
app_data: AppData::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_ui_state() -> UiState {
|
|
||||||
let mut ui_state = UiState::default();
|
|
||||||
|
|
||||||
// Initialize screen dimensions
|
|
||||||
let app_handle = get_app_handle();
|
let app_handle = get_app_handle();
|
||||||
|
|
||||||
|
let mut guard = lock_w!(FDOLL);
|
||||||
|
|
||||||
// Get primary monitor with retries
|
// 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 primary_monitor = {
|
||||||
let mut retry_count = 0;
|
let mut retry_count = 0;
|
||||||
let max_retries = 3;
|
let max_retries = 3;
|
||||||
@@ -77,23 +60,21 @@ pub fn init_ui_state() -> UiState {
|
|||||||
let logical_monitor_dimensions: tauri::LogicalSize<i32> =
|
let logical_monitor_dimensions: tauri::LogicalSize<i32> =
|
||||||
monitor_dimensions.to_logical(monitor_scale_factor);
|
monitor_dimensions.to_logical(monitor_scale_factor);
|
||||||
|
|
||||||
ui_state.app_data.scene.display.screen_width = logical_monitor_dimensions.width;
|
guard.user_data.scene.display.screen_width = logical_monitor_dimensions.width;
|
||||||
ui_state.app_data.scene.display.screen_height = logical_monitor_dimensions.height;
|
guard.user_data.scene.display.screen_height = logical_monitor_dimensions.height;
|
||||||
ui_state.app_data.scene.display.monitor_scale_factor = monitor_scale_factor;
|
guard.user_data.scene.display.monitor_scale_factor = monitor_scale_factor;
|
||||||
ui_state.app_data.scene.grid_size = 600; // Hardcoded grid size
|
guard.user_data.scene.grid_size = 600; // Hardcoded grid size
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Initialized global AppData with screen dimensions: {}x{}, scale: {}, grid: {}",
|
"Initialized global AppData with screen dimensions: {}x{}, scale: {}, grid: {}",
|
||||||
logical_monitor_dimensions.width,
|
logical_monitor_dimensions.width,
|
||||||
logical_monitor_dimensions.height,
|
logical_monitor_dimensions.height,
|
||||||
monitor_scale_factor,
|
monitor_scale_factor,
|
||||||
ui_state.app_data.scene.grid_size
|
guard.user_data.scene.grid_size
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
warn!("Could not initialize screen dimensions in global state - no monitor found");
|
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
|
||||||
@@ -109,15 +90,6 @@ pub enum AppDataRefreshScope {
|
|||||||
Dolls,
|
Dolls,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// To be called in init state or need to refresh data.
|
|
||||||
/// Populate user data in app state from the server.
|
|
||||||
///
|
|
||||||
/// This is a convenience wrapper that refreshes all data.
|
|
||||||
/// For more control, use `init_app_data_scoped`.
|
|
||||||
pub async fn init_app_data() {
|
|
||||||
init_app_data_scoped(AppDataRefreshScope::All).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
static REFRESH_IN_FLIGHT: LazyLock<Mutex<HashSet<AppDataRefreshScope>>> =
|
static REFRESH_IN_FLIGHT: LazyLock<Mutex<HashSet<AppDataRefreshScope>>> =
|
||||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
LazyLock::new(|| Mutex::new(HashSet::new()));
|
||||||
static REFRESH_PENDING: LazyLock<Mutex<HashSet<AppDataRefreshScope>>> =
|
static REFRESH_PENDING: LazyLock<Mutex<HashSet<AppDataRefreshScope>>> =
|
||||||
@@ -159,7 +131,7 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
|
|||||||
match user_remote.get_user(None).await {
|
match user_remote.get_user(None).await {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let mut guard = lock_w!(crate::state::FDOLL);
|
let mut guard = lock_w!(crate::state::FDOLL);
|
||||||
guard.ui.app_data.user = Some(user);
|
guard.user_data.user = Some(user);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to fetch user profile: {}", e);
|
warn!("Failed to fetch user profile: {}", e);
|
||||||
@@ -187,7 +159,7 @@ 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!(crate::state::FDOLL);
|
let mut guard = lock_w!(crate::state::FDOLL);
|
||||||
guard.ui.app_data.friends = Some(friends);
|
guard.user_data.friends = Some(friends);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to fetch friends list: {}", e);
|
warn!("Failed to fetch friends list: {}", e);
|
||||||
@@ -212,7 +184,7 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
|
|||||||
match dolls_remote.get_dolls().await {
|
match dolls_remote.get_dolls().await {
|
||||||
Ok(dolls) => {
|
Ok(dolls) => {
|
||||||
let mut guard = lock_w!(crate::state::FDOLL);
|
let mut guard = lock_w!(crate::state::FDOLL);
|
||||||
guard.ui.app_data.dolls = Some(dolls);
|
guard.user_data.dolls = Some(dolls);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to fetch dolls list: {}", e);
|
warn!("Failed to fetch dolls list: {}", e);
|
||||||
@@ -235,7 +207,7 @@ 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!(crate::state::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.ui.app_data.clone();
|
let app_data_clone = guard.user_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) {
|
||||||
@@ -281,3 +253,10 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
|
|||||||
break;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
use crate::{get_app_handle, lock_r, services::app_menu::open_app_menu_window, state::FDOLL};
|
use crate::{
|
||||||
|
get_app_handle, lock_r, lock_w, services::app_menu::open_app_menu_window, state::FDOLL,
|
||||||
|
};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
menu::{Menu, MenuItem},
|
menu::{Menu, MenuItem},
|
||||||
tray::TrayIconBuilder,
|
tray::TrayIconBuilder,
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
pub fn init_system_tray() -> tauri::tray::TrayIcon {
|
/// Constructs app system tray.
|
||||||
|
/// Uses Tauri.
|
||||||
|
pub fn init_system_tray() {
|
||||||
let app = get_app_handle();
|
let app = get_app_handle();
|
||||||
|
|
||||||
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap();
|
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap();
|
||||||
@@ -17,7 +21,7 @@ pub fn init_system_tray() -> tauri::tray::TrayIcon {
|
|||||||
Err(err) => todo!("Handle error: {}", err),
|
Err(err) => todo!("Handle error: {}", err),
|
||||||
};
|
};
|
||||||
|
|
||||||
TrayIconBuilder::new()
|
let tray = TrayIconBuilder::new()
|
||||||
.menu(&menu)
|
.menu(&menu)
|
||||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||||
"quit" => {
|
"quit" => {
|
||||||
@@ -32,9 +36,17 @@ pub fn init_system_tray() -> tauri::tray::TrayIcon {
|
|||||||
})
|
})
|
||||||
.icon(app.default_window_icon().unwrap().clone())
|
.icon(app.default_window_icon().unwrap().clone())
|
||||||
.build(app)
|
.build(app)
|
||||||
.unwrap_or_else(|err| panic!("Failed to build tray: {}", err))
|
.unwrap_or_else(|err| panic!("Failed to build tray: {}", err));
|
||||||
|
{
|
||||||
|
let mut guard = lock_w!(FDOLL);
|
||||||
|
guard.tray = Some(tray);
|
||||||
|
};
|
||||||
|
|
||||||
|
update_system_tray(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle the "Open App Menu" item in the system tray.
|
||||||
|
/// Used for when user is signed in vs not signed in.
|
||||||
pub fn update_system_tray(is_logged_in: bool) {
|
pub fn update_system_tray(is_logged_in: bool) {
|
||||||
let app = get_app_handle();
|
let app = get_app_handle();
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ export async function initCursorTracking() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Start tracking
|
|
||||||
await invoke("start_cursor_tracking");
|
|
||||||
|
|
||||||
// Listen to cursor position events (each window subscribes independently)
|
// Listen to cursor position events (each window subscribes independently)
|
||||||
unlistenCursor = await listen<CursorPositions>(
|
unlistenCursor = await listen<CursorPositions>(
|
||||||
"cursor-position",
|
"cursor-position",
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { page } from "$app/stores";
|
||||||
|
|
||||||
let errorMessage = "";
|
let errorMessage = "";
|
||||||
let unlisten: (() => void) | null = null;
|
|
||||||
let isRestarting = false;
|
let isRestarting = false;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(() => {
|
||||||
unlisten = await listen<string>("health-error", (event) => {
|
errorMessage = $page.url.searchParams.get("err") || "";
|
||||||
errorMessage = event.payload;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (unlisten) {
|
|
||||||
unlisten();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const tryAgain = async () => {
|
const tryAgain = async () => {
|
||||||
@@ -34,13 +25,17 @@
|
|||||||
|
|
||||||
<div class="size-full p-4">
|
<div class="size-full p-4">
|
||||||
<div class="flex flex-col gap-4 size-full justify-between">
|
<div class="flex flex-col gap-4 size-full justify-between">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-md font-light">Something is not right...</p>
|
<p class="text-md font-light">Something is not right...</p>
|
||||||
<p class="opacity-70 text-3xl font-bold">
|
<p class="opacity-70 text-3xl font-bold">
|
||||||
Seems like the server is inaccessible. Check your network?
|
Seems like the server is inaccessible. Check your network?
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<p class="text-sm opacity-70 wrap-break-word">{errorMessage}</p>
|
<p class="text-xs opacity-70 wrap-break-word font-mono">
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
@@ -69,6 +64,5 @@
|
|||||||
Advanced options
|
Advanced options
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user