event broadcasting & nuking foreground app listener

This commit is contained in:
2026-02-16 12:38:30 +08:00
parent c76e436529
commit 279ac11c0e
22 changed files with 114 additions and 973 deletions

View File

@@ -1,13 +1,13 @@
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { writable } from "svelte/store";
import type { AppMetadata } from "../types/bindings/AppMetadata";
import type { PresenceStatus } from "../types/bindings/PresenceStatus";
export type FriendUserStatus = {
appMetadata: AppMetadata;
export type UserStatus = {
presenceStatus: PresenceStatus;
state: "idle" | "resting";
};
export const friendsUserStatuses = writable<Record<string, FriendUserStatus>>({});
export const friendsUserStatuses = writable<Record<string, UserStatus>>({});
let unlistenStatus: UnlistenFn | null = null;
let unlistenFriendDisconnected: UnlistenFn | null = null;
@@ -29,52 +29,53 @@ export async function initUserStatusListeners() {
}
const userId = payload?.userId as string | undefined;
const status = payload?.status as FriendUserStatus | undefined;
const status = payload?.status as UserStatus | undefined;
if (!userId || !status) return;
if (!status.appMetadata) return;
if (!status.presenceStatus) return;
// Validate that appMetadata has at least one valid name
const hasValidName =
(typeof status.appMetadata.localized === "string" && status.appMetadata.localized.trim() !== "") ||
(typeof status.appMetadata.unlocalized === "string" && status.appMetadata.unlocalized.trim() !== "");
const hasValidName =
(typeof status.presenceStatus.title === "string" &&
status.presenceStatus.title.trim() !== "") ||
(typeof status.presenceStatus.subtitle === "string" &&
status.presenceStatus.subtitle.trim() !== "");
if (!hasValidName) return;
if (status.state !== "idle" && status.state !== "resting") return;
friendsUserStatuses.update((current) => ({
...current,
[userId]: {
appMetadata: status.appMetadata,
presenceStatus: status.presenceStatus,
state: status.state,
},
}));
});
unlistenFriendDisconnected = await listen<[{ userId: string }] | { userId: string } | string>(
"friend-disconnected",
(event) => {
let payload = event.payload as any;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (error) {
console.error("Failed to parse friend-disconnected payload", error);
return;
}
unlistenFriendDisconnected = await listen<
[{ userId: string }] | { userId: string } | string
>("friend-disconnected", (event) => {
let payload = event.payload as any;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (error) {
console.error("Failed to parse friend-disconnected payload", error);
return;
}
}
const data = Array.isArray(payload) ? payload[0] : payload;
const userId = data?.userId as string | undefined;
if (!userId) return;
const data = Array.isArray(payload) ? payload[0] : payload;
const userId = data?.userId as string | undefined;
if (!userId) return;
friendsUserStatuses.update((current) => {
const next = { ...current };
delete next[userId];
return next;
});
},
);
friendsUserStatuses.update((current) => {
const next = { ...current };
delete next[userId];
return next;
});
});
isListening = true;
} catch (error) {

View File

@@ -6,12 +6,15 @@
} from "../../events/cursor";
import { appData } from "../../events/app-data";
import { sceneInteractive } from "../../events/scene-interactive";
import { friendsUserStatuses } from "../../events/user-status";
import {
friendsUserStatuses,
type UserStatus,
} from "../../events/user-status";
import { invoke } from "@tauri-apps/api/core";
import DesktopPet from "./components/DesktopPet.svelte";
import { listen } from "@tauri-apps/api/event";
import { onMount } from "svelte";
import type { AppMetadata } from "../../types/bindings/AppMetadata";
import type { PresenceStatus } from "../../types/bindings/PresenceStatus";
import type { DollDto } from "../../types/bindings/DollDto";
let innerWidth = $state(0);
@@ -43,11 +46,12 @@
return $appData?.dolls?.find((d) => d.id === user.activeDollId);
}
let appMetadata: AppMetadata | null = $state(null);
let presenceStatus: PresenceStatus | null = $state(null);
onMount(() => {
const unlisten = listen<AppMetadata>("active-app-changed", (event) => {
appMetadata = event.payload;
const unlisten = listen<UserStatus>("user-status-changed", (event) => {
console.log("event received");
presenceStatus = event.payload.presenceStatus;
});
return () => {
@@ -89,14 +93,14 @@
</span>
<span class="font-mono text-xs badge py-3 flex items-center gap-2">
{#if appMetadata?.appIconB64}
{#if presenceStatus?.graphicsB64}
<img
src={`data:image/png;base64,${appMetadata.appIconB64}`}
src={`data:image/png;base64,${presenceStatus.graphicsB64}`}
alt="Active app icon"
class="size-4"
/>
{/if}
{appMetadata?.localized}
{presenceStatus?.title}
</span>
{#if Object.keys($friendsCursorPositions).length > 0}
@@ -115,15 +119,15 @@
{#if status}
<span class="flex items-center gap-1">
{status.state} in
{#if status.appMetadata.appIconB64}
{#if status.presenceStatus.graphicsB64}
<img
src={`data:image/png;base64,${status.appMetadata.appIconB64}`}
src={`data:image/png;base64,${status.presenceStatus.graphicsB64}`}
alt="Friend's active app icon"
class="size-4"
/>
{/if}
{status.appMetadata.localized ||
status.appMetadata.unlocalized}
{status.presenceStatus.title ||
status.presenceStatus.subtitle}
</span>
{/if}
</div>
@@ -163,8 +167,8 @@
username: $appData.user.username,
activeDoll: getUserDoll() ?? null,
}}
userStatus={appMetadata
? { appMetadata: appMetadata, state: "idle" }
userStatus={presenceStatus
? { presenceStatus: presenceStatus, state: "idle" }
: undefined}
doll={getUserDoll()}
isInteractive={false}

View File

@@ -12,14 +12,14 @@
import PetMenu from "./PetMenu.svelte";
import type { DollDto } from "../../../types/bindings/DollDto";
import type { UserBasicDto } from "../../../types/bindings/UserBasicDto";
import type { AppMetadata } from "../../../types/bindings/AppMetadata";
import type { FriendUserStatus } from "../../../events/user-status";
import type { PresenceStatus } from "../../../types/bindings/PresenceStatus";
import type { UserStatus } from "../../../events/user-status";
export let id = "";
export let targetX = 0;
export let targetY = 0;
export let user: UserBasicDto;
export let userStatus: FriendUserStatus | undefined = undefined;
export let userStatus: UserStatus | undefined = undefined;
export let doll: DollDto | undefined = undefined;
export let isInteractive = false;
@@ -160,9 +160,9 @@
{/if}
{#if userStatus}
<div class="absolute -top-5 left-0 right-0 w-max mx-auto">
{#if userStatus.appMetadata.appIconB64}
{#if userStatus.presenceStatus.graphicsB64}
<img
src={`data:image/png;base64,${userStatus.appMetadata.appIconB64}`}
src={`data:image/png;base64,${userStatus.presenceStatus.graphicsB64}`}
alt="Friend's active app icon"
class="size-4"
/>

View File

@@ -3,11 +3,11 @@
import { type DollDto } from "../../../types/bindings/DollDto";
import type { UserBasicDto } from "../../../types/bindings/UserBasicDto";
import type { SendInteractionDto } from "../../../types/bindings/SendInteractionDto";
import type { FriendUserStatus } from "../../../events/user-status";
import type { UserStatus } from "../../../events/user-status";
export let doll: DollDto;
export let user: UserBasicDto;
export let userStatus: FriendUserStatus | undefined = undefined;
export let userStatus: UserStatus | undefined = undefined;
export let receivedMessage: string | undefined = undefined;
let showMessageInput = false;
@@ -49,15 +49,15 @@
</div>
{#if userStatus}
<div class="card bg-base-200 px-2 py-1 flex flex-row gap-2 items-center">
{#if userStatus.appMetadata.appIconB64}
{#if userStatus.presenceStatus.graphicsB64}
<img
src={`data:image/png;base64,${userStatus.appMetadata.appIconB64}`}
src={`data:image/png;base64,${userStatus.presenceStatus.graphicsB64}`}
alt="Friend's active app icon"
class="size-3"
/>
{/if}
<p class="text-[0.6rem] font-mono text-ellipsis line-clamp-1">
{userStatus.appMetadata.localized}
{userStatus.presenceStatus.title}
</p>
</div>
{/if}

View File

@@ -1,6 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Metadata for the currently active application, including localized and unlocalized names, and an optional base64-encoded icon.
*/
export type AppMetadata = { localized: string | null, unlocalized: string | null, appIconB64: string | null, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UpdateUserDto = Record<string, never>;
export type PresenceStatus = { title: string | null, subtitle: string | null, graphicsB64: string | null, };