minor consolidation & handling of data from frontend to rust side
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
lock_r,
|
lock_r,
|
||||||
models::app_data::UserData,
|
models::app_data::UserData,
|
||||||
services::presence_modules::models::ModuleMetadata,
|
services::{presence_modules::models::ModuleMetadata, presence_state::PresenceStateSnapshot},
|
||||||
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,3 +26,9 @@ pub fn get_modules() -> Result<Vec<ModuleMetadata>, String> {
|
|||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
Ok(guard.modules.metadatas.clone())
|
Ok(guard.modules.metadatas.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn get_presence_state() -> Result<PresenceStateSnapshot, String> {
|
||||||
|
Ok(crate::services::presence_state::get_presence_state_snapshot())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
get_app_handle,
|
||||||
models::dolls::{CreateDollDto, DollDto, UpdateDollDto},
|
models::dolls::{CreateDollDto, DollDto, UpdateDollDto},
|
||||||
remotes::{
|
remotes::{
|
||||||
dolls::DollsRemote,
|
dolls::DollsRemote,
|
||||||
@@ -7,6 +8,9 @@ use crate::{
|
|||||||
state::AppDataRefreshScope,
|
state::AppDataRefreshScope,
|
||||||
commands::{refresh_app_data, refresh_app_data_conditionally, is_active_doll},
|
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]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[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;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,5 +107,8 @@ pub async fn remove_active_doll() -> Result<(), String> {
|
|||||||
|
|
||||||
refresh_app_data(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]).await;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ pub mod friends;
|
|||||||
pub mod interaction;
|
pub mod interaction;
|
||||||
pub mod sprite;
|
pub mod sprite;
|
||||||
pub mod petpet;
|
pub mod petpet;
|
||||||
|
pub mod scene;
|
||||||
|
|
||||||
use crate::lock_r;
|
use crate::lock_r;
|
||||||
use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL};
|
use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL};
|
||||||
|
|||||||
32
src-tauri/src/commands/scene.rs
Normal file
32
src-tauri/src/commands/scene.rs
Normal file
@@ -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<Option<DollDto>, 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<Vec<SceneFriendNeko>, String> {
|
||||||
|
Ok(crate::services::scene_friends::get_scene_friends_snapshot())
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
commands::app_state::get_modules,
|
commands::app_state::{get_modules, get_presence_state},
|
||||||
services::{
|
services::{
|
||||||
doll_editor::open_doll_editor_window,
|
doll_editor::open_doll_editor_window,
|
||||||
scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive},
|
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::{quit_app, restart_app, retry_connection};
|
||||||
use commands::app_state::{get_app_data, refresh_app_data};
|
use commands::app_state::{get_app_data, refresh_app_data};
|
||||||
use commands::auth::{change_password, login, logout_and_restart, register, reset_password};
|
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::{
|
use crate::services::app_events::{
|
||||||
AppDataRefreshed, CreateDoll, CursorMoved, EditDoll, FriendActiveDollChanged,
|
AppDataRefreshed, CreateDoll, CursorMoved, EditDoll, FriendActiveDollChanged,
|
||||||
FriendCursorPositionUpdated, FriendDisconnected, FriendRequestAccepted,
|
FriendCursorPositionUpdated, FriendDisconnected, FriendRequestAccepted,
|
||||||
FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, PresenceStateUpdated,
|
||||||
|
SceneFriendsUpdated,
|
||||||
InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged,
|
InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged,
|
||||||
SetInteractionOverlay, Unfriended, UserStatusChanged,
|
SetInteractionOverlay, Unfriended, UserActiveDollUpdated, UserStatusChanged,
|
||||||
};
|
};
|
||||||
|
|
||||||
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
||||||
@@ -93,13 +95,16 @@ pub fn run() {
|
|||||||
get_scene_interactive,
|
get_scene_interactive,
|
||||||
set_scene_interactive,
|
set_scene_interactive,
|
||||||
set_pet_menu_state,
|
set_pet_menu_state,
|
||||||
|
get_user_active_doll,
|
||||||
|
get_scene_friends,
|
||||||
login,
|
login,
|
||||||
register,
|
register,
|
||||||
change_password,
|
change_password,
|
||||||
reset_password,
|
reset_password,
|
||||||
logout_and_restart,
|
logout_and_restart,
|
||||||
send_interaction_cmd,
|
send_interaction_cmd,
|
||||||
get_modules
|
get_modules,
|
||||||
|
get_presence_state
|
||||||
])
|
])
|
||||||
.events(collect_events![
|
.events(collect_events![
|
||||||
CursorMoved,
|
CursorMoved,
|
||||||
@@ -109,10 +114,13 @@ pub fn run() {
|
|||||||
EditDoll,
|
EditDoll,
|
||||||
CreateDoll,
|
CreateDoll,
|
||||||
UserStatusChanged,
|
UserStatusChanged,
|
||||||
|
UserActiveDollUpdated,
|
||||||
FriendCursorPositionUpdated,
|
FriendCursorPositionUpdated,
|
||||||
FriendDisconnected,
|
FriendDisconnected,
|
||||||
FriendActiveDollChanged,
|
FriendActiveDollChanged,
|
||||||
FriendUserStatusChanged,
|
FriendUserStatusChanged,
|
||||||
|
PresenceStateUpdated,
|
||||||
|
SceneFriendsUpdated,
|
||||||
InteractionReceived,
|
InteractionReceived,
|
||||||
InteractionDeliveryFailed,
|
InteractionDeliveryFailed,
|
||||||
FriendRequestReceived,
|
FriendRequestReceived,
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ pub mod friends;
|
|||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod interaction;
|
pub mod interaction;
|
||||||
pub mod remote_error;
|
pub mod remote_error;
|
||||||
|
pub mod scene;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
12
src-tauri/src/models/scene.rs
Normal file
12
src-tauri/src/models/scene.rs
Normal file
@@ -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,
|
||||||
|
}
|
||||||
@@ -5,14 +5,19 @@ use tauri_specta::Event;
|
|||||||
use crate::{
|
use crate::{
|
||||||
models::{
|
models::{
|
||||||
app_data::UserData,
|
app_data::UserData,
|
||||||
|
dolls::DollDto,
|
||||||
event_payloads::{
|
event_payloads::{
|
||||||
FriendActiveDollChangedPayload, FriendDisconnectedPayload,
|
FriendActiveDollChangedPayload, FriendDisconnectedPayload,
|
||||||
FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload,
|
FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload,
|
||||||
FriendUserStatusPayload, UnfriendedPayload, UserStatusPayload,
|
FriendUserStatusPayload, UnfriendedPayload, UserStatusPayload,
|
||||||
},
|
},
|
||||||
interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
@@ -43,6 +48,10 @@ pub struct CreateDoll;
|
|||||||
#[tauri_specta(event_name = "user-status-changed")]
|
#[tauri_specta(event_name = "user-status-changed")]
|
||||||
pub struct UserStatusChanged(pub UserStatusPayload);
|
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<DollDto>);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "friend-cursor-position")]
|
#[tauri_specta(event_name = "friend-cursor-position")]
|
||||||
pub struct FriendCursorPositionUpdated(pub OutgoingFriendCursorPayload);
|
pub struct FriendCursorPositionUpdated(pub OutgoingFriendCursorPayload);
|
||||||
@@ -59,6 +68,14 @@ pub struct FriendActiveDollChanged(pub FriendActiveDollChangedPayload);
|
|||||||
#[tauri_specta(event_name = "friend-user-status")]
|
#[tauri_specta(event_name = "friend-user-status")]
|
||||||
pub struct FriendUserStatusChanged(pub FriendUserStatusPayload);
|
pub struct FriendUserStatusChanged(pub FriendUserStatusPayload);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
|
#[tauri_specta(event_name = "scene-friends-updated")]
|
||||||
|
pub struct SceneFriendsUpdated(pub Vec<SceneFriendNeko>);
|
||||||
|
|
||||||
|
#[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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "interaction-received")]
|
#[tauri_specta(event_name = "interaction-received")]
|
||||||
pub struct InteractionReceived(pub InteractionPayloadDto);
|
pub struct InteractionReceived(pub InteractionPayloadDto);
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ pub mod health_manager;
|
|||||||
pub mod health_monitor;
|
pub mod health_monitor;
|
||||||
pub mod interaction;
|
pub mod interaction;
|
||||||
pub mod petpet;
|
pub mod petpet;
|
||||||
|
pub mod presence_state;
|
||||||
pub mod presence_modules;
|
pub mod presence_modules;
|
||||||
pub mod scene;
|
pub mod scene;
|
||||||
|
pub mod scene_friends;
|
||||||
pub mod sprite_recolor;
|
pub mod sprite_recolor;
|
||||||
pub mod welcome;
|
pub mod welcome;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|||||||
71
src-tauri/src/services/presence_state.rs
Normal file
71
src-tauri/src/services/presence_state.rs
Normal file
@@ -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<UserStatusPayload>,
|
||||||
|
pub friends: std::collections::HashMap<String, UserStatusPayload>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<std::collections::HashSet<_>>()
|
||||||
|
})
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src-tauri/src/services/scene_friends.rs
Normal file
91
src-tauri/src/services/scene_friends.rs
Normal file
@@ -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<SceneFriendNeko> {
|
||||||
|
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<DollDto>) {
|
||||||
|
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::<std::collections::HashSet<_>>()
|
||||||
|
})
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,10 @@ use crate::services::app_events::{
|
|||||||
FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
||||||
Unfriended,
|
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 crate::state::AppDataRefreshScope;
|
||||||
|
|
||||||
use super::{
|
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));
|
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) =
|
if let Ok(data) =
|
||||||
utils::extract_and_parse::<FriendDisconnectedPayload>(payload, "friend-disconnected")
|
utils::extract_and_parse::<FriendDisconnectedPayload>(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));
|
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,
|
payload,
|
||||||
"friend-active-doll-changed",
|
"friend-active-doll-changed",
|
||||||
) {
|
) {
|
||||||
|
scene_friends::set_friend_active_doll(data.friend_id.clone(), data.doll.clone());
|
||||||
emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data));
|
emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data));
|
||||||
|
scene_friends::emit_scene_friends_updated();
|
||||||
refresh::refresh_app_data(AppDataRefreshScope::Friends);
|
refresh::refresh_app_data(AppDataRefreshScope::Friends);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +139,8 @@ pub fn on_friend_user_status(payload: Payload, _socket: RawClient) {
|
|||||||
if let Ok(data) =
|
if let Ok(data) =
|
||||||
utils::extract_and_parse::<FriendUserStatusPayload>(payload, "friend-user-status")
|
utils::extract_and_parse::<FriendUserStatusPayload>(payload, "friend-user-status")
|
||||||
{
|
{
|
||||||
|
presence_state::set_friend_presence(data.user_id.clone(), data.status.clone());
|
||||||
emitter::emit_to_frontend_typed(&FriendUserStatusChanged(data));
|
emitter::emit_to_frontend_typed(&FriendUserStatusChanged(data));
|
||||||
|
presence_state::emit_presence_state_updated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use tracing::warn;
|
|||||||
use crate::models::event_payloads::UserStatusPayload;
|
use crate::models::event_payloads::UserStatusPayload;
|
||||||
|
|
||||||
use crate::services::app_events::UserStatusChanged;
|
use crate::services::app_events::UserStatusChanged;
|
||||||
|
use crate::services::presence_state;
|
||||||
|
|
||||||
use super::{emitter, types::WS_EVENT};
|
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}");
|
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
|
// Schedule new report after 500ms
|
||||||
let handle = async_runtime::spawn(async move {
|
let handle = async_runtime::spawn(async move {
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
// in app-core/src/state.rs
|
// in app-core/src/state.rs
|
||||||
use crate::{
|
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 std::sync::{Arc, LazyLock, RwLock};
|
||||||
use tauri::tray::TrayIcon;
|
use tauri::tray::TrayIcon;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -20,12 +26,26 @@ pub struct Modules {
|
|||||||
pub metadatas: Vec<ModuleMetadata>,
|
pub metadatas: Vec<ModuleMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct FriendSceneRuntimeState {
|
||||||
|
pub cursor_positions: HashMap<String, CursorPositions>,
|
||||||
|
pub active_dolls: HashMap<String, Option<DollDto>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct PresenceRuntimeState {
|
||||||
|
pub current: Option<crate::models::event_payloads::UserStatusPayload>,
|
||||||
|
pub friends: HashMap<String, crate::models::event_payloads::UserStatusPayload>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub app_config: crate::services::client_config_manager::AppConfig,
|
pub app_config: crate::services::client_config_manager::AppConfig,
|
||||||
pub network: NetworkState,
|
pub network: NetworkState,
|
||||||
pub auth: AuthState,
|
pub auth: AuthState,
|
||||||
pub user_data: UserData,
|
pub user_data: UserData,
|
||||||
|
pub friend_scene: FriendSceneRuntimeState,
|
||||||
|
pub presence: PresenceRuntimeState,
|
||||||
pub tray: Option<TrayIcon>,
|
pub tray: Option<TrayIcon>,
|
||||||
pub modules: Modules,
|
pub modules: Modules,
|
||||||
}
|
}
|
||||||
@@ -45,6 +65,8 @@ pub fn init_app_state() {
|
|||||||
guard.network = init_network_state();
|
guard.network = init_network_state();
|
||||||
guard.auth = init_auth_state();
|
guard.auth = init_auth_state();
|
||||||
guard.user_data = UserData::default();
|
guard.user_data = UserData::default();
|
||||||
|
guard.friend_scene = FriendSceneRuntimeState::default();
|
||||||
|
guard.presence = PresenceRuntimeState::default();
|
||||||
guard.modules = Modules::default();
|
guard.modules = Modules::default();
|
||||||
}
|
}
|
||||||
update_display_dimensions_for_scene_state();
|
update_display_dimensions_for_scene_state();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle, lock_r, lock_w,
|
get_app_handle, lock_r, lock_w,
|
||||||
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
||||||
services::app_events::AppDataRefreshed,
|
services::{app_events::AppDataRefreshed, presence_state, scene_friends},
|
||||||
state::FDOLL,
|
state::FDOLL,
|
||||||
};
|
};
|
||||||
use std::{collections::HashSet, sync::LazyLock};
|
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();
|
let app_data_clone = guard.user_data.clone();
|
||||||
drop(guard); // Drop lock before emitting to prevent potential deadlocks
|
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()) {
|
if let Err(e) = AppDataRefreshed(app_data_clone).emit(get_app_handle()) {
|
||||||
warn!("Failed to emit app-data-refreshed event: {}", e);
|
warn!("Failed to emit app-data-refreshed event: {}", e);
|
||||||
use tauri_plugin_dialog::MessageDialogBuilder;
|
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.",
|
"Could not broadcast refreshed data to the UI. Some data may be stale.",
|
||||||
)
|
)
|
||||||
.kind(MessageDialogKind::Error)
|
.kind(MessageDialogKind::Error)
|
||||||
.show(|_| {});
|
.show(|_| {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scene_friends::emit_scene_friends_updated();
|
||||||
|
presence_state::emit_presence_state_updated();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -260,4 +266,8 @@ pub fn clear_app_data() {
|
|||||||
guard.user_data.dolls = None;
|
guard.user_data.dolls = None;
|
||||||
guard.user_data.user = None;
|
guard.user_data.user = None;
|
||||||
guard.user_data.friends = 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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +1,16 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import {
|
import { commands, events, type SceneFriendNeko } from "$lib/bindings";
|
||||||
events,
|
import { createEventSource } from "./listener-utils";
|
||||||
type CursorPositions,
|
|
||||||
type DollDto,
|
|
||||||
type OutgoingFriendCursorPayload,
|
|
||||||
} from "$lib/bindings";
|
|
||||||
import { createEventSource, removeFromStore } from "./listener-utils";
|
|
||||||
|
|
||||||
type FriendCursorData = {
|
export const sceneFriends = writable<SceneFriendNeko[]>([]);
|
||||||
position: CursorPositions;
|
|
||||||
lastUpdated: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const friendsCursorPositions = writable<Record<string, CursorPositions>>(
|
export const { start: startFriendCursorTracking, stop: stopFriendCursorTracking } =
|
||||||
{},
|
createEventSource(async (addEventListener) => {
|
||||||
);
|
sceneFriends.set(await commands.getSceneFriends());
|
||||||
export const friendsActiveDolls = writable<Record<string, DollDto | null>>({});
|
|
||||||
|
|
||||||
export const {
|
addEventListener(
|
||||||
start: startFriendCursorTracking,
|
await events.sceneFriendsUpdated.listen((event) => {
|
||||||
stop: stopFriendCursorTracking,
|
sceneFriends.set(event.payload);
|
||||||
} = createEventSource(async (addEventListener) => {
|
}),
|
||||||
let friendCursorState: Record<string, FriendCursorData> = {};
|
);
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,44 +1,32 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import { events, type UserStatusPayload } from "$lib/bindings";
|
import {
|
||||||
import { createEventSource, removeFromStore } from "./listener-utils";
|
commands,
|
||||||
|
events,
|
||||||
|
type PresenceStateSnapshot,
|
||||||
|
type UserStatusPayload,
|
||||||
|
} from "$lib/bindings";
|
||||||
|
import { createEventSource } from "./listener-utils";
|
||||||
|
|
||||||
export const friendsPresenceStates = writable<
|
export const friendsPresenceStates = writable<Record<string, UserStatusPayload>>({});
|
||||||
Record<string, UserStatusPayload>
|
|
||||||
>({});
|
|
||||||
export const currentPresenceState = writable<UserStatusPayload | null>(null);
|
export const currentPresenceState = writable<UserStatusPayload | null>(null);
|
||||||
|
|
||||||
|
function applyPresenceSnapshot(snapshot: PresenceStateSnapshot) {
|
||||||
|
currentPresenceState.set(snapshot.current);
|
||||||
|
|
||||||
|
const friends = Object.fromEntries(
|
||||||
|
Object.entries(snapshot.friends).filter(([, status]) => status !== undefined),
|
||||||
|
) as Record<string, UserStatusPayload>;
|
||||||
|
|
||||||
|
friendsPresenceStates.set(friends);
|
||||||
|
}
|
||||||
|
|
||||||
export const { start: startUserStatus, stop: stopUserStatus } =
|
export const { start: startUserStatus, stop: stopUserStatus } =
|
||||||
createEventSource(async (addEventListener) => {
|
createEventSource(async (addEventListener) => {
|
||||||
addEventListener(
|
applyPresenceSnapshot(await commands.getPresenceState());
|
||||||
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,
|
|
||||||
}));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
addEventListener(
|
addEventListener(
|
||||||
await events.userStatusChanged.listen((event) => {
|
await events.presenceStateUpdated.listen((event) => {
|
||||||
currentPresenceState.set(event.payload);
|
applyPresenceSnapshot(event.payload);
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
addEventListener(
|
|
||||||
await events.friendDisconnected.listen((event) => {
|
|
||||||
const { userId } = event.payload;
|
|
||||||
friendsPresenceStates.update((current) =>
|
|
||||||
removeFromStore(current, userId),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -98,6 +98,12 @@ async setSceneInteractive(interactive: boolean, shouldClick: boolean) : Promise<
|
|||||||
async setPetMenuState(id: string, open: boolean) : Promise<void> {
|
async setPetMenuState(id: string, open: boolean) : Promise<void> {
|
||||||
await TAURI_INVOKE("set_pet_menu_state", { id, open });
|
await TAURI_INVOKE("set_pet_menu_state", { id, open });
|
||||||
},
|
},
|
||||||
|
async getUserActiveDoll() : Promise<DollDto | null> {
|
||||||
|
return await TAURI_INVOKE("get_user_active_doll");
|
||||||
|
},
|
||||||
|
async getSceneFriends() : Promise<SceneFriendNeko[]> {
|
||||||
|
return await TAURI_INVOKE("get_scene_friends");
|
||||||
|
},
|
||||||
async login(email: string, password: string) : Promise<null> {
|
async login(email: string, password: string) : Promise<null> {
|
||||||
return await TAURI_INVOKE("login", { email, password });
|
return await TAURI_INVOKE("login", { email, password });
|
||||||
},
|
},
|
||||||
@@ -118,6 +124,9 @@ async sendInteractionCmd(dto: SendInteractionDto) : Promise<null> {
|
|||||||
},
|
},
|
||||||
async getModules() : Promise<ModuleMetadata[]> {
|
async getModules() : Promise<ModuleMetadata[]> {
|
||||||
return await TAURI_INVOKE("get_modules");
|
return await TAURI_INVOKE("get_modules");
|
||||||
|
},
|
||||||
|
async getPresenceState() : Promise<PresenceStateSnapshot> {
|
||||||
|
return await TAURI_INVOKE("get_presence_state");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,9 +147,12 @@ friendRequestReceived: FriendRequestReceived,
|
|||||||
friendUserStatusChanged: FriendUserStatusChanged,
|
friendUserStatusChanged: FriendUserStatusChanged,
|
||||||
interactionDeliveryFailed: InteractionDeliveryFailed,
|
interactionDeliveryFailed: InteractionDeliveryFailed,
|
||||||
interactionReceived: InteractionReceived,
|
interactionReceived: InteractionReceived,
|
||||||
|
presenceStateUpdated: PresenceStateUpdated,
|
||||||
|
sceneFriendsUpdated: SceneFriendsUpdated,
|
||||||
sceneInteractiveChanged: SceneInteractiveChanged,
|
sceneInteractiveChanged: SceneInteractiveChanged,
|
||||||
setInteractionOverlay: SetInteractionOverlay,
|
setInteractionOverlay: SetInteractionOverlay,
|
||||||
unfriended: Unfriended,
|
unfriended: Unfriended,
|
||||||
|
userActiveDollUpdated: UserActiveDollUpdated,
|
||||||
userStatusChanged: UserStatusChanged
|
userStatusChanged: UserStatusChanged
|
||||||
}>({
|
}>({
|
||||||
appDataRefreshed: "app-data-refreshed",
|
appDataRefreshed: "app-data-refreshed",
|
||||||
@@ -156,9 +168,12 @@ friendRequestReceived: "friend-request-received",
|
|||||||
friendUserStatusChanged: "friend-user-status-changed",
|
friendUserStatusChanged: "friend-user-status-changed",
|
||||||
interactionDeliveryFailed: "interaction-delivery-failed",
|
interactionDeliveryFailed: "interaction-delivery-failed",
|
||||||
interactionReceived: "interaction-received",
|
interactionReceived: "interaction-received",
|
||||||
|
presenceStateUpdated: "presence-state-updated",
|
||||||
|
sceneFriendsUpdated: "scene-friends-updated",
|
||||||
sceneInteractiveChanged: "scene-interactive-changed",
|
sceneInteractiveChanged: "scene-interactive-changed",
|
||||||
setInteractionOverlay: "set-interaction-overlay",
|
setInteractionOverlay: "set-interaction-overlay",
|
||||||
unfriended: "unfriended",
|
unfriended: "unfriended",
|
||||||
|
userActiveDollUpdated: "user-active-doll-updated",
|
||||||
userStatusChanged: "user-status-changed"
|
userStatusChanged: "user-status-changed"
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -204,8 +219,12 @@ export type ModuleMetadata = { id: string; name: string; version: string; descri
|
|||||||
* Outgoing friend cursor position to frontend
|
* Outgoing friend cursor position to frontend
|
||||||
*/
|
*/
|
||||||
export type OutgoingFriendCursorPayload = { userId: string; position: CursorPositions }
|
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 PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null }
|
||||||
export type SceneData = { display: DisplayData; grid_size: number }
|
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 SceneInteractiveChanged = boolean
|
||||||
export type SendFriendRequestDto = { receiverId: string }
|
export type SendFriendRequestDto = { receiverId: string }
|
||||||
export type SendInteractionDto = { recipientUserId: string; content: string; type: string }
|
export type SendInteractionDto = { recipientUserId: string; content: string; type: string }
|
||||||
@@ -213,6 +232,7 @@ export type SetInteractionOverlay = boolean
|
|||||||
export type Unfriended = UnfriendedPayload
|
export type Unfriended = UnfriendedPayload
|
||||||
export type UnfriendedPayload = { friendId: string }
|
export type UnfriendedPayload = { friendId: string }
|
||||||
export type UpdateDollDto = { name: string | null; configuration: DollConfigurationDto | null }
|
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 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 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 }
|
export type UserProfile = { id: string; name: string; email: string; username: string | null; roles: string[]; createdAt: string; updatedAt: string; lastLoginAt: string | null; activeDollId: string | null }
|
||||||
|
|||||||
15
src/lib/scene.ts
Normal file
15
src/lib/scene.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { commands } from "$lib/bindings";
|
import { commands, type DollColorSchemeDto } from "$lib/bindings";
|
||||||
import onekoGif from "../../assets/oneko/oneko.gif";
|
import onekoGif from "../../assets/oneko/oneko.gif";
|
||||||
|
|
||||||
export interface RecolorOptions {
|
export interface RecolorOptions {
|
||||||
@@ -8,17 +8,17 @@ export interface RecolorOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getSpriteSheetUrl(
|
export async function getSpriteSheetUrl(
|
||||||
options?: RecolorOptions,
|
options?: DollColorSchemeDto,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!options || !options.bodyColor || !options.outlineColor) {
|
if (!options || !options.body || !options.outline) {
|
||||||
return onekoGif;
|
return onekoGif;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await commands.recolorGifBase64(
|
const result = await commands.recolorGifBase64(
|
||||||
options.bodyColor,
|
options.body,
|
||||||
options.outlineColor,
|
options.outline,
|
||||||
options.applyTexture ?? true,
|
true, // default texture to true at this stage, maybe one day open up more customization options
|
||||||
);
|
);
|
||||||
return `data:image/gif;base64,${result}`;
|
return `data:image/gif;base64,${result}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
import { SPRITE_SETS, SPRITE_SIZE } from "$lib/constants/pet-sprites";
|
import { SPRITE_SETS, SPRITE_SIZE } from "$lib/constants/pet-sprites";
|
||||||
import { getSpriteSheetUrl } from "$lib/utils/sprite-utils";
|
import { getSpriteSheetUrl } from "$lib/utils/sprite-utils";
|
||||||
import PetSprite from "$lib/components/PetSprite.svelte";
|
import PetSprite from "$lib/components/PetSprite.svelte";
|
||||||
|
import type { DollColorSchemeDto } from "$lib/bindings";
|
||||||
|
|
||||||
export let bodyColor: string;
|
export let dollColorScheme: DollColorSchemeDto;
|
||||||
export let outlineColor: string;
|
|
||||||
export let applyTexture: boolean = true;
|
|
||||||
|
|
||||||
let previewBase64: string | null = null;
|
let previewBase64: string | null = null;
|
||||||
let error: string | null = null;
|
let error: string | null = null;
|
||||||
@@ -21,11 +20,7 @@
|
|||||||
|
|
||||||
function generatePreview() {
|
function generatePreview() {
|
||||||
error = null;
|
error = null;
|
||||||
getSpriteSheetUrl({
|
getSpriteSheetUrl(dollColorScheme)
|
||||||
bodyColor,
|
|
||||||
outlineColor,
|
|
||||||
applyTexture,
|
|
||||||
})
|
|
||||||
.then((url: string) => {
|
.then((url: string) => {
|
||||||
previewBase64 = url;
|
previewBase64 = url;
|
||||||
})
|
})
|
||||||
@@ -70,7 +65,7 @@
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (bodyColor && outlineColor) {
|
$: if (dollColorScheme) {
|
||||||
debouncedGeneratePreview();
|
debouncedGeneratePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +98,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="size-full skeleton" style:background-color={bodyColor}></div>
|
<div
|
||||||
|
class="size-full skeleton"
|
||||||
|
style:background-color={dollColorScheme.body}
|
||||||
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,10 +43,7 @@
|
|||||||
class="flex flex-col w-full text-center py-6 gap-2 *:mx-auto hover:opacity-70 hover:cursor-pointer"
|
class="flex flex-col w-full text-center py-6 gap-2 *:mx-auto hover:opacity-70 hover:cursor-pointer"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<DollPreview
|
<DollPreview dollColorScheme={doll.configuration.colorScheme} />
|
||||||
bodyColor={doll.configuration.colorScheme.body}
|
|
||||||
outlineColor={doll.configuration.colorScheme.outline}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
style:background-color={doll.configuration.colorScheme.body}
|
style:background-color={doll.configuration.colorScheme.body}
|
||||||
|
|||||||
@@ -108,7 +108,9 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="h-full w-full p-4 gap-4 flex flex-col">
|
<div class="h-full w-full p-4 gap-4 flex flex-col">
|
||||||
<div class="flex justify-center mt-4">
|
<div class="flex justify-center mt-4">
|
||||||
<DollPreview {bodyColor} {outlineColor} />
|
<DollPreview
|
||||||
|
dollColorScheme={{ body: bodyColor, outline: outlineColor }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control w-full">
|
<div class="form-control w-full">
|
||||||
<label class="label" for="name-input">
|
<label class="label" for="name-input">
|
||||||
|
|||||||
@@ -1,22 +1,56 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cursorPositionOnScreen } from "../../events/cursor";
|
import { cursorPositionOnScreen } from "../../events/cursor";
|
||||||
import { friendsCursorPositions } from "../../events/friend-cursor";
|
import { sceneFriends } from "../../events/friend-cursor";
|
||||||
import { appData } from "../../events/app-data";
|
import { appData } from "../../events/app-data";
|
||||||
import { sceneInteractive } from "../../events/scene-interactive";
|
import { sceneInteractive } from "../../events/scene-interactive";
|
||||||
import {
|
import {
|
||||||
friendsPresenceStates,
|
friendsPresenceStates,
|
||||||
currentPresenceState,
|
currentPresenceState,
|
||||||
} from "../../events/user-status";
|
} from "../../events/user-status";
|
||||||
import { commands } from "$lib/bindings";
|
import { commands, events, type SceneFriendNeko } from "$lib/bindings";
|
||||||
import { getSpriteSheetUrl } from "$lib/utils/sprite-utils";
|
import { getSpriteSheetUrl } from "$lib/utils/sprite-utils";
|
||||||
import DebugBar from "./components/debug-bar.svelte";
|
import DebugBar from "./components/debug-bar.svelte";
|
||||||
import Neko from "./components/neko/neko.svelte";
|
import Neko from "./components/neko/neko.svelte";
|
||||||
|
|
||||||
let spriteUrl = $state("");
|
let userSpriteUrl = $state("");
|
||||||
|
let friendSpriteUrls = $state<Record<string, string>>({});
|
||||||
|
|
||||||
|
async function fetchUserSprite() {
|
||||||
|
try {
|
||||||
|
const doll = await commands.getUserActiveDoll();
|
||||||
|
const url = await getSpriteSheetUrl(doll?.configuration.colorScheme);
|
||||||
|
userSpriteUrl = url;
|
||||||
|
} catch {
|
||||||
|
const url = await getSpriteSheetUrl();
|
||||||
|
userSpriteUrl = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
getSpriteSheetUrl().then((url) => {
|
fetchUserSprite();
|
||||||
spriteUrl = url;
|
|
||||||
|
events.userActiveDollUpdated.listen(() => {
|
||||||
|
fetchUserSprite();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const friends = $sceneFriends;
|
||||||
|
|
||||||
|
if (friends.length === 0) {
|
||||||
|
friendSpriteUrls = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
friends.map(async (friend: SceneFriendNeko) => {
|
||||||
|
return [
|
||||||
|
friend.id,
|
||||||
|
await getSpriteSheetUrl(friend.activeDoll.configuration.colorScheme),
|
||||||
|
] as const;
|
||||||
|
}),
|
||||||
|
).then((entries) => {
|
||||||
|
friendSpriteUrls = Object.fromEntries(entries);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -32,14 +66,23 @@
|
|||||||
<Neko
|
<Neko
|
||||||
targetX={$cursorPositionOnScreen.raw.x}
|
targetX={$cursorPositionOnScreen.raw.x}
|
||||||
targetY={$cursorPositionOnScreen.raw.y}
|
targetY={$cursorPositionOnScreen.raw.y}
|
||||||
{spriteUrl}
|
spriteUrl={userSpriteUrl}
|
||||||
/>
|
/>
|
||||||
|
{#each $sceneFriends as friend (friend.id)}
|
||||||
|
<Neko
|
||||||
|
targetX={friend.position.raw.x}
|
||||||
|
targetY={friend.position.raw.y}
|
||||||
|
spriteUrl={friendSpriteUrls[friend.id] ?? ""}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
<div id="debug-bar">
|
<div id="debug-bar">
|
||||||
<DebugBar
|
<DebugBar
|
||||||
isInteractive={$sceneInteractive}
|
isInteractive={$sceneInteractive}
|
||||||
cursorPosition={$cursorPositionOnScreen}
|
cursorPosition={$cursorPositionOnScreen}
|
||||||
presenceStatus={$currentPresenceState?.presenceStatus ?? null}
|
presenceStatus={$currentPresenceState?.presenceStatus ?? null}
|
||||||
friendsCursorPositions={$friendsCursorPositions}
|
friendsCursorPositions={Object.fromEntries(
|
||||||
|
$sceneFriends.map((friend) => [friend.id, friend.position]),
|
||||||
|
)}
|
||||||
friends={$appData?.friends ?? []}
|
friends={$appData?.friends ?? []}
|
||||||
friendsPresenceStates={$friendsPresenceStates}
|
friendsPresenceStates={$friendsPresenceStates}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PresenceStatus, UserStatusPayload } from "$lib/bindings";
|
import type {
|
||||||
|
CursorPositions,
|
||||||
|
PresenceStatus,
|
||||||
|
UserStatusPayload,
|
||||||
|
} from "$lib/bindings";
|
||||||
|
|
||||||
interface Friend {
|
interface Friend {
|
||||||
friend?: {
|
friend?: {
|
||||||
@@ -10,9 +14,9 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isInteractive: boolean;
|
isInteractive: boolean;
|
||||||
cursorPosition: { mapped: { x: number; y: number } };
|
cursorPosition: CursorPositions;
|
||||||
presenceStatus: PresenceStatus | null;
|
presenceStatus: PresenceStatus | null;
|
||||||
friendsCursorPositions: Record<string, { mapped: { x: number; y: number } }>;
|
friendsCursorPositions: Record<string, CursorPositions>;
|
||||||
friends: Friend[];
|
friends: Friend[];
|
||||||
friendsPresenceStates: Record<string, UserStatusPayload>;
|
friendsPresenceStates: Record<string, UserStatusPayload>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,9 @@
|
|||||||
<input
|
<input
|
||||||
class="input input-bordered input-sm"
|
class="input input-bordered input-sm"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete={useRegister ? "new-password" : "current-password"}
|
autocomplete={useRegister
|
||||||
|
? "new-password"
|
||||||
|
: "current-password"}
|
||||||
bind:value={form.password}
|
bind:value={form.password}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
@@ -146,7 +148,9 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{useRegister ? "Already have an account? Sign in" : "New here? Create an account"}
|
{useRegister
|
||||||
|
? "Already have an account? Sign in"
|
||||||
|
: "New here? Create an account"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-link p-0 btn-sm text-base-content w-max"
|
class="btn btn-link p-0 btn-sm text-base-content w-max"
|
||||||
@@ -175,7 +179,7 @@
|
|||||||
>
|
>
|
||||||
<div></div>
|
<div></div>
|
||||||
<div class="flex flex-col scale-200 origin-bottom-right">
|
<div class="flex flex-col scale-200 origin-bottom-right">
|
||||||
<DollPreview bodyColor="b7f2ff" outlineColor="496065" />
|
<DollPreview dollColorScheme={{ body: "b7f2ff", outline: "496065" }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user