doll active state <-> doll visibility toggle
This commit is contained in:
@@ -29,6 +29,12 @@ async fn construct_app() {
|
||||
});
|
||||
|
||||
let init_ws = tauri::async_runtime::spawn(async {
|
||||
// init_ws_client calls get_access_token().await.
|
||||
// During a fresh login, this token might be in the process of being saved/refreshed
|
||||
// or the client initialization might be racing.
|
||||
// However, construct_app is called after auth success, so tokens should be there.
|
||||
// The issue might be that init_ws_client is idempotent but if called twice or early...
|
||||
// Actually, init_ws_client handles creating the socket.
|
||||
crate::services::ws::init_ws_client().await;
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::{lock_r, services::auth::with_auth, state::FDOLL};
|
||||
use crate::{lock_r, remotes::dolls::DollDto, services::auth::with_auth, state::FDOLL};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RemoteError {
|
||||
@@ -22,7 +22,7 @@ pub struct UserBasicDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub username: Option<String>,
|
||||
pub active_doll_id: Option<String>,
|
||||
pub active_doll: Option<DollDto>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use rust_socketio::{ClientBuilder, Payload, RawClient};
|
||||
use rust_socketio::{ClientBuilder, Event, Payload, RawClient};
|
||||
use serde_json::json;
|
||||
use tauri::{async_runtime, Emitter};
|
||||
use tracing::{error, info};
|
||||
@@ -39,9 +39,39 @@ impl WS_EVENT {
|
||||
pub const FRIEND_DOLL_CREATED: &str = "friend-doll-created";
|
||||
pub const FRIEND_DOLL_UPDATED: &str = "friend-doll-updated";
|
||||
pub const FRIEND_DOLL_DELETED: &str = "friend-doll-deleted";
|
||||
pub const FRIEND_ACTIVE_DOLL_CHANGED: &str = "friend-active-doll-changed";
|
||||
pub const DOLL_CREATED: &str = "doll_created";
|
||||
pub const DOLL_UPDATED: &str = "doll_updated";
|
||||
pub const DOLL_DELETED: &str = "doll_deleted";
|
||||
pub const CLIENT_INITIALIZE: &str = "client-initialize";
|
||||
pub const INITIALIZED: &str = "initialized";
|
||||
}
|
||||
|
||||
fn on_connected(_payload: Payload, socket: RawClient) {
|
||||
info!("WebSocket connected. Sending initialization request.");
|
||||
if let Err(e) = socket.emit(WS_EVENT::CLIENT_INITIALIZE, json!({})) {
|
||||
error!("Failed to emit client-initialize: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_initialized(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
if let Some(first_value) = values.first() {
|
||||
info!("Received initialized event: {:?}", first_value);
|
||||
|
||||
// Mark WebSocket as initialized
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if let Some(clients) = guard.clients.as_mut() {
|
||||
clients.is_ws_initialized = true;
|
||||
info!("WebSocket marked as initialized and ready for business");
|
||||
}
|
||||
} else {
|
||||
info!("Received initialized event with empty payload");
|
||||
}
|
||||
}
|
||||
_ => error!("Received unexpected payload format for initialized"),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_friend_request_received(payload: Payload, _socket: RawClient) {
|
||||
@@ -187,6 +217,29 @@ fn on_friend_doll_deleted(payload: Payload, _socket: RawClient) {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
if let Some(first_value) = values.first() {
|
||||
info!(
|
||||
"Received friend-active-doll-changed event: {:?}",
|
||||
first_value
|
||||
);
|
||||
if let Err(e) =
|
||||
get_app_handle().emit(WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED, first_value)
|
||||
{
|
||||
error!("Failed to emit friend-active-doll-changed event: {:?}", e);
|
||||
} else {
|
||||
info!("Emitted friend-active-doll-changed to frontend");
|
||||
}
|
||||
} else {
|
||||
info!("Received friend-active-doll-changed event with empty payload");
|
||||
}
|
||||
}
|
||||
_ => error!("Received unexpected payload format for friend-active-doll-changed"),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_doll_created(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
@@ -240,11 +293,15 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
||||
// and if clients are actually initialized.
|
||||
let client_opt = {
|
||||
let guard = lock_r!(FDOLL);
|
||||
guard
|
||||
.clients
|
||||
.as_ref()
|
||||
.and_then(|c| c.ws_client.as_ref())
|
||||
.cloned()
|
||||
if let Some(clients) = &guard.clients {
|
||||
if clients.is_ws_initialized {
|
||||
clients.ws_client.as_ref().cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(client) = client_opt {
|
||||
@@ -261,9 +318,7 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
||||
Err(e) => error!("Failed to execute blocking task for cursor report: {}", e),
|
||||
}
|
||||
} else {
|
||||
// Quietly fail if client is not ready (e.g. during startup/shutdown)
|
||||
// or debug log it.
|
||||
// debug!("WebSocket client not ready to report cursor data");
|
||||
// Quietly fail if client is not ready (e.g. during startup/shutdown or before initialization)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,9 +378,15 @@ pub async fn build_ws_client(
|
||||
.on(WS_EVENT::FRIEND_DOLL_CREATED, on_friend_doll_created)
|
||||
.on(WS_EVENT::FRIEND_DOLL_UPDATED, on_friend_doll_updated)
|
||||
.on(WS_EVENT::FRIEND_DOLL_DELETED, on_friend_doll_deleted)
|
||||
.on(
|
||||
WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED,
|
||||
on_friend_active_doll_changed,
|
||||
)
|
||||
.on(WS_EVENT::DOLL_CREATED, on_doll_created)
|
||||
.on(WS_EVENT::DOLL_UPDATED, on_doll_updated)
|
||||
.on(WS_EVENT::DOLL_DELETED, on_doll_deleted)
|
||||
.on(WS_EVENT::INITIALIZED, on_initialized)
|
||||
.on(Event::Connect, on_connected)
|
||||
.auth(json!({ "token": token }))
|
||||
.connect()
|
||||
})
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct OAuthFlowTracker {
|
||||
pub struct Clients {
|
||||
pub http_client: reqwest::Client,
|
||||
pub ws_client: Option<rust_socketio::client::Client>,
|
||||
pub is_ws_initialized: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -78,6 +79,7 @@ pub fn init_fdoll_state(tracing_guard: Option<tracing_appender::non_blocking::Wo
|
||||
guard.clients = Some(Clients {
|
||||
http_client,
|
||||
ws_client: None,
|
||||
is_ws_initialized: false,
|
||||
});
|
||||
info!("Initialized HTTP client");
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { writable } from "svelte/store";
|
||||
import type { CursorPositions } from "../types/bindings/CursorPositions";
|
||||
import type { CursorPosition } from "../types/bindings/CursorPosition";
|
||||
import type { DollDto } from "../types/bindings/DollDto";
|
||||
|
||||
export let cursorPositionOnScreen = writable<CursorPositions>({
|
||||
raw: { x: 0, y: 0 },
|
||||
@@ -24,13 +25,13 @@ type FriendCursorData = {
|
||||
// The exported store will only expose the position part to consumers,
|
||||
// but internally we manage the full data.
|
||||
// Actually, it's easier if we just export the positions and manage state internally.
|
||||
export let friendsCursorPositions = writable<Record<string, CursorPositions>>(
|
||||
{},
|
||||
);
|
||||
export let friendsCursorPositions = writable<Record<string, CursorPositions>>({});
|
||||
export let friendsActiveDolls = writable<Record<string, DollDto | null>>({});
|
||||
|
||||
let unlistenCursor: UnlistenFn | null = null;
|
||||
let unlistenFriendCursor: UnlistenFn | null = null;
|
||||
let unlistenFriendDisconnected: UnlistenFn | null = null;
|
||||
let unlistenFriendActiveDollChanged: UnlistenFn | null = null;
|
||||
let isListening = false;
|
||||
|
||||
// Internal state to track timestamps
|
||||
@@ -89,7 +90,10 @@ export async function initCursorTracking() {
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch (e) {
|
||||
console.error("[Cursor] Failed to parse friend disconnected payload:", e);
|
||||
console.error(
|
||||
"[Cursor] Failed to parse friend disconnected payload:",
|
||||
e,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -107,9 +111,75 @@ export async function initCursorTracking() {
|
||||
delete next[data.userId];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Listen to friend active doll changed events
|
||||
unlistenFriendActiveDollChanged = await listen<
|
||||
| string
|
||||
| {
|
||||
friendId: string;
|
||||
doll: DollDto | null;
|
||||
}
|
||||
>("friend-active-doll-changed", (event) => {
|
||||
let data = event.payload;
|
||||
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"[Cursor] Failed to parse friend-active-doll-changed payload:",
|
||||
e,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Cast to expected type after parsing
|
||||
const payload = data as { friendId: string; doll: DollDto | null };
|
||||
|
||||
console.log(
|
||||
"[Cursor] Received friend-active-doll-changed event:",
|
||||
payload,
|
||||
);
|
||||
|
||||
if (!payload.doll) {
|
||||
// If doll is null, it means the friend deactivated their doll.
|
||||
console.log(
|
||||
`[Cursor] Removing doll for friend ${payload.friendId} due to deactivation`,
|
||||
);
|
||||
|
||||
// Update the active dolls store to explicitly set this friend's doll to null
|
||||
// We MUST set it to null instead of deleting it, otherwise the UI might
|
||||
// fall back to the initial appData snapshot which might still have the old doll.
|
||||
friendsActiveDolls.update((current) => {
|
||||
const next = { ...current };
|
||||
next[payload.friendId] = null;
|
||||
return next;
|
||||
});
|
||||
|
||||
// Also remove from cursor positions so the sprite disappears
|
||||
friendsCursorPositions.update((current) => {
|
||||
const next = { ...current };
|
||||
delete next[payload.friendId];
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
// Update or add the new doll configuration
|
||||
console.log(
|
||||
`[Cursor] Updating doll for friend ${payload.friendId}:`,
|
||||
payload.doll,
|
||||
);
|
||||
friendsActiveDolls.update((current) => {
|
||||
return {
|
||||
...current,
|
||||
[payload.friendId]: payload.doll!,
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
isListening = true;
|
||||
} catch (err) {
|
||||
console.error("[Cursor] Failed to initialize cursor tracking:", err);
|
||||
@@ -134,6 +204,10 @@ export function stopCursorTracking() {
|
||||
unlistenFriendDisconnected();
|
||||
unlistenFriendDisconnected = null;
|
||||
}
|
||||
if (unlistenFriendActiveDollChanged) {
|
||||
unlistenFriendActiveDollChanged();
|
||||
unlistenFriendActiveDollChanged = null;
|
||||
}
|
||||
isListening = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import {
|
||||
cursorPositionOnScreen,
|
||||
friendsCursorPositions,
|
||||
friendsActiveDolls,
|
||||
} from "../../events/cursor";
|
||||
import { appData } from "../../events/app-data";
|
||||
|
||||
@@ -14,6 +15,18 @@
|
||||
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
||||
return friend ? friend.friend.name : userId.slice(0, 8) + "...";
|
||||
}
|
||||
|
||||
function getFriendDollConfig(userId: string) {
|
||||
// 1. Try to get from real-time store (most up-to-date)
|
||||
// Check if key exists to distinguish between "unknown" (undefined) and "no doll" (null)
|
||||
if (userId in $friendsActiveDolls) {
|
||||
return $friendsActiveDolls[userId]?.configuration;
|
||||
}
|
||||
|
||||
// 2. Fallback to initial app data (snapshot on load)
|
||||
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
||||
return friend?.friend.activeDoll?.configuration;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth bind:innerHeight />
|
||||
@@ -66,11 +79,16 @@
|
||||
<div class="absolute inset-0 size-full">
|
||||
{#if Object.keys($friendsCursorPositions).length > 0}
|
||||
{#each Object.entries($friendsCursorPositions) as [userId, position]}
|
||||
{@const config = getFriendDollConfig(userId)}
|
||||
{#if config}
|
||||
<DesktopPet
|
||||
targetX={position.mapped.x * innerWidth}
|
||||
targetY={position.mapped.y * innerHeight}
|
||||
name={getFriendName(userId)}
|
||||
bodyColor={config?.colorScheme?.body}
|
||||
outlineColor={config?.colorScheme?.outline}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import onekoGif from "../../assets/oneko/oneko.gif";
|
||||
|
||||
export let targetX = 0;
|
||||
export let targetY = 0;
|
||||
export let name = "";
|
||||
export let bodyColor: string | undefined = undefined;
|
||||
export let outlineColor: string | undefined = undefined;
|
||||
|
||||
let nekoPosX = 32;
|
||||
let nekoPosY = 32;
|
||||
@@ -18,6 +21,35 @@
|
||||
let animationFrameId: number;
|
||||
let lastFrameTimestamp: number;
|
||||
|
||||
let spriteSheetUrl = onekoGif; // Default to standard GIF
|
||||
let isCustomSprite = false;
|
||||
|
||||
// Watch for color changes to regenerate sprite
|
||||
$: if (bodyColor && outlineColor) {
|
||||
updateSpriteSheet(bodyColor, outlineColor);
|
||||
} else {
|
||||
// Revert to default if colors are missing
|
||||
spriteSheetUrl = onekoGif;
|
||||
isCustomSprite = false;
|
||||
}
|
||||
|
||||
async function updateSpriteSheet(body: string, outline: string) {
|
||||
try {
|
||||
const result = await invoke<string>("recolor_gif_base64", {
|
||||
whiteColorHex: body,
|
||||
blackColorHex: outline,
|
||||
applyTexture: true,
|
||||
});
|
||||
spriteSheetUrl = `data:image/gif;base64,${result}`;
|
||||
isCustomSprite = true;
|
||||
} catch (e) {
|
||||
console.error("Failed to recolor sprite:", e);
|
||||
// Fallback
|
||||
spriteSheetUrl = onekoGif;
|
||||
isCustomSprite = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Sprite constants from oneko.js
|
||||
const spriteSets: Record<string, [number, number][]> = {
|
||||
idle: [[-3, -3]],
|
||||
@@ -231,7 +263,7 @@
|
||||
style="
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-image: url({onekoGif});
|
||||
background-image: url('{spriteSheetUrl}');
|
||||
background-position: {currentSprite.x}px {currentSprite.y}px;
|
||||
"
|
||||
></div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DollDto } from "./DollDto";
|
||||
|
||||
export type UserBasicDto = { id: string, name: string, username: string | null, activeDollId: string | null, };
|
||||
export type UserBasicDto = { id: string, name: string, username: string | null, activeDoll: DollDto | null, };
|
||||
|
||||
Reference in New Issue
Block a user