From 0b1686653c400c540d51bad306ba27b3bbb09f01 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Mon, 26 Jan 2026 11:16:55 +0800 Subject: [PATCH] user active app reporting --- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/user_status.rs | 9 +++ src-tauri/src/lib.rs | 5 +- src-tauri/src/services/ws/friend.rs | 15 ++++ src-tauri/src/services/ws/handlers.rs | 1 + src-tauri/src/services/ws/mod.rs | 4 + src-tauri/src/services/ws/user_status.rs | 71 ++++++++++++++++++ src/events/user-status.ts | 94 ++++++++++++++++++++++++ src/routes/+layout.svelte | 3 + src/routes/scene/+page.svelte | 23 +++++- 10 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 src-tauri/src/commands/user_status.rs create mode 100644 src-tauri/src/services/ws/user_status.rs create mode 100644 src/events/user-status.ts diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index cacb98f..8d75da1 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -6,6 +6,7 @@ pub mod dolls; pub mod friends; pub mod interaction; pub mod sprite; +pub mod user_status; use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}; use crate::lock_r; diff --git a/src-tauri/src/commands/user_status.rs b/src-tauri/src/commands/user_status.rs new file mode 100644 index 0000000..ca1695d --- /dev/null +++ b/src-tauri/src/commands/user_status.rs @@ -0,0 +1,9 @@ +use crate::services::ws::UserStatusPayload; +use crate::services::ws::report_user_status; + +#[tauri::command] +pub async fn send_user_status_cmd(active_app: String, state: String) -> Result<(), String> { + let payload = UserStatusPayload { active_app, state }; + report_user_status(payload).await; + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 252144a..d55239e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,6 +16,7 @@ use commands::friends::{ }; use commands::interaction::send_interaction_cmd; use commands::sprite::recolor_gif_base64; +use commands::user_status::send_user_status_cmd; use tauri::async_runtime; use tauri::Manager; use tracing_subscriber::{self, util::SubscriberInitExt}; @@ -32,6 +33,7 @@ mod state; mod system_tray; mod utilities; + /// Tauri app handle pub fn get_app_handle<'a>() -> &'a tauri::AppHandle { APP_HANDLE @@ -138,7 +140,8 @@ pub fn run() { set_pet_menu_state, start_auth_flow, logout_and_restart, - send_interaction_cmd + send_interaction_cmd, + send_user_status_cmd ]) .setup(|app| { APP_HANDLE diff --git a/src-tauri/src/services/ws/friend.rs b/src-tauri/src/services/ws/friend.rs index 6dcf324..80efa74 100644 --- a/src-tauri/src/services/ws/friend.rs +++ b/src-tauri/src/services/ws/friend.rs @@ -174,3 +174,18 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { _ => error!("Received unexpected payload format for friend-active-doll-changed"), } } + +pub fn on_friend_user_status(payload: Payload, _socket: RawClient) { + match payload { + Payload::Text(values) => { + if let Some(first_value) = values.first() { + if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_USER_STATUS, first_value) { + error!("Failed to emit friend-user-status event: {:?}", e); + } + } else { + info!("Received friend-user-status event with empty payload"); + } + } + _ => error!("Received unexpected payload format for friend-user-status"), + } +} diff --git a/src-tauri/src/services/ws/handlers.rs b/src-tauri/src/services/ws/handlers.rs index 7454d0c..24337d0 100644 --- a/src-tauri/src/services/ws/handlers.rs +++ b/src-tauri/src/services/ws/handlers.rs @@ -41,6 +41,7 @@ pub fn register_event_handlers(builder: ClientBuilder) -> ClientBuilder { WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED, super::friend::on_friend_active_doll_changed, ) + .on(WS_EVENT::FRIEND_USER_STATUS, super::friend::on_friend_user_status) .on(WS_EVENT::DOLL_CREATED, super::doll::on_doll_created) .on(WS_EVENT::DOLL_UPDATED, super::doll::on_doll_updated) .on(WS_EVENT::DOLL_DELETED, super::doll::on_doll_deleted) diff --git a/src-tauri/src/services/ws/mod.rs b/src-tauri/src/services/ws/mod.rs index a3904bb..98e7e44 100644 --- a/src-tauri/src/services/ws/mod.rs +++ b/src-tauri/src/services/ws/mod.rs @@ -29,6 +29,8 @@ impl WS_EVENT { pub const FRIEND_DOLL_UPDATED: &str = "friend-doll-updated"; pub const FRIEND_DOLL_DELETED: &str = "friend-doll-deleted"; pub const FRIEND_ACTIVE_DOLL_CHANGED: &str = "friend-active-doll-changed"; + pub const FRIEND_USER_STATUS: &str = "friend-user-status"; + pub const CLIENT_REPORT_USER_STATUS: &str = "client-report-user-status"; pub const DOLL_CREATED: &str = "doll_created"; pub const DOLL_UPDATED: &str = "doll_updated"; pub const DOLL_DELETED: &str = "doll_deleted"; @@ -46,6 +48,8 @@ mod doll; mod friend; mod handlers; mod interaction; +mod user_status; pub use client::init_ws_client; pub use cursor::report_cursor_data; +pub use user_status::{report_user_status, UserStatusPayload}; diff --git a/src-tauri/src/services/ws/user_status.rs b/src-tauri/src/services/ws/user_status.rs new file mode 100644 index 0000000..5222c20 --- /dev/null +++ b/src-tauri/src/services/ws/user_status.rs @@ -0,0 +1,71 @@ +use rust_socketio::Payload; +use tauri::async_runtime; +use tracing::error; + +use crate::{ + lock_r, + services::health_manager::show_health_manager_with_error, + state::FDOLL, +}; + +use super::WS_EVENT; + +#[derive(Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserStatusPayload { + pub active_app: String, + pub state: String, +} + +pub async fn report_user_status(status: UserStatusPayload) { + let payload_value = match serde_json::to_value(&status) { + Ok(val) => val, + Err(e) => { + error!("Failed to serialize user status payload: {}", e); + return; + } + }; + + let (client_opt, is_initialized) = { + let guard = lock_r!(FDOLL); + if let Some(clients) = &guard.network.clients { + ( + clients.ws_client.as_ref().cloned(), + clients.is_ws_initialized, + ) + } else { + (None, false) + } + }; + + if let Some(client) = client_opt { + if !is_initialized { + return; + } + + match async_runtime::spawn_blocking(move || { + client.emit( + WS_EVENT::CLIENT_REPORT_USER_STATUS, + Payload::Text(vec![payload_value]), + ) + }) + .await + { + Ok(Ok(_)) => (), + Ok(Err(e)) => { + error!("Failed to emit user status report: {}", e); + show_health_manager_with_error(Some(format!( + "WebSocket emit failed: {}", + e + ))); + } + Err(e) => { + error!("Failed to execute blocking task for user status report: {}", e); + show_health_manager_with_error(Some(format!( + "WebSocket task failed: {}", + e + ))); + } + } + } +} diff --git a/src/events/user-status.ts b/src/events/user-status.ts new file mode 100644 index 0000000..cdd4263 --- /dev/null +++ b/src/events/user-status.ts @@ -0,0 +1,94 @@ +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { writable } from "svelte/store"; + +export type FriendUserStatus = { + activeApp: string; + state: "idle" | "resting"; +}; + +export const friendsUserStatuses = writable>({}); + +let unlistenStatus: UnlistenFn | null = null; +let unlistenFriendDisconnected: UnlistenFn | null = null; +let isListening = false; + +export async function initUserStatusListeners() { + if (isListening) return; + + try { + unlistenStatus = await listen("friend-user-status", (event) => { + let payload = event.payload as any; + if (typeof payload === "string") { + try { + payload = JSON.parse(payload); + } catch (error) { + console.error("Failed to parse friend-user-status payload", error); + return; + } + } + + const userId = payload?.userId as string | undefined; + const status = payload?.status as FriendUserStatus | undefined; + + if (!userId || !status) return; + if (typeof status.activeApp !== "string" || status.activeApp.trim() === "") return; + if (status.state !== "idle" && status.state !== "resting") return; + + friendsUserStatuses.update((current) => ({ + ...current, + [userId]: { + activeApp: status.activeApp, + state: status.state, + }, + })); + }); + + unlistenFriendDisconnected = await listen<[{ userId: string }] | { userId: string } | string>( + "friend-disconnected", + (event) => { + let payload = event.payload as any; + if (typeof payload === "string") { + try { + payload = JSON.parse(payload); + } catch (error) { + console.error("Failed to parse friend-disconnected payload", error); + return; + } + } + + const data = Array.isArray(payload) ? payload[0] : payload; + const userId = data?.userId as string | undefined; + if (!userId) return; + + friendsUserStatuses.update((current) => { + const next = { ...current }; + delete next[userId]; + return next; + }); + }, + ); + + isListening = true; + } catch (error) { + console.error("Failed to initialize user status listeners", error); + throw error; + } +} + +export function stopUserStatusListeners() { + if (unlistenStatus) { + unlistenStatus(); + unlistenStatus = null; + } + if (unlistenFriendDisconnected) { + unlistenFriendDisconnected(); + unlistenFriendDisconnected = null; + } + isListening = false; +} + +if (import.meta.hot) { + import.meta.hot.dispose(() => { + stopUserStatusListeners(); + }); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index f1a114d..daf4b17 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -8,6 +8,7 @@ initSceneInteractiveListener, stopSceneInteractiveListener, } from "../events/scene-interactive"; + import { initUserStatusListeners, stopUserStatusListeners } from "../events/user-status"; let { children } = $props(); if (browser) { @@ -17,6 +18,7 @@ await initAppDataListener(); await initSceneInteractiveListener(); await initInteractionListeners(); + await initUserStatusListeners(); } catch (err) { console.error("Failed to initialize event listeners:", err); } @@ -26,6 +28,7 @@ stopCursorTracking(); stopSceneInteractiveListener(); stopInteractionListeners(); + stopUserStatusListeners(); }); } diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte index e94c522..f3f3f01 100644 --- a/src/routes/scene/+page.svelte +++ b/src/routes/scene/+page.svelte @@ -6,6 +6,7 @@ } from "../../events/cursor"; import { appData } from "../../events/app-data"; import { sceneInteractive } from "../../events/scene-interactive"; + import { friendsUserStatuses } from "../../events/user-status"; import { invoke } from "@tauri-apps/api/core"; @@ -33,11 +34,25 @@ return friend?.friend.activeDoll; } + function getFriendStatus(userId: string) { + return $friendsUserStatuses[userId]; + } + let appMetadata: AppMetadata | null = $state(null); onMount(() => { const unlisten = listen("active-app-changed", (event) => { appMetadata = event.payload; + const activeAppValue = + appMetadata?.localized ?? appMetadata?.unlocalized ?? ""; + if (activeAppValue.trim()) { + invoke("send_user_status_cmd", { + activeApp: activeAppValue, + state: "idle", + }).catch((error) => { + console.error("Failed to send user status", error); + }); + } }); return () => { @@ -86,14 +101,20 @@
{#each Object.entries($friendsCursorPositions) as [userId, position]} + {@const status = getFriendStatus(userId)}
{getFriendById(userId).name} -
+
({position.mapped.x.toFixed(3)}, {position.mapped.y.toFixed( 3, )}) + {#if status} + + {status.state} in {status.activeApp} + + {/if}
{/each}