minor refinements
This commit is contained in:
@@ -28,5 +28,10 @@
|
|||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.3"
|
"vite": "^6.0.3"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"esbuild"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
onlyBuiltDependencies:
|
|
||||||
- esbuild
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
API_BASE_URL=http://127.0.0.1:3000
|
API_BASE_URL=http://127.0.0.1:3000
|
||||||
AUTH_URL=https://auth.example.com/realms/friendolls/protocol/openid-connect
|
AUTH_URL=https://auth.example.com/realms/friendolls/protocol/openid-connect
|
||||||
JWT_AUDIENCE=friendolls-desktop
|
JWT_AUDIENCE=friendolls-desktop
|
||||||
REDIRECT_URI=http://localhost:8582/callback
|
|
||||||
REDIRECT_HOST=localhost:8582
|
|
||||||
|
|||||||
198
src-tauri/Cargo.lock
generated
198
src-tauri/Cargo.lock
generated
@@ -53,6 +53,27 @@ version = "1.0.100"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ashpd"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df"
|
||||||
|
dependencies = [
|
||||||
|
"enumflags2",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"raw-window-handle",
|
||||||
|
"serde",
|
||||||
|
"serde_repr",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-broadcast"
|
name = "async-broadcast"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@@ -803,6 +824,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
|
"block2 0.6.2",
|
||||||
|
"libc",
|
||||||
"objc2 0.6.3",
|
"objc2 0.6.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -817,6 +840,15 @@ dependencies = [
|
|||||||
"syn 2.0.110",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dlib"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||||
|
dependencies = [
|
||||||
|
"libloading",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dlopen2"
|
name = "dlopen2"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -846,6 +878,12 @@ version = "0.15.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "downcast-rs"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dpi"
|
name = "dpi"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -1102,12 +1140,14 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-positioner",
|
"tauri-plugin-positioner",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"url",
|
"url",
|
||||||
@@ -2962,7 +3002,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"indexmap 2.12.0",
|
"indexmap 2.12.0",
|
||||||
"quick-xml",
|
"quick-xml 0.38.4",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
@@ -3091,6 +3131,15 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.37.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.38.4"
|
version = "0.38.4"
|
||||||
@@ -3356,6 +3405,31 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfd"
|
||||||
|
version = "0.15.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
|
||||||
|
dependencies = [
|
||||||
|
"ashpd",
|
||||||
|
"block2 0.6.2",
|
||||||
|
"dispatch2",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"gtk-sys",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"objc2 0.6.3",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation 0.3.2",
|
||||||
|
"raw-window-handle",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -3550,6 +3624,12 @@ dependencies = [
|
|||||||
"syn 2.0.110",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scoped-tls"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -4248,6 +4328,46 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-dialog"
|
||||||
|
version = "2.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"raw-window-handle",
|
||||||
|
"rfd",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-plugin-fs",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-fs"
|
||||||
|
version = "2.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"dunce",
|
||||||
|
"glob",
|
||||||
|
"percent-encoding",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-utils",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"toml 0.9.8",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-global-shortcut"
|
name = "tauri-plugin-global-shortcut"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -4534,8 +4654,10 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
|
"tracing",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4748,6 +4870,18 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-appender"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"time",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.30"
|
version = "0.1.30"
|
||||||
@@ -5162,6 +5296,66 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-backend"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"downcast-rs",
|
||||||
|
"rustix",
|
||||||
|
"scoped-tls",
|
||||||
|
"smallvec",
|
||||||
|
"wayland-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-client"
|
||||||
|
version = "0.31.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"rustix",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-scanner",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-protocols"
|
||||||
|
version = "0.32.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-scanner",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-scanner"
|
||||||
|
version = "0.31.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quick-xml 0.37.5",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-sys"
|
||||||
|
version = "0.31.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142"
|
||||||
|
dependencies = [
|
||||||
|
"dlib",
|
||||||
|
"log",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.77"
|
version = "0.3.77"
|
||||||
@@ -5908,6 +6102,7 @@ dependencies = [
|
|||||||
"ordered-stream",
|
"ordered-stream",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uds_windows",
|
"uds_windows",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -6048,6 +6243,7 @@ dependencies = [
|
|||||||
"endi",
|
"endi",
|
||||||
"enumflags2",
|
"enumflags2",
|
||||||
"serde",
|
"serde",
|
||||||
|
"url",
|
||||||
"winnow 0.7.13",
|
"winnow 0.7.13",
|
||||||
"zvariant_derive",
|
"zvariant_derive",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ tracing-subscriber = "0.3"
|
|||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
flate2 = "1.0.28"
|
flate2 = "1.0.28"
|
||||||
rust_socketio = "0.6.0"
|
rust_socketio = "0.6.0"
|
||||||
|
tracing-appender = "0.2.4"
|
||||||
|
tauri-plugin-dialog = "2.4.2"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-global-shortcut = "2"
|
tauri-plugin-global-shortcut = "2"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
|
"dialog:default",
|
||||||
"core:event:allow-listen",
|
"core:event:allow-listen",
|
||||||
"core:event:allow-unlisten",
|
"core:event:allow-unlisten",
|
||||||
"core:window:allow-close"
|
"core:window:allow-close"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub async fn start_fdoll() {
|
|||||||
|
|
||||||
async fn construct_app() {
|
async fn construct_app() {
|
||||||
init_app_data().await;
|
init_app_data().await;
|
||||||
|
crate::services::ws::init_ws_client().await;
|
||||||
open_scene_window();
|
open_scene_window();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
state::{init_app_data, FDOLL},
|
state::{init_app_data, FDOLL},
|
||||||
};
|
};
|
||||||
use tauri::async_runtime;
|
use tauri::async_runtime;
|
||||||
|
use tauri::Manager;
|
||||||
use tracing_subscriber;
|
use tracing_subscriber;
|
||||||
|
|
||||||
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
||||||
@@ -29,11 +30,37 @@ pub fn get_app_handle<'a>() -> &'a tauri::AppHandle<tauri::Wry> {
|
|||||||
|
|
||||||
fn setup_fdoll() -> Result<(), tauri::Error> {
|
fn setup_fdoll() -> Result<(), tauri::Error> {
|
||||||
// Initialize tracing subscriber for logging
|
// Initialize tracing subscriber for logging
|
||||||
|
|
||||||
|
// Set up file appender
|
||||||
|
let app_handle = get_app_handle();
|
||||||
|
let app_log_dir = app_handle
|
||||||
|
.path()
|
||||||
|
.app_log_dir()
|
||||||
|
.expect("Could not determine app log dir");
|
||||||
|
|
||||||
|
// Create the directory if it doesn't exist
|
||||||
|
if let Err(e) = std::fs::create_dir_all(&app_log_dir) {
|
||||||
|
eprintln!("Failed to create log directory: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
tracing_subscriber::fmt()
|
||||||
.with_target(false)
|
.with_target(false)
|
||||||
.with_thread_ids(false)
|
.with_thread_ids(false)
|
||||||
.with_file(true)
|
.with_file(true)
|
||||||
.with_line_number(true)
|
.with_line_number(true)
|
||||||
|
.with_writer(non_blocking) // Log to file
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
state::init_fdoll_state();
|
state::init_fdoll_state();
|
||||||
@@ -153,7 +180,7 @@ pub fn run() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_positioner::init())
|
.plugin(tauri_plugin_positioner::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
start_cursor_tracking,
|
start_cursor_tracking,
|
||||||
get_app_data,
|
get_app_data,
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub struct AuthConfig {
|
pub struct AuthConfig {
|
||||||
pub audience: String,
|
pub audience: String,
|
||||||
pub auth_url: String,
|
pub auth_url: String,
|
||||||
pub redirect_uri: String,
|
|
||||||
pub redirect_host: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ pub fn clear_auth_pass() -> Result<(), OAuthError> {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// use crate::core::services::auth::logout;
|
/// use crate::services::auth::logout;
|
||||||
///
|
///
|
||||||
/// logout().expect("Failed to logout");
|
/// logout().expect("Failed to logout");
|
||||||
/// ```
|
/// ```
|
||||||
@@ -342,7 +342,7 @@ pub fn logout() -> Result<(), OAuthError> {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// use crate::core::services::auth::with_auth;
|
/// use crate::services::auth::with_auth;
|
||||||
///
|
///
|
||||||
/// let client = reqwest::Client::new();
|
/// let client = reqwest::Client::new();
|
||||||
/// let request = client.get("https://api.example.com/user");
|
/// let request = client.get("https://api.example.com/user");
|
||||||
@@ -370,6 +370,7 @@ pub async fn with_auth(request: reqwest::RequestBuilder) -> reqwest::RequestBuil
|
|||||||
///
|
///
|
||||||
/// Returns `OAuthError` if the exchange fails or the server returns an error.
|
/// Returns `OAuthError` if the exchange fails or the server returns an error.
|
||||||
pub async fn exchange_code_for_auth_pass(
|
pub async fn exchange_code_for_auth_pass(
|
||||||
|
redirect_uri: &str,
|
||||||
callback_params: OAuthCallbackParams,
|
callback_params: OAuthCallbackParams,
|
||||||
code_verifier: &str,
|
code_verifier: &str,
|
||||||
) -> Result<AuthPass, OAuthError> {
|
) -> Result<AuthPass, OAuthError> {
|
||||||
@@ -393,7 +394,7 @@ pub async fn exchange_code_for_auth_pass(
|
|||||||
let body = form_urlencoded::Serializer::new(String::new())
|
let body = form_urlencoded::Serializer::new(String::new())
|
||||||
.append_pair("client_id", &app_config.auth.audience)
|
.append_pair("client_id", &app_config.auth.audience)
|
||||||
.append_pair("grant_type", "authorization_code")
|
.append_pair("grant_type", "authorization_code")
|
||||||
.append_pair("redirect_uri", &app_config.auth.redirect_uri)
|
.append_pair("redirect_uri", redirect_uri)
|
||||||
.append_pair("code", &callback_params.code)
|
.append_pair("code", &callback_params.code)
|
||||||
.append_pair("code_verifier", code_verifier)
|
.append_pair("code_verifier", code_verifier)
|
||||||
.finish();
|
.finish();
|
||||||
@@ -469,7 +470,7 @@ pub async fn exchange_code_for_auth_pass(
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// use crate::core::services::auth::init_auth_code_retrieval;
|
/// use crate::services::auth::init_auth_code_retrieval;
|
||||||
///
|
///
|
||||||
/// init_auth_code_retrieval();
|
/// init_auth_code_retrieval();
|
||||||
/// // User will be prompted to login in their browser
|
/// // User will be prompted to login in their browser
|
||||||
@@ -511,24 +512,19 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("client_id", &app_config.auth.audience)
|
|
||||||
.append_pair("response_type", "code")
|
|
||||||
.append_pair("redirect_uri", &app_config.auth.redirect_uri)
|
|
||||||
.append_pair("scope", "openid email profile")
|
|
||||||
.append_pair("state", &state)
|
|
||||||
.append_pair("code_challenge", &code_challenge)
|
|
||||||
.append_pair("code_challenge_method", "S256");
|
|
||||||
|
|
||||||
info!("Initiating OAuth flow");
|
info!("Initiating OAuth flow");
|
||||||
|
|
||||||
// Bind the server FIRST to ensure port is open
|
// Bind the server FIRST to ensure port is open
|
||||||
// We bind synchronously using std::net::TcpListener then convert to tokio::net::TcpListener
|
// We bind synchronously using std::net::TcpListener then convert to tokio::net::TcpListener
|
||||||
// to ensure the port is bound before we open the browser.
|
// 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) {
|
// Bind to port 0 (ephemeral port),
|
||||||
|
// The OS will assign an available port.
|
||||||
|
let bind_addr = "localhost:0";
|
||||||
|
|
||||||
|
info!("Attempting to bind to: {}", bind_addr);
|
||||||
|
let std_listener = match std::net::TcpListener::bind(&bind_addr) {
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
info!("Successfully bound to {}", app_config.auth.redirect_host);
|
|
||||||
s.set_nonblocking(true).unwrap();
|
s.set_nonblocking(true).unwrap();
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
@@ -538,68 +534,75 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
info!(
|
// Get the actual port assigned by the OS
|
||||||
"Listening on {} for /callback",
|
let local_addr = std_listener
|
||||||
app_config.auth.redirect_host
|
.local_addr()
|
||||||
);
|
.map_err(|e| OAuthError::ServerBindError(e.to_string()))?;
|
||||||
|
let port = local_addr.port();
|
||||||
|
info!("Successfully bound to {}", local_addr);
|
||||||
|
info!("Listening on port {} for /callback", port);
|
||||||
|
|
||||||
|
let redirect_uri = format!("http://localhost:{}/callback", port);
|
||||||
|
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair("client_id", &app_config.auth.audience)
|
||||||
|
.append_pair("response_type", "code")
|
||||||
|
.append_pair("redirect_uri", &redirect_uri)
|
||||||
|
.append_pair("scope", "openid email profile")
|
||||||
|
.append_pair("state", &state)
|
||||||
|
.append_pair("code_challenge", &code_challenge)
|
||||||
|
.append_pair("code_challenge_method", "S256");
|
||||||
|
let redirect_uri_clone = redirect_uri.clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
|
info!("Starting callback listener task");
|
||||||
let listener = match TcpListener::from_std(std_listener) {
|
let listener = match TcpListener::from_std(std_listener) {
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to create async listener: {}", e);
|
error!("Failed to convert listener: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match listen_for_callback(listener).await {
|
match listen_for_callback(listener).await {
|
||||||
Ok(callback_params) => {
|
Ok(params) => {
|
||||||
// Validate state
|
let (stored_state, stored_verifier) = {
|
||||||
let stored_state = lock_r!(FDOLL).oauth_flow.state.clone();
|
let guard = lock_r!(FDOLL);
|
||||||
|
(
|
||||||
if stored_state.as_ref() != Some(&callback_params.state) {
|
guard.oauth_flow.state.clone(),
|
||||||
error!("State mismatch - possible CSRF attack!");
|
guard.oauth_flow.code_verifier.clone(),
|
||||||
return;
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve code_verifier
|
|
||||||
let code_verifier = match lock_r!(FDOLL).oauth_flow.code_verifier.clone() {
|
|
||||||
Some(cv) => cv,
|
|
||||||
None => {
|
|
||||||
error!("Code verifier not found in state");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear OAuth flow state after successful callback
|
if stored_state.as_deref() != Some(params.state.as_str()) {
|
||||||
lock_w!(FDOLL).oauth_flow = Default::default();
|
error!("State mismatch");
|
||||||
|
|
||||||
match exchange_code_for_auth_pass(callback_params, &code_verifier).await {
|
|
||||||
Ok(auth_pass) => {
|
|
||||||
lock_w!(FDOLL).auth_pass = Some(auth_pass.clone());
|
|
||||||
if let Err(e) = save_auth_pass(&auth_pass) {
|
|
||||||
error!("Failed to save auth pass: {}", e);
|
|
||||||
} else {
|
|
||||||
info!("Authentication successful!");
|
|
||||||
crate::services::ws::init_ws_client().await;
|
|
||||||
on_success();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(code_verifier) = stored_verifier else {
|
||||||
|
error!("Code verifier missing");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match exchange_code_for_auth_pass(&redirect_uri_clone, params, &code_verifier).await
|
||||||
|
{
|
||||||
|
Ok(auth_pass) => {
|
||||||
|
{
|
||||||
|
let mut guard = lock_w!(FDOLL);
|
||||||
|
guard.auth_pass = Some(auth_pass.clone());
|
||||||
|
guard.oauth_flow = Default::default();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
if let Err(e) = save_auth_pass(&auth_pass) {
|
||||||
error!("Failed to exchange code for tokens: {}", e);
|
error!("Failed to save auth pass: {}", e);
|
||||||
|
}
|
||||||
|
on_success();
|
||||||
|
}
|
||||||
|
Err(e) => error!("Token exchange failed: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Err(e) => error!("Callback listener error: {}", e),
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to receive callback: {}", e);
|
|
||||||
// Clear OAuth flow state on error
|
|
||||||
lock_w!(FDOLL).oauth_flow = Default::default();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("Opening auth URL: {}", url);
|
|
||||||
if let Err(e) = app_handle.opener().open_url(url, None::<&str>) {
|
if let Err(e) = app_handle.opener().open_url(url, None::<&str>) {
|
||||||
error!("Failed to open auth portal: {}", e);
|
error!("Failed to open auth portal: {}", e);
|
||||||
return Err(OAuthError::OpenPortalFailed(e));
|
return Err(OAuthError::OpenPortalFailed(e));
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use device_query::{DeviceEvents, DeviceEventsHandler};
|
use device_query::{DeviceEvents, DeviceEventsHandler};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use tracing::{error, info};
|
use tokio::sync::mpsc;
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::{get_app_handle, lock_r, state::FDOLL};
|
use crate::{get_app_handle, lock_r, state::FDOLL};
|
||||||
@@ -14,8 +13,8 @@ use crate::{get_app_handle, lock_r, state::FDOLL};
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct CursorPosition {
|
pub struct CursorPosition {
|
||||||
pub x: i32,
|
pub x: f64,
|
||||||
pub y: i32,
|
pub y: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, TS)]
|
#[derive(Debug, Clone, Serialize, TS)]
|
||||||
@@ -28,29 +27,27 @@ pub struct CursorPositions {
|
|||||||
|
|
||||||
static CURSOR_TRACKER: OnceCell<()> = OnceCell::new();
|
static CURSOR_TRACKER: OnceCell<()> = OnceCell::new();
|
||||||
|
|
||||||
/// Convert absolute screen coordinates to grid coordinates
|
/// Convert absolute screen coordinates to normalized coordinates (0.0 - 1.0)
|
||||||
pub fn absolute_position_to_grid(pos: &CursorPosition) -> CursorPosition {
|
pub fn absolute_to_normalized(pos: &CursorPosition) -> CursorPosition {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
let grid_size = guard.app_data.scene.grid_size;
|
let screen_w = guard.app_data.scene.display.screen_width as f64;
|
||||||
let screen_w = guard.app_data.scene.display.screen_width;
|
let screen_h = guard.app_data.scene.display.screen_height as f64;
|
||||||
let screen_h = guard.app_data.scene.display.screen_height;
|
|
||||||
|
|
||||||
CursorPosition {
|
CursorPosition {
|
||||||
x: pos.x * grid_size / screen_w,
|
x: (pos.x / screen_w).clamp(0.0, 1.0),
|
||||||
y: pos.y * grid_size / screen_h,
|
y: (pos.y / screen_h).clamp(0.0, 1.0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert grid coordinates to absolute screen coordinates
|
/// Convert normalized coordinates to absolute screen coordinates
|
||||||
pub fn grid_to_absolute_position(grid: &CursorPosition) -> CursorPosition {
|
pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
let grid_size = guard.app_data.scene.grid_size;
|
let screen_w = guard.app_data.scene.display.screen_width as f64;
|
||||||
let screen_w = guard.app_data.scene.display.screen_width;
|
let screen_h = guard.app_data.scene.display.screen_height as f64;
|
||||||
let screen_h = guard.app_data.scene.display.screen_height;
|
|
||||||
|
|
||||||
CursorPosition {
|
CursorPosition {
|
||||||
x: (grid.x * screen_w + grid_size / 2) / grid_size,
|
x: (normalized.x * screen_w).round(),
|
||||||
y: (grid.y * screen_h + grid_size / 2) / grid_size,
|
y: (normalized.y * screen_h).round(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,19 +73,40 @@ pub async fn start_cursor_tracking() -> Result<(), String> {
|
|||||||
|
|
||||||
async fn init_cursor_tracking() -> Result<(), String> {
|
async fn init_cursor_tracking() -> Result<(), String> {
|
||||||
info!("Initializing cursor tracking...");
|
info!("Initializing cursor tracking...");
|
||||||
|
|
||||||
|
// Create a channel to decouple event generation (producer) from processing (consumer).
|
||||||
|
// Capacity 100 is plenty for 500ms polling (2Hz).
|
||||||
|
let (tx, mut rx) = mpsc::channel::<CursorPositions>(100);
|
||||||
|
|
||||||
|
// Spawn the consumer task
|
||||||
|
// This task handles WebSocket reporting and local broadcasting.
|
||||||
|
// It runs independently of the device event loop.
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
info!("Cursor event consumer started");
|
||||||
let app_handle = get_app_handle();
|
let app_handle = get_app_handle();
|
||||||
|
|
||||||
|
while let Some(positions) = rx.recv().await {
|
||||||
|
let mapped_for_ws = positions.mapped.clone();
|
||||||
|
|
||||||
|
// 1. WebSocket reporting
|
||||||
|
crate::services::ws::report_cursor_data(mapped_for_ws).await;
|
||||||
|
|
||||||
|
// 2. Broadcast to local windows
|
||||||
|
if let Err(e) = app_handle.emit("cursor-position", &positions) {
|
||||||
|
error!("Failed to emit cursor position event: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
warn!("Cursor event consumer stopped (channel closed)");
|
||||||
|
});
|
||||||
|
|
||||||
// Try to initialize the device event handler
|
// Try to initialize the device event handler
|
||||||
|
// Using 500ms sleep as requested by user to reduce CPU usage
|
||||||
let device_state = DeviceEventsHandler::new(Duration::from_millis(500))
|
let device_state = DeviceEventsHandler::new(Duration::from_millis(500))
|
||||||
.ok_or("Failed to create device event handler (already running?)")?;
|
.ok_or("Failed to create device event handler (already running?)")?;
|
||||||
|
|
||||||
info!("Device event handler created successfully");
|
info!("Device event handler created successfully");
|
||||||
info!("Setting up mouse move handler for event broadcasting...");
|
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();
|
|
||||||
|
|
||||||
// Get scale factor from global state
|
// Get scale factor from global state
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let scale_factor = {
|
let scale_factor = {
|
||||||
@@ -96,8 +114,11 @@ async fn init_cursor_tracking() -> Result<(), String> {
|
|||||||
guard.app_data.scene.display.monitor_scale_factor
|
guard.app_data.scene.display.monitor_scale_factor
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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)| {
|
let _guard = device_state.on_mouse_move(move |position: &(i32, i32)| {
|
||||||
|
|
||||||
// `device_query` crate appears to behave
|
// `device_query` crate appears to behave
|
||||||
// differently on Windows vs other platforms.
|
// differently on Windows vs other platforms.
|
||||||
//
|
//
|
||||||
@@ -105,46 +126,33 @@ async fn init_cursor_tracking() -> Result<(), String> {
|
|||||||
// factor on Windows, so we handle it manually.
|
// factor on Windows, so we handle it manually.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let raw = CursorPosition {
|
let raw = CursorPosition {
|
||||||
x: (position.0 as f64 / scale_factor) as i32,
|
x: position.0 as f64 / scale_factor,
|
||||||
y: (position.1 as f64 / scale_factor) as i32,
|
y: position.1 as f64 / scale_factor,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
let raw = CursorPosition {
|
let raw = CursorPosition {
|
||||||
x: position.0,
|
x: position.0 as f64,
|
||||||
y: position.1,
|
y: position.1 as f64,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mapped = absolute_position_to_grid(&raw);
|
let mapped = absolute_to_normalized(&raw);
|
||||||
|
|
||||||
let positions = CursorPositions {
|
let positions = CursorPositions {
|
||||||
raw,
|
raw,
|
||||||
mapped: mapped.clone(),
|
mapped,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Report to server (existing functionality)
|
// Send to consumer channel (non-blocking)
|
||||||
let mapped_for_ws = mapped.clone();
|
if let Err(e) = tx_clone.try_send(positions) {
|
||||||
tauri::async_runtime::spawn(async move {
|
debug!("Failed to send cursor position to channel: {:?}", e);
|
||||||
crate::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");
|
info!("Mouse move handler registered - now broadcasting cursor events to all windows");
|
||||||
|
|
||||||
// Keep the handler alive forever
|
// Keep the handler alive forever
|
||||||
|
// This loop is necessary to keep `_guard` and `device_state` in scope.
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(3600)).await;
|
tokio::time::sleep(Duration::from_secs(3600)).await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ use tauri::{async_runtime, Emitter};
|
|||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle, lock_r, lock_w, models::app_config::AppConfig,
|
get_app_handle, lock_r, lock_w,
|
||||||
services::cursor::{grid_to_absolute_position, CursorPosition, CursorPositions},
|
models::app_config::AppConfig,
|
||||||
|
services::cursor::{normalized_to_absolute, CursorPosition, CursorPositions},
|
||||||
state::FDOLL,
|
state::FDOLL,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -41,9 +42,9 @@ fn on_friend_request_received(payload: Payload, _socket: RawClient) {
|
|||||||
match payload {
|
match payload {
|
||||||
Payload::Text(str) => {
|
Payload::Text(str) => {
|
||||||
println!("Received friend request: {:?}", str);
|
println!("Received friend request: {:?}", str);
|
||||||
get_app_handle()
|
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_REQUEST_RECEIVED, str) {
|
||||||
.emit(WS_EVENT::FRIEND_REQUEST_RECEIVED, str)
|
error!("Failed to emit friend request received event: {:?}", e);
|
||||||
.unwrap();
|
}
|
||||||
}
|
}
|
||||||
_ => error!("Received unexpected payload format for friend request received"),
|
_ => error!("Received unexpected payload format for friend request received"),
|
||||||
}
|
}
|
||||||
@@ -53,9 +54,9 @@ fn on_friend_request_accepted(payload: Payload, _socket: RawClient) {
|
|||||||
match payload {
|
match payload {
|
||||||
Payload::Text(str) => {
|
Payload::Text(str) => {
|
||||||
println!("Received friend request accepted: {:?}", str);
|
println!("Received friend request accepted: {:?}", str);
|
||||||
get_app_handle()
|
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_REQUEST_ACCEPTED, str) {
|
||||||
.emit(WS_EVENT::FRIEND_REQUEST_ACCEPTED, str)
|
error!("Failed to emit friend request accepted event: {:?}", e);
|
||||||
.unwrap();
|
}
|
||||||
}
|
}
|
||||||
_ => error!("Received unexpected payload format for friend request accepted"),
|
_ => error!("Received unexpected payload format for friend request accepted"),
|
||||||
}
|
}
|
||||||
@@ -65,9 +66,9 @@ fn on_friend_request_denied(payload: Payload, _socket: RawClient) {
|
|||||||
match payload {
|
match payload {
|
||||||
Payload::Text(str) => {
|
Payload::Text(str) => {
|
||||||
println!("Received friend request denied: {:?}", str);
|
println!("Received friend request denied: {:?}", str);
|
||||||
get_app_handle()
|
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_REQUEST_DENIED, str) {
|
||||||
.emit(WS_EVENT::FRIEND_REQUEST_DENIED, str)
|
error!("Failed to emit friend request denied event: {:?}", e);
|
||||||
.unwrap();
|
}
|
||||||
}
|
}
|
||||||
_ => error!("Received unexpected payload format for friend request denied"),
|
_ => error!("Received unexpected payload format for friend request denied"),
|
||||||
}
|
}
|
||||||
@@ -77,7 +78,9 @@ fn on_unfriended(payload: Payload, _socket: RawClient) {
|
|||||||
match payload {
|
match payload {
|
||||||
Payload::Text(str) => {
|
Payload::Text(str) => {
|
||||||
println!("Received unfriended: {:?}", str);
|
println!("Received unfriended: {:?}", str);
|
||||||
get_app_handle().emit(WS_EVENT::UNFRIENDED, str).unwrap();
|
if let Err(e) = get_app_handle().emit(WS_EVENT::UNFRIENDED, str) {
|
||||||
|
error!("Failed to emit unfriended event: {:?}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => error!("Received unexpected payload format for unfriended"),
|
_ => error!("Received unexpected payload format for unfriended"),
|
||||||
}
|
}
|
||||||
@@ -88,15 +91,16 @@ fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
|
|||||||
Payload::Text(values) => {
|
Payload::Text(values) => {
|
||||||
// values is Vec<serde_json::Value>
|
// values is Vec<serde_json::Value>
|
||||||
if let Some(first_value) = values.first() {
|
if let Some(first_value) = values.first() {
|
||||||
let incoming_data: Result<IncomingFriendCursorPayload, _> = serde_json::from_value(first_value.clone());
|
let incoming_data: Result<IncomingFriendCursorPayload, _> =
|
||||||
|
serde_json::from_value(first_value.clone());
|
||||||
|
|
||||||
match incoming_data {
|
match incoming_data {
|
||||||
Ok(friend_data) => {
|
Ok(friend_data) => {
|
||||||
// We received grid coordinates (mapped)
|
// We received normalized coordinates (mapped)
|
||||||
let mapped_pos = &friend_data.position;
|
let mapped_pos = &friend_data.position;
|
||||||
|
|
||||||
// Convert grid coordinates back to absolute screen coordinates (raw)
|
// Convert normalized coordinates back to absolute screen coordinates (raw)
|
||||||
let raw_pos = grid_to_absolute_position(mapped_pos);
|
let raw_pos = normalized_to_absolute(mapped_pos);
|
||||||
|
|
||||||
let outgoing_payload = OutgoingFriendCursorPayload {
|
let outgoing_payload = OutgoingFriendCursorPayload {
|
||||||
user_id: friend_data.user_id.clone(),
|
user_id: friend_data.user_id.clone(),
|
||||||
@@ -106,9 +110,11 @@ fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
get_app_handle()
|
if let Err(e) = get_app_handle()
|
||||||
.emit(WS_EVENT::FRIEND_CURSOR_POSITION, outgoing_payload)
|
.emit(WS_EVENT::FRIEND_CURSOR_POSITION, outgoing_payload)
|
||||||
.unwrap();
|
{
|
||||||
|
error!("Failed to emit friend cursor position event: {:?}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to parse friend cursor position data: {}", e);
|
error!("Failed to parse friend cursor position data: {}", e);
|
||||||
@@ -126,27 +132,27 @@ fn on_friend_disconnected(payload: Payload, _socket: RawClient) {
|
|||||||
match payload {
|
match payload {
|
||||||
Payload::Text(str) => {
|
Payload::Text(str) => {
|
||||||
println!("Received friend disconnected: {:?}", str);
|
println!("Received friend disconnected: {:?}", str);
|
||||||
get_app_handle()
|
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_DISCONNECTED, str) {
|
||||||
.emit(WS_EVENT::FRIEND_DISCONNECTED, str)
|
error!("Failed to emit friend disconnected event: {:?}", e);
|
||||||
.unwrap();
|
}
|
||||||
}
|
}
|
||||||
_ => error!("Received unexpected payload format for friend disconnected"),
|
_ => error!("Received unexpected payload format for friend disconnected"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
||||||
let client = {
|
// Only attempt to get clients if lock_r succeeds (it should, but safety first)
|
||||||
|
// and if clients are actually initialized.
|
||||||
|
let client_opt = {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
guard
|
guard
|
||||||
.clients
|
.clients
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("Clients are initialized")
|
.and_then(|c| c.ws_client.as_ref())
|
||||||
.ws_client
|
.cloned()
|
||||||
.as_ref()
|
|
||||||
.expect("WebSocket client is initialized")
|
|
||||||
.clone()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(client) = client_opt {
|
||||||
match async_runtime::spawn_blocking(move || {
|
match async_runtime::spawn_blocking(move || {
|
||||||
client.emit(
|
client.emit(
|
||||||
WS_EVENT::CURSOR_REPORT_POSITION,
|
WS_EVENT::CURSOR_REPORT_POSITION,
|
||||||
@@ -156,8 +162,13 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Ok(_)) => (),
|
Ok(Ok(_)) => (),
|
||||||
Ok(Err(e)) => error!("Failed to emit ping: {}", e),
|
Ok(Err(e)) => error!("Failed to emit cursor report: {}", e),
|
||||||
Err(e) => error!("Failed to execute blocking task: {}", e),
|
Err(e) => error!("Failed to execute blocking task for cursor report: {}", e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Quietly fail if client is not ready (e.g. during startup/shutdown)
|
||||||
|
// or debug log it.
|
||||||
|
// debug!("WebSocket client not ready to report cursor data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,28 +178,39 @@ pub async fn init_ws_client() {
|
|||||||
guard.app_config.clone()
|
guard.app_config.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let ws_client = build_ws_client(&app_config).await;
|
match build_ws_client(&app_config).await {
|
||||||
|
Ok(ws_client) => {
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
if let Some(clients) = guard.clients.as_mut() {
|
if let Some(clients) = guard.clients.as_mut() {
|
||||||
clients.ws_client = Some(ws_client);
|
clients.ws_client = Some(ws_client);
|
||||||
}
|
}
|
||||||
info!("WebSocket client initialized after authentication");
|
info!("WebSocket client initialized after authentication");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to initialize WebSocket client: {}", e);
|
||||||
|
// We should probably inform the user or retry here, but for now we just log it.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn build_ws_client(app_config: &AppConfig) -> rust_socketio::client::Client {
|
pub async fn build_ws_client(
|
||||||
let token = crate::services::auth::get_access_token()
|
app_config: &AppConfig,
|
||||||
.await
|
) -> Result<rust_socketio::client::Client, String> {
|
||||||
.expect("No access token available for WebSocket connection");
|
let token_result = crate::services::auth::get_access_token().await;
|
||||||
|
|
||||||
|
let token = match token_result {
|
||||||
|
Some(t) => t,
|
||||||
|
None => return Err("No access token available for WebSocket connection".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
info!("Building WebSocket client with authentication");
|
info!("Building WebSocket client with authentication");
|
||||||
|
|
||||||
let api_base_url = app_config
|
let api_base_url = app_config
|
||||||
.api_base_url
|
.api_base_url
|
||||||
.clone()
|
.clone()
|
||||||
.expect("Missing API base URL");
|
.ok_or("Missing API base URL")?;
|
||||||
|
|
||||||
let client = async_runtime::spawn_blocking(move || {
|
let client_result = async_runtime::spawn_blocking(move || {
|
||||||
ClientBuilder::new(api_base_url)
|
ClientBuilder::new(api_base_url)
|
||||||
.namespace("/")
|
.namespace("/")
|
||||||
.on(
|
.on(
|
||||||
@@ -206,20 +228,24 @@ pub async fn build_ws_client(app_config: &AppConfig) -> rust_socketio::client::C
|
|||||||
.auth(json!({ "token": token }))
|
.auth(json!({ "token": token }))
|
||||||
.connect()
|
.connect()
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
.expect("Failed to spawn blocking task");
|
|
||||||
|
|
||||||
match client {
|
match client_result {
|
||||||
|
Ok(connect_result) => match connect_result {
|
||||||
Ok(c) => {
|
Ok(c) => {
|
||||||
info!("WebSocket client connected successfully");
|
info!("WebSocket client connected successfully");
|
||||||
c
|
Ok(c)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to connect WebSocket: {:?}", e);
|
let err_msg = format!("Failed to connect WebSocket: {:?}", e);
|
||||||
panic!(
|
error!("{}", err_msg);
|
||||||
"TODO: better error handling for WebSocket connection - {}",
|
Err(err_msg)
|
||||||
e
|
}
|
||||||
);
|
},
|
||||||
|
Err(e) => {
|
||||||
|
let err_msg = format!("Failed to spawn blocking task: {:?}", e);
|
||||||
|
error!("{}", err_msg);
|
||||||
|
Err(err_msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// in app-core/src/state.rs
|
// in app-core/src/state.rs
|
||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle, lock_w,
|
get_app_handle, lock_r, lock_w,
|
||||||
models::{
|
models::{
|
||||||
app_config::{AppConfig, AuthConfig},
|
app_config::{AppConfig, AuthConfig},
|
||||||
app_data::AppData,
|
app_data::AppData,
|
||||||
@@ -53,8 +53,6 @@ pub fn init_fdoll_state() {
|
|||||||
auth: AuthConfig {
|
auth: AuthConfig {
|
||||||
audience: env::var("JWT_AUDIENCE").expect("JWT_AUDIENCE must be set"),
|
audience: env::var("JWT_AUDIENCE").expect("JWT_AUDIENCE must be set"),
|
||||||
auth_url: env::var("AUTH_URL").expect("AUTH_URL must be set"),
|
auth_url: env::var("AUTH_URL").expect("AUTH_URL must be set"),
|
||||||
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() {
|
guard.auth_pass = match load_auth_pass() {
|
||||||
@@ -81,8 +79,6 @@ pub fn init_fdoll_state() {
|
|||||||
});
|
});
|
||||||
info!("Initialized HTTP client");
|
info!("Initialized HTTP client");
|
||||||
|
|
||||||
let has_auth = guard.auth_pass.is_some();
|
|
||||||
|
|
||||||
// Initialize screen dimensions
|
// Initialize screen dimensions
|
||||||
let app_handle = get_app_handle();
|
let app_handle = get_app_handle();
|
||||||
|
|
||||||
@@ -100,7 +96,10 @@ pub fn init_fdoll_state() {
|
|||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
retry_count += 1;
|
retry_count += 1;
|
||||||
if retry_count >= max_retries {
|
if retry_count >= max_retries {
|
||||||
warn!("No primary monitor found after {} retries during state init", max_retries);
|
warn!(
|
||||||
|
"No primary monitor found after {} retries during state init",
|
||||||
|
max_retries
|
||||||
|
);
|
||||||
break None;
|
break None;
|
||||||
}
|
}
|
||||||
warn!(
|
warn!(
|
||||||
@@ -146,17 +145,9 @@ pub fn init_fdoll_state() {
|
|||||||
} else {
|
} else {
|
||||||
warn!("Could not initialize screen dimensions in global state - no monitor found");
|
warn!("Could not initialize screen dimensions in global state - no monitor found");
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(guard);
|
|
||||||
|
|
||||||
if has_auth {
|
|
||||||
async_runtime::spawn(async move {
|
|
||||||
crate::services::ws::init_ws_client().await;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)");
|
info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// To be called in init state or need to refresh data.
|
/// To be called in init state or need to refresh data.
|
||||||
@@ -164,21 +155,52 @@ pub fn init_fdoll_state() {
|
|||||||
pub async fn init_app_data() {
|
pub async fn init_app_data() {
|
||||||
let user_remote = UserRemote::new();
|
let user_remote = UserRemote::new();
|
||||||
let friend_remote = FriendRemote::new();
|
let friend_remote = FriendRemote::new();
|
||||||
let user = user_remote
|
|
||||||
.get_user(None)
|
|
||||||
.await
|
|
||||||
.expect("TODO: handle user profile fetch failure");
|
|
||||||
let friends = friend_remote
|
|
||||||
.get_friends()
|
|
||||||
.await
|
|
||||||
.expect("TODO: handle friends fetch failure");
|
|
||||||
|
|
||||||
{
|
// Fetch user profile
|
||||||
|
match user_remote.get_user(None).await {
|
||||||
|
Ok(user) => {
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
guard.app_data.user = Some(user);
|
guard.app_data.user = Some(user);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to fetch user profile: {}", e);
|
||||||
|
use tauri_plugin_dialog::MessageDialogBuilder;
|
||||||
|
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
||||||
|
|
||||||
|
let handle = get_app_handle();
|
||||||
|
MessageDialogBuilder::new(
|
||||||
|
handle.dialog().clone(),
|
||||||
|
"Network Error",
|
||||||
|
"Failed to fetch user profile. You may be offline.",
|
||||||
|
)
|
||||||
|
.kind(MessageDialogKind::Error)
|
||||||
|
.show(|_| {});
|
||||||
|
// We continue execution to see if other parts (like friends) can be loaded or if we just stay in a partial state.
|
||||||
|
// Alternatively, return early here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch friends list
|
||||||
|
match friend_remote.get_friends().await {
|
||||||
|
Ok(friends) => {
|
||||||
|
let mut guard = lock_w!(FDOLL);
|
||||||
guard.app_data.friends = Some(friends);
|
guard.app_data.friends = Some(friends);
|
||||||
get_app_handle()
|
}
|
||||||
.emit("app-data-refreshed", json!(guard.app_data))
|
Err(e) => {
|
||||||
.expect("TODO: handle event emit fail");
|
warn!("Failed to fetch friends list: {}", e);
|
||||||
|
// Optionally show another dialog or just log it.
|
||||||
|
// Showing too many dialogs on startup is annoying, so we might skip this one if user profile failed too.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit event regardless of partial success, frontend should handle nulls/empty states
|
||||||
|
{
|
||||||
|
let guard = lock_r!(FDOLL); // Use read lock to get data
|
||||||
|
let app_data_clone = guard.app_data.clone();
|
||||||
|
drop(guard); // Drop lock before emitting to prevent potential deadlocks
|
||||||
|
|
||||||
|
if let Err(e) = get_app_handle().emit("app-data-refreshed", json!(app_data_clone)) {
|
||||||
|
warn!("Failed to emit app-data-refreshed event: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,17 @@
|
|||||||
|
|
||||||
import DesktopPet from "./DesktopPet.svelte";
|
import DesktopPet from "./DesktopPet.svelte";
|
||||||
|
|
||||||
|
let innerWidth = 0;
|
||||||
|
let innerHeight = 0;
|
||||||
|
|
||||||
function getFriendName(userId: string) {
|
function getFriendName(userId: string) {
|
||||||
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
||||||
return friend ? friend.friend.name : userId.slice(0, 8) + "...";
|
return friend ? friend.friend.name : userId.slice(0, 8) + "...";
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window bind:innerWidth bind:innerHeight />
|
||||||
|
|
||||||
<div class="w-svw h-svh p-4 relative overflow-hidden">
|
<div class="w-svw h-svh p-4 relative overflow-hidden">
|
||||||
<div
|
<div
|
||||||
class="size-max mx-auto bg-base-100 border-base-200 border px-4 py-3 rounded-xl"
|
class="size-max mx-auto bg-base-100 border-base-200 border px-4 py-3 rounded-xl"
|
||||||
@@ -26,8 +31,9 @@
|
|||||||
.y})
|
.y})
|
||||||
</span>
|
</span>
|
||||||
<span class="font-mono text-sm">
|
<span class="font-mono text-sm">
|
||||||
Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen
|
Mapped: ({$cursorPositionOnScreen.mapped.x.toFixed(3)}, {$cursorPositionOnScreen.mapped.y.toFixed(
|
||||||
.mapped.y})
|
3,
|
||||||
|
)})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -45,7 +51,9 @@
|
|||||||
Raw: ({position.raw.x}, {position.raw.y})
|
Raw: ({position.raw.x}, {position.raw.y})
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Mapped: ({position.mapped.x}, {position.mapped.y})
|
Mapped: ({position.mapped.x.toFixed(3)}, {position.mapped.y.toFixed(
|
||||||
|
3,
|
||||||
|
)})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,8 +67,8 @@
|
|||||||
{#if Object.keys($friendsCursorPositions).length > 0}
|
{#if Object.keys($friendsCursorPositions).length > 0}
|
||||||
{#each Object.entries($friendsCursorPositions) as [userId, position]}
|
{#each Object.entries($friendsCursorPositions) as [userId, position]}
|
||||||
<DesktopPet
|
<DesktopPet
|
||||||
targetX={position.raw.x}
|
targetX={position.mapped.x * innerWidth}
|
||||||
targetY={position.raw.y}
|
targetY={position.mapped.y * innerHeight}
|
||||||
name={getFriendName(userId)}
|
name={getFriendName(userId)}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
Reference in New Issue
Block a user