diff --git a/src-tauri/src/commands/app_state.rs b/src-tauri/src/commands/app_state.rs index a6838cd..a40d8b6 100644 --- a/src-tauri/src/commands/app_state.rs +++ b/src-tauri/src/commands/app_state.rs @@ -1,7 +1,7 @@ use crate::{ lock_r, models::app_data::UserData, - services::presence_modules::models::ModuleMetadata, + services::{presence_modules::models::ModuleMetadata, presence_state::PresenceStateSnapshot}, state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}, }; @@ -26,3 +26,9 @@ pub fn get_modules() -> Result, String> { let guard = lock_r!(FDOLL); Ok(guard.modules.metadatas.clone()) } + +#[tauri::command] +#[specta::specta] +pub fn get_presence_state() -> Result { + Ok(crate::services::presence_state::get_presence_state_snapshot()) +} diff --git a/src-tauri/src/commands/dolls.rs b/src-tauri/src/commands/dolls.rs index 6da5e68..5278b1c 100644 --- a/src-tauri/src/commands/dolls.rs +++ b/src-tauri/src/commands/dolls.rs @@ -1,4 +1,5 @@ use crate::{ + get_app_handle, models::dolls::{CreateDollDto, DollDto, UpdateDollDto}, remotes::{ dolls::DollsRemote, @@ -7,6 +8,9 @@ use crate::{ state::AppDataRefreshScope, commands::{refresh_app_data, refresh_app_data_conditionally, is_active_doll}, }; +use crate::commands::scene::get_user_active_doll; +use crate::services::app_events::UserActiveDollUpdated; +use tauri_specta::Event as _; #[tauri::command] #[specta::specta] @@ -87,6 +91,9 @@ pub async fn set_active_doll(doll_id: String) -> Result<(), String> { refresh_app_data(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]).await; + let active_doll = get_user_active_doll().ok().flatten(); + let _ = UserActiveDollUpdated(active_doll).emit(get_app_handle()); + Ok(()) } @@ -100,5 +107,8 @@ pub async fn remove_active_doll() -> Result<(), String> { refresh_app_data(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]).await; + let active_doll = get_user_active_doll().ok().flatten(); + let _ = UserActiveDollUpdated(active_doll).emit(get_app_handle()); + Ok(()) } diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 6cf9b25..72afe8a 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -7,6 +7,7 @@ pub mod friends; pub mod interaction; pub mod sprite; pub mod petpet; +pub mod scene; use crate::lock_r; use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}; diff --git a/src-tauri/src/commands/scene.rs b/src-tauri/src/commands/scene.rs new file mode 100644 index 0000000..788f693 --- /dev/null +++ b/src-tauri/src/commands/scene.rs @@ -0,0 +1,32 @@ +use crate::{ + lock_r, + models::{dolls::DollDto, scene::SceneFriendNeko}, + state::FDOLL, +}; + +#[tauri::command] +#[specta::specta] +pub fn get_user_active_doll() -> Result, String> { + let guard = lock_r!(FDOLL); + + let Some(user) = &guard.user_data.user else { + return Ok(None); + }; + + let Some(active_doll_id) = &user.active_doll_id else { + return Ok(None); + }; + + Ok(guard.user_data.dolls.as_ref().and_then(|dolls| { + dolls + .iter() + .find(|doll| doll.id == *active_doll_id) + .cloned() + })) +} + +#[tauri::command] +#[specta::specta] +pub fn get_scene_friends() -> Result, String> { + Ok(crate::services::scene_friends::get_scene_friends_snapshot()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ce7a759..4390e34 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,10 +1,11 @@ use crate::{ - commands::app_state::get_modules, + commands::app_state::{get_modules, get_presence_state}, services::{ doll_editor::open_doll_editor_window, scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive}, }, }; +use commands::scene::{get_scene_friends, get_user_active_doll}; use commands::app::{quit_app, restart_app, retry_connection}; use commands::app_state::{get_app_data, refresh_app_data}; use commands::auth::{change_password, login, logout_and_restart, register, reset_password}; @@ -26,9 +27,10 @@ use tauri_specta::{Builder as SpectaBuilder, ErrorHandlingMode, collect_commands use crate::services::app_events::{ AppDataRefreshed, CreateDoll, CursorMoved, EditDoll, FriendActiveDollChanged, FriendCursorPositionUpdated, FriendDisconnected, FriendRequestAccepted, - FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, + FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, PresenceStateUpdated, + SceneFriendsUpdated, InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged, - SetInteractionOverlay, Unfriended, UserStatusChanged, + SetInteractionOverlay, Unfriended, UserActiveDollUpdated, UserStatusChanged, }; static APP_HANDLE: std::sync::OnceLock> = std::sync::OnceLock::new(); @@ -93,13 +95,16 @@ pub fn run() { get_scene_interactive, set_scene_interactive, set_pet_menu_state, + get_user_active_doll, + get_scene_friends, login, register, change_password, reset_password, logout_and_restart, send_interaction_cmd, - get_modules + get_modules, + get_presence_state ]) .events(collect_events![ CursorMoved, @@ -109,10 +114,13 @@ pub fn run() { EditDoll, CreateDoll, UserStatusChanged, + UserActiveDollUpdated, FriendCursorPositionUpdated, FriendDisconnected, FriendActiveDollChanged, FriendUserStatusChanged, + PresenceStateUpdated, + SceneFriendsUpdated, InteractionReceived, InteractionDeliveryFailed, FriendRequestReceived, diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index a40165e..4735dc9 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -5,4 +5,5 @@ pub mod friends; pub mod health; pub mod interaction; pub mod remote_error; +pub mod scene; pub mod user; diff --git a/src-tauri/src/models/scene.rs b/src-tauri/src/models/scene.rs new file mode 100644 index 0000000..c27f96a --- /dev/null +++ b/src-tauri/src/models/scene.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +use crate::{models::dolls::DollDto, services::cursor::CursorPositions}; + +#[derive(Clone, Serialize, Deserialize, Debug, Type)] +#[serde(rename_all = "camelCase")] +pub struct SceneFriendNeko { + pub id: String, + pub position: CursorPositions, + pub active_doll: DollDto, +} diff --git a/src-tauri/src/services/app_events.rs b/src-tauri/src/services/app_events.rs index 2f6ccbb..4e27d13 100644 --- a/src-tauri/src/services/app_events.rs +++ b/src-tauri/src/services/app_events.rs @@ -5,14 +5,19 @@ use tauri_specta::Event; use crate::{ models::{ app_data::UserData, + dolls::DollDto, event_payloads::{ FriendActiveDollChangedPayload, FriendDisconnectedPayload, FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload, FriendUserStatusPayload, UnfriendedPayload, UserStatusPayload, }, interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto}, + scene::SceneFriendNeko, + }, + services::{ + cursor::CursorPositions, presence_state::PresenceStateSnapshot, + ws::OutgoingFriendCursorPayload, }, - services::{cursor::CursorPositions, ws::OutgoingFriendCursorPayload}, }; #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] @@ -43,6 +48,10 @@ pub struct CreateDoll; #[tauri_specta(event_name = "user-status-changed")] pub struct UserStatusChanged(pub UserStatusPayload); +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "user-active-doll-updated")] +pub struct UserActiveDollUpdated(pub Option); + #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] #[tauri_specta(event_name = "friend-cursor-position")] pub struct FriendCursorPositionUpdated(pub OutgoingFriendCursorPayload); @@ -59,6 +68,14 @@ pub struct FriendActiveDollChanged(pub FriendActiveDollChangedPayload); #[tauri_specta(event_name = "friend-user-status")] pub struct FriendUserStatusChanged(pub FriendUserStatusPayload); +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "scene-friends-updated")] +pub struct SceneFriendsUpdated(pub Vec); + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "presence-state-updated")] +pub struct PresenceStateUpdated(pub PresenceStateSnapshot); + #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] #[tauri_specta(event_name = "interaction-received")] pub struct InteractionReceived(pub InteractionPayloadDto); diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index c694080..531b2c4 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -12,8 +12,10 @@ pub mod health_manager; pub mod health_monitor; pub mod interaction; pub mod petpet; +pub mod presence_state; pub mod presence_modules; pub mod scene; +pub mod scene_friends; pub mod sprite_recolor; pub mod welcome; pub mod ws; diff --git a/src-tauri/src/services/presence_state.rs b/src-tauri/src/services/presence_state.rs new file mode 100644 index 0000000..79b6e1f --- /dev/null +++ b/src-tauri/src/services/presence_state.rs @@ -0,0 +1,71 @@ +use crate::{ + get_app_handle, lock_r, lock_w, models::event_payloads::UserStatusPayload, + services::app_events::PresenceStateUpdated, state::FDOLL, +}; +use serde::{Deserialize, Serialize}; +use specta::Type; +use tauri_specta::Event as _; +use tracing::error; + +#[derive(Clone, Serialize, Deserialize, Debug, Type)] +#[serde(rename_all = "camelCase")] +pub struct PresenceStateSnapshot { + pub current: Option, + pub friends: std::collections::HashMap, +} + +pub fn get_presence_state_snapshot() -> PresenceStateSnapshot { + let guard = lock_r!(FDOLL); + PresenceStateSnapshot { + current: guard.presence.current.clone(), + friends: guard.presence.friends.clone(), + } +} + +pub fn set_current_presence(status: UserStatusPayload) { + let mut guard = lock_w!(FDOLL); + guard.presence.current = Some(status); +} + +pub fn set_friend_presence(friend_id: String, status: UserStatusPayload) { + let mut guard = lock_w!(FDOLL); + guard.presence.friends.insert(friend_id, status); +} + +pub fn remove_friend_presence(friend_id: &str) { + let mut guard = lock_w!(FDOLL); + guard.presence.friends.remove(friend_id); +} + +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() + }; + + let mut guard = lock_w!(FDOLL); + guard + .presence + .friends + .retain(|friend_id, _| friend_ids.contains(friend_id)); +} + +pub fn emit_presence_state_updated() { + let snapshot = get_presence_state_snapshot(); + + if let Err(err) = PresenceStateUpdated(snapshot).emit(get_app_handle()) { + error!("Failed to emit presence state updated event: {}", err); + } +} diff --git a/src-tauri/src/services/scene_friends.rs b/src-tauri/src/services/scene_friends.rs new file mode 100644 index 0000000..28fe981 --- /dev/null +++ b/src-tauri/src/services/scene_friends.rs @@ -0,0 +1,91 @@ +use crate::{ + get_app_handle, lock_r, lock_w, + models::{dolls::DollDto, scene::SceneFriendNeko}, + services::{app_events::SceneFriendsUpdated, cursor::CursorPositions}, + state::FDOLL, +}; +use tauri_specta::Event as _; +use tracing::error; + +pub fn get_scene_friends_snapshot() -> Vec { + let guard = lock_r!(FDOLL); + + (guard.user_data.friends.as_ref()) + .into_iter() + .flatten() + .filter_map(|friendship| { + let friend = friendship.friend.as_ref()?; + let position = guard.friend_scene.cursor_positions.get(&friend.id)?.clone(); + + let active_doll = guard + .friend_scene + .active_dolls + .get(&friend.id) + .cloned() + .flatten() + .or_else(|| friend.active_doll.clone())?; + + Some(SceneFriendNeko { + id: friend.id.clone(), + position, + active_doll, + }) + }) + .collect() +} + +pub fn set_friend_cursor_position(friend_id: String, position: CursorPositions) { + let mut guard = lock_w!(FDOLL); + guard + .friend_scene + .cursor_positions + .insert(friend_id, position); +} + +pub fn set_friend_active_doll(friend_id: String, doll: Option) { + let mut guard = lock_w!(FDOLL); + guard.friend_scene.active_dolls.insert(friend_id, doll); +} + +pub fn remove_friend(friend_id: &str) { + let mut guard = lock_w!(FDOLL); + guard.friend_scene.cursor_positions.remove(friend_id); + guard.friend_scene.active_dolls.remove(friend_id); +} + +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() + }; + + let mut guard = lock_w!(FDOLL); + guard + .friend_scene + .cursor_positions + .retain(|friend_id, _| friend_ids.contains(friend_id)); + guard + .friend_scene + .active_dolls + .retain(|friend_id, _| friend_ids.contains(friend_id)); +} + +pub fn emit_scene_friends_updated() { + let snapshot = get_scene_friends_snapshot(); + + if let Err(err) = SceneFriendsUpdated(snapshot).emit(get_app_handle()) { + error!("Failed to emit scene friends updated event: {}", err); + } +} diff --git a/src-tauri/src/services/ws/friend.rs b/src-tauri/src/services/ws/friend.rs index 03693db..065df3d 100644 --- a/src-tauri/src/services/ws/friend.rs +++ b/src-tauri/src/services/ws/friend.rs @@ -11,7 +11,10 @@ use crate::services::app_events::{ FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, Unfriended, }; -use crate::services::cursor::{normalized_to_absolute, CursorPositions}; +use crate::services::{ + cursor::{normalized_to_absolute, CursorPositions}, + presence_state, scene_friends, +}; use crate::state::AppDataRefreshScope; use super::{ @@ -72,7 +75,13 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) { }, }; + 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(); } } @@ -81,7 +90,11 @@ 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); emitter::emit_to_frontend_typed(&FriendDisconnected(data)); + scene_friends::emit_scene_friends_updated(); + presence_state::emit_presence_state_updated(); } } @@ -114,7 +127,9 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { payload, "friend-active-doll-changed", ) { + 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); } } @@ -124,6 +139,8 @@ 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()); emitter::emit_to_frontend_typed(&FriendUserStatusChanged(data)); + 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 32a6360..1741671 100644 --- a/src-tauri/src/services/ws/user_status.rs +++ b/src-tauri/src/services/ws/user_status.rs @@ -8,6 +8,7 @@ use tracing::warn; use crate::models::event_payloads::UserStatusPayload; use crate::services::app_events::UserStatusChanged; +use crate::services::presence_state; use super::{emitter, types::WS_EVENT}; @@ -28,6 +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(); + // Schedule new report after 500ms let handle = async_runtime::spawn(async move { tokio::time::sleep(Duration::from_millis(500)).await; diff --git a/src-tauri/src/state/mod.rs b/src-tauri/src/state/mod.rs index 142f831..20b98de 100644 --- a/src-tauri/src/state/mod.rs +++ b/src-tauri/src/state/mod.rs @@ -1,7 +1,13 @@ // in app-core/src/state.rs use crate::{ - lock_w, models::app_data::UserData, services::presence_modules::models::ModuleMetadata, + lock_w, + models::{app_data::UserData, dolls::DollDto}, + services::{ + cursor::CursorPositions, + presence_modules::models::ModuleMetadata, + }, }; +use std::collections::HashMap; use std::sync::{Arc, LazyLock, RwLock}; use tauri::tray::TrayIcon; use tracing::info; @@ -20,12 +26,26 @@ pub struct Modules { pub metadatas: Vec, } +#[derive(Default, Clone)] +pub struct FriendSceneRuntimeState { + pub cursor_positions: HashMap, + pub active_dolls: HashMap>, +} + +#[derive(Default, Clone)] +pub struct PresenceRuntimeState { + pub current: Option, + pub friends: HashMap, +} + #[derive(Default)] pub struct AppState { pub app_config: crate::services::client_config_manager::AppConfig, pub network: NetworkState, pub auth: AuthState, pub user_data: UserData, + pub friend_scene: FriendSceneRuntimeState, + pub presence: PresenceRuntimeState, pub tray: Option, pub modules: Modules, } @@ -45,6 +65,8 @@ pub fn init_app_state() { guard.network = init_network_state(); guard.auth = init_auth_state(); guard.user_data = UserData::default(); + guard.friend_scene = FriendSceneRuntimeState::default(); + guard.presence = PresenceRuntimeState::default(); guard.modules = Modules::default(); } update_display_dimensions_for_scene_state(); diff --git a/src-tauri/src/state/ui.rs b/src-tauri/src/state/ui.rs index bda3834..7a67e33 100644 --- a/src-tauri/src/state/ui.rs +++ b/src-tauri/src/state/ui.rs @@ -1,7 +1,7 @@ use crate::{ get_app_handle, lock_r, lock_w, remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, - services::app_events::AppDataRefreshed, + services::{app_events::AppDataRefreshed, presence_state, scene_friends}, state::FDOLL, }; use std::{collections::HashSet, sync::LazyLock}; @@ -211,6 +211,9 @@ 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(); + if let Err(e) = AppDataRefreshed(app_data_clone).emit(get_app_handle()) { warn!("Failed to emit app-data-refreshed event: {}", e); use tauri_plugin_dialog::MessageDialogBuilder; @@ -223,8 +226,11 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { "Could not broadcast refreshed data to the UI. Some data may be stale.", ) .kind(MessageDialogKind::Error) - .show(|_| {}); + .show(|_| {}); } + + scene_friends::emit_scene_friends_updated(); + presence_state::emit_presence_state_updated(); } Ok(()) @@ -260,4 +266,8 @@ pub fn clear_app_data() { guard.user_data.dolls = None; guard.user_data.user = None; guard.user_data.friends = None; + guard.friend_scene.cursor_positions.clear(); + guard.friend_scene.active_dolls.clear(); + guard.presence.current = None; + guard.presence.friends.clear(); } diff --git a/src/events/friend-cursor.ts b/src/events/friend-cursor.ts index 731d14c..91439e2 100644 --- a/src/events/friend-cursor.ts +++ b/src/events/friend-cursor.ts @@ -1,81 +1,16 @@ import { writable } from "svelte/store"; -import { - events, - type CursorPositions, - type DollDto, - type OutgoingFriendCursorPayload, -} from "$lib/bindings"; -import { createEventSource, removeFromStore } from "./listener-utils"; +import { commands, events, type SceneFriendNeko } from "$lib/bindings"; +import { createEventSource } from "./listener-utils"; -type FriendCursorData = { - position: CursorPositions; - lastUpdated: number; -}; +export const sceneFriends = writable([]); -export const friendsCursorPositions = writable>( - {}, -); -export const friendsActiveDolls = writable>({}); +export const { start: startFriendCursorTracking, stop: stopFriendCursorTracking } = + createEventSource(async (addEventListener) => { + sceneFriends.set(await commands.getSceneFriends()); -export const { - start: startFriendCursorTracking, - stop: stopFriendCursorTracking, -} = createEventSource(async (addEventListener) => { - let friendCursorState: Record = {}; - addEventListener( - await events.friendCursorPositionUpdated.listen((event) => { - const data: OutgoingFriendCursorPayload = event.payload; - - friendCursorState[data.userId] = { - position: data.position, - lastUpdated: Date.now(), - }; - - friendsCursorPositions.update((current) => { - return { - ...current, - [data.userId]: data.position, - }; - }); - }), - ); - - addEventListener( - await events.friendDisconnected.listen((event) => { - const data = event.payload; - - if (friendCursorState[data.userId]) { - delete friendCursorState[data.userId]; - } - - friendsCursorPositions.update((current) => - removeFromStore(current, data.userId), - ); - }), - ); - - addEventListener( - await events.friendActiveDollChanged.listen((event) => { - const payload = event.payload; - - if (!payload.doll) { - friendsActiveDolls.update((current) => { - const next = { ...current }; - next[payload.friendId] = null; - return next; - }); - - friendsCursorPositions.update((current) => - removeFromStore(current, payload.friendId), - ); - } else { - friendsActiveDolls.update((current) => { - return { - ...current, - [payload.friendId]: payload.doll, - }; - }); - } - }), - ); -}); + addEventListener( + await events.sceneFriendsUpdated.listen((event) => { + sceneFriends.set(event.payload); + }), + ); + }); diff --git a/src/events/user-status.ts b/src/events/user-status.ts index 0ccd3cc..8ec9b67 100644 --- a/src/events/user-status.ts +++ b/src/events/user-status.ts @@ -1,44 +1,32 @@ import { writable } from "svelte/store"; -import { events, type UserStatusPayload } from "$lib/bindings"; -import { createEventSource, removeFromStore } from "./listener-utils"; +import { + commands, + events, + type PresenceStateSnapshot, + type UserStatusPayload, +} from "$lib/bindings"; +import { createEventSource } from "./listener-utils"; -export const friendsPresenceStates = writable< - Record ->({}); +export const friendsPresenceStates = writable>({}); export const currentPresenceState = writable(null); +function applyPresenceSnapshot(snapshot: PresenceStateSnapshot) { + currentPresenceState.set(snapshot.current); + + const friends = Object.fromEntries( + Object.entries(snapshot.friends).filter(([, status]) => status !== undefined), + ) as Record; + + friendsPresenceStates.set(friends); +} + export const { start: startUserStatus, stop: stopUserStatus } = createEventSource(async (addEventListener) => { - addEventListener( - await events.friendUserStatusChanged.listen((event) => { - const { userId, status } = event.payload; - - const hasValidName = - (typeof status.presenceStatus.title === "string" && - status.presenceStatus.title.trim() !== "") || - (typeof status.presenceStatus.subtitle === "string" && - status.presenceStatus.subtitle.trim() !== ""); - if (!hasValidName) return; - - friendsPresenceStates.update((current) => ({ - ...current, - [userId]: status, - })); - }), - ); + applyPresenceSnapshot(await commands.getPresenceState()); addEventListener( - await events.userStatusChanged.listen((event) => { - currentPresenceState.set(event.payload); - }), - ); - - addEventListener( - await events.friendDisconnected.listen((event) => { - const { userId } = event.payload; - friendsPresenceStates.update((current) => - removeFromStore(current, userId), - ); + await events.presenceStateUpdated.listen((event) => { + applyPresenceSnapshot(event.payload); }), ); }); diff --git a/src/lib/bindings.ts b/src/lib/bindings.ts index e5f00cf..54a3a48 100644 --- a/src/lib/bindings.ts +++ b/src/lib/bindings.ts @@ -98,6 +98,12 @@ async setSceneInteractive(interactive: boolean, shouldClick: boolean) : Promise< async setPetMenuState(id: string, open: boolean) : Promise { await TAURI_INVOKE("set_pet_menu_state", { id, open }); }, +async getUserActiveDoll() : Promise { + return await TAURI_INVOKE("get_user_active_doll"); +}, +async getSceneFriends() : Promise { + return await TAURI_INVOKE("get_scene_friends"); +}, async login(email: string, password: string) : Promise { return await TAURI_INVOKE("login", { email, password }); }, @@ -118,6 +124,9 @@ async sendInteractionCmd(dto: SendInteractionDto) : Promise { }, async getModules() : Promise { return await TAURI_INVOKE("get_modules"); +}, +async getPresenceState() : Promise { + return await TAURI_INVOKE("get_presence_state"); } } @@ -138,9 +147,12 @@ friendRequestReceived: FriendRequestReceived, friendUserStatusChanged: FriendUserStatusChanged, interactionDeliveryFailed: InteractionDeliveryFailed, interactionReceived: InteractionReceived, +presenceStateUpdated: PresenceStateUpdated, +sceneFriendsUpdated: SceneFriendsUpdated, sceneInteractiveChanged: SceneInteractiveChanged, setInteractionOverlay: SetInteractionOverlay, unfriended: Unfriended, +userActiveDollUpdated: UserActiveDollUpdated, userStatusChanged: UserStatusChanged }>({ appDataRefreshed: "app-data-refreshed", @@ -156,9 +168,12 @@ friendRequestReceived: "friend-request-received", friendUserStatusChanged: "friend-user-status-changed", interactionDeliveryFailed: "interaction-delivery-failed", interactionReceived: "interaction-received", +presenceStateUpdated: "presence-state-updated", +sceneFriendsUpdated: "scene-friends-updated", sceneInteractiveChanged: "scene-interactive-changed", setInteractionOverlay: "set-interaction-overlay", unfriended: "unfriended", +userActiveDollUpdated: "user-active-doll-updated", userStatusChanged: "user-status-changed" }) @@ -204,8 +219,12 @@ export type ModuleMetadata = { id: string; name: string; version: string; descri * Outgoing friend cursor position to frontend */ export type OutgoingFriendCursorPayload = { userId: string; position: CursorPositions } +export type PresenceStateSnapshot = { current: UserStatusPayload | null; friends: Partial<{ [key in string]: UserStatusPayload }> } +export type PresenceStateUpdated = PresenceStateSnapshot export type PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null } export type SceneData = { display: DisplayData; grid_size: number } +export type SceneFriendNeko = { id: string; position: CursorPositions; activeDoll: DollDto } +export type SceneFriendsUpdated = SceneFriendNeko[] export type SceneInteractiveChanged = boolean export type SendFriendRequestDto = { receiverId: string } export type SendInteractionDto = { recipientUserId: string; content: string; type: string } @@ -213,6 +232,7 @@ export type SetInteractionOverlay = boolean export type Unfriended = UnfriendedPayload export type UnfriendedPayload = { friendId: string } export type UpdateDollDto = { name: string | null; configuration: DollConfigurationDto | null } +export type UserActiveDollUpdated = DollDto | null export type UserBasicDto = { id: string; name: string; username: string | null; activeDoll: DollDto | null } export type UserData = { user: UserProfile | null; friends: FriendshipResponseDto[] | null; dolls: DollDto[] | null; scene: SceneData } export type UserProfile = { id: string; name: string; email: string; username: string | null; roles: string[]; createdAt: string; updatedAt: string; lastLoginAt: string | null; activeDollId: string | null } diff --git a/src/lib/scene.ts b/src/lib/scene.ts new file mode 100644 index 0000000..22f2a14 --- /dev/null +++ b/src/lib/scene.ts @@ -0,0 +1,15 @@ +import type { DollDto } from "$lib/bindings"; +import type { RecolorOptions } from "$lib/utils/sprite-utils"; + +export function getSpriteOptions( + doll: DollDto | null | undefined, +): RecolorOptions | undefined { + if (!doll) { + return undefined; + } + + return { + bodyColor: doll.configuration.colorScheme.body, + outlineColor: doll.configuration.colorScheme.outline, + }; +} diff --git a/src/lib/utils/sprite-utils.ts b/src/lib/utils/sprite-utils.ts index e2aff2e..d49479b 100644 --- a/src/lib/utils/sprite-utils.ts +++ b/src/lib/utils/sprite-utils.ts @@ -1,4 +1,4 @@ -import { commands } from "$lib/bindings"; +import { commands, type DollColorSchemeDto } from "$lib/bindings"; import onekoGif from "../../assets/oneko/oneko.gif"; export interface RecolorOptions { @@ -8,17 +8,17 @@ export interface RecolorOptions { } export async function getSpriteSheetUrl( - options?: RecolorOptions, + options?: DollColorSchemeDto, ): Promise { - if (!options || !options.bodyColor || !options.outlineColor) { + if (!options || !options.body || !options.outline) { return onekoGif; } try { const result = await commands.recolorGifBase64( - options.bodyColor, - options.outlineColor, - options.applyTexture ?? true, + 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) { diff --git a/src/routes/app-menu/components/doll-preview.svelte b/src/routes/app-menu/components/doll-preview.svelte index c9f260e..1dbf78b 100644 --- a/src/routes/app-menu/components/doll-preview.svelte +++ b/src/routes/app-menu/components/doll-preview.svelte @@ -3,10 +3,9 @@ import { SPRITE_SETS, SPRITE_SIZE } from "$lib/constants/pet-sprites"; import { getSpriteSheetUrl } from "$lib/utils/sprite-utils"; import PetSprite from "$lib/components/PetSprite.svelte"; + import type { DollColorSchemeDto } from "$lib/bindings"; - export let bodyColor: string; - export let outlineColor: string; - export let applyTexture: boolean = true; + export let dollColorScheme: DollColorSchemeDto; let previewBase64: string | null = null; let error: string | null = null; @@ -21,11 +20,7 @@ function generatePreview() { error = null; - getSpriteSheetUrl({ - bodyColor, - outlineColor, - applyTexture, - }) + getSpriteSheetUrl(dollColorScheme) .then((url: string) => { previewBase64 = url; }) @@ -70,7 +65,7 @@ }, 3000); } - $: if (bodyColor && outlineColor) { + $: if (dollColorScheme) { debouncedGeneratePreview(); } @@ -103,7 +98,10 @@ /> {:else} -
+
{/if} diff --git a/src/routes/app-menu/tabs/your-dolls/dolls-list.svelte b/src/routes/app-menu/tabs/your-dolls/dolls-list.svelte index 27fa848..f8f6ebb 100644 --- a/src/routes/app-menu/tabs/your-dolls/dolls-list.svelte +++ b/src/routes/app-menu/tabs/your-dolls/dolls-list.svelte @@ -43,10 +43,7 @@ class="flex flex-col w-full text-center py-6 gap-2 *:mx-auto hover:opacity-70 hover:cursor-pointer" >
- +

- +