websocket cursor data broadcast
This commit is contained in:
@@ -2,3 +2,4 @@ API_BASE_URL=http://127.0.0.1:3000
|
||||
AUTH_URL=https://auth.example.com/realms/friendolls/protocol/openid-connect/auth
|
||||
JWT_AUDIENCE=friendolls-desktop
|
||||
REDIRECT_URI=http://localhost:8582/callback
|
||||
REDIRECT_HOST=localhost:8582
|
||||
|
||||
144
src-tauri/Cargo.lock
generated
144
src-tauri/Cargo.lock
generated
@@ -8,6 +8,12 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -161,6 +167,28 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||
dependencies = [
|
||||
"async-stream-impl",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream-impl"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
@@ -213,6 +241,17 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "backoff"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"instant",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -656,6 +695,12 @@ dependencies = [
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.9"
|
||||
@@ -1051,6 +1096,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"rand 0.9.2",
|
||||
"reqwest",
|
||||
"rust_socketio",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -1852,6 +1898,15 @@ dependencies = [
|
||||
"cfb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@@ -3315,6 +3370,50 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_engineio"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3d3572ceba6c5d79eecedf3be93640ca9512fa4100dff6a70f96c514adf4f1f"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"native-tls",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tungstenite",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_socketio"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6a8672db895d567b3c0b8a4c0d3e98113ebb32badf6ce66004e743e5ee1e1e"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
"backoff",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"log",
|
||||
"native-tls",
|
||||
"rand 0.8.5",
|
||||
"rust_engineio",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3691,6 +3790,17 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@@ -4460,6 +4570,20 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.16"
|
||||
@@ -4720,6 +4844,26 @@ dependencies = [
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"native-tls",
|
||||
"rand 0.8.5",
|
||||
"sha1",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
|
||||
@@ -39,6 +39,7 @@ tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
once_cell = "1"
|
||||
flate2 = "1.0.28"
|
||||
rust_socketio = "0.6.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"windows": ["main", "scene", "preferences"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
"opener:default",
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-unlisten"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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