minor adjustments to the control panel UI

This commit is contained in:
2026-08-30 02:13:01 +08:00
parent 902093d75f
commit 446f31fe49
3 changed files with 123 additions and 248 deletions
@@ -1,112 +0,0 @@
<script lang="ts">
import { friendName, friends } from "$lib/listeners/friends";
import {
liveMetadata,
liveMetadataListenerError,
} from "$lib/listeners/live-metadata";
import { onMount } from "svelte";
import PanelMessage from "./panel-message.svelte";
import type { RegisterPanel } from "./types";
let { register }: { register: RegisterPanel } = $props();
const actions = { apply: async () => true, reset: () => undefined };
onMount(() => register("activity", actions, { dirty: false, busy: false }));
function userIds() {
return [
...new Set([
$liveMetadata.localId,
...Object.keys($liveMetadata.cursorPositions),
...$liveMetadata.foregroundApps.keys(),
]),
].filter(Boolean);
}
function nameFor(id: string) {
if (id === $liveMetadata.localId) return "You";
return friendName(
$friends.find((friend) => friend.id === id),
"Unknown friend",
);
}
function compactId(id: string) {
return id.length > 18 ? `${id.slice(0, 9)}…${id.slice(-6)}` : id;
}
</script>
<div class="flex h-full min-h-0 flex-col gap-2">
{#if $liveMetadataListenerError}
<PanelMessage kind="error">{$liveMetadataListenerError}</PanelMessage>
{:else if userIds().length === 0}
<PanelMessage
>Waiting for activity data. This usually appears after Wyd has started
monitoring.</PanelMessage
>
{/if}
<div
class="min-h-0 flex-1 overflow-y-auto border border-base-300 bg-base-100"
>
{#each userIds() as userId (userId)}
{@const cursor = $liveMetadata.cursorPositions[userId]}
{@const app = $liveMetadata.foregroundApps.get(userId)}
<article class="border-b border-base-300 p-2 last:border-b-0">
<div class="flex items-center gap-2">
{#if app?.ico}
<img
class="pixelated size-8 border border-base-300 bg-base-200 p-0.5"
src={`data:image/png;base64,${app.ico}`}
alt=""
width="32"
height="32"
/>
{:else}
<div
class="grid size-8 shrink-0 place-content-center border border-base-300 bg-base-200 text-sm"
aria-hidden="true"
>
▣
</div>
{/if}
<div class="min-w-0 flex-1">
<div class="flex items-center gap-1.5">
<strong class="truncate text-xs">{nameFor(userId)}</strong>
{#if userId === $liveMetadata.localId}<span class="badge badge-xs"
>Local</span
>{/if}
</div>
<p
class="truncate font-mono text-[9px] text-base-content/55"
title={userId}
>
{compactId(userId)}
</p>
</div>
</div>
<dl
class="mt-2 grid grid-cols-[5rem_1fr] gap-x-2 gap-y-0.5 text-[10px]"
>
<dt class="text-base-content/60">Foreground app</dt>
<dd class="truncate">
{app?.local ?? app?.unlocal ?? "Not available"}
</dd>
<dt class="text-base-content/60">Cursor</dt>
<dd>
{cursor
? `${Math.round(cursor.raw.x)}, ${Math.round(cursor.raw.y)} px`
: "Not available"}
</dd>
<dt class="text-base-content/60">Mapped</dt>
<dd>
{cursor
? `${cursor.mapped.x.toFixed(3)}, ${cursor.mapped.y.toFixed(3)}`
: "Not available"}
</dd>
</dl>
</article>
{/each}
</div>
</div>
@@ -109,62 +109,57 @@
}
</script>
<div class="space-y-3">
<div class="space-y-1">
{#if error || $sceneConfigurationListenerError || $profileListenerError}
<PanelMessage kind="error">
{error || $sceneConfigurationListenerError || $profileListenerError}
</PanelMessage>
{/if}
<fieldset class="fieldset border border-base-300 bg-base-100 p-3">
<legend class="fieldset-legend px-1">Puppets</legend>
<div
class="grid grid-cols-[minmax(0,3fr)_minmax(0,4fr)] items-center gap-3"
>
<div class="flex flex-col gap-2">
<div class="aspect-square min-w-0">
{#if $profile}
<div
class="border border-primary relative shadow-[inset_0_0_10px] bg-primary/5 shadow-primary card"
>
<div class="grid grid-cols-[minmax(0,3fr)_minmax(0,4fr)] items-center gap-3">
<div class="flex flex-col gap-2">
<div class="aspect-square min-w-0 mt-2">
{#if $profile}
<div
class="border border-primary relative shadow-[inset_0_0_10px] bg-primary/5 shadow-primary card"
>
<div class="size-full absolute bg-gridded opacity-25 inset-0"></div>
<div class="size-full absolute">
<div
class="size-full absolute bg-gridded opacity-25 inset-0"
></div>
<div class="size-full absolute">
<div
class="flex flex-row size-full items-end justify-between text-[10px] text-primary p-1"
>
<div class="text-start flex flex-col">
<p>Scale</p>
<p>Opacity</p>
</div>
<div class="text-end flex flex-col">
<p>{(puppetScale * 100).toFixed(0)}%</p>
<p>{(puppetOpacity * 100).toFixed(0)}%</p>
</div>
class="flex flex-row size-full items-end justify-between text-[10px] text-primary p-1"
>
<div class="text-start flex flex-col">
<p>Scale</p>
<p>Opacity</p>
</div>
<div class="text-end flex flex-col">
<p>{(puppetScale * 100).toFixed(0)}%</p>
<p>{(puppetOpacity * 100).toFixed(0)}%</p>
</div>
</div>
<div class="size-full z-10">
<PuppetPreview
userId={$profile.id}
skinHash={$profile.skinHash}
skinSource={skinPreviewUrl}
scale={puppetScale}
opacity={puppetOpacity}
/>
</div>
</div>
{:else}
<div
class="skeleton aspect-square size-full"
aria-label="Loading puppet preview"
></div>
{/if}
</div>
<div class="size-full z-10">
<PuppetPreview
userId={$profile.id}
skinHash={$profile.skinHash}
skinSource={skinPreviewUrl}
scale={puppetScale}
opacity={puppetOpacity}
/>
</div>
</div>
{:else}
<div
class="skeleton aspect-square size-full"
aria-label="Loading puppet preview"
></div>
{/if}
</div>
</div>
<div class="min-w-0 space-y-2">
<div class="min-w-0 space-y-2">
<fieldset class="fieldset border border-base-300 bg-base-100 p-3 pt-1">
<legend class="fieldset-legend px-1">All puppets</legend>
<div>
<label class="fieldset-label" for="puppet-scale">Scale</label>
<input
@@ -196,98 +191,93 @@
aria-describedby="puppet-opacity-help"
/>
</div>
<div>
<label class="fieldset-label" for="puppet-skin">Custom Skin</label>
<div>
<button
class="btn w-full"
type="button"
disabled={!$profile || busy}
onclick={() => skinInput?.click()}
>
Choose file
</button>
<input
id="skin-file"
class="hidden"
type="file"
accept="image/png"
bind:this={skinInput}
onchange={(event) =>
selectSkinFile(event.currentTarget.files?.[0] ?? null)}
/>
</div>
</div>
</fieldset>
<div>
<button
class="btn w-full"
type="button"
disabled={!$profile || busy}
onclick={() => skinInput?.click()}
>
Choose a custom skin
</button>
<input
id="skin-file"
class="hidden"
type="file"
accept="image/png"
bind:this={skinInput}
onchange={(event) =>
selectSkinFile(event.currentTarget.files?.[0] ?? null)}
/>
</div>
</div>
<fieldset class="mt-4">
<legend class="fieldset-label mb-2">Movement</legend>
<div class="grid grid-cols-2 gap-2">
<label
class:border-primary={puppetMovementMode === "free"}
class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200"
</div>
<fieldset class="fieldset border border-base-300 bg-base-100 p-3 pt-1">
<legend class="fieldset-legend px-1">Puppets reposition</legend>
<div class="grid grid-cols-2 gap-2">
<label
class:border-primary={puppetMovementMode === "free"}
class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200"
>
<img
src="/puppet-movement-free.svg"
alt="Puppets moving freely across the full viewport"
class="w-full object-cover bg-linear-to-b from-base-100 to-base-300"
class:from-primary-content={puppetMovementMode === "free"}
/>
<span
class="card-body items-center gap-2 p-2 text-center"
class:bg-primary-content={puppetMovementMode === "free"}
class:text-primary={puppetMovementMode === "free"}
>
<img
src="/puppet-movement-free.svg"
alt="Puppets moving freely across the full viewport"
class="w-full object-cover bg-linear-to-b from-base-100 to-base-300"
class:from-primary-content={puppetMovementMode === "free"}
<span class="text-xs font-medium">Freeroam around</span>
<input
class="hidden"
type="radio"
name="puppet-movement-mode"
value="free"
checked={puppetMovementMode === "free"}
onchange={() => {
puppetMovementMode = "free";
updateDirty();
}}
disabled={busy}
/>
<span
class="card-body items-center gap-2 p-2 text-center"
class:bg-primary-content={puppetMovementMode === "free"}
class:text-primary={puppetMovementMode === "free"}
>
<span class="text-xs font-medium">Freeroam around</span>
<input
class="hidden"
type="radio"
name="puppet-movement-mode"
value="free"
checked={puppetMovementMode === "free"}
onchange={() => {
puppetMovementMode = "free";
updateDirty();
}}
disabled={busy}
/>
</span>
</label>
</span>
</label>
<label
class:border-primary={puppetMovementMode === "bottom"}
class:text-primary={puppetMovementMode === "bottom"}
class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200"
<label
class:border-primary={puppetMovementMode === "bottom"}
class:text-primary={puppetMovementMode === "bottom"}
class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200"
>
<img
src="/puppet-movement-bottom.svg"
alt="Puppets moving along the bottom of the viewport"
class="w-full object-cover bg-linear-to-b from-base-100 to-base-300"
class:from-primary-content={puppetMovementMode === "bottom"}
/>
<span
class="card-body items-center gap-2 p-2 text-center"
class:bg-primary-content={puppetMovementMode === "bottom"}
>
<img
src="/puppet-movement-bottom.svg"
alt="Puppets moving along the bottom of the viewport"
class="w-full object-cover bg-linear-to-b from-base-100 to-base-300"
class:from-primary-content={puppetMovementMode === "bottom"}
<span class="text-xs font-medium">Stay on the ground</span>
<input
class="hidden"
type="radio"
name="puppet-movement-mode"
value="bottom"
checked={puppetMovementMode === "bottom"}
onchange={() => {
puppetMovementMode = "bottom";
updateDirty();
}}
disabled={busy}
/>
<span
class="card-body items-center gap-2 p-2 text-center"
class:bg-primary-content={puppetMovementMode === "bottom"}
>
<span class="text-xs font-medium">Stay on the ground</span>
<input
class="hidden"
type="radio"
name="puppet-movement-mode"
value="bottom"
checked={puppetMovementMode === "bottom"}
onchange={() => {
puppetMovementMode = "bottom";
updateDirty();
}}
disabled={busy}
/>
</span>
</label>
</div>
</fieldset>
</span>
</label>
</div>
</fieldset>
</div>
+4 -7
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { getCurrentWindow } from "@tauri-apps/api/window";
import ActivityPanel from "$lib/components/control-panel/activity-panel.svelte";
import FriendsPanel from "$lib/components/control-panel/friends-panel.svelte";
import GeneralPanel from "$lib/components/control-panel/general-panel.svelte";
import NetworkPanel from "$lib/components/control-panel/network-panel.svelte";
@@ -11,22 +10,20 @@
} from "$lib/components/control-panel/types";
const tabs = [
{ id: "general", label: "General" },
{ id: "scene", label: "Scene" },
{ id: "general", label: "Account" },
{ id: "friends", label: "Friends" },
{ id: "network", label: "Network" },
{ id: "activity", label: "Activity" },
] as const;
type TabId = (typeof tabs)[number]["id"];
let activeTab = $state<TabId>("general");
let activeTab = $state<TabId>("scene");
let actions: Partial<Record<TabId, PanelActions>> = {};
let panelStates = $state<Record<TabId, PanelState>>({
general: { dirty: false, busy: false },
scene: { dirty: false, busy: false },
friends: { dirty: false, busy: false },
network: { dirty: false, busy: false },
activity: { dirty: false, busy: false },
});
let anyDirty = $derived(tabs.some((tab) => panelStates[tab.id].dirty));
let anyBusy = $derived(tabs.some((tab) => panelStates[tab.id].busy));
@@ -87,7 +84,7 @@
type="radio"
name="control_panel_tabs"
class="tab text-xs"
aria-label={`${tab.label}${panelStates[tab.id].dirty ? " *" : ""}`}
aria-label={tab.label}
value={tab.id}
bind:group={activeTab}
/>
@@ -106,7 +103,7 @@
{:else if tab.id === "network"}
<NetworkPanel {register} />
{:else}
<ActivityPanel {register} />
<p>You're not supposed to see this 👀</p>
{/if}
</div>
</div>