style: apply prettier formatting to all files
All checks were successful
Build and Push Backend Image / build (push) Successful in 46s
Build and Push Frontend Image / build (push) Successful in 5m12s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 22:27:54 +01:00
parent 18116072c9
commit efc7624ba3
184 changed files with 10327 additions and 10220 deletions

View File

@@ -1,19 +1,19 @@
import { getRecording } from "$lib/services";
export async function load({ locals, url, fetch }) {
const recordingId = url.searchParams.get("recording");
const recordingId = url.searchParams.get("recording");
let recording = null;
if (recordingId && locals.authStatus.authenticated) {
try {
recording = await getRecording(recordingId, fetch);
} catch (error) {
console.error("Failed to load recording:", error);
}
}
let recording = null;
if (recordingId && locals.authStatus.authenticated) {
try {
recording = await getRecording(recordingId, fetch);
} catch (error) {
console.error("Failed to load recording:", error);
}
}
return {
authStatus: locals.authStatus,
recording,
};
return {
authStatus: locals.authStatus,
recording,
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,171 +1,166 @@
<script lang="ts">
import { _ } from "svelte-i18n";
import { SvelteMap } from "svelte/reactivity";
import * as Dialog from "$lib/components/ui/dialog";
import Button from "$lib/components/ui/button/button.svelte";
import type { BluetoothDevice, DeviceInfo } from "$lib/types";
import { _ } from "svelte-i18n";
import { SvelteMap } from "svelte/reactivity";
import * as Dialog from "$lib/components/ui/dialog";
import Button from "$lib/components/ui/button/button.svelte";
import type { BluetoothDevice, DeviceInfo } from "$lib/types";
interface Props {
open: boolean;
recordedDevices: DeviceInfo[];
connectedDevices: BluetoothDevice[];
onConfirm: (mappings: Map<string, BluetoothDevice>) => void;
onCancel: () => void;
}
interface Props {
open: boolean;
recordedDevices: DeviceInfo[];
connectedDevices: BluetoothDevice[];
onConfirm: (mappings: Map<string, BluetoothDevice>) => void;
onCancel: () => void;
}
let { open, recordedDevices, connectedDevices, onConfirm, onCancel }: Props = $props();
let { open, recordedDevices, connectedDevices, onConfirm, onCancel }: Props = $props();
// Device mappings: recorded device name -> connected device
let mappings = new SvelteMap<string, BluetoothDevice>();
// Device mappings: recorded device name -> connected device
let mappings = new SvelteMap<string, BluetoothDevice>();
// Check if a connected device is compatible with a recorded device
function isCompatible(recordedDevice: DeviceInfo, connectedDevice: BluetoothDevice): boolean {
const connectedActuators = connectedDevice.actuators.map(
(a) => a.outputType,
);
// Check if a connected device is compatible with a recorded device
function isCompatible(recordedDevice: DeviceInfo, connectedDevice: BluetoothDevice): boolean {
const connectedActuators = connectedDevice.actuators.map((a) => a.outputType);
// Check if all required actuator types from recording exist on connected device
return recordedDevice.capabilities.every(requiredType =>
connectedActuators.includes(requiredType)
);
}
// Check if all required actuator types from recording exist on connected device
return recordedDevice.capabilities.every((requiredType) =>
connectedActuators.includes(requiredType),
);
}
// Get compatible devices for a recorded device
function getCompatibleDevices(recordedDevice: DeviceInfo): BluetoothDevice[] {
return connectedDevices.filter(device => isCompatible(recordedDevice, device));
}
// Get compatible devices for a recorded device
function getCompatibleDevices(recordedDevice: DeviceInfo): BluetoothDevice[] {
return connectedDevices.filter((device) => isCompatible(recordedDevice, device));
}
// Auto-map devices on open
$effect(() => {
if (open && recordedDevices.length > 0 && connectedDevices.length > 0) {
const newMappings = new SvelteMap<string, BluetoothDevice>();
// Auto-map devices on open
$effect(() => {
if (open && recordedDevices.length > 0 && connectedDevices.length > 0) {
const newMappings = new SvelteMap<string, BluetoothDevice>();
recordedDevices.forEach(recordedDevice => {
// Try to find exact name match first
let match = connectedDevices.find(d => d.name === recordedDevice.name);
recordedDevices.forEach((recordedDevice) => {
// Try to find exact name match first
let match = connectedDevices.find((d) => d.name === recordedDevice.name);
// If no exact match, find first compatible device
if (!match) {
const compatible = getCompatibleDevices(recordedDevice);
if (compatible.length > 0) {
match = compatible[0];
}
}
// If no exact match, find first compatible device
if (!match) {
const compatible = getCompatibleDevices(recordedDevice);
if (compatible.length > 0) {
match = compatible[0];
}
}
if (match) {
newMappings.set(recordedDevice.name, match);
}
});
if (match) {
newMappings.set(recordedDevice.name, match);
}
});
mappings = newMappings;
}
});
mappings = newMappings;
}
});
function handleConfirm() {
// Validate that all devices are mapped
const allMapped = recordedDevices.every(rd => mappings.has(rd.name));
if (!allMapped) {
return;
}
onConfirm(mappings);
}
function handleConfirm() {
// Validate that all devices are mapped
const allMapped = recordedDevices.every((rd) => mappings.has(rd.name));
if (!allMapped) {
return;
}
onConfirm(mappings);
}
function handleDeviceSelect(recordedDeviceName: string, deviceId: string) {
if (!deviceId) return;
function handleDeviceSelect(recordedDeviceName: string, deviceId: string) {
if (!deviceId) return;
const device = connectedDevices.find(d => d.id === deviceId);
if (device) {
const newMappings = new SvelteMap(mappings);
newMappings.set(recordedDeviceName, device);
mappings = newMappings;
}
}
const device = connectedDevices.find((d) => d.id === deviceId);
if (device) {
const newMappings = new SvelteMap(mappings);
newMappings.set(recordedDeviceName, device);
mappings = newMappings;
}
}
const allDevicesMapped = $derived(
recordedDevices.every(rd => mappings.has(rd.name))
);
const allDevicesMapped = $derived(recordedDevices.every((rd) => mappings.has(rd.name)));
</script>
<Dialog.Root {open}>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title>Map Devices for Playback</Dialog.Title>
<Dialog.Description>
Assign your connected devices to match the recorded devices. Only compatible devices are shown.
</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title>Map Devices for Playback</Dialog.Title>
<Dialog.Description>
Assign your connected devices to match the recorded devices. Only compatible devices are
shown.
</Dialog.Description>
</Dialog.Header>
<div class="space-y-4 py-4">
{#each recordedDevices as recordedDevice (recordedDevice.name)}
{@const compatibleDevices = getCompatibleDevices(recordedDevice)}
{@const currentMapping = mappings.get(recordedDevice.name)}
<div class="space-y-4 py-4">
{#each recordedDevices as recordedDevice (recordedDevice.name)}
{@const compatibleDevices = getCompatibleDevices(recordedDevice)}
{@const currentMapping = mappings.get(recordedDevice.name)}
<div class="flex items-center gap-4 p-4 bg-muted/30 rounded-lg border border-border/50">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="icon-[ri--router-line] w-5 h-5 text-primary"></span>
<h3 class="font-semibold text-card-foreground">{recordedDevice.name}</h3>
</div>
<div class="flex flex-wrap gap-1">
{#each recordedDevice.capabilities as capability (capability)}
<span class="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20">
{capability}
</span>
{/each}
</div>
</div>
<div class="flex items-center gap-4 p-4 bg-muted/30 rounded-lg border border-border/50">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="icon-[ri--router-line] w-5 h-5 text-primary"></span>
<h3 class="font-semibold text-card-foreground">{recordedDevice.name}</h3>
</div>
<div class="flex flex-wrap gap-1">
{#each recordedDevice.capabilities as capability (capability)}
<span
class="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20"
>
{capability}
</span>
{/each}
</div>
</div>
<div class="w-px h-12 bg-border"></div>
<div class="w-px h-12 bg-border"></div>
<div class="flex-1">
{#if compatibleDevices.length === 0}
<div class="flex items-center gap-2 text-destructive">
<span class="icon-[ri--error-warning-line] w-5 h-5"></span>
<span class="text-sm">No compatible devices</span>
</div>
{:else}
<select
class="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
value={currentMapping?.id || ''}
onchange={(e) => handleDeviceSelect(recordedDevice.name, e.currentTarget.value)}
>
<option value="" disabled>Select device...</option>
{#each compatibleDevices as device (device.name)}
<option value={device.id}>
{device.name}
{#if device.name === recordedDevice.name}(exact match){/if}
</option>
{/each}
</select>
{/if}
</div>
</div>
{/each}
<div class="flex-1">
{#if compatibleDevices.length === 0}
<div class="flex items-center gap-2 text-destructive">
<span class="icon-[ri--error-warning-line] w-5 h-5"></span>
<span class="text-sm">No compatible devices</span>
</div>
{:else}
<select
class="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
value={currentMapping?.id || ""}
onchange={(e) => handleDeviceSelect(recordedDevice.name, e.currentTarget.value)}
>
<option value="" disabled>Select device...</option>
{#each compatibleDevices as device (device.name)}
<option value={device.id}>
{device.name}
{#if device.name === recordedDevice.name}(exact match){/if}
</option>
{/each}
</select>
{/if}
</div>
</div>
{/each}
{#if recordedDevices.length === 0}
<div class="text-center py-8 text-muted-foreground">
No devices in this recording
</div>
{/if}
</div>
{#if recordedDevices.length === 0}
<div class="text-center py-8 text-muted-foreground">No devices in this recording</div>
{/if}
</div>
<Dialog.Footer class="flex gap-2">
<Button variant="outline" onclick={onCancel} class="cursor-pointer">
Cancel
</Button>
<Button
onclick={handleConfirm}
disabled={!allDevicesMapped}
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{#if !allDevicesMapped}
<span class="icon-[ri--error-warning-line] w-4 h-4 mr-2"></span>
Map All Devices
{:else}
<span class="icon-[ri--play-fill] w-4 h-4 mr-2"></span>
Start Playback
{/if}
</Button>
</Dialog.Footer>
</Dialog.Content>
<Dialog.Footer class="flex gap-2">
<Button variant="outline" onclick={onCancel} class="cursor-pointer">Cancel</Button>
<Button
onclick={handleConfirm}
disabled={!allDevicesMapped}
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{#if !allDevicesMapped}
<span class="icon-[ri--error-warning-line] w-4 h-4 mr-2"></span>
Map All Devices
{:else}
<span class="icon-[ri--play-fill] w-4 h-4 mr-2"></span>
Start Playback
{/if}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -1,174 +1,157 @@
<script lang="ts">
import { _ } from "svelte-i18n";
import * as Dialog from "$lib/components/ui/dialog";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Textarea } from "$lib/components/ui/textarea";
import { TagsInput } from "$lib/components/ui/tags-input";
import type { RecordedEvent, DeviceInfo } from "$lib/types";
import { _ } from "svelte-i18n";
import * as Dialog from "$lib/components/ui/dialog";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Textarea } from "$lib/components/ui/textarea";
import { TagsInput } from "$lib/components/ui/tags-input";
import type { RecordedEvent, DeviceInfo } from "$lib/types";
interface Props {
open: boolean;
events: RecordedEvent[];
deviceInfo: DeviceInfo[];
duration: number;
onSave: (data: {
title: string;
description: string;
tags: string[];
}) => Promise<void>;
onCancel: () => void;
}
interface Props {
open: boolean;
events: RecordedEvent[];
deviceInfo: DeviceInfo[];
duration: number;
onSave: (data: { title: string; description: string; tags: string[] }) => Promise<void>;
onCancel: () => void;
}
let { open, events, deviceInfo, duration, onSave, onCancel }: Props = $props();
let { open, events, deviceInfo, duration, onSave, onCancel }: Props = $props();
let title = $state("");
let description = $state("");
let tags = $state<string[]>([]);
let isSaving = $state(false);
let title = $state("");
let description = $state("");
let tags = $state<string[]>([]);
let isSaving = $state(false);
function formatDuration(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
function formatDuration(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
async function handleSave() {
if (!title.trim()) return;
async function handleSave() {
if (!title.trim()) return;
isSaving = true;
try {
await onSave({ title: title.trim(), description: description.trim(), tags });
// Reset form
title = "";
description = "";
tags = [];
} finally {
isSaving = false;
}
}
isSaving = true;
try {
await onSave({ title: title.trim(), description: description.trim(), tags });
// Reset form
title = "";
description = "";
tags = [];
} finally {
isSaving = false;
}
}
function handleCancel() {
title = "";
description = "";
tags = [];
onCancel();
}
function handleCancel() {
title = "";
description = "";
tags = [];
onCancel();
}
</script>
<Dialog.Root {open} onOpenChange={(isOpen) => !isOpen && handleCancel()}>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title>Save Recording</Dialog.Title>
<Dialog.Description>
Save your recording to view and play it later from your dashboard
</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title>Save Recording</Dialog.Title>
<Dialog.Description>
Save your recording to view and play it later from your dashboard
</Dialog.Description>
</Dialog.Header>
<div class="space-y-6 py-4">
<!-- Recording Stats -->
<div class="grid grid-cols-3 gap-4">
<div
class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30"
>
<span class="icon-[ri--time-line] w-5 h-5 text-primary mb-2"></span>
<span class="text-xs text-muted-foreground mb-1">Duration</span>
<span class="font-semibold">{formatDuration(duration)}</span>
</div>
<div
class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30"
>
<span class="icon-[ri--pulse-line] w-5 h-5 text-accent mb-2"></span>
<span class="text-xs text-muted-foreground mb-1">Events</span>
<span class="font-semibold">{events.length}</span>
</div>
<div
class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30"
>
<span class="icon-[ri--gamepad-line] w-5 h-5 text-primary mb-2"></span>
<span class="text-xs text-muted-foreground mb-1">Devices</span>
<span class="font-semibold">{deviceInfo.length}</span>
</div>
</div>
<div class="space-y-6 py-4">
<!-- Recording Stats -->
<div class="grid grid-cols-3 gap-4">
<div class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30">
<span class="icon-[ri--time-line] w-5 h-5 text-primary mb-2"></span>
<span class="text-xs text-muted-foreground mb-1">Duration</span>
<span class="font-semibold">{formatDuration(duration)}</span>
</div>
<div class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30">
<span class="icon-[ri--pulse-line] w-5 h-5 text-accent mb-2"></span>
<span class="text-xs text-muted-foreground mb-1">Events</span>
<span class="font-semibold">{events.length}</span>
</div>
<div class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30">
<span class="icon-[ri--gamepad-line] w-5 h-5 text-primary mb-2"></span>
<span class="text-xs text-muted-foreground mb-1">Devices</span>
<span class="font-semibold">{deviceInfo.length}</span>
</div>
</div>
<!-- Device Info -->
<div class="space-y-2">
<Label>Devices Used</Label>
{#each deviceInfo as device (device.name)}
<div
class="flex items-center gap-2 text-sm bg-muted/20 rounded px-3 py-2"
>
<span class="icon-[ri--rocket-line] w-4 h-4"></span>
<span class="font-medium">{device.name}</span>
<span class="text-muted-foreground text-xs">
{device.capabilities.join(", ")}
</span>
</div>
{/each}
</div>
<!-- Device Info -->
<div class="space-y-2">
<Label>Devices Used</Label>
{#each deviceInfo as device (device.name)}
<div class="flex items-center gap-2 text-sm bg-muted/20 rounded px-3 py-2">
<span class="icon-[ri--rocket-line] w-4 h-4"></span>
<span class="font-medium">{device.name}</span>
<span class="text-muted-foreground text-xs">
{device.capabilities.join(", ")}
</span>
</div>
{/each}
</div>
<!-- Form Fields -->
<div class="space-y-4">
<div class="space-y-2">
<Label for="title">Title *</Label>
<Input
id="title"
bind:value={title}
placeholder="My awesome pattern"
required
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<!-- Form Fields -->
<div class="space-y-4">
<div class="space-y-2">
<Label for="title">Title *</Label>
<Input
id="title"
bind:value={title}
placeholder="My awesome pattern"
required
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-2">
<Label for="description">Description</Label>
<Textarea
id="description"
bind:value={description}
placeholder="Describe your recording..."
rows={3}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-2">
<Label for="description">Description</Label>
<Textarea
id="description"
bind:value={description}
placeholder="Describe your recording..."
rows={3}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-2">
<Label for="tags">Tags</Label>
<TagsInput
id="tags"
bind:value={tags}
placeholder="Add tags..."
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
</div>
</div>
<div class="space-y-2">
<Label for="tags">Tags</Label>
<TagsInput
id="tags"
bind:value={tags}
placeholder="Add tags..."
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
</div>
</div>
<Dialog.Footer>
<Button
variant="outline"
onclick={handleCancel}
disabled={isSaving}
class="cursor-pointer"
>
Cancel
</Button>
<Button
onclick={handleSave}
disabled={!title.trim() || isSaving}
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{#if isSaving}
<div
class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"
></div>
Saving...
{:else}
<span class="icon-[ri--save-line] w-4 h-4 mr-2"></span>
Save Recording
{/if}
</Button>
</Dialog.Footer>
</Dialog.Content>
<Dialog.Footer>
<Button variant="outline" onclick={handleCancel} disabled={isSaving} class="cursor-pointer">
Cancel
</Button>
<Button
onclick={handleSave}
disabled={!title.trim() || isSaving}
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{#if isSaving}
<div
class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"
></div>
Saving...
{:else}
<span class="icon-[ri--save-line] w-4 h-4 mr-2"></span>
Save Recording
{/if}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>