fullscreen headpat
This commit is contained in:
2
src/lib/constants/interaction.ts
Normal file
2
src/lib/constants/interaction.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const INTERACTION_TYPE_HEADPAT = "headpat";
|
||||
export const INTERACTION_TYPE_TEXT = "text";
|
||||
@@ -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<string | null>(null);
|
||||
|
||||
// Queue for pending headpats (when modal is already showing)
|
||||
let headpatQueue = $state<Array<{ userId: string; content: string }>>([]);
|
||||
|
||||
// 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}
|
||||
</div>
|
||||
|
||||
<FullscreenModal
|
||||
bind:visible={showFullscreenModal}
|
||||
imageSrc={fullscreenImageSrc}
|
||||
senderName={getHeadpatSenderName(headpatSenderId)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
33
src/routes/scene/components/FullscreenModal.svelte
Normal file
33
src/routes/scene/components/FullscreenModal.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
imageSrc,
|
||||
visible = $bindable(false),
|
||||
senderName = "",
|
||||
}: { imageSrc: string; visible: boolean; senderName?: string } = $props();
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
$effect(() => {
|
||||
if (visible) {
|
||||
timer = setTimeout(() => {
|
||||
visible = false;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div class="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black/80">
|
||||
{#if senderName}
|
||||
<div class="mb-4 text-white text-lg font-medium">{senderName} gave you a headpat!</div>
|
||||
{/if}
|
||||
<img src={imageSrc} alt="Headpat" class="max-w-full max-h-full object-contain" />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -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 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if receivedInteraction}
|
||||
{#if receivedInteraction && receivedInteraction.type !== INTERACTION_TYPE_HEADPAT}
|
||||
<div class="">
|
||||
{#if receivedInteraction.type === "headpat"}
|
||||
<img
|
||||
src={`data:image/gif;base64,${receivedInteraction.content}`}
|
||||
alt="Headpat GIF"
|
||||
class="max-w-[140px] h-auto"
|
||||
/>
|
||||
{:else}
|
||||
<div class="text-sm max-w-[140px]">
|
||||
{receivedInteraction.content}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-sm max-w-[140px]">
|
||||
{receivedInteraction.content}
|
||||
</div>
|
||||
</div>
|
||||
{:else if showMessageInput}
|
||||
<div class="flex flex-col gap-1">
|
||||
|
||||
Reference in New Issue
Block a user