Recordings were just a thin named pointer over an already-captured session's events, so the whole separate feature (recordings table, API routes, pages, UI) is gone: any completed session can now be named and replayed directly. Replaying no longer creates a session or duplicates events of its own - it just bumps the source session's playCount/lastPlayedAt. Also renames play_sessions/playSession(s) to sessions/session(s) throughout the schema, queries, API routes, and UI for consistency, and updates the README to match the new flow. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { autoMapDeviceSlots } from "@/lib/buttplug/device-remap";
|
|
import type { ReplayDeviceSlot, ConnectedDeviceInfo } from "@/lib/buttplug/types";
|
|
|
|
interface DeviceRemapDialogProps {
|
|
open: boolean;
|
|
deviceSlots: ReplayDeviceSlot[];
|
|
connectedDevices: ConnectedDeviceInfo[];
|
|
onCancel: () => void;
|
|
onConfirm: (mapping: Map<number, number>) => void;
|
|
}
|
|
|
|
export function DeviceRemapDialog({
|
|
open,
|
|
deviceSlots,
|
|
connectedDevices,
|
|
onCancel,
|
|
onConfirm,
|
|
}: DeviceRemapDialogProps) {
|
|
const autoMapped = autoMapDeviceSlots(deviceSlots, connectedDevices);
|
|
const [assignments, setAssignments] = useState<Record<number, number | null>>(
|
|
Object.fromEntries(deviceSlots.map((slot, i) => [slot.sourceSessionDeviceId, autoMapped[i]?.matchedDeviceIndex ?? null])),
|
|
);
|
|
|
|
const allAssigned = deviceSlots.every((s) => assignments[s.sourceSessionDeviceId] !== null);
|
|
|
|
function handleConfirm() {
|
|
const mapping = new Map<number, number>();
|
|
for (const slot of deviceSlots) {
|
|
const deviceIndex = assignments[slot.sourceSessionDeviceId];
|
|
if (deviceIndex !== null && deviceIndex !== undefined) mapping.set(slot.sourceSessionDeviceId, deviceIndex);
|
|
}
|
|
onConfirm(mapping);
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(o) => !o && onCancel()}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Match devices for replay</DialogTitle>
|
|
<DialogDescription>
|
|
Web Bluetooth doesn't expose a stable device id across sessions, so match each recorded
|
|
device to a currently-connected one. Two identically-named devices can't be told apart
|
|
automatically.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="flex flex-col gap-3">
|
|
{deviceSlots.map((slot) => (
|
|
<div key={slot.sourceSessionDeviceId} className="flex items-center justify-between gap-3">
|
|
<span className="text-sm font-medium">{slot.slotLabel}</span>
|
|
<Select
|
|
value={assignments[slot.sourceSessionDeviceId]?.toString() ?? undefined}
|
|
onValueChange={(v) =>
|
|
setAssignments((prev) => ({ ...prev, [slot.sourceSessionDeviceId]: Number(v) }))
|
|
}
|
|
>
|
|
<SelectTrigger size="sm" className="w-48">
|
|
<SelectValue placeholder="Unmatched" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{connectedDevices.map((d) => (
|
|
<SelectItem key={d.index} value={d.index.toString()}>
|
|
{d.displayName ?? d.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="ghost" onClick={onCancel}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleConfirm} disabled={!allAssigned}>
|
|
Start replay
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|