feat: add device mapping UI for playback compatibility

- Create DeviceMappingDialog component with compatibility checking
- Check actuator type compatibility between recorded and connected devices
- Auto-map devices by name with fallback to compatible devices
- Show device mapping dialog before playback starts
- Store and use device mappings during playback execution
- Update executeEvent to use mapped devices instead of name matching
- Validate all devices are mapped before starting playback

Features:
- Visual device pairing interface
- Compatibility badges showing actuator types
- Exact name match highlighting
- Auto-mapping with smart fallback
- Real-time mapping validation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Valknar XXX
2025-10-28 05:41:37 +01:00
parent 50ceda94b7
commit 9d9f72dec1
2 changed files with 224 additions and 6 deletions

View File

@@ -20,6 +20,7 @@ import { onMount } from "svelte";
import { goto } from "$app/navigation";
import DeviceCard from "$lib/components/device-card/device-card.svelte";
import RecordingSaveDialog from "./components/recording-save-dialog.svelte";
import DeviceMappingDialog from "./components/device-mapping-dialog.svelte";
import type { BluetoothDevice, RecordedEvent, DeviceInfo } from "$lib/types";
import { toast } from "svelte-sonner";
@@ -41,6 +42,8 @@ let playbackProgress = $state(0);
let playbackStartTime = $state<number | null>(null);
let playbackTimeoutId = $state<number | null>(null);
let currentEventIndex = $state(0);
let showMappingDialog = $state(false);
let deviceMappings = $state<Map<string, BluetoothDevice>>(new Map());
async function init() {
const connector = new ButtplugWasmClientConnector();
@@ -237,11 +240,26 @@ function handleCancelSave() {
// Playback functions
function startPlayback() {
if (!data.recording || devices.length === 0) {
if (!data.recording) {
return;
}
if (devices.length === 0) {
toast.error("Please connect devices before playing recording");
return;
}
// Check if we need to map devices
if (deviceMappings.size === 0 && data.recording.device_info.length > 0) {
showMappingDialog = true;
return;
}
// Start playback with existing mappings
beginPlayback();
}
function beginPlayback() {
isPlaying = true;
playbackStartTime = performance.now();
playbackProgress = 0;
@@ -249,6 +267,16 @@ function startPlayback() {
scheduleNextEvent();
}
function handleMappingConfirm(mappings: Map<string, BluetoothDevice>) {
deviceMappings = mappings;
showMappingDialog = false;
beginPlayback();
}
function handleMappingCancel() {
showMappingDialog = false;
}
function stopPlayback() {
isPlaying = false;
if (playbackTimeoutId !== null) {
@@ -309,19 +337,19 @@ function scheduleNextEvent() {
}
function executeEvent(event: RecordedEvent) {
// Find matching device by name
const device = devices.find(d => d.name === event.deviceName);
// Get mapped device
const device = deviceMappings.get(event.deviceName);
if (!device) {
console.warn(`Device not found: ${event.deviceName}`);
console.warn(`No device mapping for: ${event.deviceName}`);
return;
}
// Find matching actuator
// Find matching actuator by type
const scalarCmd = device.info.messageAttributes.ScalarCmd.find(
cmd => cmd.ActuatorType === event.actuatorType
);
if (!scalarCmd) {
console.warn(`Actuator not found: ${event.actuatorType} on ${device.name}`);
console.warn(`Actuator type ${event.actuatorType} not found on ${device.name}`);
return;
}
@@ -576,4 +604,15 @@ onMount(() => {
onSave={handleSaveRecording}
onCancel={handleCancelSave}
/>
<!-- Device Mapping Dialog -->
{#if data.recording}
<DeviceMappingDialog
open={showMappingDialog}
recordedDevices={data.recording.device_info}
connectedDevices={devices}
onConfirm={handleMappingConfirm}
onCancel={handleMappingCancel}
/>
{/if}
</div>