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 {
|
interface Props {
|
||||||
recording: Recording;
|
recording: Recording;
|
||||||
onPlay?: (id: string) => void;
|
onPlay?: (id: string) => void;
|
||||||
|
onPublish?: (id: string) => void;
|
||||||
|
onUnpublish?: (id: string) => void;
|
||||||
onDelete?: (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 {
|
function formatDuration(ms: number): string {
|
||||||
const totalSeconds = Math.floor(ms / 1000);
|
const totalSeconds = Math.floor(ms / 1000);
|
||||||
@@ -149,12 +151,35 @@
|
|||||||
{$_("recording_card.play")}
|
{$_("recording_card.play")}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/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}
|
{#if onDelete}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => onDelete?.(recording.id)}
|
onclick={() => onDelete?.(recording.id)}
|
||||||
class="cursor-pointer border-destructive/20 hover:bg-destructive/10 hover:text-destructive"
|
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>
|
<span class="icon-[ri--delete-bin-line] w-4 h-4"></span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ export default {
|
|||||||
delete_confirm: "Are you sure you want to delete this recording?",
|
delete_confirm: "Are you sure you want to delete this recording?",
|
||||||
delete_success: "Recording deleted successfully",
|
delete_success: "Recording deleted successfully",
|
||||||
delete_error: "Failed to delete recording",
|
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: {
|
recording_card: {
|
||||||
@@ -161,6 +165,8 @@ export default {
|
|||||||
status_draft: "Draft",
|
status_draft: "Draft",
|
||||||
status_published: "Published",
|
status_published: "Published",
|
||||||
play: "Play",
|
play: "Play",
|
||||||
|
publish: "Publish",
|
||||||
|
unpublish: "Unpublish",
|
||||||
edit: "Edit",
|
edit: "Edit",
|
||||||
delete: "Delete",
|
delete: "Delete",
|
||||||
public: "Public",
|
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`
|
const DELETE_RECORDING_MUTATION = gql`
|
||||||
mutation DeleteRecording($id: String!) {
|
mutation DeleteRecording($id: String!) {
|
||||||
deleteRecording(id: $id)
|
deleteRecording(id: $id)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import { deleteRecording } from "$lib/services";
|
import { deleteRecording, updateRecording } from "$lib/services";
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
import * as Empty from "$lib/components/ui/empty";
|
import * as Empty from "$lib/components/ui/empty";
|
||||||
import * as Dialog from "$lib/components/ui/dialog";
|
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) {
|
function handlePlayRecording(id: string) {
|
||||||
goto(`/play?recording=${id}`);
|
goto(`/play?recording=${id}`);
|
||||||
}
|
}
|
||||||
@@ -46,15 +66,8 @@
|
|||||||
<Meta title={$_("me.recordings.title")} />
|
<Meta title={$_("me.recordings.title")} />
|
||||||
|
|
||||||
<div class="py-3 sm:py-6 lg:pl-6">
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
{#if recordings.length === 0}
|
{#if recordings.length === 0}
|
||||||
@@ -82,6 +95,8 @@
|
|||||||
<RecordingCard
|
<RecordingCard
|
||||||
{recording}
|
{recording}
|
||||||
onPlay={handlePlayRecording}
|
onPlay={handlePlayRecording}
|
||||||
|
onPublish={handlePublishRecording}
|
||||||
|
onUnpublish={handleUnpublishRecording}
|
||||||
onDelete={handleDeleteRecording}
|
onDelete={handleDeleteRecording}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import DeviceMappingDialog from "./components/device-mapping-dialog.svelte";
|
import DeviceMappingDialog from "./components/device-mapping-dialog.svelte";
|
||||||
import type { BluetoothDevice, RecordedEvent, DeviceInfo } from "$lib/types";
|
import type { BluetoothDevice, RecordedEvent, DeviceInfo } from "$lib/types";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
|
import { createRecording } from "$lib/services";
|
||||||
import SexyBackground from "$lib/components/background/background.svelte";
|
import SexyBackground from "$lib/components/background/background.svelte";
|
||||||
|
|
||||||
// Runtime buttplug values — loaded dynamically from the buttplug nginx container
|
// Runtime buttplug values — loaded dynamically from the buttplug nginx container
|
||||||
@@ -173,26 +174,16 @@
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/sexy/recordings", {
|
await createRecording({
|
||||||
method: "POST",
|
title: data.title,
|
||||||
headers: {
|
description: data.description,
|
||||||
"Content-Type": "application/json",
|
duration: Math.round(recordingDuration),
|
||||||
},
|
events: recordedEvents,
|
||||||
body: JSON.stringify({
|
device_info: deviceInfo,
|
||||||
title: data.title,
|
tags: data.tags,
|
||||||
description: data.description,
|
status: "draft",
|
||||||
duration: 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!");
|
toast.success("Recording saved successfully!");
|
||||||
showSaveDialog = false;
|
showSaveDialog = false;
|
||||||
recordedEvents = [];
|
recordedEvents = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user