minor cursor handling refactor improvemnts
This commit is contained in:
@@ -5,6 +5,7 @@ use crate::{
|
|||||||
app_data::{init_app_data_scoped, AppDataRefreshScope},
|
app_data::{init_app_data_scoped, AppDataRefreshScope},
|
||||||
app_state,
|
app_state,
|
||||||
friends,
|
friends,
|
||||||
|
neko_positions,
|
||||||
presence_modules::models::ModuleMetadata,
|
presence_modules::models::ModuleMetadata,
|
||||||
sprite,
|
sprite,
|
||||||
},
|
},
|
||||||
@@ -53,6 +54,12 @@ pub fn get_app_state() -> Result<AppState, String> {
|
|||||||
Ok(app_state::get_snapshot())
|
Ok(app_state::get_snapshot())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn get_neko_positions() -> Result<neko_positions::NekoPositionsDto, String> {
|
||||||
|
Ok(neko_positions::get_snapshot())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
|
pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::services::{
|
|||||||
};
|
};
|
||||||
use commands::app::{quit_app, restart_app, retry_connection};
|
use commands::app::{quit_app, restart_app, retry_connection};
|
||||||
use commands::app_state::{
|
use commands::app_state::{
|
||||||
get_active_doll_sprite_base64, get_app_data, get_app_state,
|
get_active_doll_sprite_base64, get_app_data, get_app_state, get_neko_positions,
|
||||||
get_friend_active_doll_sprites_base64, get_modules, refresh_app_data,
|
get_friend_active_doll_sprites_base64, get_modules, refresh_app_data,
|
||||||
set_scene_setup_nekos_opacity, set_scene_setup_nekos_position, set_scene_setup_nekos_scale,
|
set_scene_setup_nekos_opacity, set_scene_setup_nekos_position, set_scene_setup_nekos_scale,
|
||||||
};
|
};
|
||||||
@@ -26,11 +26,10 @@ use tauri_specta::{collect_commands, collect_events, Builder as SpectaBuilder, E
|
|||||||
|
|
||||||
use crate::services::app_events::{
|
use crate::services::app_events::{
|
||||||
ActiveDollSpriteChanged, AppDataRefreshed, AppStateChanged, AuthFlowUpdated, CreateDoll,
|
ActiveDollSpriteChanged, AppDataRefreshed, AppStateChanged, AuthFlowUpdated, CreateDoll,
|
||||||
CursorMoved, EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated,
|
EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated, FriendDisconnected,
|
||||||
FriendCursorPositionsUpdated, FriendDisconnected, FriendRequestAccepted,
|
FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
||||||
FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
InteractionDeliveryFailed, InteractionReceived, NekoPositionsUpdated,
|
||||||
InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged,
|
SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged,
|
||||||
SetInteractionOverlay, Unfriended, 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();
|
||||||
@@ -68,6 +67,7 @@ pub fn run() {
|
|||||||
.commands(collect_commands![
|
.commands(collect_commands![
|
||||||
get_app_data,
|
get_app_data,
|
||||||
get_app_state,
|
get_app_state,
|
||||||
|
get_neko_positions,
|
||||||
get_active_doll_sprite_base64,
|
get_active_doll_sprite_base64,
|
||||||
get_friend_active_doll_sprites_base64,
|
get_friend_active_doll_sprites_base64,
|
||||||
refresh_app_data,
|
refresh_app_data,
|
||||||
@@ -108,16 +108,15 @@ pub fn run() {
|
|||||||
set_scene_setup_nekos_scale
|
set_scene_setup_nekos_scale
|
||||||
])
|
])
|
||||||
.events(collect_events![
|
.events(collect_events![
|
||||||
CursorMoved,
|
|
||||||
SceneInteractiveChanged,
|
SceneInteractiveChanged,
|
||||||
AppDataRefreshed,
|
AppDataRefreshed,
|
||||||
AppStateChanged,
|
AppStateChanged,
|
||||||
|
NekoPositionsUpdated,
|
||||||
ActiveDollSpriteChanged,
|
ActiveDollSpriteChanged,
|
||||||
SetInteractionOverlay,
|
SetInteractionOverlay,
|
||||||
EditDoll,
|
EditDoll,
|
||||||
CreateDoll,
|
CreateDoll,
|
||||||
UserStatusChanged,
|
UserStatusChanged,
|
||||||
FriendCursorPositionsUpdated,
|
|
||||||
FriendDisconnected,
|
FriendDisconnected,
|
||||||
FriendActiveDollChanged,
|
FriendActiveDollChanged,
|
||||||
FriendActiveDollSpritesUpdated,
|
FriendActiveDollSpritesUpdated,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
||||||
services::{
|
services::{
|
||||||
app_events::{ActiveDollSpriteChanged, AppDataRefreshed},
|
app_events::{ActiveDollSpriteChanged, AppDataRefreshed},
|
||||||
friends, sprite,
|
friends, neko_positions, sprite,
|
||||||
},
|
},
|
||||||
state::FDOLL,
|
state::FDOLL,
|
||||||
};
|
};
|
||||||
@@ -51,6 +51,8 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
|
|||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
guard.user_data.user = Some(user);
|
guard.user_data.user = Some(user);
|
||||||
|
drop(guard);
|
||||||
|
neko_positions::sync_from_app_data();
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
warn!("Failed to fetch user profile: {}", error);
|
warn!("Failed to fetch user profile: {}", error);
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
||||||
},
|
},
|
||||||
services::{
|
services::{friends::FriendActiveDollSpritesDto, neko_positions::NekoPositionsDto},
|
||||||
cursor::CursorPositions,
|
|
||||||
friends::{FriendActiveDollSpritesDto, FriendCursorPositionsDto},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||||
@@ -36,10 +33,6 @@ pub struct AuthFlowUpdatedPayload {
|
|||||||
pub message: Option<String>,
|
pub message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
|
||||||
#[tauri_specta(event_name = "cursor-position")]
|
|
||||||
pub struct CursorMoved(pub CursorPositions);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "scene-interactive")]
|
#[tauri_specta(event_name = "scene-interactive")]
|
||||||
pub struct SceneInteractiveChanged(pub bool);
|
pub struct SceneInteractiveChanged(pub bool);
|
||||||
@@ -73,8 +66,8 @@ pub struct CreateDoll;
|
|||||||
pub struct UserStatusChanged(pub UserStatusPayload);
|
pub struct UserStatusChanged(pub UserStatusPayload);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "friend-cursor-positions")]
|
#[tauri_specta(event_name = "neko-positions")]
|
||||||
pub struct FriendCursorPositionsUpdated(pub FriendCursorPositionsDto);
|
pub struct NekoPositionsUpdated(pub NekoPositionsDto);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "friend-disconnected")]
|
#[tauri_specta(event_name = "friend-disconnected")]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use tracing::warn;
|
|||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle,
|
get_app_handle,
|
||||||
models::app_state::{AppState, NekoPosition},
|
models::app_state::{AppState, NekoPosition},
|
||||||
services::app_events::AppStateChanged,
|
services::{app_events::AppStateChanged, neko_positions},
|
||||||
};
|
};
|
||||||
|
|
||||||
static APP_STATE: LazyLock<Arc<RwLock<AppState>>> =
|
static APP_STATE: LazyLock<Arc<RwLock<AppState>>> =
|
||||||
@@ -21,6 +21,8 @@ pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
|
|||||||
let mut guard = APP_STATE.write().expect("app state lock poisoned");
|
let mut guard = APP_STATE.write().expect("app state lock poisoned");
|
||||||
guard.scene_setup.nekos_position = nekos_position;
|
guard.scene_setup.nekos_position = nekos_position;
|
||||||
emit_snapshot(&guard);
|
emit_snapshot(&guard);
|
||||||
|
drop(guard);
|
||||||
|
neko_positions::refresh_from_scene_setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) {
|
pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) {
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ use std::time::Duration;
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
use crate::{get_app_handle, lock_r, services::app_events::CursorMoved, state::FDOLL};
|
use crate::{
|
||||||
use tauri_specta::Event as _;
|
lock_r,
|
||||||
|
services::{neko_positions, ws::report_cursor_data},
|
||||||
|
state::FDOLL,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -59,8 +62,7 @@ pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize cursor tracking. Broadcasts cursor
|
/// Initialize cursor tracking.
|
||||||
/// position changes via `cursor-position` event.
|
|
||||||
pub async fn init_cursor_tracking() {
|
pub async fn init_cursor_tracking() {
|
||||||
info!("start_cursor_tracking called");
|
info!("start_cursor_tracking called");
|
||||||
|
|
||||||
@@ -88,22 +90,19 @@ async fn init_cursor_tracking_i() -> Result<(), String> {
|
|||||||
let (tx, mut rx) = mpsc::channel::<CursorPositions>(100);
|
let (tx, mut rx) = mpsc::channel::<CursorPositions>(100);
|
||||||
|
|
||||||
// Spawn the consumer task
|
// Spawn the consumer task
|
||||||
// This task handles WebSocket reporting and local broadcasting.
|
// This task handles WebSocket reporting and local position projection updates.
|
||||||
// It runs independently of the device event loop.
|
// It runs independently of the device event loop.
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
info!("Cursor event consumer started");
|
info!("Cursor event consumer started");
|
||||||
let app_handle = get_app_handle();
|
|
||||||
|
|
||||||
while let Some(positions) = rx.recv().await {
|
while let Some(positions) = rx.recv().await {
|
||||||
let mapped_for_ws = positions.mapped.clone();
|
let mapped_for_ws = positions.mapped.clone();
|
||||||
|
|
||||||
// 1. WebSocket reporting
|
// 1. WebSocket reporting
|
||||||
crate::services::ws::report_cursor_data(mapped_for_ws).await;
|
report_cursor_data(mapped_for_ws).await;
|
||||||
|
|
||||||
// 2. Broadcast to local windows
|
// 2. Update unified neko positions projection
|
||||||
if let Err(e) = CursorMoved(positions).emit(app_handle) {
|
neko_positions::update_self_cursor(positions);
|
||||||
error!("Failed to emit cursor position event: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
warn!("Cursor event consumer stopped (channel closed)");
|
warn!("Cursor event consumer stopped (channel closed)");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
sync::{Arc, LazyLock, RwLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use specta::Type;
|
|
||||||
use tauri_specta::Event as _;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
get_app_handle, lock_r,
|
|
||||||
services::{app_events::FriendCursorPositionsUpdated, cursor::CursorPositions},
|
|
||||||
state::FDOLL,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Type)]
|
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct FriendCursorPositionsDto(pub HashMap<String, CursorPositions>);
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct FriendCursorProjection {
|
|
||||||
active_dolls: HashMap<String, bool>,
|
|
||||||
positions: HashMap<String, CursorPositions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
static FRIEND_CURSOR_PROJECTION: LazyLock<Arc<RwLock<FriendCursorProjection>>> =
|
|
||||||
LazyLock::new(|| Arc::new(RwLock::new(FriendCursorProjection::default())));
|
|
||||||
|
|
||||||
pub fn sync_from_app_data() {
|
|
||||||
let friends = {
|
|
||||||
let guard = lock_r!(FDOLL);
|
|
||||||
guard.user_data.friends.clone().unwrap_or_default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut projection = FRIEND_CURSOR_PROJECTION
|
|
||||||
.write()
|
|
||||||
.expect("friend cursor projection lock poisoned");
|
|
||||||
|
|
||||||
projection.active_dolls = friends
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|friendship| {
|
|
||||||
friendship.friend.map(|friend| {
|
|
||||||
let has_active_doll = friend.active_doll.is_some();
|
|
||||||
(friend.id, has_active_doll)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let active_dolls = projection.active_dolls.clone();
|
|
||||||
projection
|
|
||||||
.positions
|
|
||||||
.retain(|user_id, _| active_dolls.get(user_id) == Some(&true));
|
|
||||||
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear() {
|
|
||||||
let mut projection = FRIEND_CURSOR_PROJECTION
|
|
||||||
.write()
|
|
||||||
.expect("friend cursor projection lock poisoned");
|
|
||||||
|
|
||||||
projection.active_dolls.clear();
|
|
||||||
projection.positions.clear();
|
|
||||||
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_position(user_id: String, position: CursorPositions) {
|
|
||||||
let mut projection = FRIEND_CURSOR_PROJECTION
|
|
||||||
.write()
|
|
||||||
.expect("friend cursor projection lock poisoned");
|
|
||||||
|
|
||||||
if !has_active_doll(&mut projection, &user_id) {
|
|
||||||
if projection.positions.remove(&user_id).is_some() {
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
projection.positions.insert(user_id, position);
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_friend(user_id: &str) {
|
|
||||||
let mut projection = FRIEND_CURSOR_PROJECTION
|
|
||||||
.write()
|
|
||||||
.expect("friend cursor projection lock poisoned");
|
|
||||||
|
|
||||||
let removed_active_doll = projection.active_dolls.remove(user_id).is_some();
|
|
||||||
let removed_position = projection.positions.remove(user_id).is_some();
|
|
||||||
|
|
||||||
if removed_active_doll || removed_position {
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_active_doll(user_id: &str, has_active_doll: bool) {
|
|
||||||
let mut projection = FRIEND_CURSOR_PROJECTION
|
|
||||||
.write()
|
|
||||||
.expect("friend cursor projection lock poisoned");
|
|
||||||
|
|
||||||
projection
|
|
||||||
.active_dolls
|
|
||||||
.insert(user_id.to_string(), has_active_doll);
|
|
||||||
|
|
||||||
if !has_active_doll && projection.positions.remove(user_id).is_some() {
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_active_doll(projection: &mut FriendCursorProjection, user_id: &str) -> bool {
|
|
||||||
if let Some(has_active_doll) = projection.active_dolls.get(user_id) {
|
|
||||||
return *has_active_doll;
|
|
||||||
}
|
|
||||||
|
|
||||||
let has_active_doll = {
|
|
||||||
let guard = lock_r!(FDOLL);
|
|
||||||
guard
|
|
||||||
.user_data
|
|
||||||
.friends
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|friends| {
|
|
||||||
friends.iter().find_map(|friendship| {
|
|
||||||
let friend = friendship.friend.as_ref()?;
|
|
||||||
(friend.id == user_id).then_some(friend)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.and_then(|friend| friend.active_doll.as_ref())
|
|
||||||
.is_some()
|
|
||||||
};
|
|
||||||
|
|
||||||
projection
|
|
||||||
.active_dolls
|
|
||||||
.insert(user_id.to_string(), has_active_doll);
|
|
||||||
|
|
||||||
has_active_doll
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_snapshot(positions: &HashMap<String, CursorPositions>) {
|
|
||||||
let payload = FriendCursorPositionsDto(positions.clone());
|
|
||||||
|
|
||||||
if let Err(err) = FriendCursorPositionsUpdated(payload).emit(get_app_handle()) {
|
|
||||||
tracing::warn!("Failed to emit friend cursor positions update: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,27 @@
|
|||||||
mod active_doll_sprites;
|
mod active_doll_sprites;
|
||||||
mod cursor_positions;
|
|
||||||
|
|
||||||
use crate::{models::dolls::DollDto, services::cursor::CursorPositions};
|
use crate::{models::dolls::DollDto, services::neko_positions};
|
||||||
|
|
||||||
pub use active_doll_sprites::FriendActiveDollSpritesDto;
|
pub use active_doll_sprites::FriendActiveDollSpritesDto;
|
||||||
pub use cursor_positions::FriendCursorPositionsDto;
|
|
||||||
|
|
||||||
pub fn sync_from_app_data() {
|
pub fn sync_from_app_data() {
|
||||||
active_doll_sprites::sync_from_app_data();
|
active_doll_sprites::sync_from_app_data();
|
||||||
cursor_positions::sync_from_app_data();
|
neko_positions::sync_from_app_data();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear() {
|
pub fn clear() {
|
||||||
active_doll_sprites::clear();
|
active_doll_sprites::clear();
|
||||||
cursor_positions::clear();
|
neko_positions::clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_friend(user_id: &str) {
|
pub fn remove_friend(user_id: &str) {
|
||||||
active_doll_sprites::remove_friend(user_id);
|
active_doll_sprites::remove_friend(user_id);
|
||||||
cursor_positions::remove_friend(user_id);
|
neko_positions::remove_friend(user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_active_doll(user_id: &str, doll: Option<&DollDto>) {
|
pub fn set_active_doll(user_id: &str, doll: Option<&DollDto>) {
|
||||||
active_doll_sprites::set_active_doll(user_id, doll);
|
active_doll_sprites::set_active_doll(user_id, doll);
|
||||||
cursor_positions::set_active_doll(user_id, doll.is_some());
|
neko_positions::set_friend_active_doll(user_id, doll.is_some());
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_cursor_position(user_id: String, position: CursorPositions) {
|
|
||||||
cursor_positions::update_position(user_id, position);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_active_doll_sprites_from_app_data() {
|
pub fn sync_active_doll_sprites_from_app_data() {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub mod friends;
|
|||||||
pub mod health_manager;
|
pub mod health_manager;
|
||||||
pub mod health_monitor;
|
pub mod health_monitor;
|
||||||
pub mod interaction;
|
pub mod interaction;
|
||||||
|
pub mod neko_positions;
|
||||||
pub mod petpet;
|
pub mod petpet;
|
||||||
pub mod presence_modules;
|
pub mod presence_modules;
|
||||||
pub mod scene;
|
pub mod scene;
|
||||||
|
|||||||
307
src-tauri/src/services/neko_positions.rs
Normal file
307
src-tauri/src/services/neko_positions.rs
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{Arc, LazyLock, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
use tauri_specta::Event as _;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
get_app_handle, lock_r,
|
||||||
|
models::app_state::NekoPosition,
|
||||||
|
services::{
|
||||||
|
app_events::NekoPositionsUpdated,
|
||||||
|
app_state,
|
||||||
|
cursor::{CursorPosition, CursorPositions},
|
||||||
|
},
|
||||||
|
state::FDOLL,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NekoPositionDto {
|
||||||
|
pub user_id: String,
|
||||||
|
pub is_self: bool,
|
||||||
|
pub cursor: CursorPositions,
|
||||||
|
pub target: CursorPosition,
|
||||||
|
pub override_applied: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct NekoPositionsDto(pub HashMap<String, NekoPositionDto>);
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct NekoPositionsProjection {
|
||||||
|
self_cursor: Option<CursorPositions>,
|
||||||
|
friend_cursors: HashMap<String, CursorPositions>,
|
||||||
|
friend_active_dolls: HashMap<String, bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static NEKO_POSITIONS: LazyLock<Arc<RwLock<NekoPositionsProjection>>> =
|
||||||
|
LazyLock::new(|| Arc::new(RwLock::new(NekoPositionsProjection::default())));
|
||||||
|
|
||||||
|
pub fn sync_from_app_data() {
|
||||||
|
let friends = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
guard.user_data.friends.clone().unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut projection = NEKO_POSITIONS
|
||||||
|
.write()
|
||||||
|
.expect("neko positions projection lock poisoned");
|
||||||
|
|
||||||
|
projection.friend_active_dolls = friends
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|friendship| {
|
||||||
|
friendship.friend.map(|friend| {
|
||||||
|
let has_active_doll = friend.active_doll.is_some();
|
||||||
|
(friend.id, has_active_doll)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let active_dolls = projection.friend_active_dolls.clone();
|
||||||
|
projection
|
||||||
|
.friend_cursors
|
||||||
|
.retain(|user_id, _| active_dolls.get(user_id) == Some(&true));
|
||||||
|
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear() {
|
||||||
|
let mut projection = NEKO_POSITIONS
|
||||||
|
.write()
|
||||||
|
.expect("neko positions projection lock poisoned");
|
||||||
|
|
||||||
|
projection.self_cursor = None;
|
||||||
|
projection.friend_cursors.clear();
|
||||||
|
projection.friend_active_dolls.clear();
|
||||||
|
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_self_cursor(position: CursorPositions) {
|
||||||
|
let mut projection = NEKO_POSITIONS
|
||||||
|
.write()
|
||||||
|
.expect("neko positions projection lock poisoned");
|
||||||
|
|
||||||
|
projection.self_cursor = Some(position);
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_friend_cursor(user_id: String, position: CursorPositions) {
|
||||||
|
let mut projection = NEKO_POSITIONS
|
||||||
|
.write()
|
||||||
|
.expect("neko positions projection lock poisoned");
|
||||||
|
|
||||||
|
if !has_friend_active_doll(&mut projection, &user_id) {
|
||||||
|
if projection.friend_cursors.remove(&user_id).is_some() {
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projection.friend_cursors.insert(user_id, position);
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_friend(user_id: &str) {
|
||||||
|
let mut projection = NEKO_POSITIONS
|
||||||
|
.write()
|
||||||
|
.expect("neko positions projection lock poisoned");
|
||||||
|
|
||||||
|
let removed_active_doll = projection.friend_active_dolls.remove(user_id).is_some();
|
||||||
|
let removed_position = projection.friend_cursors.remove(user_id).is_some();
|
||||||
|
|
||||||
|
if removed_active_doll || removed_position {
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_friend_active_doll(user_id: &str, has_active_doll: bool) {
|
||||||
|
let mut projection = NEKO_POSITIONS
|
||||||
|
.write()
|
||||||
|
.expect("neko positions projection lock poisoned");
|
||||||
|
|
||||||
|
projection
|
||||||
|
.friend_active_dolls
|
||||||
|
.insert(user_id.to_string(), has_active_doll);
|
||||||
|
|
||||||
|
if !has_active_doll && projection.friend_cursors.remove(user_id).is_some() {
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_from_scene_setup() {
|
||||||
|
let projection = NEKO_POSITIONS
|
||||||
|
.read()
|
||||||
|
.expect("neko positions projection lock poisoned");
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_snapshot() -> NekoPositionsDto {
|
||||||
|
let projection = NEKO_POSITIONS
|
||||||
|
.read()
|
||||||
|
.expect("neko positions projection lock poisoned");
|
||||||
|
build_snapshot(&projection)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_friend_active_doll(projection: &mut NekoPositionsProjection, user_id: &str) -> bool {
|
||||||
|
if let Some(has_active_doll) = projection.friend_active_dolls.get(user_id) {
|
||||||
|
return *has_active_doll;
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_active_doll = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
guard
|
||||||
|
.user_data
|
||||||
|
.friends
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|friends| {
|
||||||
|
friends.iter().find_map(|friendship| {
|
||||||
|
let friend = friendship.friend.as_ref()?;
|
||||||
|
(friend.id == user_id).then_some(friend)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.and_then(|friend| friend.active_doll.as_ref())
|
||||||
|
.is_some()
|
||||||
|
};
|
||||||
|
|
||||||
|
projection
|
||||||
|
.friend_active_dolls
|
||||||
|
.insert(user_id.to_string(), has_active_doll);
|
||||||
|
|
||||||
|
has_active_doll
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_self_active_doll() -> bool {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
guard
|
||||||
|
.user_data
|
||||||
|
.user
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|user| user.active_doll_id.as_ref())
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_self_user_id() -> Option<String> {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
guard.user_data.user.as_ref().map(|user| user.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_display_size() -> (f64, f64) {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
(
|
||||||
|
guard.user_data.scene.display.screen_width as f64,
|
||||||
|
guard.user_data.scene.display.screen_height as f64,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_snapshot(projection: &NekoPositionsProjection) {
|
||||||
|
let payload = build_snapshot(projection);
|
||||||
|
|
||||||
|
if let Err(err) = NekoPositionsUpdated(payload).emit(get_app_handle()) {
|
||||||
|
tracing::warn!("Failed to emit neko positions update: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_snapshot(projection: &NekoPositionsProjection) -> NekoPositionsDto {
|
||||||
|
let mut entries: Vec<(String, bool, CursorPositions)> = Vec::new();
|
||||||
|
|
||||||
|
if has_self_active_doll() {
|
||||||
|
if let (Some(self_user_id), Some(self_cursor)) =
|
||||||
|
(get_self_user_id(), projection.self_cursor.clone())
|
||||||
|
{
|
||||||
|
entries.push((self_user_id, true, self_cursor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (user_id, cursor) in &projection.friend_cursors {
|
||||||
|
if projection.friend_active_dolls.get(user_id) == Some(&true) {
|
||||||
|
entries.push((user_id.clone(), false, cursor.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
let app_state = app_state::get_snapshot();
|
||||||
|
let override_anchor = app_state.scene_setup.nekos_position;
|
||||||
|
let (screen_width, screen_height) = get_display_size();
|
||||||
|
|
||||||
|
let total = entries.len();
|
||||||
|
|
||||||
|
NekoPositionsDto(
|
||||||
|
entries
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, (user_id, is_self, cursor))| {
|
||||||
|
let (target, override_applied) = match &override_anchor {
|
||||||
|
Some(anchor) => (
|
||||||
|
get_cluster_target(
|
||||||
|
anchor.clone(),
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
screen_width,
|
||||||
|
screen_height,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
None => (cursor.raw.clone(), false),
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
user_id.clone(),
|
||||||
|
NekoPositionDto {
|
||||||
|
user_id,
|
||||||
|
is_self,
|
||||||
|
cursor,
|
||||||
|
target,
|
||||||
|
override_applied,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cluster_target(
|
||||||
|
anchor: NekoPosition,
|
||||||
|
index: usize,
|
||||||
|
count: usize,
|
||||||
|
screen_width: f64,
|
||||||
|
screen_height: f64,
|
||||||
|
) -> CursorPosition {
|
||||||
|
let spacing = 36.0;
|
||||||
|
let margin = 28.0;
|
||||||
|
|
||||||
|
let columns = (count as f64).sqrt().ceil().max(1.0) as usize;
|
||||||
|
let rows = count.div_ceil(columns).max(1);
|
||||||
|
let col = index % columns;
|
||||||
|
let row = index / columns;
|
||||||
|
|
||||||
|
let block_width = (columns.saturating_sub(1)) as f64 * spacing;
|
||||||
|
let block_height = (rows.saturating_sub(1)) as f64 * spacing;
|
||||||
|
|
||||||
|
let start_x = match anchor {
|
||||||
|
NekoPosition::TopLeft | NekoPosition::Left | NekoPosition::BottomLeft => margin,
|
||||||
|
NekoPosition::Top | NekoPosition::Bottom => (screen_width - block_width) / 2.0,
|
||||||
|
NekoPosition::TopRight | NekoPosition::Right | NekoPosition::BottomRight => {
|
||||||
|
screen_width - margin - block_width
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let start_y = match anchor {
|
||||||
|
NekoPosition::TopLeft | NekoPosition::Top | NekoPosition::TopRight => margin,
|
||||||
|
NekoPosition::Left | NekoPosition::Right => (screen_height - block_height) / 2.0,
|
||||||
|
NekoPosition::BottomLeft | NekoPosition::Bottom | NekoPosition::BottomRight => {
|
||||||
|
screen_height - margin - block_height
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CursorPosition {
|
||||||
|
x: (start_x + col as f64 * spacing).clamp(0.0, screen_width),
|
||||||
|
y: (start_y + row as f64 * spacing).clamp(0.0, screen_height),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ use crate::services::app_events::{
|
|||||||
};
|
};
|
||||||
use crate::services::{
|
use crate::services::{
|
||||||
cursor::{normalized_to_absolute, CursorPositions},
|
cursor::{normalized_to_absolute, CursorPositions},
|
||||||
friends,
|
friends, neko_positions,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{emitter, refresh, types::IncomingFriendCursorPayload, utils};
|
use super::{emitter, refresh, types::IncomingFriendCursorPayload, utils};
|
||||||
@@ -62,13 +62,12 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
|
|||||||
let mapped_pos = &friend_data.position;
|
let mapped_pos = &friend_data.position;
|
||||||
let raw_pos = normalized_to_absolute(mapped_pos);
|
let raw_pos = normalized_to_absolute(mapped_pos);
|
||||||
|
|
||||||
friends::update_cursor_position(
|
let position = CursorPositions {
|
||||||
friend_data.user_id,
|
|
||||||
CursorPositions {
|
|
||||||
raw: raw_pos,
|
raw: raw_pos,
|
||||||
mapped: mapped_pos.clone(),
|
mapped: mapped_pos.clone(),
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
neko_positions::update_friend_cursor(friend_data.user_id, position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { writable } from "svelte/store";
|
|
||||||
import { events, type CursorPositions } from "$lib/bindings";
|
|
||||||
import { createEventSource } from "./listener-utils";
|
|
||||||
|
|
||||||
export const cursorPositionOnScreen = writable<CursorPositions>({
|
|
||||||
raw: { x: 0, y: 0 },
|
|
||||||
mapped: { x: 0, y: 0 },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { start: startCursorTracking, stop: stopCursorTracking } =
|
|
||||||
createEventSource(async (addEventListener) => {
|
|
||||||
addEventListener(
|
|
||||||
await events.cursorMoved.listen((event) => {
|
|
||||||
cursorPositionOnScreen.set(event.payload);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { writable } from "svelte/store";
|
|
||||||
import { events, type CursorPositions } from "$lib/bindings";
|
|
||||||
import { createEventSource } from "./listener-utils";
|
|
||||||
|
|
||||||
export const friendsCursorPositions = writable<Record<string, CursorPositions>>(
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Here for now. Will extract into shared
|
|
||||||
// util when there's more similar cases.
|
|
||||||
function toCursorPositionsRecord(
|
|
||||||
payload: Partial<Record<string, CursorPositions>>,
|
|
||||||
): Record<string, CursorPositions> {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(payload).filter(
|
|
||||||
(entry): entry is [string, CursorPositions] => {
|
|
||||||
return entry[1] !== undefined;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const {
|
|
||||||
start: startFriendCursorTracking,
|
|
||||||
stop: stopFriendCursorTracking,
|
|
||||||
} = createEventSource(async (addEventListener) => {
|
|
||||||
addEventListener(
|
|
||||||
await events.friendCursorPositionsUpdated.listen((event) => {
|
|
||||||
friendsCursorPositions.set(toCursorPositionsRecord(event.payload));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
16
src/events/neko-positions.ts
Normal file
16
src/events/neko-positions.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { commands, events, type NekoPositionsDto } from "$lib/bindings";
|
||||||
|
import { createEventSource } from "./listener-utils";
|
||||||
|
|
||||||
|
export const nekoPositions = writable<NekoPositionsDto>({});
|
||||||
|
|
||||||
|
export const { start: startNekoPositions, stop: stopNekoPositions } =
|
||||||
|
createEventSource(async (addEventListener) => {
|
||||||
|
nekoPositions.set(await commands.getNekoPositions());
|
||||||
|
|
||||||
|
addEventListener(
|
||||||
|
await events.nekoPositionsUpdated.listen((event) => {
|
||||||
|
nekoPositions.set(event.payload);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -11,6 +11,9 @@ async getAppData() : Promise<UserData> {
|
|||||||
async getAppState() : Promise<AppState> {
|
async getAppState() : Promise<AppState> {
|
||||||
return await TAURI_INVOKE("get_app_state");
|
return await TAURI_INVOKE("get_app_state");
|
||||||
},
|
},
|
||||||
|
async getNekoPositions() : Promise<NekoPositionsDto> {
|
||||||
|
return await TAURI_INVOKE("get_neko_positions");
|
||||||
|
},
|
||||||
async getActiveDollSpriteBase64() : Promise<string | null> {
|
async getActiveDollSpriteBase64() : Promise<string | null> {
|
||||||
return await TAURI_INVOKE("get_active_doll_sprite_base64");
|
return await TAURI_INVOKE("get_active_doll_sprite_base64");
|
||||||
},
|
},
|
||||||
@@ -142,11 +145,9 @@ appDataRefreshed: AppDataRefreshed,
|
|||||||
appStateChanged: AppStateChanged,
|
appStateChanged: AppStateChanged,
|
||||||
authFlowUpdated: AuthFlowUpdated,
|
authFlowUpdated: AuthFlowUpdated,
|
||||||
createDoll: CreateDoll,
|
createDoll: CreateDoll,
|
||||||
cursorMoved: CursorMoved,
|
|
||||||
editDoll: EditDoll,
|
editDoll: EditDoll,
|
||||||
friendActiveDollChanged: FriendActiveDollChanged,
|
friendActiveDollChanged: FriendActiveDollChanged,
|
||||||
friendActiveDollSpritesUpdated: FriendActiveDollSpritesUpdated,
|
friendActiveDollSpritesUpdated: FriendActiveDollSpritesUpdated,
|
||||||
friendCursorPositionsUpdated: FriendCursorPositionsUpdated,
|
|
||||||
friendDisconnected: FriendDisconnected,
|
friendDisconnected: FriendDisconnected,
|
||||||
friendRequestAccepted: FriendRequestAccepted,
|
friendRequestAccepted: FriendRequestAccepted,
|
||||||
friendRequestDenied: FriendRequestDenied,
|
friendRequestDenied: FriendRequestDenied,
|
||||||
@@ -154,6 +155,7 @@ friendRequestReceived: FriendRequestReceived,
|
|||||||
friendUserStatusChanged: FriendUserStatusChanged,
|
friendUserStatusChanged: FriendUserStatusChanged,
|
||||||
interactionDeliveryFailed: InteractionDeliveryFailed,
|
interactionDeliveryFailed: InteractionDeliveryFailed,
|
||||||
interactionReceived: InteractionReceived,
|
interactionReceived: InteractionReceived,
|
||||||
|
nekoPositionsUpdated: NekoPositionsUpdated,
|
||||||
sceneInteractiveChanged: SceneInteractiveChanged,
|
sceneInteractiveChanged: SceneInteractiveChanged,
|
||||||
setInteractionOverlay: SetInteractionOverlay,
|
setInteractionOverlay: SetInteractionOverlay,
|
||||||
unfriended: Unfriended,
|
unfriended: Unfriended,
|
||||||
@@ -164,11 +166,9 @@ appDataRefreshed: "app-data-refreshed",
|
|||||||
appStateChanged: "app-state-changed",
|
appStateChanged: "app-state-changed",
|
||||||
authFlowUpdated: "auth-flow-updated",
|
authFlowUpdated: "auth-flow-updated",
|
||||||
createDoll: "create-doll",
|
createDoll: "create-doll",
|
||||||
cursorMoved: "cursor-moved",
|
|
||||||
editDoll: "edit-doll",
|
editDoll: "edit-doll",
|
||||||
friendActiveDollChanged: "friend-active-doll-changed",
|
friendActiveDollChanged: "friend-active-doll-changed",
|
||||||
friendActiveDollSpritesUpdated: "friend-active-doll-sprites-updated",
|
friendActiveDollSpritesUpdated: "friend-active-doll-sprites-updated",
|
||||||
friendCursorPositionsUpdated: "friend-cursor-positions-updated",
|
|
||||||
friendDisconnected: "friend-disconnected",
|
friendDisconnected: "friend-disconnected",
|
||||||
friendRequestAccepted: "friend-request-accepted",
|
friendRequestAccepted: "friend-request-accepted",
|
||||||
friendRequestDenied: "friend-request-denied",
|
friendRequestDenied: "friend-request-denied",
|
||||||
@@ -176,6 +176,7 @@ 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",
|
||||||
|
nekoPositionsUpdated: "neko-positions-updated",
|
||||||
sceneInteractiveChanged: "scene-interactive-changed",
|
sceneInteractiveChanged: "scene-interactive-changed",
|
||||||
setInteractionOverlay: "set-interaction-overlay",
|
setInteractionOverlay: "set-interaction-overlay",
|
||||||
unfriended: "unfriended",
|
unfriended: "unfriended",
|
||||||
@@ -201,7 +202,6 @@ export type AuthFlowUpdated = AuthFlowUpdatedPayload
|
|||||||
export type AuthFlowUpdatedPayload = { provider: string; status: AuthFlowStatus; message: string | null }
|
export type AuthFlowUpdatedPayload = { provider: string; status: AuthFlowStatus; message: string | null }
|
||||||
export type CreateDoll = null
|
export type CreateDoll = null
|
||||||
export type CreateDollDto = { name: string; configuration: DollConfigurationDto | null }
|
export type CreateDollDto = { name: string; configuration: DollConfigurationDto | null }
|
||||||
export type CursorMoved = CursorPositions
|
|
||||||
export type CursorPosition = { x: number; y: number }
|
export type CursorPosition = { x: number; y: number }
|
||||||
export type CursorPositions = { raw: CursorPosition; mapped: CursorPosition }
|
export type CursorPositions = { raw: CursorPosition; mapped: CursorPosition }
|
||||||
export type DisplayData = { screen_width: number; screen_height: number; monitor_scale_factor: number }
|
export type DisplayData = { screen_width: number; screen_height: number; monitor_scale_factor: number }
|
||||||
@@ -213,8 +213,6 @@ export type FriendActiveDollChanged = FriendActiveDollChangedPayload
|
|||||||
export type FriendActiveDollChangedPayload = { friendId: string; doll: DollDto | null }
|
export type FriendActiveDollChangedPayload = { friendId: string; doll: DollDto | null }
|
||||||
export type FriendActiveDollSpritesDto = Partial<{ [key in string]: string }>
|
export type FriendActiveDollSpritesDto = Partial<{ [key in string]: string }>
|
||||||
export type FriendActiveDollSpritesUpdated = FriendActiveDollSpritesDto
|
export type FriendActiveDollSpritesUpdated = FriendActiveDollSpritesDto
|
||||||
export type FriendCursorPositionsDto = Partial<{ [key in string]: CursorPositions }>
|
|
||||||
export type FriendCursorPositionsUpdated = FriendCursorPositionsDto
|
|
||||||
export type FriendDisconnected = FriendDisconnectedPayload
|
export type FriendDisconnected = FriendDisconnectedPayload
|
||||||
export type FriendDisconnectedPayload = { userId: string }
|
export type FriendDisconnectedPayload = { userId: string }
|
||||||
export type FriendRequestAccepted = FriendRequestAcceptedPayload
|
export type FriendRequestAccepted = FriendRequestAcceptedPayload
|
||||||
@@ -234,6 +232,9 @@ export type InteractionReceived = InteractionPayloadDto
|
|||||||
export type KeyboardAccelerator = { modifiers?: AcceleratorModifier[]; key?: AcceleratorKey | null }
|
export type KeyboardAccelerator = { modifiers?: AcceleratorModifier[]; key?: AcceleratorKey | null }
|
||||||
export type ModuleMetadata = { id: string; name: string; version: string; description: string | null }
|
export type ModuleMetadata = { id: string; name: string; version: string; description: string | null }
|
||||||
export type NekoPosition = "top-left" | "top" | "top-right" | "left" | "right" | "bottom-left" | "bottom" | "bottom-right"
|
export type NekoPosition = "top-left" | "top" | "top-right" | "left" | "right" | "bottom-left" | "bottom" | "bottom-right"
|
||||||
|
export type NekoPositionDto = { userId: string; isSelf: boolean; cursor: CursorPositions; target: CursorPosition; overrideApplied: boolean }
|
||||||
|
export type NekoPositionsDto = Partial<{ [key in string]: NekoPositionDto }>
|
||||||
|
export type NekoPositionsUpdated = NekoPositionsDto
|
||||||
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 SceneInteractiveChanged = boolean
|
export type SceneInteractiveChanged = boolean
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { startCursorTracking, stopCursorTracking } from "../events/cursor";
|
|
||||||
import {
|
import {
|
||||||
startFriendCursorTracking,
|
startNekoPositions,
|
||||||
stopFriendCursorTracking,
|
stopNekoPositions,
|
||||||
} from "../events/friend-cursor";
|
} from "../events/neko-positions";
|
||||||
import {
|
import {
|
||||||
startActiveDollSprite,
|
startActiveDollSprite,
|
||||||
stopActiveDollSprite,
|
stopActiveDollSprite,
|
||||||
@@ -31,8 +30,7 @@
|
|||||||
await startAppState();
|
await startAppState();
|
||||||
await startActiveDollSprite();
|
await startActiveDollSprite();
|
||||||
await startFriendActiveDollSprite();
|
await startFriendActiveDollSprite();
|
||||||
await startCursorTracking();
|
await startNekoPositions();
|
||||||
await startFriendCursorTracking();
|
|
||||||
await startSceneInteractive();
|
await startSceneInteractive();
|
||||||
await startInteraction();
|
await startInteraction();
|
||||||
await startUserStatus();
|
await startUserStatus();
|
||||||
@@ -42,8 +40,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
stopCursorTracking();
|
stopNekoPositions();
|
||||||
stopFriendCursorTracking();
|
|
||||||
stopActiveDollSprite();
|
stopActiveDollSprite();
|
||||||
stopFriendActiveDollSprite();
|
stopFriendActiveDollSprite();
|
||||||
stopSceneInteractive();
|
stopSceneInteractive();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { cursorPositionOnScreen } from "../../events/cursor";
|
import { nekoPositions } from "../../events/neko-positions";
|
||||||
import { friendsCursorPositions } from "../../events/friend-cursor";
|
|
||||||
import { appData } from "../../events/app-data";
|
import { appData } from "../../events/app-data";
|
||||||
import { activeDollSpriteUrl } from "../../events/active-doll-sprite";
|
import { activeDollSpriteUrl } from "../../events/active-doll-sprite";
|
||||||
import { friendActiveDollSpriteUrls } from "../../events/friend-active-doll-sprite";
|
import { friendActiveDollSpriteUrls } from "../../events/friend-active-doll-sprite";
|
||||||
@@ -10,7 +9,7 @@
|
|||||||
friendsPresenceStates,
|
friendsPresenceStates,
|
||||||
currentPresenceState,
|
currentPresenceState,
|
||||||
} from "../../events/user-status";
|
} from "../../events/user-status";
|
||||||
import { commands } from "$lib/bindings";
|
import { commands, type NekoPositionDto } from "$lib/bindings";
|
||||||
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";
|
||||||
import PetMenu from "./components/pet-menu/pet-menu.svelte";
|
import PetMenu from "./components/pet-menu/pet-menu.svelte";
|
||||||
@@ -19,7 +18,7 @@
|
|||||||
import type { UserBasicDto } from "$lib/bindings";
|
import type { UserBasicDto } from "$lib/bindings";
|
||||||
import { appState } from "../../events/app-state";
|
import { appState } from "../../events/app-state";
|
||||||
|
|
||||||
let debugMode = false;
|
let debugMode = $state(false);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const config = await commands.getClientConfig();
|
const config = await commands.getClientConfig();
|
||||||
@@ -32,6 +31,12 @@
|
|||||||
?.friend ?? undefined
|
?.friend ?? undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let nekoEntries = $derived.by(() => {
|
||||||
|
return Object.entries($nekoPositions).filter(
|
||||||
|
(entry): entry is [string, NekoPositionDto] => entry[1] !== undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-svw h-svh p-4 relative overflow-hidden">
|
<div class="w-svw h-svh p-4 relative overflow-hidden">
|
||||||
@@ -42,30 +47,26 @@
|
|||||||
await commands.setSceneInteractive(false, true);
|
await commands.setSceneInteractive(false, true);
|
||||||
}}> </button
|
}}> </button
|
||||||
>
|
>
|
||||||
{#if $appData?.user?.activeDollId}
|
{#each nekoEntries as [userId, position] (userId)}
|
||||||
|
{@const spriteUrl = position.isSelf
|
||||||
|
? $activeDollSpriteUrl
|
||||||
|
: $friendActiveDollSpriteUrls[userId]}
|
||||||
|
{#if spriteUrl}
|
||||||
|
{@const friend = position.isSelf ? undefined : getFriend(userId)}
|
||||||
<Neko
|
<Neko
|
||||||
targetX={$cursorPositionOnScreen.raw.x}
|
targetX={position.target.x}
|
||||||
targetY={$cursorPositionOnScreen.raw.y}
|
targetY={position.target.y}
|
||||||
spriteUrl={$activeDollSpriteUrl}
|
spriteUrl={spriteUrl}
|
||||||
scale={$appState.sceneSetup.nekosScale}
|
initialX={position.target.x}
|
||||||
opacity={$appState.sceneSetup.nekosOpacity}
|
initialY={position.target.y}
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#each Object.entries($friendsCursorPositions) as [friendId, position] (friendId)}
|
|
||||||
{#if $friendActiveDollSpriteUrls[friendId]}
|
|
||||||
{@const friend = getFriend(friendId)}
|
|
||||||
<Neko
|
|
||||||
targetX={position.raw.x}
|
|
||||||
targetY={position.raw.y}
|
|
||||||
spriteUrl={$friendActiveDollSpriteUrls[friendId]}
|
|
||||||
initialX={position.raw.x}
|
|
||||||
initialY={position.raw.y}
|
|
||||||
scale={$appState.sceneSetup.nekosScale}
|
scale={$appState.sceneSetup.nekosScale}
|
||||||
opacity={$appState.sceneSetup.nekosOpacity}
|
opacity={$appState.sceneSetup.nekosOpacity}
|
||||||
>
|
>
|
||||||
<PetMenu user={friend!} ariaLabel={`Open ${friend?.name} actions`} />
|
{#if !position.isSelf && friend}
|
||||||
<PetMessagePop userId={friendId} />
|
<PetMenu user={friend} ariaLabel={`Open ${friend.name} actions`} />
|
||||||
<PetMessageSend userId={friendId} userName={friend?.name ?? "Friend"} />
|
<PetMessagePop userId={userId} />
|
||||||
|
<PetMessageSend userId={userId} userName={friend.name} />
|
||||||
|
{/if}
|
||||||
</Neko>
|
</Neko>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
@@ -73,9 +74,8 @@
|
|||||||
<div id="debug-bar">
|
<div id="debug-bar">
|
||||||
<DebugBar
|
<DebugBar
|
||||||
isInteractive={$sceneInteractive}
|
isInteractive={$sceneInteractive}
|
||||||
cursorPosition={$cursorPositionOnScreen}
|
nekoPositions={$nekoPositions}
|
||||||
presenceStatus={$currentPresenceState?.presenceStatus ?? null}
|
presenceStatus={$currentPresenceState?.presenceStatus ?? null}
|
||||||
friendsCursorPositions={$friendsCursorPositions}
|
|
||||||
friends={$appData?.friends ?? []}
|
friends={$appData?.friends ?? []}
|
||||||
friendsPresenceStates={$friendsPresenceStates}
|
friendsPresenceStates={$friendsPresenceStates}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PresenceStatus, UserStatusPayload } from "$lib/bindings";
|
import type {
|
||||||
|
NekoPositionDto,
|
||||||
|
NekoPositionsDto,
|
||||||
|
PresenceStatus,
|
||||||
|
UserStatusPayload,
|
||||||
|
} from "$lib/bindings";
|
||||||
|
|
||||||
interface Friend {
|
interface Friend {
|
||||||
friend?: {
|
friend?: {
|
||||||
@@ -10,22 +15,32 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isInteractive: boolean;
|
isInteractive: boolean;
|
||||||
cursorPosition: { mapped: { x: number; y: number } };
|
nekoPositions: NekoPositionsDto;
|
||||||
presenceStatus: PresenceStatus | null;
|
presenceStatus: PresenceStatus | null;
|
||||||
friendsCursorPositions: Record<string, { mapped: { x: number; y: number } }>;
|
|
||||||
friends: Friend[];
|
friends: Friend[];
|
||||||
friendsPresenceStates: Record<string, UserStatusPayload>;
|
friendsPresenceStates: Record<string, UserStatusPayload>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
isInteractive,
|
isInteractive,
|
||||||
cursorPosition,
|
nekoPositions,
|
||||||
presenceStatus,
|
presenceStatus,
|
||||||
friendsCursorPositions,
|
|
||||||
friends,
|
friends,
|
||||||
friendsPresenceStates,
|
friendsPresenceStates,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
let selfCursor = $derived(
|
||||||
|
Object.values(nekoPositions).find((position) => position?.isSelf)?.cursor,
|
||||||
|
);
|
||||||
|
|
||||||
|
let friendEntries = $derived.by(() => {
|
||||||
|
return Object.entries(nekoPositions).filter(
|
||||||
|
(entry): entry is [string, NekoPositionDto] => {
|
||||||
|
return entry[1] !== undefined && !entry[1].isSelf;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function getFriendById(userId: string) {
|
function getFriendById(userId: string) {
|
||||||
const friend = friends.find((f) => f.friend?.id === userId);
|
const friend = friends.find((f) => f.friend?.id === userId);
|
||||||
return friend?.friend;
|
return friend?.friend;
|
||||||
@@ -49,9 +64,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if selfCursor}
|
||||||
<span class="font-mono text-xs badge py-3">
|
<span class="font-mono text-xs badge py-3">
|
||||||
{cursorPosition.mapped.x.toFixed(3)}, {cursorPosition.mapped.y.toFixed(3)}
|
{selfCursor.mapped.x.toFixed(3)}, {selfCursor.mapped.y.toFixed(3)}
|
||||||
</span>
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if presenceStatus}
|
{#if presenceStatus}
|
||||||
<span class="font-mono text-xs badge py-3 flex items-center gap-2">
|
<span class="font-mono text-xs badge py-3 flex items-center gap-2">
|
||||||
@@ -66,16 +83,17 @@
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if Object.keys(friendsCursorPositions).length > 0}
|
{#if friendEntries.length > 0}
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div>
|
<div>
|
||||||
{#each Object.entries(friendsCursorPositions) as [userId, position]}
|
{#each friendEntries as [userId, position]}
|
||||||
{@const status = getFriendStatus(userId)}
|
{@const status = getFriendStatus(userId)}
|
||||||
<div class="badge py-3 text-xs text-left flex flex-row gap-2">
|
<div class="badge py-3 text-xs text-left flex flex-row gap-2">
|
||||||
<span class="font-bold">{getFriendById(userId)?.name}</span>
|
<span class="font-bold">{getFriendById(userId)?.name}</span>
|
||||||
<div class="flex flex-row font-mono gap-2">
|
<div class="flex flex-row font-mono gap-2">
|
||||||
<span>
|
<span>
|
||||||
{position.mapped.x.toFixed(3)}, {position.mapped.y.toFixed(3)}
|
{position.cursor.mapped.x.toFixed(3)},
|
||||||
|
{position.cursor.mapped.y.toFixed(3)}
|
||||||
</span>
|
</span>
|
||||||
{#if status}
|
{#if status}
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user