minor refactoring of app startup sequence & some extra trivial matters
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
use crate::{
|
||||
lock_r,
|
||||
models::app_data::AppData,
|
||||
state::{init_app_data, FDOLL},
|
||||
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_data() -> Result<AppData, String> {
|
||||
let guard = lock_r!(FDOLL);
|
||||
Ok(guard.ui.app_data.clone())
|
||||
Ok(guard.user_data.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
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);
|
||||
Ok(guard.ui.app_data.clone())
|
||||
Ok(guard.user_data.clone())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use tauri;
|
||||
use tracing;
|
||||
|
||||
use crate::init::lifecycle;
|
||||
use crate::{init::lifecycle::construct_user_session, services::scene::close_splash_window};
|
||||
|
||||
#[tauri::command]
|
||||
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::init_auth_code_retrieval(|| {
|
||||
tracing::info!("Authentication successful, creating scene...");
|
||||
// Close welcome window if it's still open
|
||||
tracing::info!("Authentication successful, constructing user session...");
|
||||
crate::services::welcome::close_welcome_window();
|
||||
tauri::async_runtime::spawn(async {
|
||||
lifecycle::handle_authentication_flow().await;
|
||||
construct_user_session().await;
|
||||
close_splash_window();
|
||||
});
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
|
||||
@@ -8,8 +8,8 @@ pub mod interaction;
|
||||
pub mod sprite;
|
||||
pub mod user_status;
|
||||
|
||||
use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL};
|
||||
use crate::lock_r;
|
||||
use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL};
|
||||
use tauri::async_runtime;
|
||||
|
||||
/// 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)
|
||||
/// }
|
||||
/// ```
|
||||
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();
|
||||
if let Some(extra_scopes) = conditional_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 {
|
||||
let guard = lock_r!(FDOLL);
|
||||
guard
|
||||
.ui
|
||||
.app_data
|
||||
.user_data
|
||||
.user
|
||||
.as_ref()
|
||||
.and_then(|u| u.active_doll_id.as_ref())
|
||||
|
||||
@@ -1,59 +1,58 @@
|
||||
use reqwest::StatusCode;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{info, warn};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
init::startup::{initialize_app_data_and_connections, transition_to_main_interface},
|
||||
lock_w,
|
||||
models::health::HealthError,
|
||||
remotes::health::HealthRemote,
|
||||
services::{
|
||||
active_app::init_active_app_changes_listener,
|
||||
auth::get_tokens,
|
||||
health_manager::show_health_manager_with_error,
|
||||
scene::close_splash_window,
|
||||
welcome::open_welcome_window,
|
||||
close_all_windows,
|
||||
health_manager::open_health_manager_window,
|
||||
scene::open_scene_window,
|
||||
ws::client::{clear_ws_client, establish_websocket_connection},
|
||||
},
|
||||
state::FDOLL,
|
||||
system_tray::{init_system_tray, update_system_tray},
|
||||
state::{clear_app_data, init_app_data_scoped, AppDataRefreshScope},
|
||||
system_tray::update_system_tray,
|
||||
};
|
||||
|
||||
/// Initializes and starts the core app lifecycle after initial setup.
|
||||
///
|
||||
/// This function handles:
|
||||
/// - System tray initialization and storage in app state
|
||||
/// - Active app change listener setup
|
||||
/// - 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
|
||||
init_active_app_changes_listener();
|
||||
|
||||
if let Err(err) = validate_environment_and_start_app().await {
|
||||
tracing::warn!("Startup sequence encountered an error: {}", err);
|
||||
show_health_manager_with_error(Some(err.to_string()));
|
||||
}
|
||||
/// Connects the user profile and opens the scene window.
|
||||
pub async fn construct_user_session() {
|
||||
connect_user_profile().await;
|
||||
open_scene_window();
|
||||
update_system_tray(true);
|
||||
}
|
||||
|
||||
/// Perform checks for environment, network condition
|
||||
/// and handle situations where startup would not be appropriate.
|
||||
pub async fn validate_environment_and_start_app() -> Result<(), HealthError> {
|
||||
/// Disconnects the user profile and closes the scene window.
|
||||
pub async fn destruct_user_session() {
|
||||
disconnect_user_profile().await;
|
||||
close_all_windows();
|
||||
update_system_tray(false);
|
||||
}
|
||||
|
||||
/// Initializes the user profile and establishes a WebSocket connection.
|
||||
async fn connect_user_profile() {
|
||||
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()?;
|
||||
|
||||
// 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 {
|
||||
match health_remote.get_health().await {
|
||||
Ok(_) => {
|
||||
handle_authentication_flow().await;
|
||||
return Ok(());
|
||||
}
|
||||
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(
|
||||
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 startup;
|
||||
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;
|
||||
|
||||
/// Initialize `tracing_subscriber` for logging to file & console
|
||||
pub fn setup_logging() {
|
||||
pub fn init_logging() {
|
||||
// Set up file appender
|
||||
let app_handle = get_app_handle();
|
||||
let app_log_dir = app_handle
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
use crate::{
|
||||
init::tracing::setup_logging,
|
||||
services::{
|
||||
cursor::start_cursor_tracking,
|
||||
doll_editor::open_doll_editor_window,
|
||||
scene::{open_splash_window, set_pet_menu_state, set_scene_interactive},
|
||||
},
|
||||
use crate::services::{
|
||||
doll_editor::open_doll_editor_window,
|
||||
scene::{set_pet_menu_state, set_scene_interactive},
|
||||
};
|
||||
use commands::app::{quit_app, restart_app};
|
||||
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")
|
||||
}
|
||||
|
||||
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) {
|
||||
if let tauri::RunEvent::ExitRequested { api, code, .. } = event {
|
||||
if code.is_none() {
|
||||
@@ -66,7 +54,6 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
start_cursor_tracking,
|
||||
get_app_data,
|
||||
refresh_app_data,
|
||||
list_friends,
|
||||
@@ -102,7 +89,7 @@ pub fn run() {
|
||||
APP_HANDLE
|
||||
.set(app.handle().to_owned())
|
||||
.expect("Failed to init app handle.");
|
||||
initialize_app_environment().expect("Failed to setup app.");
|
||||
async_runtime::spawn(async move { init::launch_app().await });
|
||||
Ok(())
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
|
||||
@@ -43,5 +43,5 @@ pub struct AppData {
|
||||
pub user: Option<UserProfile>,
|
||||
pub friends: Option<Vec<FriendshipResponseDto>>,
|
||||
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";
|
||||
|
||||
/// Initializes the active app change listener and emits events to the Tauri app on changes.
|
||||
pub fn init_active_app_changes_listener() {
|
||||
/// Initializes the foreground app change 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();
|
||||
listen_for_active_app_changes(|app_names: AppMetadata| {
|
||||
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
|
||||
/// access token, refresh token, expire time etc.
|
||||
/// Automatically refreshes if expired.
|
||||
pub async fn get_tokens() -> Option<AuthPass> {
|
||||
pub async fn get_session_token() -> Option<AuthPass> {
|
||||
info!("Retrieving tokens");
|
||||
let Some(auth_pass) = ({ lock_r!(FDOLL).auth.auth_pass.clone() }) else {
|
||||
return None;
|
||||
@@ -173,7 +173,7 @@ pub async fn get_tokens() -> Option<AuthPass> {
|
||||
|
||||
/// Helper function to get the current access token.
|
||||
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.
|
||||
@@ -367,7 +367,7 @@ pub fn clear_auth_pass() -> Result<(), OAuthError> {
|
||||
/// ```
|
||||
pub fn logout() -> Result<(), OAuthError> {
|
||||
info!("Logging out user");
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
clear_auth_pass()?;
|
||||
|
||||
// Clear OAuth flow state as well
|
||||
@@ -386,8 +386,16 @@ pub async fn logout_and_restart() -> Result<(), OAuthError> {
|
||||
let (refresh_token, session_state, base_url) = {
|
||||
let guard = lock_r!(FDOLL);
|
||||
(
|
||||
guard.auth.auth_pass.as_ref().map(|p| p.refresh_token.clone()),
|
||||
guard.auth.auth_pass.as_ref().map(|p| p.session_state.clone()),
|
||||
guard
|
||||
.auth
|
||||
.auth_pass
|
||||
.as_ref()
|
||||
.map(|p| p.refresh_token.clone()),
|
||||
guard
|
||||
.auth
|
||||
.auth_pass
|
||||
.as_ref()
|
||||
.map(|p| p.session_state.clone()),
|
||||
guard
|
||||
.app_config
|
||||
.api_base_url
|
||||
@@ -676,7 +684,7 @@ where
|
||||
{
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
guard.auth.auth_pass = Some(auth_pass.clone());
|
||||
guard.auth.oauth_flow = Default::default();
|
||||
guard.auth.oauth_flow = Default::default();
|
||||
}
|
||||
if let Err(e) = save_auth_pass(&auth_pass) {
|
||||
error!("Failed to save auth pass: {}", e);
|
||||
@@ -720,7 +728,8 @@ pub async fn refresh_token(refresh_token: &str) -> Result<AuthPass, OAuthError>
|
||||
(
|
||||
guard.app_config.clone(),
|
||||
guard
|
||||
.network.clients
|
||||
.network
|
||||
.clients
|
||||
.as_ref()
|
||||
.expect("clients present")
|
||||
.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)
|
||||
pub fn absolute_to_normalized(pos: &CursorPosition) -> CursorPosition {
|
||||
let guard = lock_r!(FDOLL);
|
||||
let screen_w = guard.ui.app_data.scene.display.screen_width as f64;
|
||||
let screen_h = guard.ui.app_data.scene.display.screen_height as f64;
|
||||
let screen_w = guard.user_data.scene.display.screen_width as f64;
|
||||
let screen_h = guard.user_data.scene.display.screen_height as f64;
|
||||
|
||||
CursorPosition {
|
||||
x: (pos.x / screen_w).clamp(0.0, 1.0),
|
||||
@@ -52,8 +52,8 @@ pub fn absolute_to_normalized(pos: &CursorPosition) -> CursorPosition {
|
||||
/// Convert normalized coordinates to absolute screen coordinates
|
||||
pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition {
|
||||
let guard = lock_r!(FDOLL);
|
||||
let screen_w = guard.ui.app_data.scene.display.screen_width as f64;
|
||||
let screen_h = guard.ui.app_data.scene.display.screen_height as f64;
|
||||
let screen_w = guard.user_data.scene.display.screen_width as f64;
|
||||
let screen_h = guard.user_data.scene.display.screen_height as f64;
|
||||
|
||||
CursorPosition {
|
||||
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
|
||||
/// Only the first call will actually start tracking, subsequent calls are no-ops
|
||||
#[tauri::command]
|
||||
pub async fn start_cursor_tracking() -> Result<(), String> {
|
||||
/// Initialize cursor tracking. Broadcasts cursor
|
||||
/// position changes via `cursor-position` event.
|
||||
pub async fn init_cursor_tracking() {
|
||||
info!("start_cursor_tracking called");
|
||||
|
||||
// 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");
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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...");
|
||||
|
||||
// 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")]
|
||||
let scale_factor = {
|
||||
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.
|
||||
|
||||
@@ -1,36 +1,15 @@
|
||||
use crate::get_app_handle;
|
||||
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_positioner::WindowExt;
|
||||
use tracing::{error, info};
|
||||
|
||||
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.
|
||||
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();
|
||||
// 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);
|
||||
|
||||
@@ -45,12 +24,6 @@ pub fn show_health_manager_with_error(error_message: Option<String>) {
|
||||
.kind(MessageDialogKind::Error)
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -58,7 +31,13 @@ pub fn show_health_manager_with_error(error_message: Option<String>) {
|
||||
let webview_window = match tauri::WebviewWindowBuilder::new(
|
||||
app_handle,
|
||||
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")
|
||||
.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);
|
||||
}
|
||||
|
||||
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() {
|
||||
error!("Failed to show health manager window: {}", e);
|
||||
MessageDialogBuilder::new(
|
||||
@@ -117,7 +90,7 @@ pub fn close_health_manager_window() {
|
||||
} else {
|
||||
info!("Health manager window closed");
|
||||
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);
|
||||
update_system_tray(is_logged_in);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::get_app_handle;
|
||||
|
||||
pub mod active_app;
|
||||
pub mod app_menu;
|
||||
pub mod auth;
|
||||
@@ -10,3 +14,11 @@ pub mod scene;
|
||||
pub mod sprite_recolor;
|
||||
pub mod welcome;
|
||||
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 tauri::async_runtime;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
lock_r, lock_w,
|
||||
services::client_config_manager::AppConfig,
|
||||
services::{auth::get_access_token, client_config_manager::AppConfig},
|
||||
state::FDOLL,
|
||||
};
|
||||
|
||||
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() {
|
||||
let app_config = {
|
||||
let guard = lock_r!(FDOLL);
|
||||
@@ -26,16 +43,19 @@ pub async fn init_ws_client() {
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to initialize WebSocket client: {}", e);
|
||||
// If we failed because no token, clear the WS client to avoid stale retries
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if let Some(clients) = guard.network.clients.as_mut() {
|
||||
clients.ws_client = None;
|
||||
clients.is_ws_initialized = false;
|
||||
}
|
||||
clear_ws_client().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn clear_ws_client() {
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if let Some(clients) = guard.network.clients.as_mut() {
|
||||
clients.ws_client = None;
|
||||
clients.is_ws_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_ws_client(
|
||||
app_config: &AppConfig,
|
||||
) -> Result<rust_socketio::client::Client, String> {
|
||||
|
||||
@@ -3,8 +3,7 @@ use tauri::async_runtime;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
lock_r,
|
||||
services::{cursor::CursorPosition, health_manager::show_health_manager_with_error},
|
||||
init::lifecycle::handle_disasterous_failure, lock_r, services::cursor::CursorPosition,
|
||||
state::FDOLL,
|
||||
};
|
||||
|
||||
@@ -41,11 +40,11 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
||||
Ok(Ok(_)) => (),
|
||||
Ok(Err(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) => {
|
||||
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 guard = lock_r!(FDOLL);
|
||||
guard
|
||||
.ui
|
||||
.app_data
|
||||
.user_data
|
||||
.user
|
||||
.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 guard = lock_r!(FDOLL);
|
||||
guard
|
||||
.ui
|
||||
.app_data
|
||||
.user_data
|
||||
.user
|
||||
.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";
|
||||
}
|
||||
|
||||
mod client;
|
||||
pub mod client;
|
||||
mod connection;
|
||||
mod cursor;
|
||||
mod doll;
|
||||
@@ -50,6 +50,5 @@ mod handlers;
|
||||
mod interaction;
|
||||
mod user_status;
|
||||
|
||||
pub use client::init_ws_client;
|
||||
pub use cursor::report_cursor_data;
|
||||
pub use user_status::{report_user_status, UserStatusPayload};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use rust_socketio::Payload;
|
||||
use tauri::async_runtime;
|
||||
use tauri::async_runtime::{self};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::Duration;
|
||||
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;
|
||||
|
||||
@@ -66,20 +66,16 @@ pub async fn report_user_status(status: UserStatusPayload) {
|
||||
Ok(Ok(_)) => (),
|
||||
Ok(Err(e)) => {
|
||||
error!("Failed to emit user status 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) => {
|
||||
error!(
|
||||
"Failed to execute blocking task for user status report: {}",
|
||||
e
|
||||
);
|
||||
show_health_manager_with_error(Some(format!(
|
||||
"WebSocket task failed: {}",
|
||||
e
|
||||
)));
|
||||
handle_disasterous_failure(Some(format!("WebSocket task failed: {}", e)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 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 tauri::tray::TrayIcon;
|
||||
use tracing::info;
|
||||
@@ -17,7 +17,7 @@ pub struct AppState {
|
||||
pub app_config: crate::services::client_config_manager::AppConfig,
|
||||
pub network: NetworkState,
|
||||
pub auth: AuthState,
|
||||
pub ui: UiState,
|
||||
pub user_data: AppData,
|
||||
pub tray: Option<TrayIcon>,
|
||||
}
|
||||
|
||||
@@ -26,15 +26,17 @@ pub struct AppState {
|
||||
pub static FDOLL: LazyLock<Arc<RwLock<AppState>>> =
|
||||
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);
|
||||
dotenvy::dotenv().ok();
|
||||
guard.app_config = crate::services::client_config_manager::load_app_config();
|
||||
guard.network = init_network_state();
|
||||
guard.auth = init_auth_state();
|
||||
guard.ui = init_ui_state();
|
||||
guard.user_data = AppData::default();
|
||||
}
|
||||
|
||||
update_display_dimensions_for_scene_state();
|
||||
info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)");
|
||||
}
|
||||
|
||||
@@ -1,36 +1,19 @@
|
||||
use crate::{
|
||||
get_app_handle, lock_r, lock_w,
|
||||
models::app_data::AppData,
|
||||
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
||||
state::FDOLL,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::{LazyLock},
|
||||
};
|
||||
use std::{collections::HashSet, sync::LazyLock};
|
||||
use tauri::Emitter;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{info, warn};
|
||||
|
||||
pub struct UiState {
|
||||
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
|
||||
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
|
||||
// Note: This duplicates logic from init_cursor_tracking, but we need it here for global state
|
||||
let primary_monitor = {
|
||||
let mut retry_count = 0;
|
||||
let max_retries = 3;
|
||||
@@ -77,23 +60,21 @@ pub fn init_ui_state() -> UiState {
|
||||
let logical_monitor_dimensions: tauri::LogicalSize<i32> =
|
||||
monitor_dimensions.to_logical(monitor_scale_factor);
|
||||
|
||||
ui_state.app_data.scene.display.screen_width = logical_monitor_dimensions.width;
|
||||
ui_state.app_data.scene.display.screen_height = logical_monitor_dimensions.height;
|
||||
ui_state.app_data.scene.display.monitor_scale_factor = monitor_scale_factor;
|
||||
ui_state.app_data.scene.grid_size = 600; // Hardcoded grid size
|
||||
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,
|
||||
ui_state.app_data.scene.grid_size
|
||||
guard.user_data.scene.grid_size
|
||||
);
|
||||
} else {
|
||||
warn!("Could not initialize screen dimensions in global state - no monitor found");
|
||||
}
|
||||
|
||||
ui_state
|
||||
}
|
||||
|
||||
/// Defines which parts of AppData should be refreshed from the server
|
||||
@@ -109,15 +90,6 @@ pub enum AppDataRefreshScope {
|
||||
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>>> =
|
||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
||||
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 {
|
||||
Ok(user) => {
|
||||
let mut guard = lock_w!(crate::state::FDOLL);
|
||||
guard.ui.app_data.user = Some(user);
|
||||
guard.user_data.user = Some(user);
|
||||
}
|
||||
Err(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 {
|
||||
Ok(friends) => {
|
||||
let mut guard = lock_w!(crate::state::FDOLL);
|
||||
guard.ui.app_data.friends = Some(friends);
|
||||
guard.user_data.friends = Some(friends);
|
||||
}
|
||||
Err(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 {
|
||||
Ok(dolls) => {
|
||||
let mut guard = lock_w!(crate::state::FDOLL);
|
||||
guard.ui.app_data.dolls = Some(dolls);
|
||||
guard.user_data.dolls = Some(dolls);
|
||||
}
|
||||
Err(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
|
||||
{
|
||||
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
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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::{
|
||||
menu::{Menu, MenuItem},
|
||||
tray::TrayIconBuilder,
|
||||
};
|
||||
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 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),
|
||||
};
|
||||
|
||||
TrayIconBuilder::new()
|
||||
let tray = TrayIconBuilder::new()
|
||||
.menu(&menu)
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"quit" => {
|
||||
@@ -32,9 +36,17 @@ pub fn init_system_tray() -> tauri::tray::TrayIcon {
|
||||
})
|
||||
.icon(app.default_window_icon().unwrap().clone())
|
||||
.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) {
|
||||
let app = get_app_handle();
|
||||
let guard = lock_r!(FDOLL);
|
||||
|
||||
@@ -50,9 +50,6 @@ export async function initCursorTracking() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Start tracking
|
||||
await invoke("start_cursor_tracking");
|
||||
|
||||
// Listen to cursor position events (each window subscribes independently)
|
||||
unlistenCursor = await listen<CursorPositions>(
|
||||
"cursor-position",
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
let errorMessage = "";
|
||||
let unlisten: (() => void) | null = null;
|
||||
let isRestarting = false;
|
||||
|
||||
onMount(async () => {
|
||||
unlisten = await listen<string>("health-error", (event) => {
|
||||
errorMessage = event.payload;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
}
|
||||
onMount(() => {
|
||||
errorMessage = $page.url.searchParams.get("err") || "";
|
||||
});
|
||||
|
||||
const tryAgain = async () => {
|
||||
@@ -34,41 +25,44 @@
|
||||
|
||||
<div class="size-full p-4">
|
||||
<div class="flex flex-col gap-4 size-full justify-between">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-md font-light">Something is not right...</p>
|
||||
<p class="opacity-70 text-3xl font-bold">
|
||||
Seems like the server is inaccessible. Check your network?
|
||||
</p>
|
||||
{#if errorMessage}
|
||||
<p class="text-sm opacity-70 wrap-break-word">{errorMessage}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<button
|
||||
class="btn"
|
||||
class:btn-disabled={isRestarting}
|
||||
disabled={isRestarting}
|
||||
onclick={tryAgain}
|
||||
>
|
||||
{#if isRestarting}
|
||||
Retrying…
|
||||
{:else}
|
||||
Try again
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-md font-light">Something is not right...</p>
|
||||
<p class="opacity-70 text-3xl font-bold">
|
||||
Seems like the server is inaccessible. Check your network?
|
||||
</p>
|
||||
</div>
|
||||
{#if errorMessage}
|
||||
<p class="text-xs opacity-70 wrap-break-word font-mono">
|
||||
{errorMessage}
|
||||
</p>
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline"
|
||||
onclick={async () => {
|
||||
try {
|
||||
await invoke("open_client_config_manager");
|
||||
} catch (err) {
|
||||
errorMessage = `Failed to open config manager: ${err}`;
|
||||
}
|
||||
}}
|
||||
>
|
||||
Advanced options
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<button
|
||||
class="btn"
|
||||
class:btn-disabled={isRestarting}
|
||||
disabled={isRestarting}
|
||||
onclick={tryAgain}
|
||||
>
|
||||
{#if isRestarting}
|
||||
Retrying…
|
||||
{:else}
|
||||
Try again
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline"
|
||||
onclick={async () => {
|
||||
try {
|
||||
await invoke("open_client_config_manager");
|
||||
} catch (err) {
|
||||
errorMessage = `Failed to open config manager: ${err}`;
|
||||
}
|
||||
}}
|
||||
>
|
||||
Advanced options
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user