Add kiosk desktop app

Tauri app for the Pi. Media runs through the native LiveKit SDK because
WebKitGTK has no WebRTC, with frames encoded in Rust and drawn to a canvas
under the annotation overlay.

Claude-Session: https://claude.ai/code/session_01SS9F92jb51bMCRCKem6QtD
This commit is contained in:
SirBlob committed 2026-08-09 17:09:16 -04:00
1 parent 31d55fce86
commit a0cbd88fa4
85 files changed
+8330

No files matched your search

@@ -0,0 +1,7 @@
<script lang="ts">
import "../app.css";
let { children } = $props();
</script>
{@render children()}
+5
View File
@@ -0,0 +1,5 @@
// Tauri doesn't have a Node.js server to do proper SSR
// so we use adapter-static with a fallback to index.html to put the site in SPA mode
// See: https://svelte.dev/docs/kit/single-page-apps
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const ssr = false;
+117
View File
@@ -0,0 +1,117 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import {
AnnotationOverlay,
Logo,
NightSurface,
WhiteboardCanvas,
WidgetSurface
} from "@pistation/ui";
import { onDestroy, onMount } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import JoinBadge from "$lib/components/JoinBadge.svelte";
import TitleBar from "$lib/components/TitleBar.svelte";
import VideoCanvas from "$lib/components/VideoCanvas.svelte";
import { KioskController } from "$lib/kiosk.svelte";
let controller = $state<KioskController | null>(null);
let isKioskMode = $state(true);
const participantLabels = $derived(
new Map((controller?.participants ?? []).map((p) => [p.identity, p.displayName]))
);
onMount(() => {
void invoke<boolean>("is_kiosk_mode")
.then((value) => (isKioskMode = value))
.catch(() => (isKioskMode = true));
const instance = new KioskController();
controller = instance;
void instance.start();
});
onDestroy(() => {
void controller?.stop();
});
</script>
{#if controller}
{@const kiosk = controller}
{@const isPresenting = kiosk.mode === "presentation" && kiosk.isScreenActive}
<div class="flex h-screen w-screen flex-col overflow-hidden bg-surface-0">
{#if !isKioskMode}
<TitleBar status={kiosk.status} />
{/if}
<div class="relative min-h-0 flex-1 overflow-hidden">
<VideoCanvas visible={isPresenting} />
{#if kiosk.provisioningError}
<div class="flex h-full flex-col items-center justify-center gap-6 px-16 text-center">
<Logo size={96} />
<div>
<h1 class="text-3xl font-semibold">This kiosk is not set up yet</h1>
<p class="mt-3 max-w-2xl text-lg text-ink-2">{kiosk.provisioningError}</p>
</div>
<p class="text-sm text-ink-2">
Add the server URL and enrollment token to /boot/firmware/pistation.json and restart.
</p>
</div>
{:else if kiosk.mode === "whiteboard"}
<WhiteboardCanvas elements={kiosk.whiteboardElements} readOnly={true} />
{:else if isPresenting}
<AnnotationOverlay annotations={kiosk.annotations} labels={participantLabels} />
{:else if kiosk.layout && kiosk.isNight}
<NightSurface layout={kiosk.layout} pin={kiosk.pin} />
{:else if kiosk.layout}
<WidgetSurface
layout={kiosk.layout}
pin={kiosk.pin}
joinUrl={kiosk.joinUrl}
mediaBaseUrl={kiosk.mediaBaseUrl}
/>
{:else}
<div class="flex h-full items-center justify-center">
<Icon icon="ph:circle-notch-bold" width="48" class="animate-spin text-ink-2" />
</div>
{/if}
{#if (isPresenting || kiosk.mode === "whiteboard") && !kiosk.provisioningError}
<JoinBadge pin={kiosk.pin} joinUrl={kiosk.joinUrl} />
{/if}
{#if kiosk.participants.length > 0 && !kiosk.provisioningError}
<div
class="absolute bottom-6 left-6 flex items-center gap-2 bg-surface-1/90 px-4 py-2.5 text-ink-1"
>
<Icon icon="ph:users-bold" width="18" />
<span class="text-lg font-semibold text-ink-0">{kiosk.participants.length}</span>
<span class="text-sm">
{kiosk.participants.length === 1 ? "person connected" : "people connected"}
</span>
</div>
{/if}
{#if !kiosk.provisioningError && kiosk.status !== "connected" && kiosk.status !== "idle"}
<div class="absolute right-6 bottom-6 flex max-w-lg items-start gap-2 bg-surface-1 px-4 py-2">
<Icon
icon={kiosk.status === "connecting" ? "ph:circle-notch-bold" : "ph:wifi-slash-bold"}
width="18"
class={kiosk.status === "connecting" ? "mt-0.5 animate-spin text-ink-2" : "mt-0.5 text-danger"}
/>
<div class="min-w-0">
<span class="text-sm text-ink-1">
{kiosk.status === "connecting" ? "Connecting" : "Reconnecting"}
</span>
{#if kiosk.connectionError}
<p class="text-xs break-words text-ink-2">{kiosk.connectionError}</p>
{/if}
</div>
</div>
{/if}
</div>
</div>
{/if}