From e4fc97d8ce62f1c159694bf2f5e5790d23115e4e Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Tue, 3 Mar 2026 00:07:18 +0800 Subject: [PATCH] fullscreen headpat --- src/lib/constants/interaction.ts | 2 + src/routes/scene/+page.svelte | 67 +++++++++++++++++++ src/routes/scene/components/DesktopPet.svelte | 50 ++++++++------ .../scene/components/FullscreenModal.svelte | 33 +++++++++ src/routes/scene/components/PetMenu.svelte | 17 ++--- 5 files changed, 137 insertions(+), 32 deletions(-) create mode 100644 src/lib/constants/interaction.ts create mode 100644 src/routes/scene/components/FullscreenModal.svelte diff --git a/src/lib/constants/interaction.ts b/src/lib/constants/interaction.ts new file mode 100644 index 0000000..53c6d2c --- /dev/null +++ b/src/lib/constants/interaction.ts @@ -0,0 +1,2 @@ +export const INTERACTION_TYPE_HEADPAT = "headpat"; +export const INTERACTION_TYPE_TEXT = "text"; diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte index 0983138..c3623c7 100644 --- a/src/routes/scene/+page.svelte +++ b/src/routes/scene/+page.svelte @@ -12,6 +12,9 @@ } from "../../events/user-status"; import { invoke } from "@tauri-apps/api/core"; import DesktopPet from "./components/DesktopPet.svelte"; + import FullscreenModal from "./components/FullscreenModal.svelte"; + import { receivedInteractions, clearInteraction } from "$lib/stores/interaction-store"; + import { INTERACTION_TYPE_HEADPAT } from "$lib/constants/interaction"; import { listen } from "@tauri-apps/api/event"; import { onMount } from "svelte"; import type { PresenceStatus } from "../../types/bindings/PresenceStatus"; @@ -22,6 +25,64 @@ let isInteractive = $derived($sceneInteractive); + // Fullscreen modal state for headpats + let showFullscreenModal = $state(false); + let fullscreenImageSrc = $state(""); + let headpatSenderId = $state(null); + + // Queue for pending headpats (when modal is already showing) + let headpatQueue = $state>([]); + + // Process next headpat in queue + function processNextHeadpat() { + if (headpatQueue.length > 0) { + const next = headpatQueue.shift()!; + clearInteraction(next.userId); + fullscreenImageSrc = `data:image/gif;base64,${next.content}`; + headpatSenderId = next.userId; + showFullscreenModal = true; + } else { + fullscreenImageSrc = ""; + headpatSenderId = null; + } + } + + function getHeadpatSenderName(userId: string | null): string { + if (!userId) return ""; + const friend = getFriendById(userId); + return friend?.name ?? ""; + } + + // Watch for headpat interactions and show fullscreen modal + $effect(() => { + for (const [userId, interaction] of $receivedInteractions) { + if (interaction.type === INTERACTION_TYPE_HEADPAT) { + // Deduplicate: replace existing headpat from same user instead of queueing + const existingIndex = headpatQueue.findIndex((h) => h.userId === userId); + if (existingIndex >= 0) { + headpatQueue[existingIndex] = { userId, content: interaction.content }; + } else if (showFullscreenModal) { + headpatQueue.push({ userId, content: interaction.content }); + } else { + clearInteraction(userId); + fullscreenImageSrc = `data:image/gif;base64,${interaction.content}`; + headpatSenderId = userId; + showFullscreenModal = true; + } + } + } + }); + + // Clear headpat interaction when modal closes, then process next in queue + $effect(() => { + if (!showFullscreenModal && headpatSenderId) { + clearInteraction(headpatSenderId); + headpatSenderId = null; + // Process next headpat in queue + processNextHeadpat(); + } + }); + function getFriendById(userId: string) { const friend = $appData?.friends?.find((f) => f.friend?.id === userId); return friend?.friend; @@ -177,4 +238,10 @@ /> {/if} + + diff --git a/src/routes/scene/components/DesktopPet.svelte b/src/routes/scene/components/DesktopPet.svelte index 7ca6fae..7213d9c 100644 --- a/src/routes/scene/components/DesktopPet.svelte +++ b/src/routes/scene/components/DesktopPet.svelte @@ -9,6 +9,7 @@ receivedInteractions, clearInteraction, } from "$lib/stores/interaction-store"; + import { INTERACTION_TYPE_HEADPAT } from "$lib/constants/interaction"; import PetMenu from "./PetMenu.svelte"; import type { DollDto } from "../../../types/bindings/DollDto"; import type { UserBasicDto } from "../../../types/bindings/UserBasicDto"; @@ -43,29 +44,35 @@ const interaction = $receivedInteractions.get(user.id); if (interaction && interaction !== receivedInteraction) { receivedInteraction = interaction; - isPetMenuOpen = true; - // Make scene interactive so user can see it - invoke("set_scene_interactive", { - interactive: true, - shouldClick: false, - }); + // Headpats are handled at the page level via FullscreenModal instead of the pet menu. + // This provides a more prominent/fullscreen experience for headpat animations, + // while regular messages/bubbles are shown in the pet menu near the desktop pet. + if (interaction.type !== INTERACTION_TYPE_HEADPAT) { + isPetMenuOpen = true; - // Clear existing timer if any - if (messageTimer) clearTimeout(messageTimer); + // Make scene interactive so user can see it + invoke("set_scene_interactive", { + interactive: true, + shouldClick: false, + }); - // Auto-close and clear after 8 seconds - messageTimer = setTimeout(() => { - isPetMenuOpen = false; - receivedInteraction = undefined; - clearInteraction(user.id); - // We probably shouldn't disable interactivity globally here as other pets might be active, - // but 'set_pet_menu_state' in backend handles the window transparency logic per pet/menu. - // However, we did explicitly call set_scene_interactive(true). - // It might be safer to let the mouse-leave or other logic handle setting it back to false, - // or just leave it as is since the user might want to interact. - // For now, focusing on the message lifecycle. - }, 8000) as unknown as number; + // Clear existing timer if any + if (messageTimer) clearTimeout(messageTimer); + + // Auto-close and clear after 8 seconds + messageTimer = setTimeout(() => { + isPetMenuOpen = false; + receivedInteraction = undefined; + clearInteraction(user.id); + // We probably shouldn't disable interactivity globally here as other pets might be active, + // but 'set_pet_menu_state' in backend handles the window transparency logic per pet/menu. + // However, we did explicitly call set_scene_interactive(true). + // It might be safer to let the mouse-leave or other logic handle setting it back to false, + // or just leave it as is since the user might want to interact. + // For now, focusing on the message lifecycle. + }, 8000) as unknown as number; + } } } @@ -135,6 +142,9 @@ if (animationFrameId) { cancelAnimationFrame(animationFrameId); } + if (messageTimer) { + clearTimeout(messageTimer); + } }); diff --git a/src/routes/scene/components/FullscreenModal.svelte b/src/routes/scene/components/FullscreenModal.svelte new file mode 100644 index 0000000..830d1a9 --- /dev/null +++ b/src/routes/scene/components/FullscreenModal.svelte @@ -0,0 +1,33 @@ + + +{#if visible} +
+ {#if senderName} +
{senderName} gave you a headpat!
+ {/if} + Headpat +
+{/if} diff --git a/src/routes/scene/components/PetMenu.svelte b/src/routes/scene/components/PetMenu.svelte index 3a10280..4928305 100644 --- a/src/routes/scene/components/PetMenu.svelte +++ b/src/routes/scene/components/PetMenu.svelte @@ -5,6 +5,7 @@ import type { SendInteractionDto } from "../../../types/bindings/SendInteractionDto"; import type { UserStatus } from "../../../events/user-status"; import type { InteractionPayloadDto } from "../../../types/bindings/InteractionPayloadDto"; + import { INTERACTION_TYPE_HEADPAT } from "$lib/constants/interaction"; export let doll: DollDto; export let user: UserBasicDto; @@ -84,19 +85,11 @@ {/if} - {#if receivedInteraction} + {#if receivedInteraction && receivedInteraction.type !== INTERACTION_TYPE_HEADPAT}
- {#if receivedInteraction.type === "headpat"} - Headpat GIF - {:else} -
- {receivedInteraction.content} -
- {/if} +
+ {receivedInteraction.content} +
{:else if showMessageInput}