added a welcome window
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -1164,6 +1164,7 @@ dependencies = [
|
|||||||
"tauri-plugin-positioner",
|
"tauri-plugin-positioner",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
|||||||
tauri-plugin-global-shortcut = "2"
|
tauri-plugin-global-shortcut = "2"
|
||||||
tauri-plugin-positioner = "2"
|
tauri-plugin-positioner = "2"
|
||||||
reqwest = { version = "0.12.23", features = ["json", "native-tls", "blocking"] }
|
reqwest = { version = "0.12.23", features = ["json", "native-tls", "blocking"] }
|
||||||
|
tokio-util = "0.7"
|
||||||
ts-rs = "11.0.1"
|
ts-rs = "11.0.1"
|
||||||
device_query = "4.0.1"
|
device_query = "4.0.1"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::{
|
|||||||
services::{
|
services::{
|
||||||
auth::{get_access_token, get_tokens},
|
auth::{get_access_token, get_tokens},
|
||||||
scene::{close_splash_window, open_scene_window, open_splash_window},
|
scene::{close_splash_window, open_scene_window, open_splash_window},
|
||||||
|
welcome::open_welcome_window,
|
||||||
ws::init_ws_client,
|
ws::init_ws_client,
|
||||||
},
|
},
|
||||||
state::init_app_data,
|
state::init_app_data,
|
||||||
@@ -57,21 +58,13 @@ async fn construct_app() {
|
|||||||
|
|
||||||
pub async fn bootstrap() {
|
pub async fn bootstrap() {
|
||||||
match get_tokens().await {
|
match get_tokens().await {
|
||||||
Some(tokens) => {
|
Some(_tokens) => {
|
||||||
info!("Tokens found in keyring - restoring user session");
|
info!("Tokens found in keyring - restoring user session");
|
||||||
construct_app().await;
|
construct_app().await;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
info!("No active session found - user needs to authenticate");
|
info!("No active session found - showing welcome first");
|
||||||
match crate::services::auth::init_auth_code_retrieval(|| {
|
open_welcome_window();
|
||||||
info!("Authentication successful, creating scene...");
|
|
||||||
tauri::async_runtime::spawn(async {
|
|
||||||
construct_app().await;
|
|
||||||
});
|
|
||||||
}) {
|
|
||||||
Ok(it) => it,
|
|
||||||
Err(err) => todo!("Handle authentication error: {}", err),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ use crate::{
|
|||||||
UserBasicDto,
|
UserBasicDto,
|
||||||
},
|
},
|
||||||
remotes::user::UserRemote,
|
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},
|
state::{init_app_data, init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
||||||
};
|
};
|
||||||
use tauri::async_runtime;
|
use tauri::async_runtime;
|
||||||
@@ -327,6 +330,22 @@ fn quit_app() -> Result<(), String> {
|
|||||||
Ok(())
|
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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
@@ -354,7 +373,8 @@ pub fn run() {
|
|||||||
remove_active_doll,
|
remove_active_doll,
|
||||||
recolor_gif_base64,
|
recolor_gif_base64,
|
||||||
quit_app,
|
quit_app,
|
||||||
open_doll_editor_window
|
open_doll_editor_window,
|
||||||
|
start_auth_flow
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
APP_HANDLE
|
APP_HANDLE
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
|
|
||||||
@@ -22,6 +23,14 @@ static REFRESH_LOCK: once_cell::sync::Lazy<Mutex<()>> =
|
|||||||
static AUTH_SUCCESS_HTML: &str = include_str!("../assets/auth-success.html");
|
static AUTH_SUCCESS_HTML: &str = include_str!("../assets/auth-success.html");
|
||||||
const SERVICE_NAME: &str = "friendolls";
|
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.
|
/// Errors that can occur during OAuth authentication flow.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum OAuthError {
|
pub enum OAuthError {
|
||||||
@@ -49,6 +58,9 @@ pub enum OAuthError {
|
|||||||
#[error("Callback timeout - no response received")]
|
#[error("Callback timeout - no response received")]
|
||||||
CallbackTimeout,
|
CallbackTimeout,
|
||||||
|
|
||||||
|
#[error("Authentication flow cancelled")]
|
||||||
|
Cancelled,
|
||||||
|
|
||||||
#[error("Invalid app configuration")]
|
#[error("Invalid app configuration")]
|
||||||
InvalidConfig,
|
InvalidConfig,
|
||||||
|
|
||||||
@@ -526,6 +538,7 @@ where
|
|||||||
let code_verifier = generate_code_verifier(64);
|
let code_verifier = generate_code_verifier(64);
|
||||||
let code_challenge = generate_code_challenge(&code_verifier);
|
let code_challenge = generate_code_challenge(&code_verifier);
|
||||||
let state = generate_code_verifier(16);
|
let state = generate_code_verifier(16);
|
||||||
|
let cancel_token = CancellationToken::new();
|
||||||
|
|
||||||
// Store state and code_verifier for validation
|
// Store state and code_verifier for validation
|
||||||
let current_time = SystemTime::now()
|
let current_time = SystemTime::now()
|
||||||
@@ -538,6 +551,7 @@ where
|
|||||||
guard.oauth_flow.state = Some(state.clone());
|
guard.oauth_flow.state = Some(state.clone());
|
||||||
guard.oauth_flow.code_verifier = Some(code_verifier.clone());
|
guard.oauth_flow.code_verifier = Some(code_verifier.clone());
|
||||||
guard.oauth_flow.initiated_at = Some(current_time);
|
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)) {
|
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", &code_challenge)
|
||||||
.append_pair("code_challenge_method", "S256");
|
.append_pair("code_challenge_method", "S256");
|
||||||
let redirect_uri_clone = redirect_uri.clone();
|
let redirect_uri_clone = redirect_uri.clone();
|
||||||
|
let cancel_token_clone = cancel_token.clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
info!("Starting callback listener task");
|
info!("Starting callback listener task");
|
||||||
let listener = match TcpListener::from_std(std_listener) {
|
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) => {
|
Ok(params) => {
|
||||||
let (stored_state, stored_verifier) = {
|
let (stored_state, stored_verifier) = {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
@@ -634,19 +649,13 @@ where
|
|||||||
error!("Failed to save auth pass: {}", e);
|
error!("Failed to save auth pass: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Immediately refresh app data now that auth is available
|
// Defer app initialization to the shared bootstrap path
|
||||||
tauri::async_runtime::spawn(async {
|
|
||||||
crate::state::init_app_data_scoped(
|
|
||||||
crate::state::AppDataRefreshScope::All,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
|
|
||||||
on_success();
|
on_success();
|
||||||
}
|
}
|
||||||
Err(e) => error!("Token exchange failed: {}", e),
|
Err(e) => error!("Token exchange failed: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(OAuthError::Cancelled) => info!("Callback listener was cancelled"),
|
||||||
Err(e) => error!("Callback listener error: {}", e),
|
Err(e) => error!("Callback listener error: {}", e),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -753,7 +762,10 @@ pub async fn refresh_token(refresh_token: &str) -> Result<AuthPass, OAuthError>
|
|||||||
/// Returns `OAuthError` if:
|
/// Returns `OAuthError` if:
|
||||||
/// - Required callback parameters are missing
|
/// - Required callback parameters are missing
|
||||||
/// - Timeout is reached before callback is received
|
/// - Timeout is reached before callback is received
|
||||||
async fn listen_for_callback(listener: TcpListener) -> Result<OAuthCallbackParams, OAuthError> {
|
async fn listen_for_callback(
|
||||||
|
listener: TcpListener,
|
||||||
|
cancel_token: CancellationToken,
|
||||||
|
) -> Result<OAuthCallbackParams, OAuthError> {
|
||||||
// Set a 5-minute timeout
|
// Set a 5-minute timeout
|
||||||
let timeout = Duration::from_secs(300);
|
let timeout = Duration::from_secs(300);
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
@@ -765,9 +777,17 @@ async fn listen_for_callback(listener: TcpListener) -> Result<OAuthCallbackParam
|
|||||||
return Err(OAuthError::CallbackTimeout);
|
return Err(OAuthError::CallbackTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
let accept_result = tokio::time::timeout(timeout - elapsed, listener.accept()).await;
|
let remaining = timeout - elapsed;
|
||||||
|
|
||||||
let (mut stream, _) = match accept_result {
|
let accept_result = tokio::select! {
|
||||||
|
_ = cancel_token.cancelled() => {
|
||||||
|
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(Ok(res)) => res,
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
warn!("Accept error: {}", e);
|
warn!("Accept error: {}", e);
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ pub mod cursor;
|
|||||||
pub mod doll_editor;
|
pub mod doll_editor;
|
||||||
pub mod scene;
|
pub mod scene;
|
||||||
pub mod sprite_recolor;
|
pub mod sprite_recolor;
|
||||||
|
pub mod welcome;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|
||||||
|
|||||||
79
src-tauri/src/services/welcome.rs
Normal file
79
src-tauri/src/services/welcome.rs
Normal file
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ pub struct OAuthFlowTracker {
|
|||||||
pub state: Option<String>,
|
pub state: Option<String>,
|
||||||
pub code_verifier: Option<String>,
|
pub code_verifier: Option<String>,
|
||||||
pub initiated_at: Option<u64>,
|
pub initiated_at: Option<u64>,
|
||||||
|
pub cancel_token: Option<tokio_util::sync::CancellationToken>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Clients {
|
pub struct Clients {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<main class="card-body">
|
<main class="card-body">
|
||||||
<button class="btn btn-primary">Hello TailwindCSS!</button>
|
<p class="text-lg">Friendolls desktop app shell</p>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
40
src/routes/welcome/+page.svelte
Normal file
40
src/routes/welcome/+page.svelte
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
|
|
||||||
|
let isContinuing = false;
|
||||||
|
|
||||||
|
const handleContinue = async () => {
|
||||||
|
if (isContinuing) return;
|
||||||
|
isContinuing = true;
|
||||||
|
try {
|
||||||
|
await invoke("start_auth_flow");
|
||||||
|
await getCurrentWebviewWindow().close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to start auth flow", error);
|
||||||
|
isContinuing = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full h-full bg-base-100 flex items-center justify-center p-8">
|
||||||
|
<div class="card w-full max-w-md bg-base-200 shadow-lg">
|
||||||
|
<div class="card-body gap-4">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h1 class="text-2xl font-semibold">Friendolls</h1>
|
||||||
|
<p class="text-base text-base-content/80">
|
||||||
|
Passive social app connecting peers through mouse cursor interactions in the form of desktop pets.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions justify-end pt-2">
|
||||||
|
<button class="btn btn-primary" onclick={handleContinue} disabled={isContinuing}>
|
||||||
|
{#if isContinuing}
|
||||||
|
Loading...
|
||||||
|
{:else}
|
||||||
|
Continue
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user