added friends' neko to scene page

This commit is contained in:
2026-03-10 12:59:06 +08:00
parent a4d8601297
commit e38697faa9
13 changed files with 239 additions and 18 deletions

View File

@@ -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<Vec<ModuleMetadata>, String> {
pub fn get_active_doll_sprite_base64() -> Result<Option<String>, String> {
sprite::get_active_doll_sprite_base64()
}
#[tauri::command]
#[specta::specta]
pub fn get_friend_active_doll_sprites_base64(
) -> Result<friend_active_doll_sprite::FriendActiveDollSpritesDto, String> {
friend_active_doll_sprite::sync_from_app_data();
Ok(friend_active_doll_sprite::get_snapshot())
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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<String, String>);
static FRIEND_ACTIVE_DOLL_SPRITES: LazyLock<Arc<RwLock<HashMap<String, String>>>> =
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<String, String> {
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<String, String>) {
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);
}
}

View File

@@ -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;

View File

@@ -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<String, String> {
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<DollDto> {
let guard = lock_r!(FDOLL);
let active_doll_id = guard
@@ -20,14 +31,7 @@ pub fn get_active_doll() -> Option<DollDto> {
pub fn get_active_doll_sprite_base64() -> Result<Option<String>, 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()
}

View File

@@ -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::<FriendDisconnectedPayload>(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);
}
}

View File

@@ -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();
}