feat: fix recording save and add publish/unpublish support
- Fix broken fetch("/api/sexy/recordings") → use createRecording GraphQL service
- Round duration to integer before sending (GraphQL Int type)
- Add updateRecording mutation to services
- Add publish/unpublish buttons to RecordingCard (draft ↔ published)
- Remove "Go to Play" button from recordings page header
- Add publish/unpublish i18n keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,10 +8,12 @@
|
||||
interface Props {
|
||||
recording: Recording;
|
||||
onPlay?: (id: string) => void;
|
||||
onPublish?: (id: string) => void;
|
||||
onUnpublish?: (id: string) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
let { recording, onPlay, onDelete }: Props = $props();
|
||||
let { recording, onPlay, onPublish, onUnpublish, onDelete }: Props = $props();
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
@@ -149,12 +151,35 @@
|
||||
{$_("recording_card.play")}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if onPublish && recording.status === "draft"}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => onPublish?.(recording.id)}
|
||||
class="cursor-pointer border-primary/20 hover:bg-primary/10 hover:text-primary"
|
||||
title={$_("recording_card.publish")}
|
||||
>
|
||||
<span class="icon-[ri--send-plane-line] w-4 h-4"></span>
|
||||
</Button>
|
||||
{/if}
|
||||
{#if onUnpublish && recording.status === "published"}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => onUnpublish?.(recording.id)}
|
||||
class="cursor-pointer border-muted-foreground/20 hover:bg-muted/50 hover:text-muted-foreground"
|
||||
title={$_("recording_card.unpublish")}
|
||||
>
|
||||
<span class="icon-[ri--arrow-go-back-line] w-4 h-4"></span>
|
||||
</Button>
|
||||
{/if}
|
||||
{#if onDelete}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => onDelete?.(recording.id)}
|
||||
class="cursor-pointer border-destructive/20 hover:bg-destructive/10 hover:text-destructive"
|
||||
title={$_("common.delete")}
|
||||
>
|
||||
<span class="icon-[ri--delete-bin-line] w-4 h-4"></span>
|
||||
</Button>
|
||||
|
||||
@@ -151,6 +151,10 @@ export default {
|
||||
delete_confirm: "Are you sure you want to delete this recording?",
|
||||
delete_success: "Recording deleted successfully",
|
||||
delete_error: "Failed to delete recording",
|
||||
publish_success: "Recording published successfully",
|
||||
publish_error: "Failed to publish recording",
|
||||
unpublish_success: "Recording unpublished",
|
||||
unpublish_error: "Failed to unpublish recording",
|
||||
},
|
||||
},
|
||||
recording_card: {
|
||||
@@ -161,6 +165,8 @@ export default {
|
||||
status_draft: "Draft",
|
||||
status_published: "Published",
|
||||
play: "Play",
|
||||
publish: "Publish",
|
||||
unpublish: "Unpublish",
|
||||
edit: "Edit",
|
||||
delete: "Delete",
|
||||
public: "Public",
|
||||
|
||||
@@ -902,6 +902,32 @@ export async function createRecording(
|
||||
);
|
||||
}
|
||||
|
||||
const UPDATE_RECORDING_MUTATION = gql`
|
||||
mutation UpdateRecording($id: String!, $status: String, $public: Boolean) {
|
||||
updateRecording(id: $id, status: $status, public: $public) {
|
||||
id
|
||||
status
|
||||
public
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export async function updateRecording(
|
||||
id: string,
|
||||
fields: { status?: string; public?: boolean },
|
||||
) {
|
||||
return loggedApiCall(
|
||||
"updateRecording",
|
||||
async () => {
|
||||
const data = await getGraphQLClient().request<{ updateRecording: Recording }>(
|
||||
UPDATE_RECORDING_MUTATION,
|
||||
{ id, ...fields },
|
||||
);
|
||||
return data.updateRecording;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const DELETE_RECORDING_MUTATION = gql`
|
||||
mutation DeleteRecording($id: String!) {
|
||||
deleteRecording(id: $id)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { _ } from "svelte-i18n";
|
||||
import { goto } from "$app/navigation";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { deleteRecording } from "$lib/services";
|
||||
import { deleteRecording, updateRecording } from "$lib/services";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Empty from "$lib/components/ui/empty";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
@@ -38,6 +38,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePublishRecording(id: string) {
|
||||
try {
|
||||
await updateRecording(id, { status: "published" });
|
||||
recordings = recordings.map((r) => (r.id === id ? { ...r, status: "published" } : r));
|
||||
toast.success($_("me.recordings.publish_success"));
|
||||
} catch {
|
||||
toast.error($_("me.recordings.publish_error"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUnpublishRecording(id: string) {
|
||||
try {
|
||||
await updateRecording(id, { status: "draft" });
|
||||
recordings = recordings.map((r) => (r.id === id ? { ...r, status: "draft" } : r));
|
||||
toast.success($_("me.recordings.unpublish_success"));
|
||||
} catch {
|
||||
toast.error($_("me.recordings.unpublish_error"));
|
||||
}
|
||||
}
|
||||
|
||||
function handlePlayRecording(id: string) {
|
||||
goto(`/play?recording=${id}`);
|
||||
}
|
||||
@@ -46,15 +66,8 @@
|
||||
<Meta title={$_("me.recordings.title")} />
|
||||
|
||||
<div class="py-3 sm:py-6 lg:pl-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold">{$_("me.recordings.title")}</h1>
|
||||
<Button
|
||||
href="/play"
|
||||
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
>
|
||||
<span class="icon-[ri--rocket-line] w-4 h-4 mr-2"></span>
|
||||
{$_("me.recordings.go_to_play")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if recordings.length === 0}
|
||||
@@ -82,6 +95,8 @@
|
||||
<RecordingCard
|
||||
{recording}
|
||||
onPlay={handlePlayRecording}
|
||||
onPublish={handlePublishRecording}
|
||||
onUnpublish={handleUnpublishRecording}
|
||||
onDelete={handleDeleteRecording}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import DeviceMappingDialog from "./components/device-mapping-dialog.svelte";
|
||||
import type { BluetoothDevice, RecordedEvent, DeviceInfo } from "$lib/types";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { createRecording } from "$lib/services";
|
||||
import SexyBackground from "$lib/components/background/background.svelte";
|
||||
|
||||
// Runtime buttplug values — loaded dynamically from the buttplug nginx container
|
||||
@@ -173,26 +174,16 @@
|
||||
}));
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/sexy/recordings", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
duration: recordingDuration,
|
||||
events: recordedEvents,
|
||||
device_info: deviceInfo,
|
||||
tags: data.tags,
|
||||
status: "draft",
|
||||
}),
|
||||
await createRecording({
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
duration: Math.round(recordingDuration),
|
||||
events: recordedEvents,
|
||||
device_info: deviceInfo,
|
||||
tags: data.tags,
|
||||
status: "draft",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
toast.success("Recording saved successfully!");
|
||||
showSaveDialog = false;
|
||||
recordedEvents = [];
|
||||
|
||||
Reference in New Issue
Block a user