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> </script>
<div class="space-y-3"> <div class="space-y-1">
{#if error || $sceneConfigurationListenerError || $profileListenerError} {#if error || $sceneConfigurationListenerError || $profileListenerError}
<PanelMessage kind="error"> <PanelMessage kind="error">
{error || $sceneConfigurationListenerError || $profileListenerError} {error || $sceneConfigurationListenerError || $profileListenerError}
</PanelMessage> </PanelMessage>
{/if} {/if}
<fieldset class="fieldset border border-base-300 bg-base-100 p-3"> <div class="grid grid-cols-[minmax(0,3fr)_minmax(0,4fr)] items-center gap-3">
<legend class="fieldset-legend px-1">Puppets</legend> <div class="flex flex-col gap-2">
<div class="aspect-square min-w-0 mt-2">
<div {#if $profile}
class="grid grid-cols-[minmax(0,3fr)_minmax(0,4fr)] items-center gap-3" <div
> class="border border-primary relative shadow-[inset_0_0_10px] bg-primary/5 shadow-primary card"
<div class="flex flex-col gap-2"> >
<div class="aspect-square min-w-0"> <div class="size-full absolute bg-gridded opacity-25 inset-0"></div>
{#if $profile} <div class="size-full absolute">
<div
class="border border-primary relative shadow-[inset_0_0_10px] bg-primary/5 shadow-primary card"
>
<div <div
class="size-full absolute bg-gridded opacity-25 inset-0" class="flex flex-row size-full items-end justify-between text-[10px] text-primary p-1"
></div> >
<div class="size-full absolute"> <div class="text-start flex flex-col">
<div <p>Scale</p>
class="flex flex-row size-full items-end justify-between text-[10px] text-primary p-1" <p>Opacity</p>
> </div>
<div class="text-start flex flex-col"> <div class="text-end flex flex-col">
<p>Scale</p> <p>{(puppetScale * 100).toFixed(0)}%</p>
<p>Opacity</p> <p>{(puppetOpacity * 100).toFixed(0)}%</p>
</div>
<div class="text-end flex flex-col">
<p>{(puppetScale * 100).toFixed(0)}%</p>
<p>{(puppetOpacity * 100).toFixed(0)}%</p>
</div>
</div> </div>
</div> </div>
<div class="size-full z-10">
<PuppetPreview
userId={$profile.id}
skinHash={$profile.skinHash}
skinSource={skinPreviewUrl}
scale={puppetScale}
opacity={puppetOpacity}
/>
</div>
</div> </div>
{:else} <div class="size-full z-10">
<div <PuppetPreview
class="skeleton aspect-square size-full" userId={$profile.id}
aria-label="Loading puppet preview" skinHash={$profile.skinHash}
></div> skinSource={skinPreviewUrl}
{/if} scale={puppetScale}
</div> opacity={puppetOpacity}
/>
</div>
</div>
{:else}
<div
class="skeleton aspect-square size-full"
aria-label="Loading puppet preview"
></div>
{/if}
</div> </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> <div>
<label class="fieldset-label" for="puppet-scale">Scale</label> <label class="fieldset-label" for="puppet-scale">Scale</label>
<input <input
@@ -196,98 +191,93 @@
aria-describedby="puppet-opacity-help" aria-describedby="puppet-opacity-help"
/> />
</div> </div>
</fieldset>
<div> <div>
<label class="fieldset-label" for="puppet-skin">Custom Skin</label> <button
<div> class="btn w-full"
<button type="button"
class="btn w-full" disabled={!$profile || busy}
type="button" onclick={() => skinInput?.click()}
disabled={!$profile || busy} >
onclick={() => skinInput?.click()} Choose a custom skin
> </button>
Choose file <input
</button> id="skin-file"
<input class="hidden"
id="skin-file" type="file"
class="hidden" accept="image/png"
type="file" bind:this={skinInput}
accept="image/png" onchange={(event) =>
bind:this={skinInput} selectSkinFile(event.currentTarget.files?.[0] ?? null)}
onchange={(event) => />
selectSkinFile(event.currentTarget.files?.[0] ?? null)}
/>
</div>
</div>
</div> </div>
</div> </div>
</div>
<fieldset class="mt-4"> <fieldset class="fieldset border border-base-300 bg-base-100 p-3 pt-1">
<legend class="fieldset-label mb-2">Movement</legend> <legend class="fieldset-legend px-1">Puppets reposition</legend>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<label <label
class:border-primary={puppetMovementMode === "free"} class:border-primary={puppetMovementMode === "free"}
class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200" 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 <span class="text-xs font-medium">Freeroam around</span>
src="/puppet-movement-free.svg" <input
alt="Puppets moving freely across the full viewport" class="hidden"
class="w-full object-cover bg-linear-to-b from-base-100 to-base-300" type="radio"
class:from-primary-content={puppetMovementMode === "free"} name="puppet-movement-mode"
value="free"
checked={puppetMovementMode === "free"}
onchange={() => {
puppetMovementMode = "free";
updateDirty();
}}
disabled={busy}
/> />
<span </span>
class="card-body items-center gap-2 p-2 text-center" </label>
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>
<label <label
class:border-primary={puppetMovementMode === "bottom"} class:border-primary={puppetMovementMode === "bottom"}
class:text-primary={puppetMovementMode === "bottom"} class:text-primary={puppetMovementMode === "bottom"}
class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200" 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 <span class="text-xs font-medium">Stay on the ground</span>
src="/puppet-movement-bottom.svg" <input
alt="Puppets moving along the bottom of the viewport" class="hidden"
class="w-full object-cover bg-linear-to-b from-base-100 to-base-300" type="radio"
class:from-primary-content={puppetMovementMode === "bottom"} name="puppet-movement-mode"
value="bottom"
checked={puppetMovementMode === "bottom"}
onchange={() => {
puppetMovementMode = "bottom";
updateDirty();
}}
disabled={busy}
/> />
<span </span>
class="card-body items-center gap-2 p-2 text-center" </label>
class:bg-primary-content={puppetMovementMode === "bottom"} </div>
>
<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>
</fieldset> </fieldset>
</div> </div>
+4 -7
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { getCurrentWindow } from "@tauri-apps/api/window"; 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 FriendsPanel from "$lib/components/control-panel/friends-panel.svelte";
import GeneralPanel from "$lib/components/control-panel/general-panel.svelte"; import GeneralPanel from "$lib/components/control-panel/general-panel.svelte";
import NetworkPanel from "$lib/components/control-panel/network-panel.svelte"; import NetworkPanel from "$lib/components/control-panel/network-panel.svelte";
@@ -11,22 +10,20 @@
} from "$lib/components/control-panel/types"; } from "$lib/components/control-panel/types";
const tabs = [ const tabs = [
{ id: "general", label: "General" },
{ id: "scene", label: "Scene" }, { id: "scene", label: "Scene" },
{ id: "general", label: "Account" },
{ id: "friends", label: "Friends" }, { id: "friends", label: "Friends" },
{ id: "network", label: "Network" }, { id: "network", label: "Network" },
{ id: "activity", label: "Activity" },
] as const; ] as const;
type TabId = (typeof tabs)[number]["id"]; type TabId = (typeof tabs)[number]["id"];
let activeTab = $state<TabId>("general"); let activeTab = $state<TabId>("scene");
let actions: Partial<Record<TabId, PanelActions>> = {}; let actions: Partial<Record<TabId, PanelActions>> = {};
let panelStates = $state<Record<TabId, PanelState>>({ let panelStates = $state<Record<TabId, PanelState>>({
general: { dirty: false, busy: false }, general: { dirty: false, busy: false },
scene: { dirty: false, busy: false }, scene: { dirty: false, busy: false },
friends: { dirty: false, busy: false }, friends: { dirty: false, busy: false },
network: { 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 anyDirty = $derived(tabs.some((tab) => panelStates[tab.id].dirty));
let anyBusy = $derived(tabs.some((tab) => panelStates[tab.id].busy)); let anyBusy = $derived(tabs.some((tab) => panelStates[tab.id].busy));
@@ -87,7 +84,7 @@
type="radio" type="radio"
name="control_panel_tabs" name="control_panel_tabs"
class="tab text-xs" class="tab text-xs"
aria-label={`${tab.label}${panelStates[tab.id].dirty ? " *" : ""}`} aria-label={tab.label}
value={tab.id} value={tab.id}
bind:group={activeTab} bind:group={activeTab}
/> />
@@ -106,7 +103,7 @@
{:else if tab.id === "network"} {:else if tab.id === "network"}
<NetworkPanel {register} /> <NetworkPanel {register} />
{:else} {:else}
<ActivityPanel {register} /> <p>You're not supposed to see this 👀</p>
{/if} {/if}
</div> </div>
</div> </div>