From e17c95c76339293eb0a16cec51db6a18fc2b3046 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Sun, 28 Dec 2025 23:40:03 +0800 Subject: [PATCH] added a welcome window --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/app.rs | 15 ++---- src-tauri/src/lib.rs | 24 +++++++++- src-tauri/src/services/auth.rs | 46 +++++++++++++----- src-tauri/src/services/mod.rs | 2 + src-tauri/src/services/welcome.rs | 79 +++++++++++++++++++++++++++++++ src-tauri/src/state.rs | 1 + src/routes/+page.svelte | 2 +- src/routes/welcome/+page.svelte | 40 ++++++++++++++++ 10 files changed, 184 insertions(+), 27 deletions(-) create mode 100644 src-tauri/src/services/welcome.rs create mode 100644 src/routes/welcome/+page.svelte diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8f71a28..2c4392a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1164,6 +1164,7 @@ dependencies = [ "tauri-plugin-positioner", "thiserror 1.0.69", "tokio", + "tokio-util", "tracing", "tracing-appender", "tracing-subscriber", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index eef3075..d3f3078 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,6 +26,7 @@ tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } tauri-plugin-global-shortcut = "2" tauri-plugin-positioner = "2" reqwest = { version = "0.12.23", features = ["json", "native-tls", "blocking"] } +tokio-util = "0.7" ts-rs = "11.0.1" device_query = "4.0.1" dotenvy = "0.15.7" diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index b6aae5d..6249114 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -7,6 +7,7 @@ use crate::{ services::{ auth::{get_access_token, get_tokens}, scene::{close_splash_window, open_scene_window, open_splash_window}, + welcome::open_welcome_window, ws::init_ws_client, }, state::init_app_data, @@ -57,21 +58,13 @@ async fn construct_app() { pub async fn bootstrap() { match get_tokens().await { - Some(tokens) => { + Some(_tokens) => { info!("Tokens found in keyring - restoring user session"); construct_app().await; } None => { - info!("No active session found - user needs to authenticate"); - match crate::services::auth::init_auth_code_retrieval(|| { - info!("Authentication successful, creating scene..."); - tauri::async_runtime::spawn(async { - construct_app().await; - }); - }) { - Ok(it) => it, - Err(err) => todo!("Handle authentication error: {}", err), - }; + info!("No active session found - showing welcome first"); + open_welcome_window(); } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0740fe9..f050718 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,7 +6,10 @@ use crate::{ UserBasicDto, }, remotes::user::UserRemote, - services::{cursor::start_cursor_tracking, doll_editor::open_doll_editor_window}, + services::{ + cursor::start_cursor_tracking, + doll_editor::open_doll_editor_window, + }, state::{init_app_data, init_app_data_scoped, AppDataRefreshScope, FDOLL}, }; use tauri::async_runtime; @@ -327,6 +330,22 @@ fn quit_app() -> Result<(), String> { Ok(()) } +#[tauri::command] +fn start_auth_flow() -> Result<(), String> { + // Cancel any in-flight auth listener/state before starting a new one + 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 + crate::services::welcome::close_welcome_window(); + tauri::async_runtime::spawn(async { + crate::app::bootstrap().await; + }); + }) + .map_err(|e| e.to_string()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -354,7 +373,8 @@ pub fn run() { remove_active_doll, recolor_gif_base64, quit_app, - open_doll_editor_window + open_doll_editor_window, + start_auth_flow ]) .setup(|app| { APP_HANDLE diff --git a/src-tauri/src/services/auth.rs b/src-tauri/src/services/auth.rs index 0493425..8462258 100644 --- a/src-tauri/src/services/auth.rs +++ b/src-tauri/src/services/auth.rs @@ -11,8 +11,9 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use tauri_plugin_opener::OpenerExt; use thiserror::Error; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpListener; +use tokio::net::{TcpListener, TcpStream}; use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; use tracing::{error, info, warn}; use url::form_urlencoded; @@ -22,6 +23,14 @@ static REFRESH_LOCK: once_cell::sync::Lazy> = static AUTH_SUCCESS_HTML: &str = include_str!("../assets/auth-success.html"); const SERVICE_NAME: &str = "friendolls"; +pub fn cancel_auth_flow() { + let mut guard = lock_w!(FDOLL); + if let Some(token) = guard.oauth_flow.cancel_token.take() { + token.cancel(); + } + guard.oauth_flow = Default::default(); +} + /// Errors that can occur during OAuth authentication flow. #[derive(Debug, Error)] pub enum OAuthError { @@ -49,6 +58,9 @@ pub enum OAuthError { #[error("Callback timeout - no response received")] CallbackTimeout, + #[error("Authentication flow cancelled")] + Cancelled, + #[error("Invalid app configuration")] InvalidConfig, @@ -526,6 +538,7 @@ where let code_verifier = generate_code_verifier(64); let code_challenge = generate_code_challenge(&code_verifier); let state = generate_code_verifier(16); + let cancel_token = CancellationToken::new(); // Store state and code_verifier for validation let current_time = SystemTime::now() @@ -538,6 +551,7 @@ where guard.oauth_flow.state = Some(state.clone()); guard.oauth_flow.code_verifier = Some(code_verifier.clone()); guard.oauth_flow.initiated_at = Some(current_time); + guard.oauth_flow.cancel_token = Some(cancel_token.clone()); } let mut url = match url::Url::parse(&format!("{}/auth", &app_config.auth.auth_url)) { @@ -592,6 +606,7 @@ where .append_pair("code_challenge", &code_challenge) .append_pair("code_challenge_method", "S256"); let redirect_uri_clone = redirect_uri.clone(); + let cancel_token_clone = cancel_token.clone(); tauri::async_runtime::spawn(async move { info!("Starting callback listener task"); let listener = match TcpListener::from_std(std_listener) { @@ -602,7 +617,7 @@ where } }; - match listen_for_callback(listener).await { + match listen_for_callback(listener, cancel_token_clone).await { Ok(params) => { let (stored_state, stored_verifier) = { let guard = lock_r!(FDOLL); @@ -634,19 +649,13 @@ where error!("Failed to save auth pass: {}", e); } - // Immediately refresh app data now that auth is available - tauri::async_runtime::spawn(async { - crate::state::init_app_data_scoped( - crate::state::AppDataRefreshScope::All, - ) - .await; - }); - + // Defer app initialization to the shared bootstrap path on_success(); } Err(e) => error!("Token exchange failed: {}", e), } } + Err(OAuthError::Cancelled) => info!("Callback listener was cancelled"), Err(e) => error!("Callback listener error: {}", e), } }); @@ -753,7 +762,10 @@ pub async fn refresh_token(refresh_token: &str) -> Result /// Returns `OAuthError` if: /// - Required callback parameters are missing /// - Timeout is reached before callback is received -async fn listen_for_callback(listener: TcpListener) -> Result { +async fn listen_for_callback( + listener: TcpListener, + cancel_token: CancellationToken, +) -> Result { // Set a 5-minute timeout let timeout = Duration::from_secs(300); let start_time = Instant::now(); @@ -765,9 +777,17 @@ async fn listen_for_callback(listener: TcpListener) -> Result { + info!("Callback listener cancelled"); + return Err(OAuthError::Cancelled); + }, + res = tokio::time::timeout(remaining, listener.accept()) => res, + }; + + let (mut stream, _): (TcpStream, _) = match accept_result { Ok(Ok(res)) => res, Ok(Err(e)) => { warn!("Accept error: {}", e); diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 2910782..01c09eb 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -4,4 +4,6 @@ pub mod cursor; pub mod doll_editor; pub mod scene; pub mod sprite_recolor; +pub mod welcome; pub mod ws; + diff --git a/src-tauri/src/services/welcome.rs b/src-tauri/src/services/welcome.rs new file mode 100644 index 0000000..2eca6d9 --- /dev/null +++ b/src-tauri/src/services/welcome.rs @@ -0,0 +1,79 @@ +use crate::get_app_handle; +use tauri::Manager; +use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder, MessageDialogKind}; +use tauri_plugin_positioner::WindowExt; +use tracing::{error, info}; + +pub static WELCOME_WINDOW_LABEL: &str = "welcome"; + +pub fn open_welcome_window() { + let app_handle = get_app_handle(); + let existing_webview_window = app_handle.get_window(WELCOME_WINDOW_LABEL); + + if let Some(window) = existing_webview_window { + if let Err(e) = window.show() { + error!("Failed to show existing welcome window: {}", e); + MessageDialogBuilder::new( + app_handle.dialog().clone(), + "Window Error", + "Failed to show the welcome screen. Please restart and try again.", + ) + .kind(MessageDialogKind::Error) + .show(|_| {}); + } + return; + } + + let webview_window = match tauri::WebviewWindowBuilder::new( + app_handle, + WELCOME_WINDOW_LABEL, + tauri::WebviewUrl::App("/welcome".into()), + ) + .title("Welcome to Friendolls") + .inner_size(420.0, 420.0) + .resizable(false) + .decorations(true) + .transparent(false) + .shadow(true) + .visible(false) + .skip_taskbar(false) + .always_on_top(false) + .visible_on_all_workspaces(false) + .build() + { + Ok(window) => { + info!("{} window builder succeeded", WELCOME_WINDOW_LABEL); + window + } + Err(e) => { + error!("Failed to build {} window: {}", WELCOME_WINDOW_LABEL, e); + return; + } + }; + + if let Err(e) = webview_window.move_window(tauri_plugin_positioner::Position::Center) { + error!("Failed to move welcome window to center: {}", e); + } + + if let Err(e) = webview_window.show() { + error!("Failed to show welcome window: {}", e); + MessageDialogBuilder::new( + app_handle.dialog().clone(), + "Window Error", + "Failed to show the welcome screen. Please restart and try again.", + ) + .kind(MessageDialogKind::Error) + .show(|_| {}); + } +} + +pub fn close_welcome_window() { + let app_handle = get_app_handle(); + if let Some(window) = app_handle.get_window(WELCOME_WINDOW_LABEL) { + if let Err(e) = window.close() { + error!("Failed to close welcome window: {}", e); + } else { + info!("Welcome window closed"); + } + } +} diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index e701cff..f447d85 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -22,6 +22,7 @@ pub struct OAuthFlowTracker { pub state: Option, pub code_verifier: Option, pub initiated_at: Option, + pub cancel_token: Option, } pub struct Clients { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 79c5dd4..92ec786 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,3 +1,3 @@
- +

Friendolls desktop app shell

diff --git a/src/routes/welcome/+page.svelte b/src/routes/welcome/+page.svelte new file mode 100644 index 0000000..60fe2f0 --- /dev/null +++ b/src/routes/welcome/+page.svelte @@ -0,0 +1,40 @@ + + +
+
+
+
+

Friendolls

+

+ Passive social app connecting peers through mouse cursor interactions in the form of desktop pets. +

+
+
+ +
+
+
+