From e38697faa9e486bca2eea954982e3f5438561be6 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Tue, 10 Mar 2026 12:59:06 +0800 Subject: [PATCH] added friends' neko to scene page --- src-tauri/src/commands/app_state.rs | 12 +- src-tauri/src/lib.rs | 10 +- src-tauri/src/services/app_events.rs | 9 +- .../src/services/friend_active_doll_sprite.rs | 126 ++++++++++++++++++ src-tauri/src/services/mod.rs | 1 + src-tauri/src/services/sprite.rs | 22 +-- src-tauri/src/services/ws/friend.rs | 5 +- src-tauri/src/state/ui.rs | 4 +- src/events/friend-active-doll-sprite.ts | 36 +++++ src/lib/bindings.ts | 7 + src/routes/+layout.svelte | 6 + src/routes/scene/+page.svelte | 12 ++ src/routes/scene/components/neko/neko.svelte | 7 +- 13 files changed, 239 insertions(+), 18 deletions(-) create mode 100644 src-tauri/src/services/friend_active_doll_sprite.rs create mode 100644 src/events/friend-active-doll-sprite.ts diff --git a/src-tauri/src/commands/app_state.rs b/src-tauri/src/commands/app_state.rs index 3466ec6..92c4303 100644 --- a/src-tauri/src/commands/app_state.rs +++ b/src-tauri/src/commands/app_state.rs @@ -1,7 +1,9 @@ use crate::{ lock_r, models::app_data::UserData, - services::{presence_modules::models::ModuleMetadata, sprite}, + services::{ + friend_active_doll_sprite, presence_modules::models::ModuleMetadata, sprite, + }, state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}, }; @@ -32,3 +34,11 @@ pub fn get_modules() -> Result, String> { pub fn get_active_doll_sprite_base64() -> Result, String> { sprite::get_active_doll_sprite_base64() } + +#[tauri::command] +#[specta::specta] +pub fn get_friend_active_doll_sprites_base64( +) -> Result { + friend_active_doll_sprite::sync_from_app_data(); + Ok(friend_active_doll_sprite::get_snapshot()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index eaf24cb..ab0a9d2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,7 +6,10 @@ use crate::{ }, }; use commands::app::{quit_app, restart_app, retry_connection}; -use commands::app_state::{get_active_doll_sprite_base64, get_app_data, refresh_app_data}; +use commands::app_state::{ + get_active_doll_sprite_base64, get_app_data, get_friend_active_doll_sprites_base64, + refresh_app_data, +}; use commands::auth::{change_password, login, logout_and_restart, register, reset_password}; use commands::config::{get_client_config, open_client_config_manager, save_client_config}; use commands::dolls::{ @@ -25,7 +28,8 @@ use tauri_specta::{collect_commands, collect_events, Builder as SpectaBuilder, E use crate::services::app_events::{ ActiveDollSpriteChanged, AppDataRefreshed, CreateDoll, CursorMoved, EditDoll, - FriendActiveDollChanged, FriendCursorPositionsUpdated, FriendDisconnected, + FriendActiveDollChanged, FriendActiveDollSpritesUpdated, FriendCursorPositionsUpdated, + FriendDisconnected, FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged, @@ -66,6 +70,7 @@ pub fn run() { .commands(collect_commands![ get_app_data, get_active_doll_sprite_base64, + get_friend_active_doll_sprites_base64, refresh_app_data, list_friends, search_users, @@ -114,6 +119,7 @@ pub fn run() { FriendCursorPositionsUpdated, FriendDisconnected, FriendActiveDollChanged, + FriendActiveDollSpritesUpdated, FriendUserStatusChanged, InteractionReceived, InteractionDeliveryFailed, diff --git a/src-tauri/src/services/app_events.rs b/src-tauri/src/services/app_events.rs index 555e242..5883278 100644 --- a/src-tauri/src/services/app_events.rs +++ b/src-tauri/src/services/app_events.rs @@ -12,7 +12,10 @@ use crate::{ }, interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto}, }, - services::{cursor::CursorPositions, friend_cursor::FriendCursorPositionsDto}, + services::{ + cursor::CursorPositions, friend_active_doll_sprite::FriendActiveDollSpritesDto, + friend_cursor::FriendCursorPositionsDto, + }, }; #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] @@ -59,6 +62,10 @@ pub struct FriendDisconnected(pub FriendDisconnectedPayload); #[tauri_specta(event_name = "friend-active-doll-changed")] pub struct FriendActiveDollChanged(pub FriendActiveDollChangedPayload); +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "friend-active-doll-sprites-updated")] +pub struct FriendActiveDollSpritesUpdated(pub FriendActiveDollSpritesDto); + #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] #[tauri_specta(event_name = "friend-user-status")] pub struct FriendUserStatusChanged(pub FriendUserStatusPayload); diff --git a/src-tauri/src/services/friend_active_doll_sprite.rs b/src-tauri/src/services/friend_active_doll_sprite.rs new file mode 100644 index 0000000..ef210f5 --- /dev/null +++ b/src-tauri/src/services/friend_active_doll_sprite.rs @@ -0,0 +1,126 @@ +use std::{ + collections::HashMap, + sync::{Arc, LazyLock, RwLock}, +}; + +use serde::{Deserialize, Serialize}; +use specta::Type; +use tauri_specta::Event as _; + +use crate::{ + get_app_handle, lock_r, + models::{dolls::DollDto, friends::FriendshipResponseDto}, + services::{app_events::FriendActiveDollSpritesUpdated, sprite}, + state::FDOLL, +}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, Type)] +#[serde(transparent)] +pub struct FriendActiveDollSpritesDto(pub HashMap); + +static FRIEND_ACTIVE_DOLL_SPRITES: LazyLock>>> = + LazyLock::new(|| Arc::new(RwLock::new(HashMap::new()))); + +pub fn sync_from_app_data() { + let friends = { + let guard = lock_r!(FDOLL); + guard.user_data.friends.clone().unwrap_or_default() + }; + + let next = build_sprites(&friends); + + let mut projection = FRIEND_ACTIVE_DOLL_SPRITES + .write() + .expect("friend active doll sprite projection lock poisoned"); + *projection = next; + + emit_snapshot(&projection); +} + +pub fn clear() { + let mut projection = FRIEND_ACTIVE_DOLL_SPRITES + .write() + .expect("friend active doll sprite projection lock poisoned"); + projection.clear(); + + emit_snapshot(&projection); +} + +pub fn remove_friend(user_id: &str) { + let mut projection = FRIEND_ACTIVE_DOLL_SPRITES + .write() + .expect("friend active doll sprite projection lock poisoned"); + + if projection.remove(user_id).is_some() { + emit_snapshot(&projection); + } +} + +pub fn set_active_doll(user_id: &str, doll: Option<&DollDto>) { + let mut projection = FRIEND_ACTIVE_DOLL_SPRITES + .write() + .expect("friend active doll sprite projection lock poisoned"); + + match doll { + Some(doll) => match sprite::encode_doll_sprite_base64(doll) { + Ok(sprite_b64) => { + projection.insert(user_id.to_string(), sprite_b64); + emit_snapshot(&projection); + } + Err(err) => { + tracing::warn!( + "Failed to generate active doll sprite for friend {}: {}", + user_id, + err + ); + + if projection.remove(user_id).is_some() { + emit_snapshot(&projection); + } + } + }, + None => { + if projection.remove(user_id).is_some() { + emit_snapshot(&projection); + } + } + } +} + +pub fn get_snapshot() -> FriendActiveDollSpritesDto { + let projection = FRIEND_ACTIVE_DOLL_SPRITES + .read() + .expect("friend active doll sprite projection lock poisoned"); + + FriendActiveDollSpritesDto(projection.clone()) +} + +fn build_sprites(friends: &[FriendshipResponseDto]) -> HashMap { + friends + .iter() + .filter_map(|friendship| { + let friend = friendship.friend.as_ref()?; + let doll = friend.active_doll.as_ref()?; + + match sprite::encode_doll_sprite_base64(doll) { + Ok(sprite_b64) => Some((friend.id.clone(), sprite_b64)), + Err(err) => { + tracing::warn!( + "Failed to generate active doll sprite for friend {}: {}", + friend.id, + err + ); + None + } + } + }) + .collect() +} + +fn emit_snapshot(sprites: &HashMap) { + let payload = FriendActiveDollSpritesDto(sprites.clone()); + + if let Err(err) = FriendActiveDollSpritesUpdated(payload).emit(get_app_handle()) { + tracing::warn!("Failed to emit friend active doll sprites update: {}", err); + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index bb8e9ff..37a1054 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -8,6 +8,7 @@ pub mod auth; pub mod client_config_manager; pub mod cursor; pub mod doll_editor; +pub mod friend_active_doll_sprite; pub mod friend_cursor; pub mod health_manager; pub mod health_monitor; diff --git a/src-tauri/src/services/sprite.rs b/src-tauri/src/services/sprite.rs index c90c5cc..3fdb205 100644 --- a/src-tauri/src/services/sprite.rs +++ b/src-tauri/src/services/sprite.rs @@ -2,6 +2,17 @@ use crate::{lock_r, models::dolls::DollDto, state::FDOLL}; const APPLY_TEXTURE: bool = true; +pub fn encode_doll_sprite_base64(doll: &DollDto) -> Result { + let color_scheme = &doll.configuration.color_scheme; + + super::sprite_recolor::recolor_gif_base64( + &color_scheme.body, + &color_scheme.outline, + APPLY_TEXTURE, + ) + .map_err(|err| err.to_string()) +} + pub fn get_active_doll() -> Option { let guard = lock_r!(FDOLL); let active_doll_id = guard @@ -20,14 +31,7 @@ pub fn get_active_doll() -> Option { pub fn get_active_doll_sprite_base64() -> Result, String> { get_active_doll() - .map(|doll| { - let color_scheme = doll.configuration.color_scheme; - super::sprite_recolor::recolor_gif_base64( - &color_scheme.body, - &color_scheme.outline, - APPLY_TEXTURE, - ) - .map_err(|err| err.to_string()) - }) + .as_ref() + .map(encode_doll_sprite_base64) .transpose() } diff --git a/src-tauri/src/services/ws/friend.rs b/src-tauri/src/services/ws/friend.rs index e0eacc0..8209bf5 100644 --- a/src-tauri/src/services/ws/friend.rs +++ b/src-tauri/src/services/ws/friend.rs @@ -12,7 +12,7 @@ use crate::services::app_events::{ }; use crate::services::{ cursor::{normalized_to_absolute, CursorPositions}, - friend_cursor, + friend_active_doll_sprite, friend_cursor, }; use crate::state::AppDataRefreshScope; @@ -77,6 +77,7 @@ pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "friend-disconnected") { + friend_active_doll_sprite::remove_friend(&data.user_id); friend_cursor::remove_friend(&data.user_id); emitter::emit_to_frontend_typed(&FriendDisconnected(data)); } @@ -111,9 +112,9 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { payload, "friend-active-doll-changed", ) { + friend_active_doll_sprite::set_active_doll(&data.friend_id, data.doll.as_ref()); friend_cursor::set_active_doll(&data.friend_id, data.doll.is_some()); emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data)); - refresh::refresh_app_data(AppDataRefreshScope::Friends); } } diff --git a/src-tauri/src/state/ui.rs b/src-tauri/src/state/ui.rs index b2cb7c6..8fc723f 100644 --- a/src-tauri/src/state/ui.rs +++ b/src-tauri/src/state/ui.rs @@ -3,7 +3,7 @@ use crate::{ remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, services::{ app_events::{ActiveDollSpriteChanged, AppDataRefreshed}, - friend_cursor, sprite, + friend_active_doll_sprite, friend_cursor, sprite, }, state::FDOLL, }; @@ -165,6 +165,7 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { let mut guard = lock_w!(crate::state::FDOLL); guard.user_data.friends = Some(friends); drop(guard); + friend_active_doll_sprite::sync_from_app_data(); friend_cursor::sync_from_app_data(); } Err(e) => { @@ -286,5 +287,6 @@ pub fn clear_app_data() { guard.user_data.user = None; guard.user_data.friends = None; drop(guard); + friend_active_doll_sprite::clear(); friend_cursor::clear(); } diff --git a/src/events/friend-active-doll-sprite.ts b/src/events/friend-active-doll-sprite.ts new file mode 100644 index 0000000..49df7c8 --- /dev/null +++ b/src/events/friend-active-doll-sprite.ts @@ -0,0 +1,36 @@ +import { writable } from "svelte/store"; +import { + commands, + events, + type FriendActiveDollSpritesDto, +} from "$lib/bindings"; +import { createEventSource } from "./listener-utils"; + +export const friendActiveDollSpriteUrls = writable>({}); + +function toSpriteUrls( + spriteBase64ByFriendId: FriendActiveDollSpritesDto, +): Record { + return Object.fromEntries( + Object.entries(spriteBase64ByFriendId) + .filter((entry): entry is [string, string] => entry[1] !== undefined) + .map(([friendId, spriteBase64]) => [ + friendId, + `data:image/gif;base64,${spriteBase64}`, + ]), + ); +} + +export const { + start: startFriendActiveDollSprite, + stop: stopFriendActiveDollSprite, +} = createEventSource(async (addEventListener) => { + const initialSprites = await commands.getFriendActiveDollSpritesBase64(); + friendActiveDollSpriteUrls.set(toSpriteUrls(initialSprites)); + + addEventListener( + await events.friendActiveDollSpritesUpdated.listen((event) => { + friendActiveDollSpriteUrls.set(toSpriteUrls(event.payload)); + }), + ); +}); diff --git a/src/lib/bindings.ts b/src/lib/bindings.ts index dc1f93a..60597d4 100644 --- a/src/lib/bindings.ts +++ b/src/lib/bindings.ts @@ -11,6 +11,9 @@ async getAppData() : Promise { async getActiveDollSpriteBase64() : Promise { return await TAURI_INVOKE("get_active_doll_sprite_base64"); }, +async getFriendActiveDollSpritesBase64() : Promise { + return await TAURI_INVOKE("get_friend_active_doll_sprites_base64"); +}, async refreshAppData() : Promise { return await TAURI_INVOKE("refresh_app_data"); }, @@ -134,6 +137,7 @@ createDoll: CreateDoll, cursorMoved: CursorMoved, editDoll: EditDoll, friendActiveDollChanged: FriendActiveDollChanged, +friendActiveDollSpritesUpdated: FriendActiveDollSpritesUpdated, friendCursorPositionsUpdated: FriendCursorPositionsUpdated, friendDisconnected: FriendDisconnected, friendRequestAccepted: FriendRequestAccepted, @@ -153,6 +157,7 @@ createDoll: "create-doll", cursorMoved: "cursor-moved", editDoll: "edit-doll", friendActiveDollChanged: "friend-active-doll-changed", +friendActiveDollSpritesUpdated: "friend-active-doll-sprites-updated", friendCursorPositionsUpdated: "friend-cursor-positions-updated", friendDisconnected: "friend-disconnected", friendRequestAccepted: "friend-request-accepted", @@ -188,6 +193,8 @@ export type DollDto = { id: string; name: string; configuration: DollConfigurati export type EditDoll = string export type FriendActiveDollChanged = FriendActiveDollChangedPayload export type FriendActiveDollChangedPayload = { friendId: string; doll: DollDto | null } +export type FriendActiveDollSpritesDto = Partial<{ [key in string]: string }> +export type FriendActiveDollSpritesUpdated = FriendActiveDollSpritesDto export type FriendCursorPositionsDto = Partial<{ [key in string]: CursorPositions }> export type FriendCursorPositionsUpdated = FriendCursorPositionsDto export type FriendDisconnected = FriendDisconnectedPayload diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b0d98aa..d681b81 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -10,6 +10,10 @@ startActiveDollSprite, stopActiveDollSprite, } from "../events/active-doll-sprite"; + import { + startFriendActiveDollSprite, + stopFriendActiveDollSprite, + } from "../events/friend-active-doll-sprite"; import { startAppData } from "../events/app-data"; import { startInteraction, stopInteraction } from "../events/interaction"; import { @@ -24,6 +28,7 @@ try { await startAppData(); await startActiveDollSprite(); + await startFriendActiveDollSprite(); await startCursorTracking(); await startFriendCursorTracking(); await startSceneInteractive(); @@ -38,6 +43,7 @@ stopCursorTracking(); stopFriendCursorTracking(); stopActiveDollSprite(); + stopFriendActiveDollSprite(); stopSceneInteractive(); stopInteraction(); stopUserStatus(); diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte index 8923026..63052f1 100644 --- a/src/routes/scene/+page.svelte +++ b/src/routes/scene/+page.svelte @@ -3,6 +3,7 @@ import { friendsCursorPositions } from "../../events/friend-cursor"; import { appData } from "../../events/app-data"; import { activeDollSpriteUrl } from "../../events/active-doll-sprite"; + import { friendActiveDollSpriteUrls } from "../../events/friend-active-doll-sprite"; import { sceneInteractive } from "../../events/scene-interactive"; import { friendsPresenceStates, @@ -26,6 +27,17 @@ targetY={$cursorPositionOnScreen.raw.y} spriteUrl={$activeDollSpriteUrl} /> + {#each Object.entries($friendsCursorPositions) as [friendId, position] (friendId)} + {#if $friendActiveDollSpriteUrls[friendId]} + + {/if} + {/each}