diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index bffdee5..7a31827 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -1,7 +1,12 @@ +use std::time::Duration; +use tokio::time::{sleep, Instant}; use tracing::info; use crate::{ - services::{auth::get_tokens, scene::open_scene_window}, + services::{ + auth::get_tokens, + scene::{close_splash_window, open_scene_window, open_splash_window}, + }, state::init_app_data, system_tray::init_system_tray, }; @@ -12,8 +17,32 @@ pub async fn start_fdoll() { } async fn construct_app() { - init_app_data().await; - crate::services::ws::init_ws_client().await; + open_splash_window(); + + // Record start time for minimum splash duration + let start = Instant::now(); + + // Spawn initialization tasks in parallel + // We want to wait for them to finish, but they run concurrently + let init_data = tauri::async_runtime::spawn(async { + init_app_data().await; + }); + + let init_ws = tauri::async_runtime::spawn(async { + crate::services::ws::init_ws_client().await; + }); + + // Wait for both to complete + let _ = tokio::join!(init_data, init_ws); + + // 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(); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bc37c4b..899263a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -46,15 +46,6 @@ fn setup_fdoll() -> Result<(), tauri::Error> { let file_appender = tracing_appender::rolling::daily(&app_log_dir, "friendolls.log"); let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); - // Keep the guard alive? - // Actually `_guard` will be dropped here, which might stop logging. - // Ideally we should store the guard in the app state or use a global lazy_static if we want it to persist. - // However, `tracing_appender` docs say: "WorkerGuard should be assigned in the main function or whatever the entrypoint of the program is." - // Since we are inside `setup_fdoll` which is called from `setup`, we might lose logs if we drop it. - // But for simplicity in this context, we can just let it leak or store it in a static. - // Let's leak it for now as this is a long-running app. - Box::leak(Box::new(_guard)); - tracing_subscriber::fmt() .with_target(false) .with_thread_ids(false) @@ -63,7 +54,7 @@ fn setup_fdoll() -> Result<(), tauri::Error> { .with_writer(non_blocking) // Log to file .init(); - state::init_fdoll_state(); + state::init_fdoll_state(Some(_guard)); async_runtime::spawn(async move { app::start_fdoll().await }); Ok(()) } diff --git a/src-tauri/src/services/cursor.rs b/src-tauri/src/services/cursor.rs index 3699377..1167be7 100644 --- a/src-tauri/src/services/cursor.rs +++ b/src-tauri/src/services/cursor.rs @@ -116,8 +116,6 @@ async fn init_cursor_tracking() -> Result<(), String> { // The producer closure moves `tx` into it. // device_query runs this closure on its own thread. - // Explicitly clone tx to ensure clear capture semantics - let tx_clone = tx.clone(); let _guard = device_state.on_mouse_move(move |position: &(i32, i32)| { // `device_query` crate appears to behave // differently on Windows vs other platforms. @@ -144,7 +142,7 @@ async fn init_cursor_tracking() -> Result<(), String> { }; // Send to consumer channel (non-blocking) - if let Err(e) = tx_clone.try_send(positions) { + if let Err(e) = tx.try_send(positions) { debug!("Failed to send cursor position to channel: {:?}", e); } }); diff --git a/src-tauri/src/services/scene.rs b/src-tauri/src/services/scene.rs index b343831..7b378db 100644 --- a/src-tauri/src/services/scene.rs +++ b/src-tauri/src/services/scene.rs @@ -3,6 +3,7 @@ use tauri::Manager; use tauri_plugin_positioner::WindowExt; use tracing::{error, info}; pub static SCENE_WINDOW_LABEL: &str = "scene"; +pub static SPLASH_WINDOW_LABEL: &str = "splash"; pub fn overlay_fullscreen(window: &tauri::Window) -> Result<(), tauri::Error> { // Get the primary monitor @@ -22,6 +23,65 @@ pub fn overlay_fullscreen(window: &tauri::Window) -> Result<(), tauri::Error> { Ok(()) } +pub fn open_splash_window() { + let app_handle = get_app_handle(); + let existing_webview_window = app_handle.get_window(SPLASH_WINDOW_LABEL); + + if let Some(window) = existing_webview_window { + window.show().unwrap(); + return; + } + + info!("Starting splash window creation..."); + let webview_window = match tauri::WebviewWindowBuilder::new( + app_handle, + SPLASH_WINDOW_LABEL, + tauri::WebviewUrl::App("/splash".into()), + ) + .title("Friendolls Splash") + .inner_size(600.0, 300.0) + .resizable(false) + .decorations(false) + .transparent(true) + .shadow(false) + .visible(false) // Show it after centering + .skip_taskbar(true) + .always_on_top(true) + .build() + { + Ok(window) => { + info!("Splash window builder succeeded"); + window + } + Err(e) => { + error!("Failed to build splash window: {}", e); + return; + } + }; + + if let Err(e) = webview_window.move_window(tauri_plugin_positioner::Position::Center) { + error!("Failed to move splash window to center: {}", e); + // Continue anyway + } + + if let Err(e) = webview_window.show() { + error!("Failed to show splash window: {}", e); + } + + info!("Splash window initialized successfully."); +} + +pub fn close_splash_window() { + let app_handle = get_app_handle(); + if let Some(window) = app_handle.get_window(SPLASH_WINDOW_LABEL) { + if let Err(e) = window.close() { + error!("Failed to close splash window: {}", e); + } else { + info!("Splash window closed"); + } + } +} + pub fn open_scene_window() { let app_handle = get_app_handle(); let existing_webview_window = app_handle.get_window(SCENE_WINDOW_LABEL); diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 1c56911..3632358 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -13,7 +13,7 @@ use std::{ env, sync::{Arc, LazyLock, RwLock}, }; -use tauri::{async_runtime, Emitter}; +use tauri::Emitter; use tracing::{info, warn}; #[derive(Default, Clone)] @@ -34,6 +34,7 @@ pub struct AppState { pub clients: Option, pub auth_pass: Option, pub oauth_flow: OAuthFlowTracker, + pub tracing_guard: Option, // exposed to the frontend pub app_data: AppData, @@ -44,10 +45,11 @@ pub struct AppState { pub static FDOLL: LazyLock>> = LazyLock::new(|| Arc::new(RwLock::new(AppState::default()))); -pub fn init_fdoll_state() { +pub fn init_fdoll_state(tracing_guard: Option) { { let mut guard = lock_w!(FDOLL); dotenvy::dotenv().ok(); + guard.tracing_guard = tracing_guard; guard.app_config = AppConfig { api_base_url: Some(env::var("API_BASE_URL").expect("API_BASE_URL must be set")), auth: AuthConfig { @@ -146,7 +148,7 @@ pub fn init_fdoll_state() { warn!("Could not initialize screen dimensions in global state - no monitor found"); } } - + info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)"); } diff --git a/src/assets/media/splash-dev.jpeg b/src/assets/media/splash-dev.jpeg new file mode 100644 index 0000000..36b5fb0 Binary files /dev/null and b/src/assets/media/splash-dev.jpeg differ diff --git a/src/routes/splash/+page.svelte b/src/routes/splash/+page.svelte new file mode 100644 index 0000000..c429512 --- /dev/null +++ b/src/routes/splash/+page.svelte @@ -0,0 +1,9 @@ +
+ +