2025-10-25 22:04:41 +02:00
|
|
|
<script lang="ts">
|
|
|
|
|
import { _ } from "svelte-i18n";
|
|
|
|
|
import Meta from "$lib/components/meta/meta.svelte";
|
|
|
|
|
import {
|
|
|
|
|
ButtplugClient,
|
|
|
|
|
ButtplugMessage,
|
|
|
|
|
ButtplugWasmClientConnector,
|
|
|
|
|
DeviceList,
|
|
|
|
|
SensorReadCmd,
|
|
|
|
|
StopDeviceCmd,
|
|
|
|
|
SensorReading,
|
|
|
|
|
ScalarCmd,
|
|
|
|
|
ScalarSubcommand,
|
|
|
|
|
ButtplugDeviceMessage,
|
|
|
|
|
ButtplugClientDevice,
|
|
|
|
|
SensorType,
|
|
|
|
|
} from "@sexy.pivoine.art/buttplug";
|
|
|
|
|
import Button from "$lib/components/ui/button/button.svelte";
|
|
|
|
|
import { onMount } from "svelte";
|
|
|
|
|
import { goto } from "$app/navigation";
|
|
|
|
|
import DeviceCard from "$lib/components/device-card/device-card.svelte";
|
feat: add buttplug device recording feature (Phase 1 & 2)
Implemented complete infrastructure for recording, saving, and managing
buttplug device patterns with precise event timing.
**Phase 1: Backend & Infrastructure**
- Added Directus schema for sexy_recordings collection with all fields
(id, status, user_created, title, description, slug, duration, events,
device_info, tags, linked_video, featured, public)
- Created REST API endpoints in bundle extension:
* GET /sexy/recordings - list user recordings with filtering
* GET /sexy/recordings/:id - get single recording
* POST /sexy/recordings - create new recording with validation
* PATCH /sexy/recordings/:id - update recording (owner only)
* DELETE /sexy/recordings/:id - soft delete by archiving
- Added TypeScript types: RecordedEvent, DeviceInfo, Recording
- Created frontend services: getRecordings(), deleteRecording()
- Built RecordingCard component with stats, device info, and actions
- Added Recordings tab to /me dashboard page with grid layout
- Added i18n translations for recordings UI
**Phase 2: Recording Capture**
- Implemented recording state management in /play page
- Added Start/Stop Recording buttons with visual indicators
- Capture device events with precise timestamps during recording
- Normalize actuator values (0-100) for cross-device compatibility
- Created RecordingSaveDialog component with:
* Recording stats display (duration, events, devices)
* Form inputs (title, description, tags)
* Device information preview
- Integrated save recording API call from play page
- Added success/error toast notifications
- Automatic event filtering during recording
**Technical Details**
- Events stored as JSON array with timestamp, deviceIndex, deviceName,
actuatorIndex, actuatorType, and normalized value
- Device metadata includes name, index, and capability list
- Slug auto-generated from title for SEO-friendly URLs
- Status workflow: draft → published → archived
- Permission checks: users can only access own recordings or public ones
- Frontend uses performance.now() for millisecond precision timing
**User Flow**
1. User scans and connects devices on /play page
2. Clicks "Start Recording" to begin capturing events
3. Manipulates device sliders - all changes are recorded
4. Clicks "Stop Recording" to end capture
5. Save dialog appears with recording preview and form
6. User enters title, description, tags and saves
7. Recording appears in dashboard /me Recordings tab
8. Can play back, edit, or delete recordings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 04:05:09 +01:00
|
|
|
import RecordingSaveDialog from "./components/recording-save-dialog.svelte";
|
|
|
|
|
import type { BluetoothDevice, RecordedEvent, DeviceInfo } from "$lib/types";
|
|
|
|
|
import { toast } from "svelte-sonner";
|
|
|
|
|
import { customEndpoint } from "@directus/sdk";
|
|
|
|
|
import { getDirectusInstance } from "$lib/directus";
|
2025-10-25 22:04:41 +02:00
|
|
|
|
|
|
|
|
const client = new ButtplugClient("Sexy.Art");
|
|
|
|
|
let connected = $state(client.connected);
|
|
|
|
|
let scanning = $state(false);
|
|
|
|
|
let devices = $state<BluetoothDevice[]>([]);
|
|
|
|
|
|
feat: add buttplug device recording feature (Phase 1 & 2)
Implemented complete infrastructure for recording, saving, and managing
buttplug device patterns with precise event timing.
**Phase 1: Backend & Infrastructure**
- Added Directus schema for sexy_recordings collection with all fields
(id, status, user_created, title, description, slug, duration, events,
device_info, tags, linked_video, featured, public)
- Created REST API endpoints in bundle extension:
* GET /sexy/recordings - list user recordings with filtering
* GET /sexy/recordings/:id - get single recording
* POST /sexy/recordings - create new recording with validation
* PATCH /sexy/recordings/:id - update recording (owner only)
* DELETE /sexy/recordings/:id - soft delete by archiving
- Added TypeScript types: RecordedEvent, DeviceInfo, Recording
- Created frontend services: getRecordings(), deleteRecording()
- Built RecordingCard component with stats, device info, and actions
- Added Recordings tab to /me dashboard page with grid layout
- Added i18n translations for recordings UI
**Phase 2: Recording Capture**
- Implemented recording state management in /play page
- Added Start/Stop Recording buttons with visual indicators
- Capture device events with precise timestamps during recording
- Normalize actuator values (0-100) for cross-device compatibility
- Created RecordingSaveDialog component with:
* Recording stats display (duration, events, devices)
* Form inputs (title, description, tags)
* Device information preview
- Integrated save recording API call from play page
- Added success/error toast notifications
- Automatic event filtering during recording
**Technical Details**
- Events stored as JSON array with timestamp, deviceIndex, deviceName,
actuatorIndex, actuatorType, and normalized value
- Device metadata includes name, index, and capability list
- Slug auto-generated from title for SEO-friendly URLs
- Status workflow: draft → published → archived
- Permission checks: users can only access own recordings or public ones
- Frontend uses performance.now() for millisecond precision timing
**User Flow**
1. User scans and connects devices on /play page
2. Clicks "Start Recording" to begin capturing events
3. Manipulates device sliders - all changes are recorded
4. Clicks "Stop Recording" to end capture
5. Save dialog appears with recording preview and form
6. User enters title, description, tags and saves
7. Recording appears in dashboard /me Recordings tab
8. Can play back, edit, or delete recordings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 04:05:09 +01:00
|
|
|
// Recording state
|
|
|
|
|
let isRecording = $state(false);
|
|
|
|
|
let recordingStartTime = $state<number | null>(null);
|
|
|
|
|
let recordedEvents = $state<RecordedEvent[]>([]);
|
|
|
|
|
let showSaveDialog = $state(false);
|
|
|
|
|
let recordingDuration = $state(0);
|
|
|
|
|
|
2025-10-25 22:04:41 +02:00
|
|
|
async function init() {
|
|
|
|
|
const connector = new ButtplugWasmClientConnector();
|
|
|
|
|
// await ButtplugWasmClientConnector.activateLogging("info");
|
|
|
|
|
await client.connect(connector);
|
|
|
|
|
client.on("deviceadded", onDeviceAdded);
|
|
|
|
|
client.on("deviceremoved", (msg: ButtplugDeviceMessage) =>
|
|
|
|
|
devices.splice(msg.DeviceIndex, 1),
|
|
|
|
|
);
|
|
|
|
|
client.on("scanningfinished", () => (scanning = false));
|
|
|
|
|
connector.on("message", handleMessages);
|
|
|
|
|
connected = client.connected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function startScanning() {
|
|
|
|
|
await client.startScanning();
|
|
|
|
|
scanning = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onDeviceAdded(
|
|
|
|
|
msg: ButtplugDeviceMessage,
|
|
|
|
|
dev: ButtplugClientDevice,
|
|
|
|
|
) {
|
|
|
|
|
const device = convertDevice(dev);
|
|
|
|
|
devices.push(device);
|
|
|
|
|
|
|
|
|
|
const cmds = device.info.messageAttributes.SensorReadCmd;
|
|
|
|
|
|
|
|
|
|
cmds?.forEach(async (cmd) => {
|
|
|
|
|
await client.sendDeviceMessage(
|
|
|
|
|
{ index: device.info.index },
|
|
|
|
|
new SensorReadCmd(device.info.index, cmd.Index, cmd.SensorType),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleMessages(messages: ButtplugMessage[]) {
|
|
|
|
|
messages.forEach(async (msg) => {
|
|
|
|
|
await handleMessage(msg);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleMessage(msg: ButtplugMessage) {
|
|
|
|
|
if (msg instanceof SensorReading) {
|
|
|
|
|
const device = devices[msg.DeviceIndex];
|
|
|
|
|
if (msg.SensorType === SensorType.Battery) {
|
|
|
|
|
device.batteryLevel = msg.Data[0];
|
|
|
|
|
}
|
|
|
|
|
device.sensorValues[msg.Index] = msg.Data[0];
|
|
|
|
|
device.lastSeen = new Date();
|
|
|
|
|
} else if (msg instanceof DeviceList) {
|
|
|
|
|
devices = client.devices.map(convertDevice);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleChange(
|
|
|
|
|
device: BluetoothDevice,
|
|
|
|
|
scalarIndex: number,
|
|
|
|
|
value: number,
|
|
|
|
|
) {
|
|
|
|
|
const vibrateCmd = device.info.messageAttributes.ScalarCmd[scalarIndex];
|
|
|
|
|
await client.sendDeviceMessage(
|
|
|
|
|
{ index: device.info.index },
|
|
|
|
|
new ScalarCmd(
|
|
|
|
|
[
|
|
|
|
|
new ScalarSubcommand(
|
|
|
|
|
vibrateCmd.Index,
|
|
|
|
|
(device.actuatorValues[scalarIndex] = value),
|
|
|
|
|
vibrateCmd.ActuatorType,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
device.info.index,
|
|
|
|
|
),
|
|
|
|
|
);
|
feat: add buttplug device recording feature (Phase 1 & 2)
Implemented complete infrastructure for recording, saving, and managing
buttplug device patterns with precise event timing.
**Phase 1: Backend & Infrastructure**
- Added Directus schema for sexy_recordings collection with all fields
(id, status, user_created, title, description, slug, duration, events,
device_info, tags, linked_video, featured, public)
- Created REST API endpoints in bundle extension:
* GET /sexy/recordings - list user recordings with filtering
* GET /sexy/recordings/:id - get single recording
* POST /sexy/recordings - create new recording with validation
* PATCH /sexy/recordings/:id - update recording (owner only)
* DELETE /sexy/recordings/:id - soft delete by archiving
- Added TypeScript types: RecordedEvent, DeviceInfo, Recording
- Created frontend services: getRecordings(), deleteRecording()
- Built RecordingCard component with stats, device info, and actions
- Added Recordings tab to /me dashboard page with grid layout
- Added i18n translations for recordings UI
**Phase 2: Recording Capture**
- Implemented recording state management in /play page
- Added Start/Stop Recording buttons with visual indicators
- Capture device events with precise timestamps during recording
- Normalize actuator values (0-100) for cross-device compatibility
- Created RecordingSaveDialog component with:
* Recording stats display (duration, events, devices)
* Form inputs (title, description, tags)
* Device information preview
- Integrated save recording API call from play page
- Added success/error toast notifications
- Automatic event filtering during recording
**Technical Details**
- Events stored as JSON array with timestamp, deviceIndex, deviceName,
actuatorIndex, actuatorType, and normalized value
- Device metadata includes name, index, and capability list
- Slug auto-generated from title for SEO-friendly URLs
- Status workflow: draft → published → archived
- Permission checks: users can only access own recordings or public ones
- Frontend uses performance.now() for millisecond precision timing
**User Flow**
1. User scans and connects devices on /play page
2. Clicks "Start Recording" to begin capturing events
3. Manipulates device sliders - all changes are recorded
4. Clicks "Stop Recording" to end capture
5. Save dialog appears with recording preview and form
6. User enters title, description, tags and saves
7. Recording appears in dashboard /me Recordings tab
8. Can play back, edit, or delete recordings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 04:05:09 +01:00
|
|
|
|
|
|
|
|
// Capture event if recording
|
|
|
|
|
if (isRecording && recordingStartTime) {
|
|
|
|
|
captureEvent(device, scalarIndex, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startRecording() {
|
|
|
|
|
if (devices.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
isRecording = true;
|
|
|
|
|
recordingStartTime = performance.now();
|
|
|
|
|
recordedEvents = [];
|
|
|
|
|
recordingDuration = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stopRecording() {
|
|
|
|
|
isRecording = false;
|
|
|
|
|
if (recordedEvents.length > 0) {
|
|
|
|
|
recordingDuration = recordedEvents[recordedEvents.length - 1].timestamp;
|
|
|
|
|
showSaveDialog = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function captureEvent(
|
|
|
|
|
device: BluetoothDevice,
|
|
|
|
|
scalarIndex: number,
|
|
|
|
|
value: number,
|
|
|
|
|
) {
|
|
|
|
|
if (!recordingStartTime) return;
|
|
|
|
|
|
|
|
|
|
const timestamp = performance.now() - recordingStartTime;
|
|
|
|
|
const scalarCmd = device.info.messageAttributes.ScalarCmd[scalarIndex];
|
|
|
|
|
|
|
|
|
|
recordedEvents.push({
|
|
|
|
|
timestamp,
|
|
|
|
|
deviceIndex: device.info.index,
|
|
|
|
|
deviceName: device.name,
|
|
|
|
|
actuatorIndex: scalarIndex,
|
|
|
|
|
actuatorType: scalarCmd.ActuatorType,
|
|
|
|
|
value: (value / scalarCmd.StepCount) * 100, // Normalize to 0-100
|
|
|
|
|
});
|
2025-10-25 22:04:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleStop(device: BluetoothDevice) {
|
|
|
|
|
await client.sendDeviceMessage(
|
|
|
|
|
{ index: device.info.index },
|
|
|
|
|
new StopDeviceCmd(device.info.index),
|
|
|
|
|
);
|
|
|
|
|
device.actuatorValues = device.info.messageAttributes.ScalarCmd.map(() => 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function convertDevice(device: ButtplugClientDevice): BluetoothDevice {
|
|
|
|
|
console.log(device);
|
|
|
|
|
return {
|
|
|
|
|
id: device.index as string,
|
|
|
|
|
name: device.name as string,
|
|
|
|
|
batteryLevel: 0,
|
|
|
|
|
isConnected: true,
|
|
|
|
|
lastSeen: new Date(),
|
|
|
|
|
sensorValues: device.messageAttributes.SensorReadCmd
|
|
|
|
|
? device.messageAttributes.SensorReadCmd.map(() => 0)
|
|
|
|
|
: [],
|
|
|
|
|
actuatorValues: device.messageAttributes.ScalarCmd.map(() => 0),
|
|
|
|
|
info: device,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
feat: add buttplug device recording feature (Phase 1 & 2)
Implemented complete infrastructure for recording, saving, and managing
buttplug device patterns with precise event timing.
**Phase 1: Backend & Infrastructure**
- Added Directus schema for sexy_recordings collection with all fields
(id, status, user_created, title, description, slug, duration, events,
device_info, tags, linked_video, featured, public)
- Created REST API endpoints in bundle extension:
* GET /sexy/recordings - list user recordings with filtering
* GET /sexy/recordings/:id - get single recording
* POST /sexy/recordings - create new recording with validation
* PATCH /sexy/recordings/:id - update recording (owner only)
* DELETE /sexy/recordings/:id - soft delete by archiving
- Added TypeScript types: RecordedEvent, DeviceInfo, Recording
- Created frontend services: getRecordings(), deleteRecording()
- Built RecordingCard component with stats, device info, and actions
- Added Recordings tab to /me dashboard page with grid layout
- Added i18n translations for recordings UI
**Phase 2: Recording Capture**
- Implemented recording state management in /play page
- Added Start/Stop Recording buttons with visual indicators
- Capture device events with precise timestamps during recording
- Normalize actuator values (0-100) for cross-device compatibility
- Created RecordingSaveDialog component with:
* Recording stats display (duration, events, devices)
* Form inputs (title, description, tags)
* Device information preview
- Integrated save recording API call from play page
- Added success/error toast notifications
- Automatic event filtering during recording
**Technical Details**
- Events stored as JSON array with timestamp, deviceIndex, deviceName,
actuatorIndex, actuatorType, and normalized value
- Device metadata includes name, index, and capability list
- Slug auto-generated from title for SEO-friendly URLs
- Status workflow: draft → published → archived
- Permission checks: users can only access own recordings or public ones
- Frontend uses performance.now() for millisecond precision timing
**User Flow**
1. User scans and connects devices on /play page
2. Clicks "Start Recording" to begin capturing events
3. Manipulates device sliders - all changes are recorded
4. Clicks "Stop Recording" to end capture
5. Save dialog appears with recording preview and form
6. User enters title, description, tags and saves
7. Recording appears in dashboard /me Recordings tab
8. Can play back, edit, or delete recordings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 04:05:09 +01:00
|
|
|
async function handleSaveRecording(data: {
|
|
|
|
|
title: string;
|
|
|
|
|
description: string;
|
|
|
|
|
tags: string[];
|
|
|
|
|
}) {
|
|
|
|
|
const deviceInfo: DeviceInfo[] = devices.map((d) => ({
|
|
|
|
|
name: d.name,
|
|
|
|
|
index: d.info.index,
|
|
|
|
|
capabilities: d.info.messageAttributes.ScalarCmd.map((cmd) => cmd.ActuatorType),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const directus = getDirectusInstance();
|
|
|
|
|
await directus.request(
|
|
|
|
|
customEndpoint({
|
|
|
|
|
method: "POST",
|
|
|
|
|
path: "/sexy/recordings",
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
title: data.title,
|
|
|
|
|
description: data.description,
|
|
|
|
|
duration: recordingDuration,
|
|
|
|
|
events: recordedEvents,
|
|
|
|
|
device_info: deviceInfo,
|
|
|
|
|
tags: data.tags,
|
|
|
|
|
status: "draft",
|
|
|
|
|
}),
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
toast.success("Recording saved successfully!");
|
|
|
|
|
showSaveDialog = false;
|
|
|
|
|
recordedEvents = [];
|
|
|
|
|
recordingDuration = 0;
|
|
|
|
|
|
|
|
|
|
// Optionally navigate to dashboard
|
|
|
|
|
// goto("/me?tab=recordings");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to save recording:", error);
|
|
|
|
|
toast.error("Failed to save recording. Please try again.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleCancelSave() {
|
|
|
|
|
showSaveDialog = false;
|
|
|
|
|
recordedEvents = [];
|
|
|
|
|
recordingDuration = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 22:04:41 +02:00
|
|
|
const { data } = $props();
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
if (data.authStatus.authenticated) {
|
|
|
|
|
init();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
goto("/login");
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<Meta title={$_("play.title")} description={$_("play.description")} />
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
class="relative min-h-screen bg-gradient-to-br from-background via-primary/5 to-accent/5 overflow-hidden"
|
|
|
|
|
>
|
|
|
|
|
<!-- Global Plasma Background -->
|
|
|
|
|
<div class="absolute inset-0 pointer-events-none">
|
|
|
|
|
<div
|
|
|
|
|
class="absolute top-40 left-1/4 w-80 h-80 bg-gradient-to-r from-primary/16 via-accent/20 to-primary/12 rounded-full blur-3xl animate-blob-slow"
|
|
|
|
|
></div>
|
|
|
|
|
<div
|
|
|
|
|
class="absolute bottom-40 right-1/4 w-96 h-96 bg-gradient-to-r from-accent/16 via-primary/20 to-accent/12 rounded-full blur-3xl animate-blob-slow animation-delay-5000"
|
|
|
|
|
></div>
|
|
|
|
|
<div
|
|
|
|
|
class="absolute top-1/3 right-1/3 w-64 h-64 bg-gradient-to-r from-primary/14 via-accent/18 to-primary/10 rounded-full blur-2xl animate-blob-reverse animation-delay-2500"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="container mx-auto py-20 relative px-4">
|
|
|
|
|
<div class="max-w-4xl mx-auto">
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<div class="text-center mb-12">
|
|
|
|
|
<h1
|
|
|
|
|
class="text-4xl md:text-5xl font-bold mb-4 bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent"
|
|
|
|
|
>
|
|
|
|
|
{$_("play.title")}
|
|
|
|
|
</h1>
|
|
|
|
|
<p class="text-lg text-muted-foreground mb-10">
|
|
|
|
|
{$_("play.description")}
|
|
|
|
|
</p>
|
feat: add buttplug device recording feature (Phase 1 & 2)
Implemented complete infrastructure for recording, saving, and managing
buttplug device patterns with precise event timing.
**Phase 1: Backend & Infrastructure**
- Added Directus schema for sexy_recordings collection with all fields
(id, status, user_created, title, description, slug, duration, events,
device_info, tags, linked_video, featured, public)
- Created REST API endpoints in bundle extension:
* GET /sexy/recordings - list user recordings with filtering
* GET /sexy/recordings/:id - get single recording
* POST /sexy/recordings - create new recording with validation
* PATCH /sexy/recordings/:id - update recording (owner only)
* DELETE /sexy/recordings/:id - soft delete by archiving
- Added TypeScript types: RecordedEvent, DeviceInfo, Recording
- Created frontend services: getRecordings(), deleteRecording()
- Built RecordingCard component with stats, device info, and actions
- Added Recordings tab to /me dashboard page with grid layout
- Added i18n translations for recordings UI
**Phase 2: Recording Capture**
- Implemented recording state management in /play page
- Added Start/Stop Recording buttons with visual indicators
- Capture device events with precise timestamps during recording
- Normalize actuator values (0-100) for cross-device compatibility
- Created RecordingSaveDialog component with:
* Recording stats display (duration, events, devices)
* Form inputs (title, description, tags)
* Device information preview
- Integrated save recording API call from play page
- Added success/error toast notifications
- Automatic event filtering during recording
**Technical Details**
- Events stored as JSON array with timestamp, deviceIndex, deviceName,
actuatorIndex, actuatorType, and normalized value
- Device metadata includes name, index, and capability list
- Slug auto-generated from title for SEO-friendly URLs
- Status workflow: draft → published → archived
- Permission checks: users can only access own recordings or public ones
- Frontend uses performance.now() for millisecond precision timing
**User Flow**
1. User scans and connects devices on /play page
2. Clicks "Start Recording" to begin capturing events
3. Manipulates device sliders - all changes are recorded
4. Clicks "Stop Recording" to end capture
5. Save dialog appears with recording preview and form
6. User enters title, description, tags and saves
7. Recording appears in dashboard /me Recordings tab
8. Can play back, edit, or delete recordings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 04:05:09 +01:00
|
|
|
<div class="flex justify-center gap-4 items-center">
|
2025-10-25 22:04:41 +02:00
|
|
|
<Button
|
|
|
|
|
size="lg"
|
|
|
|
|
disabled={!connected || scanning}
|
|
|
|
|
onclick={startScanning}
|
|
|
|
|
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
|
|
|
|
>
|
|
|
|
|
{#if scanning}
|
|
|
|
|
<div
|
|
|
|
|
class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"
|
|
|
|
|
></div>
|
|
|
|
|
{$_("play.scanning")}
|
|
|
|
|
{:else}
|
|
|
|
|
{$_("play.scan")}
|
|
|
|
|
{/if}
|
|
|
|
|
</Button>
|
feat: add buttplug device recording feature (Phase 1 & 2)
Implemented complete infrastructure for recording, saving, and managing
buttplug device patterns with precise event timing.
**Phase 1: Backend & Infrastructure**
- Added Directus schema for sexy_recordings collection with all fields
(id, status, user_created, title, description, slug, duration, events,
device_info, tags, linked_video, featured, public)
- Created REST API endpoints in bundle extension:
* GET /sexy/recordings - list user recordings with filtering
* GET /sexy/recordings/:id - get single recording
* POST /sexy/recordings - create new recording with validation
* PATCH /sexy/recordings/:id - update recording (owner only)
* DELETE /sexy/recordings/:id - soft delete by archiving
- Added TypeScript types: RecordedEvent, DeviceInfo, Recording
- Created frontend services: getRecordings(), deleteRecording()
- Built RecordingCard component with stats, device info, and actions
- Added Recordings tab to /me dashboard page with grid layout
- Added i18n translations for recordings UI
**Phase 2: Recording Capture**
- Implemented recording state management in /play page
- Added Start/Stop Recording buttons with visual indicators
- Capture device events with precise timestamps during recording
- Normalize actuator values (0-100) for cross-device compatibility
- Created RecordingSaveDialog component with:
* Recording stats display (duration, events, devices)
* Form inputs (title, description, tags)
* Device information preview
- Integrated save recording API call from play page
- Added success/error toast notifications
- Automatic event filtering during recording
**Technical Details**
- Events stored as JSON array with timestamp, deviceIndex, deviceName,
actuatorIndex, actuatorType, and normalized value
- Device metadata includes name, index, and capability list
- Slug auto-generated from title for SEO-friendly URLs
- Status workflow: draft → published → archived
- Permission checks: users can only access own recordings or public ones
- Frontend uses performance.now() for millisecond precision timing
**User Flow**
1. User scans and connects devices on /play page
2. Clicks "Start Recording" to begin capturing events
3. Manipulates device sliders - all changes are recorded
4. Clicks "Stop Recording" to end capture
5. Save dialog appears with recording preview and form
6. User enters title, description, tags and saves
7. Recording appears in dashboard /me Recordings tab
8. Can play back, edit, or delete recordings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 04:05:09 +01:00
|
|
|
|
|
|
|
|
{#if devices.length > 0}
|
|
|
|
|
{#if !isRecording}
|
|
|
|
|
<Button
|
|
|
|
|
size="lg"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onclick={startRecording}
|
|
|
|
|
class="cursor-pointer border-primary/30 hover:bg-primary/10"
|
|
|
|
|
>
|
|
|
|
|
<span class="icon-[ri--record-circle-line] w-5 h-5 mr-2"></span>
|
|
|
|
|
Start Recording
|
|
|
|
|
</Button>
|
|
|
|
|
{:else}
|
|
|
|
|
<Button
|
|
|
|
|
size="lg"
|
|
|
|
|
onclick={stopRecording}
|
|
|
|
|
class="cursor-pointer bg-red-500 hover:bg-red-600 text-white"
|
|
|
|
|
>
|
|
|
|
|
<span class="icon-[ri--stop-circle-fill] w-5 h-5 mr-2 animate-pulse"></span>
|
|
|
|
|
Stop Recording ({recordedEvents.length} events)
|
|
|
|
|
</Button>
|
|
|
|
|
{/if}
|
|
|
|
|
{/if}
|
2025-10-25 22:04:41 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="container mx-auto px-4 py-12">
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
|
|
|
{#if devices}
|
|
|
|
|
{#each devices as device}
|
|
|
|
|
<DeviceCard
|
|
|
|
|
{device}
|
|
|
|
|
onChange={(scalarIndex, val) => handleChange(device, scalarIndex, val)}
|
|
|
|
|
onStop={() => handleStop(device)}
|
|
|
|
|
/>
|
|
|
|
|
{/each}
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if devices?.length === 0}
|
|
|
|
|
<div class="text-center py-12">
|
|
|
|
|
<p class="text-muted-foreground text-lg mb-4">
|
|
|
|
|
{$_("play.no_results")}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
feat: add buttplug device recording feature (Phase 1 & 2)
Implemented complete infrastructure for recording, saving, and managing
buttplug device patterns with precise event timing.
**Phase 1: Backend & Infrastructure**
- Added Directus schema for sexy_recordings collection with all fields
(id, status, user_created, title, description, slug, duration, events,
device_info, tags, linked_video, featured, public)
- Created REST API endpoints in bundle extension:
* GET /sexy/recordings - list user recordings with filtering
* GET /sexy/recordings/:id - get single recording
* POST /sexy/recordings - create new recording with validation
* PATCH /sexy/recordings/:id - update recording (owner only)
* DELETE /sexy/recordings/:id - soft delete by archiving
- Added TypeScript types: RecordedEvent, DeviceInfo, Recording
- Created frontend services: getRecordings(), deleteRecording()
- Built RecordingCard component with stats, device info, and actions
- Added Recordings tab to /me dashboard page with grid layout
- Added i18n translations for recordings UI
**Phase 2: Recording Capture**
- Implemented recording state management in /play page
- Added Start/Stop Recording buttons with visual indicators
- Capture device events with precise timestamps during recording
- Normalize actuator values (0-100) for cross-device compatibility
- Created RecordingSaveDialog component with:
* Recording stats display (duration, events, devices)
* Form inputs (title, description, tags)
* Device information preview
- Integrated save recording API call from play page
- Added success/error toast notifications
- Automatic event filtering during recording
**Technical Details**
- Events stored as JSON array with timestamp, deviceIndex, deviceName,
actuatorIndex, actuatorType, and normalized value
- Device metadata includes name, index, and capability list
- Slug auto-generated from title for SEO-friendly URLs
- Status workflow: draft → published → archived
- Permission checks: users can only access own recordings or public ones
- Frontend uses performance.now() for millisecond precision timing
**User Flow**
1. User scans and connects devices on /play page
2. Clicks "Start Recording" to begin capturing events
3. Manipulates device sliders - all changes are recorded
4. Clicks "Stop Recording" to end capture
5. Save dialog appears with recording preview and form
6. User enters title, description, tags and saves
7. Recording appears in dashboard /me Recordings tab
8. Can play back, edit, or delete recordings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 04:05:09 +01:00
|
|
|
|
|
|
|
|
<!-- Recording Save Dialog -->
|
|
|
|
|
<RecordingSaveDialog
|
|
|
|
|
open={showSaveDialog}
|
|
|
|
|
events={recordedEvents}
|
|
|
|
|
deviceInfo={devices.map((d) => ({
|
|
|
|
|
name: d.name,
|
|
|
|
|
index: d.info.index,
|
|
|
|
|
capabilities: d.info.messageAttributes.ScalarCmd.map((cmd) => cmd.ActuatorType),
|
|
|
|
|
}))}
|
|
|
|
|
duration={recordingDuration}
|
|
|
|
|
onSave={handleSaveRecording}
|
|
|
|
|
onCancel={handleCancelSave}
|
|
|
|
|
/>
|
2025-10-25 22:04:41 +02:00
|
|
|
</div>
|