websocket cursor data broadcast
This commit is contained in:
@@ -3,7 +3,7 @@ use tauri_plugin_positioner::WindowExt;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
core::services::auth::get_tokens,
|
||||
core::services::{auth::get_tokens, preferences::create_preferences_window},
|
||||
get_app_handle,
|
||||
services::overlay::{overlay_fullscreen, SCENE_WINDOW_LABEL},
|
||||
};
|
||||
@@ -16,7 +16,8 @@ pub async fn init_session() {
|
||||
match get_tokens().await {
|
||||
Some(_) => {
|
||||
info!("User session restored");
|
||||
create_scene().await;
|
||||
create_scene();
|
||||
create_preferences_window();
|
||||
}
|
||||
None => {
|
||||
info!("No active session, user needs to authenticate");
|
||||
@@ -24,14 +25,15 @@ pub async fn init_session() {
|
||||
info!("Authentication successful, creating scene...");
|
||||
tauri::async_runtime::spawn(async {
|
||||
info!("Creating scene after auth success...");
|
||||
create_scene().await;
|
||||
create_scene();
|
||||
create_preferences_window();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_scene() {
|
||||
pub fn create_scene() {
|
||||
info!("Starting scene creation...");
|
||||
let webview_window = match tauri::WebviewWindowBuilder::new(
|
||||
get_app_handle(),
|
||||
|
||||
@@ -371,9 +371,15 @@ pub async fn exchange_code_for_auth_pass(
|
||||
) -> Result<AuthPass, OAuthError> {
|
||||
let (app_config, http_client) = {
|
||||
let guard = lock_r!(FDOLL);
|
||||
let clients = guard.clients.as_ref();
|
||||
if clients.is_none() {
|
||||
error!("Clients not initialized yet!");
|
||||
return Err(OAuthError::InvalidConfig);
|
||||
}
|
||||
info!("HTTP client retrieved successfully for token exchange");
|
||||
(
|
||||
guard.app_config.clone().ok_or(OAuthError::InvalidConfig)?,
|
||||
guard.http_client.clone(),
|
||||
guard.app_config.clone(),
|
||||
clients.unwrap().http_client.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
@@ -389,13 +395,35 @@ pub async fn exchange_code_for_auth_pass(
|
||||
.finish();
|
||||
|
||||
info!("Exchanging authorization code for tokens");
|
||||
info!("Token endpoint URL: {}", url);
|
||||
info!("Request body length: {} bytes", body.len());
|
||||
|
||||
let exchange_request = http_client
|
||||
.post(url)
|
||||
.post(url.clone())
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(body);
|
||||
|
||||
let exchange_request_response = exchange_request.send().await?;
|
||||
info!("Sending token exchange request...");
|
||||
let exchange_request_response = match exchange_request.send().await {
|
||||
Ok(resp) => {
|
||||
info!("Received response with status: {}", resp.status());
|
||||
resp
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to send token exchange request: {}", e);
|
||||
error!("Error details: {:?}", e);
|
||||
if e.is_timeout() {
|
||||
error!("Request timed out");
|
||||
}
|
||||
if e.is_connect() {
|
||||
error!("Connection error - check network and DNS");
|
||||
}
|
||||
if e.is_request() {
|
||||
error!("Request error - check request format");
|
||||
}
|
||||
return Err(OAuthError::NetworkError(e));
|
||||
}
|
||||
};
|
||||
|
||||
if !exchange_request_response.status().is_success() {
|
||||
let status = exchange_request_response.status();
|
||||
@@ -446,16 +474,14 @@ pub fn init_auth_code_retrieval<F>(on_success: F)
|
||||
where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
let app_config = match lock_r!(FDOLL).app_config.clone() {
|
||||
Some(config) => config,
|
||||
None => {
|
||||
error!("Cannot initialize auth: app config not available");
|
||||
return;
|
||||
}
|
||||
};
|
||||
info!("init_auth_code_retrieval called");
|
||||
let app_config = lock_r!(FDOLL).app_config.clone();
|
||||
|
||||
let opener = match APP_HANDLE.get() {
|
||||
Some(handle) => handle.opener(),
|
||||
Some(handle) => {
|
||||
info!("APP_HANDLE retrieved successfully");
|
||||
handle.opener()
|
||||
}
|
||||
None => {
|
||||
error!("Cannot initialize auth: app handle not available");
|
||||
return;
|
||||
@@ -480,7 +506,10 @@ where
|
||||
}
|
||||
|
||||
let mut url = match url::Url::parse(&format!("{}/auth", &app_config.auth.auth_url)) {
|
||||
Ok(url) => url,
|
||||
Ok(url) => {
|
||||
info!("Parsed auth URL successfully");
|
||||
url
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Invalid auth URL configuration: {}", e);
|
||||
return;
|
||||
@@ -501,8 +530,10 @@ where
|
||||
// Bind the server FIRST to ensure port is open
|
||||
// We bind synchronously using std::net::TcpListener then convert to tokio::net::TcpListener
|
||||
// to ensure the port is bound before we open the browser.
|
||||
info!("Attempting to bind to: {}", app_config.auth.redirect_host);
|
||||
let std_listener = match std::net::TcpListener::bind(&app_config.auth.redirect_host) {
|
||||
Ok(s) => {
|
||||
info!("Successfully bound to {}", app_config.auth.redirect_host);
|
||||
s.set_nonblocking(true).unwrap();
|
||||
s
|
||||
}
|
||||
@@ -514,7 +545,7 @@ where
|
||||
|
||||
info!(
|
||||
"Listening on {} for /callback",
|
||||
&app_config.auth.redirect_host
|
||||
app_config.auth.redirect_host
|
||||
);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -571,8 +602,11 @@ where
|
||||
}
|
||||
});
|
||||
|
||||
info!("Opening auth URL: {}", url);
|
||||
if let Err(e) = opener.open_url(url, None::<&str>) {
|
||||
error!("Failed to open auth portal: {}", e);
|
||||
} else {
|
||||
info!("Successfully called open_url for auth portal");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,8 +626,13 @@ pub async fn refresh_token(refresh_token: &str) -> Result<AuthPass, OAuthError>
|
||||
let (app_config, http_client) = {
|
||||
let guard = lock_r!(FDOLL);
|
||||
(
|
||||
guard.app_config.clone().ok_or(OAuthError::InvalidConfig)?,
|
||||
guard.http_client.clone(),
|
||||
guard.app_config.clone(),
|
||||
guard
|
||||
.clients
|
||||
.as_ref()
|
||||
.expect("clients present")
|
||||
.http_client
|
||||
.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
pub mod auth;
|
||||
pub mod preferences;
|
||||
pub mod ws;
|
||||
|
||||
35
src-tauri/src/core/services/preferences.rs
Normal file
35
src-tauri/src/core/services/preferences.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::get_app_handle;
|
||||
|
||||
pub fn create_preferences_window() {
|
||||
let webview_window = match tauri::WebviewWindowBuilder::new(
|
||||
get_app_handle(),
|
||||
"preferences",
|
||||
tauri::WebviewUrl::App("/preferences".into()),
|
||||
)
|
||||
.title("Friendolls Preferences")
|
||||
.inner_size(600.0, 500.0)
|
||||
.resizable(true)
|
||||
.decorations(true)
|
||||
.transparent(false)
|
||||
.shadow(true)
|
||||
.visible(true)
|
||||
.skip_taskbar(false)
|
||||
.always_on_top(false)
|
||||
.visible_on_all_workspaces(false)
|
||||
.build()
|
||||
{
|
||||
Ok(window) => {
|
||||
info!("Preferences window builder succeeded");
|
||||
window
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to build Preferences window: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
webview_window.open_devtools();
|
||||
}
|
||||
63
src-tauri/src/core/services/ws.rs
Normal file
63
src-tauri/src/core/services/ws.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use rust_socketio::{ClientBuilder, Payload, RawClient};
|
||||
use serde_json::json;
|
||||
use tauri::async_runtime;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
core::{models::app_config::AppConfig, state::FDOLL},
|
||||
lock_r,
|
||||
services::cursor::CursorPosition,
|
||||
};
|
||||
|
||||
// Define a callback for handling incoming messages (e.g., 'pong')
|
||||
fn on_pong(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(str) => println!("Received pong: {:?}", str),
|
||||
Payload::Binary(bin) => println!("Received pong (binary): {:?}", bin),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
||||
let client = {
|
||||
let guard = lock_r!(FDOLL);
|
||||
guard
|
||||
.clients
|
||||
.as_ref()
|
||||
.expect("Clients are initialized")
|
||||
.ws_client
|
||||
.as_ref()
|
||||
.expect("WebSocket client is initialized")
|
||||
.clone()
|
||||
};
|
||||
|
||||
match async_runtime::spawn_blocking(move || {
|
||||
client.emit(
|
||||
"cursor-report-position",
|
||||
Payload::Text(vec![json!({ "position": cursor_position })]),
|
||||
)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => (),
|
||||
Ok(Err(e)) => error!("Failed to emit ping: {}", e),
|
||||
Err(e) => error!("Failed to execute blocking task: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_ws_client(app_config: &AppConfig) -> rust_socketio::client::Client {
|
||||
let client = match ClientBuilder::new(
|
||||
app_config
|
||||
.api_base_url
|
||||
.as_ref()
|
||||
.expect("Missing API base URL"),
|
||||
)
|
||||
.namespace("/")
|
||||
.on("pong", on_pong)
|
||||
.connect()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(_) => todo!("TODO error handling"),
|
||||
};
|
||||
client
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
// in app-core/src/state.rs
|
||||
use crate::{
|
||||
core::models::app_config::{AppConfig, AuthConfig},
|
||||
core::services::auth::{load_auth_pass, AuthPass},
|
||||
core::{
|
||||
models::app_config::{AppConfig, AuthConfig},
|
||||
services::{
|
||||
auth::{load_auth_pass, AuthPass},
|
||||
ws::build_ws_client,
|
||||
},
|
||||
},
|
||||
lock_w,
|
||||
};
|
||||
use reqwest::Client;
|
||||
use std::{
|
||||
env,
|
||||
sync::{Arc, LazyLock, RwLock},
|
||||
};
|
||||
use tracing::warn;
|
||||
use tauri::async_runtime;
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct OAuthFlowTracker {
|
||||
@@ -18,10 +23,15 @@ pub struct OAuthFlowTracker {
|
||||
pub initiated_at: Option<u64>,
|
||||
}
|
||||
|
||||
pub struct Clients {
|
||||
pub http_client: reqwest::Client,
|
||||
pub ws_client: Option<rust_socketio::client::Client>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AppState {
|
||||
pub app_config: Option<AppConfig>,
|
||||
pub http_client: Client,
|
||||
pub app_config: AppConfig,
|
||||
pub clients: Option<Clients>,
|
||||
pub auth_pass: Option<AuthPass>,
|
||||
pub oauth_flow: OAuthFlowTracker,
|
||||
}
|
||||
@@ -35,7 +45,7 @@ pub fn init_fdoll_state() {
|
||||
{
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
dotenvy::dotenv().ok();
|
||||
guard.app_config = Some(AppConfig {
|
||||
guard.app_config = AppConfig {
|
||||
api_base_url: Some(env::var("API_BASE_URL").expect("API_BASE_URL must be set")),
|
||||
auth: AuthConfig {
|
||||
audience: env::var("JWT_AUDIENCE").expect("JWT_AUDIENCE must be set"),
|
||||
@@ -43,7 +53,7 @@ pub fn init_fdoll_state() {
|
||||
redirect_uri: env::var("REDIRECT_URI").expect("REDIRECT_URI must be set"),
|
||||
redirect_host: env::var("REDIRECT_HOST").expect("REDIRECT_HOST must be set"),
|
||||
},
|
||||
});
|
||||
};
|
||||
guard.auth_pass = match load_auth_pass() {
|
||||
Ok(pass) => pass,
|
||||
Err(e) => {
|
||||
@@ -51,9 +61,42 @@ pub fn init_fdoll_state() {
|
||||
None
|
||||
}
|
||||
};
|
||||
guard.http_client = reqwest::ClientBuilder::new()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
info!("Loaded auth pass");
|
||||
|
||||
// Initialize HTTP client immediately (non-blocking)
|
||||
let http_client = reqwest::ClientBuilder::new()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.user_agent("friendolls-desktop/0.1.0")
|
||||
.build()
|
||||
.expect("Client should build");
|
||||
|
||||
// Store HTTP client immediately - WebSocket client will be added later
|
||||
guard.clients = Some(Clients {
|
||||
http_client,
|
||||
ws_client: None,
|
||||
});
|
||||
info!("Initialized HTTP client");
|
||||
|
||||
// Clone app_config for async task
|
||||
let app_config = guard.app_config.clone();
|
||||
|
||||
// Drop the write lock before spawning async task
|
||||
drop(guard);
|
||||
|
||||
// Initialize WebSocket client in a blocking task to avoid runtime conflicts
|
||||
async_runtime::spawn(async move {
|
||||
let ws_client = async_runtime::spawn_blocking(move || build_ws_client(&app_config))
|
||||
.await
|
||||
.expect("Failed to initialize WebSocket client");
|
||||
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if let Some(clients) = guard.clients.as_mut() {
|
||||
clients.ws_client = Some(ws_client);
|
||||
}
|
||||
info!("Initialized FDOLL state with WebSocket client");
|
||||
});
|
||||
|
||||
info!("Initialized FDOLL state (WebSocket client initializing asynchronously)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::services::cursor::channel_cursor_positions;
|
||||
use crate::services::cursor::start_cursor_tracking;
|
||||
use tauri::async_runtime;
|
||||
use tracing_subscriber;
|
||||
|
||||
@@ -48,7 +48,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_positioner::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![channel_cursor_positions])
|
||||
.invoke_handler(tauri::generate_handler![start_cursor_tracking])
|
||||
.setup(|app| {
|
||||
APP_HANDLE
|
||||
.set(app.handle().to_owned())
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use device_query::{DeviceEvents, DeviceEventsHandler};
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tauri::ipc::Channel;
|
||||
use tauri::Emitter;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::get_app_handle;
|
||||
|
||||
@@ -19,6 +23,8 @@ pub struct CursorPositions {
|
||||
pub mapped: CursorPosition,
|
||||
}
|
||||
|
||||
static CURSOR_TRACKER: OnceCell<()> = OnceCell::new();
|
||||
|
||||
fn map_to_grid(
|
||||
pos: &CursorPosition,
|
||||
grid_size: i32,
|
||||
@@ -31,17 +37,90 @@ fn map_to_grid(
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 channel_cursor_positions(on_event: Channel<CursorPositions>) {
|
||||
pub async fn start_cursor_tracking() -> Result<(), String> {
|
||||
info!("start_cursor_tracking called");
|
||||
|
||||
// Use OnceCell to ensure this only runs once, even if called from multiple windows
|
||||
CURSOR_TRACKER.get_or_init(|| {
|
||||
info!("First call to start_cursor_tracking - spawning cursor tracking task");
|
||||
tauri::async_runtime::spawn(async {
|
||||
if let Err(e) = init_cursor_tracking().await {
|
||||
error!("Failed to initialize cursor tracking: {}", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
info!("Cursor tracking initialization registered");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn init_cursor_tracking() -> Result<(), String> {
|
||||
info!("Initializing cursor tracking...");
|
||||
let app_handle = get_app_handle();
|
||||
let primary_monitor = app_handle.primary_monitor().unwrap().unwrap();
|
||||
|
||||
// Get primary monitor with retries
|
||||
let primary_monitor = {
|
||||
let mut retry_count = 0;
|
||||
let max_retries = 3;
|
||||
loop {
|
||||
match app_handle.primary_monitor() {
|
||||
Ok(Some(monitor)) => {
|
||||
info!("Primary monitor acquired");
|
||||
break monitor;
|
||||
}
|
||||
Ok(None) => {
|
||||
retry_count += 1;
|
||||
if retry_count >= max_retries {
|
||||
return Err(format!(
|
||||
"No primary monitor found after {} retries",
|
||||
max_retries
|
||||
));
|
||||
}
|
||||
warn!(
|
||||
"Primary monitor not available, retrying... ({}/{})",
|
||||
retry_count, max_retries
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
Err(e) => {
|
||||
retry_count += 1;
|
||||
if retry_count >= max_retries {
|
||||
return Err(format!("Failed to get primary monitor: {}", e));
|
||||
}
|
||||
warn!(
|
||||
"Error getting primary monitor, retrying... ({}/{}): {}",
|
||||
retry_count, max_retries, e
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let monitor_dimensions = primary_monitor.size();
|
||||
let logical_monitor_dimensions: tauri::LogicalSize<i32> =
|
||||
monitor_dimensions.to_logical(primary_monitor.scale_factor());
|
||||
let device_state =
|
||||
DeviceEventsHandler::new(Duration::from_millis(200)).expect("Failed to start event loop");
|
||||
|
||||
let _guard = device_state.on_mouse_move(move |position| {
|
||||
info!(
|
||||
"Monitor dimensions: {}x{}",
|
||||
logical_monitor_dimensions.width, logical_monitor_dimensions.height
|
||||
);
|
||||
|
||||
// Try to initialize the device event handler
|
||||
let device_state = DeviceEventsHandler::new(Duration::from_millis(500))
|
||||
.ok_or("Failed to create device event handler (already running?)")?;
|
||||
|
||||
info!("Device event handler created successfully");
|
||||
info!("Setting up mouse move handler for event broadcasting...");
|
||||
|
||||
let send_count = Arc::new(AtomicU64::new(0));
|
||||
let send_count_clone = Arc::clone(&send_count);
|
||||
let app_handle_clone = app_handle.clone();
|
||||
|
||||
let _guard = device_state.on_mouse_move(move |position: &(i32, i32)| {
|
||||
let raw = CursorPosition {
|
||||
x: position.0,
|
||||
y: position.1,
|
||||
@@ -52,13 +131,36 @@ pub async fn channel_cursor_positions(on_event: Channel<CursorPositions>) {
|
||||
logical_monitor_dimensions.width,
|
||||
logical_monitor_dimensions.height,
|
||||
);
|
||||
let positions = CursorPositions { raw, mapped };
|
||||
let _ = on_event.send(positions);
|
||||
let positions = CursorPositions {
|
||||
raw,
|
||||
mapped: mapped.clone(),
|
||||
};
|
||||
|
||||
// Report to server (existing functionality)
|
||||
let mapped_for_ws = mapped.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
crate::core::services::ws::report_cursor_data(mapped_for_ws).await;
|
||||
});
|
||||
|
||||
// Broadcast to ALL windows using events
|
||||
match app_handle_clone.emit("cursor-position", &positions) {
|
||||
Ok(_) => {
|
||||
let count = send_count_clone.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if count % 100 == 0 {
|
||||
info!("Broadcast {} cursor position updates to all windows. Latest: raw({}, {}), mapped({}, {})",
|
||||
count, positions.raw.x, positions.raw.y, positions.mapped.x, positions.mapped.y);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to emit cursor position event: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
info!("Mouse move handler registered - now broadcasting cursor events to all windows");
|
||||
|
||||
// Keep the handler alive forever
|
||||
loop {
|
||||
// for whatever reason this sleep is not taking effect but it
|
||||
// does reduce CPU usage on my Mac from 100% to 6% so...cool!
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await
|
||||
tokio::time::sleep(Duration::from_secs(3600)).await;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user