diff --git a/src-tauri/src/models/dolls.rs b/src-tauri/src/models/dolls.rs index f45e46c..6c76b42 100644 --- a/src-tauri/src/models/dolls.rs +++ b/src-tauri/src/models/dolls.rs @@ -1,34 +1,34 @@ use serde::{Deserialize, Serialize}; use specta::Type; -#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct DollColorSchemeDto { pub outline: String, pub body: String, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct DollConfigurationDto { pub color_scheme: DollColorSchemeDto, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct CreateDollDto { pub name: String, pub configuration: Option, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct UpdateDollDto { pub name: Option, pub configuration: Option, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct DollDto { pub id: String, diff --git a/src-tauri/src/models/event_payloads.rs b/src-tauri/src/models/event_payloads.rs index 5762383..657d9db 100644 --- a/src-tauri/src/models/event_payloads.rs +++ b/src-tauri/src/models/event_payloads.rs @@ -5,14 +5,14 @@ use super::dolls::DollDto; use super::friends::UserBasicDto; use crate::services::presence_modules::models::PresenceStatus; -#[derive(Clone, Serialize, Deserialize, Debug, Type)] +#[derive(Clone, Serialize, Deserialize, Debug, Type, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum UserStatusState { Idle, Resting, } -#[derive(Clone, Serialize, Deserialize, Debug, Type)] +#[derive(Clone, Serialize, Deserialize, Debug, Type, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct UserStatusPayload { pub presence_status: PresenceStatus, diff --git a/src-tauri/src/services/cursor.rs b/src-tauri/src/services/cursor.rs index ac91b93..9224f9a 100644 --- a/src-tauri/src/services/cursor.rs +++ b/src-tauri/src/services/cursor.rs @@ -15,14 +15,14 @@ use crate::{ }; use tauri_specta::Event as _; -#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[derive(Debug, Clone, Serialize, Deserialize, Type, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CursorPosition { pub x: f64, pub y: f64, } -#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[derive(Debug, Clone, Serialize, Deserialize, Type, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CursorPositions { pub raw: CursorPosition, diff --git a/src-tauri/src/services/presence_modules/models.rs b/src-tauri/src/services/presence_modules/models.rs index 53d9d13..fc74964 100644 --- a/src-tauri/src/services/presence_modules/models.rs +++ b/src-tauri/src/services/presence_modules/models.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use specta::Type; -#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[derive(Debug, Clone, Serialize, Deserialize, Type, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct PresenceStatus { pub title: Option, diff --git a/src-tauri/src/services/presence_state.rs b/src-tauri/src/services/presence_state.rs index 79b6e1f..b4fbbd0 100644 --- a/src-tauri/src/services/presence_state.rs +++ b/src-tauri/src/services/presence_state.rs @@ -1,6 +1,8 @@ use crate::{ - get_app_handle, lock_r, lock_w, models::event_payloads::UserStatusPayload, - services::app_events::PresenceStateUpdated, state::FDOLL, + get_app_handle, lock_r, lock_w, + models::event_payloads::UserStatusPayload, + services::app_events::PresenceStateUpdated, + state::{get_known_friend_ids, FDOLL}, }; use serde::{Deserialize, Serialize}; use specta::Type; @@ -22,44 +24,44 @@ pub fn get_presence_state_snapshot() -> PresenceStateSnapshot { } } -pub fn set_current_presence(status: UserStatusPayload) { +pub fn set_current_presence(status: UserStatusPayload) -> bool { let mut guard = lock_w!(FDOLL); + + if guard.presence.current.as_ref() == Some(&status) { + return false; + } + guard.presence.current = Some(status); + true } -pub fn set_friend_presence(friend_id: String, status: UserStatusPayload) { +pub fn set_friend_presence(friend_id: String, status: UserStatusPayload) -> bool { let mut guard = lock_w!(FDOLL); + + if guard.presence.friends.get(&friend_id) == Some(&status) { + return false; + } + guard.presence.friends.insert(friend_id, status); + true } -pub fn remove_friend_presence(friend_id: &str) { +pub fn remove_friend_presence(friend_id: &str) -> bool { let mut guard = lock_w!(FDOLL); - guard.presence.friends.remove(friend_id); + guard.presence.friends.remove(friend_id).is_some() } -pub fn clear_missing_friends_from_presence_state() { - let friend_ids = { - let guard = lock_r!(FDOLL); - guard - .user_data - .friends - .as_ref() - .map(|friends| { - friends - .iter() - .filter_map(|friendship| { - friendship.friend.as_ref().map(|friend| friend.id.clone()) - }) - .collect::>() - }) - .unwrap_or_default() - }; +pub fn clear_missing_friends_from_presence_state() -> bool { + let friend_ids = get_known_friend_ids(); let mut guard = lock_w!(FDOLL); + let initial_count = guard.presence.friends.len(); guard .presence .friends .retain(|friend_id, _| friend_ids.contains(friend_id)); + + guard.presence.friends.len() != initial_count } pub fn emit_presence_state_updated() { diff --git a/src-tauri/src/services/scene_friends.rs b/src-tauri/src/services/scene_friends.rs index 28fe981..da05a7d 100644 --- a/src-tauri/src/services/scene_friends.rs +++ b/src-tauri/src/services/scene_friends.rs @@ -2,7 +2,7 @@ use crate::{ get_app_handle, lock_r, lock_w, models::{dolls::DollDto, scene::SceneFriendNeko}, services::{app_events::SceneFriendsUpdated, cursor::CursorPositions}, - state::FDOLL, + state::{get_known_friend_ids, FDOLL}, }; use tauri_specta::Event as _; use tracing::error; @@ -34,44 +34,49 @@ pub fn get_scene_friends_snapshot() -> Vec { .collect() } -pub fn set_friend_cursor_position(friend_id: String, position: CursorPositions) { +pub fn set_friend_cursor_position(friend_id: String, position: CursorPositions) -> bool { let mut guard = lock_w!(FDOLL); + + if guard.friend_scene.cursor_positions.get(&friend_id) == Some(&position) { + return false; + } + guard .friend_scene .cursor_positions .insert(friend_id, position); + true } -pub fn set_friend_active_doll(friend_id: String, doll: Option) { +pub fn set_friend_active_doll(friend_id: String, doll: Option) -> bool { let mut guard = lock_w!(FDOLL); + + if guard.friend_scene.active_dolls.get(&friend_id) == Some(&doll) { + return false; + } + guard.friend_scene.active_dolls.insert(friend_id, doll); + true } -pub fn remove_friend(friend_id: &str) { +pub fn remove_friend(friend_id: &str) -> bool { let mut guard = lock_w!(FDOLL); - guard.friend_scene.cursor_positions.remove(friend_id); - guard.friend_scene.active_dolls.remove(friend_id); + let removed_cursor = guard + .friend_scene + .cursor_positions + .remove(friend_id) + .is_some(); + let removed_doll = guard.friend_scene.active_dolls.remove(friend_id).is_some(); + + removed_cursor || removed_doll } -pub fn clear_missing_friends_from_runtime_state() { - let friend_ids = { - let guard = lock_r!(FDOLL); - guard - .user_data - .friends - .as_ref() - .map(|friends| { - friends - .iter() - .filter_map(|friendship| { - friendship.friend.as_ref().map(|friend| friend.id.clone()) - }) - .collect::>() - }) - .unwrap_or_default() - }; +pub fn clear_missing_friends_from_runtime_state() -> bool { + let friend_ids = get_known_friend_ids(); let mut guard = lock_w!(FDOLL); + let initial_cursor_count = guard.friend_scene.cursor_positions.len(); + let initial_active_doll_count = guard.friend_scene.active_dolls.len(); guard .friend_scene .cursor_positions @@ -80,6 +85,9 @@ pub fn clear_missing_friends_from_runtime_state() { .friend_scene .active_dolls .retain(|friend_id, _| friend_ids.contains(friend_id)); + + guard.friend_scene.cursor_positions.len() != initial_cursor_count + || guard.friend_scene.active_dolls.len() != initial_active_doll_count } pub fn emit_scene_friends_updated() { diff --git a/src-tauri/src/services/ws/friend.rs b/src-tauri/src/services/ws/friend.rs index 065df3d..11f959a 100644 --- a/src-tauri/src/services/ws/friend.rs +++ b/src-tauri/src/services/ws/friend.rs @@ -75,13 +75,15 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) { }, }; - scene_friends::set_friend_cursor_position( + let scene_friends_changed = scene_friends::set_friend_cursor_position( outgoing_payload.user_id.clone(), outgoing_payload.position.clone(), ); emitter::emit_to_frontend_typed(&FriendCursorPositionUpdated(outgoing_payload)); - scene_friends::emit_scene_friends_updated(); + if scene_friends_changed { + scene_friends::emit_scene_friends_updated(); + } } } @@ -90,11 +92,15 @@ pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "friend-disconnected") { - scene_friends::remove_friend(&data.user_id); - presence_state::remove_friend_presence(&data.user_id); + let scene_friends_changed = scene_friends::remove_friend(&data.user_id); + let presence_changed = presence_state::remove_friend_presence(&data.user_id); emitter::emit_to_frontend_typed(&FriendDisconnected(data)); - scene_friends::emit_scene_friends_updated(); - presence_state::emit_presence_state_updated(); + if scene_friends_changed { + scene_friends::emit_scene_friends_updated(); + } + if presence_changed { + presence_state::emit_presence_state_updated(); + } } } @@ -129,7 +135,6 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { ) { scene_friends::set_friend_active_doll(data.friend_id.clone(), data.doll.clone()); emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data)); - scene_friends::emit_scene_friends_updated(); refresh::refresh_app_data(AppDataRefreshScope::Friends); } } @@ -139,8 +144,11 @@ pub fn on_friend_user_status(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "friend-user-status") { - presence_state::set_friend_presence(data.user_id.clone(), data.status.clone()); + let presence_changed = + presence_state::set_friend_presence(data.user_id.clone(), data.status.clone()); emitter::emit_to_frontend_typed(&FriendUserStatusChanged(data)); - presence_state::emit_presence_state_updated(); + if presence_changed { + presence_state::emit_presence_state_updated(); + } } } diff --git a/src-tauri/src/services/ws/user_status.rs b/src-tauri/src/services/ws/user_status.rs index 1741671..ee67236 100644 --- a/src-tauri/src/services/ws/user_status.rs +++ b/src-tauri/src/services/ws/user_status.rs @@ -29,8 +29,9 @@ pub async fn report_user_status(status: UserStatusPayload) { warn!("Failed to emit user-status-changed event: {e}"); } - presence_state::set_current_presence(status.clone()); - presence_state::emit_presence_state_updated(); + if presence_state::set_current_presence(status.clone()) { + presence_state::emit_presence_state_updated(); + } // Schedule new report after 500ms let handle = async_runtime::spawn(async move { diff --git a/src-tauri/src/state/mod.rs b/src-tauri/src/state/mod.rs index 20b98de..fde72ad 100644 --- a/src-tauri/src/state/mod.rs +++ b/src-tauri/src/state/mod.rs @@ -1,5 +1,6 @@ // in app-core/src/state.rs use crate::{ + lock_r, lock_w, models::{app_data::UserData, dolls::DollDto}, services::{ @@ -7,7 +8,7 @@ use crate::{ presence_modules::models::ModuleMetadata, }, }; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::{Arc, LazyLock, RwLock}; use tauri::tray::TrayIcon; use tracing::info; @@ -55,6 +56,22 @@ pub struct AppState { pub static FDOLL: LazyLock>> = LazyLock::new(|| Arc::new(RwLock::new(AppState::default()))); +pub fn get_known_friend_ids() -> HashSet { + let guard = lock_r!(FDOLL); + + guard + .user_data + .friends + .as_ref() + .map(|friends| { + friends + .iter() + .filter_map(|friendship| friendship.friend.as_ref().map(|friend| friend.id.clone())) + .collect() + }) + .unwrap_or_default() +} + /// Populate app state with initial /// values and necesary client instances. pub fn init_app_state() { diff --git a/src-tauri/src/state/ui.rs b/src-tauri/src/state/ui.rs index 7a67e33..c888083 100644 --- a/src-tauri/src/state/ui.rs +++ b/src-tauri/src/state/ui.rs @@ -211,8 +211,14 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { let app_data_clone = guard.user_data.clone(); drop(guard); // Drop lock before emitting to prevent potential deadlocks - scene_friends::clear_missing_friends_from_runtime_state(); - presence_state::clear_missing_friends_from_presence_state(); + let refreshes_friend_runtime = matches!( + scope, + AppDataRefreshScope::All | AppDataRefreshScope::Friends + ); + if refreshes_friend_runtime { + scene_friends::clear_missing_friends_from_runtime_state(); + presence_state::clear_missing_friends_from_presence_state(); + } if let Err(e) = AppDataRefreshed(app_data_clone).emit(get_app_handle()) { warn!("Failed to emit app-data-refreshed event: {}", e); @@ -229,8 +235,12 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { .show(|_| {}); } - scene_friends::emit_scene_friends_updated(); - presence_state::emit_presence_state_updated(); + if refreshes_friend_runtime { + scene_friends::emit_scene_friends_updated(); + } + if refreshes_friend_runtime { + presence_state::emit_presence_state_updated(); + } } Ok(()) diff --git a/src/lib/scene.ts b/src/lib/scene.ts index 22f2a14..a0516d6 100644 --- a/src/lib/scene.ts +++ b/src/lib/scene.ts @@ -1,15 +1,11 @@ -import type { DollDto } from "$lib/bindings"; -import type { RecolorOptions } from "$lib/utils/sprite-utils"; +import type { DollColorSchemeDto, DollDto } from "$lib/bindings"; -export function getSpriteOptions( +export function getDollColorScheme( doll: DollDto | null | undefined, -): RecolorOptions | undefined { +): DollColorSchemeDto | undefined { if (!doll) { return undefined; } - return { - bodyColor: doll.configuration.colorScheme.body, - outlineColor: doll.configuration.colorScheme.outline, - }; + return doll.configuration.colorScheme; } diff --git a/src/lib/utils/sprite-utils.ts b/src/lib/utils/sprite-utils.ts index d49479b..c268ee3 100644 --- a/src/lib/utils/sprite-utils.ts +++ b/src/lib/utils/sprite-utils.ts @@ -1,10 +1,10 @@ import { commands, type DollColorSchemeDto } from "$lib/bindings"; import onekoGif from "../../assets/oneko/oneko.gif"; -export interface RecolorOptions { - bodyColor: string; - outlineColor: string; - applyTexture?: boolean; +const spriteSheetUrlCache = new Map>(); + +function getSpriteCacheKey(options: DollColorSchemeDto): string { + return `${options.body}:${options.outline}`; } export async function getSpriteSheetUrl( @@ -14,15 +14,26 @@ export async function getSpriteSheetUrl( return onekoGif; } - try { - const result = await commands.recolorGifBase64( + const cacheKey = getSpriteCacheKey(options); + const cachedSpriteSheet = spriteSheetUrlCache.get(cacheKey); + + if (cachedSpriteSheet) { + return cachedSpriteSheet; + } + + const spriteSheetPromise = commands + .recolorGifBase64( options.body, options.outline, true, // default texture to true at this stage, maybe one day open up more customization options - ); - return `data:image/gif;base64,${result}`; - } catch (e) { - console.error("Failed to recolor sprite:", e); - return onekoGif; - } + ) + .then((result) => `data:image/gif;base64,${result}`) + .catch((e) => { + console.error("Failed to recolor sprite:", e); + spriteSheetUrlCache.delete(cacheKey); + return onekoGif; + }); + + spriteSheetUrlCache.set(cacheKey, spriteSheetPromise); + return spriteSheetPromise; } diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte index 43b8a31..6a43ab3 100644 --- a/src/routes/scene/+page.svelte +++ b/src/routes/scene/+page.svelte @@ -1,4 +1,6 @@ @@ -81,7 +143,7 @@ cursorPosition={$cursorPositionOnScreen} presenceStatus={$currentPresenceState?.presenceStatus ?? null} friendsCursorPositions={Object.fromEntries( - $sceneFriends.map((friend) => [friend.id, friend.position]), + $sceneFriends.map((friend: SceneFriendNeko) => [friend.id, friend.position]), )} friends={$appData?.friends ?? []} friendsPresenceStates={$friendsPresenceStates}