summaryrefslogtreecommitdiff
path: root/apps/web/src/actions
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/actions')
-rw-r--r--apps/web/src/actions/__tests__/freeze-frame-store.test.ts98
-rw-r--r--apps/web/src/actions/components/freeze-frame-dialog.tsx109
-rw-r--r--apps/web/src/actions/freeze-frame-store.ts72
-rw-r--r--apps/web/src/actions/use-editor-actions.ts12
4 files changed, 3 insertions, 288 deletions
diff --git a/apps/web/src/actions/__tests__/freeze-frame-store.test.ts b/apps/web/src/actions/__tests__/freeze-frame-store.test.ts
deleted file mode 100644
index 94e1566e..00000000
--- a/apps/web/src/actions/__tests__/freeze-frame-store.test.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { beforeEach, describe, expect, test } from "bun:test";
-import {
- DEFAULT_FREEZE_DURATION_SECONDS,
- isValidFreezeDuration,
- MAX_FREEZE_DURATION_SECONDS,
- useFreezeFrameStore,
-} from "../freeze-frame-store";
-
-function resetStore() {
- useFreezeFrameStore.setState({
- isOpen: false,
- lastDurationSeconds: DEFAULT_FREEZE_DURATION_SECONDS,
- resolver: null,
- });
-}
-
-describe("freeze-frame store", () => {
- beforeEach(() => {
- resetStore();
- });
-
- describe("isValidFreezeDuration", () => {
- test("accepts positive durations up to the max, including decimals", () => {
- expect(isValidFreezeDuration({ seconds: 3 })).toBe(true);
- expect(isValidFreezeDuration({ seconds: 2.5 })).toBe(true);
- expect(isValidFreezeDuration({ seconds: 0.1 })).toBe(true);
- expect(
- isValidFreezeDuration({ seconds: MAX_FREEZE_DURATION_SECONDS }),
- ).toBe(true);
- });
-
- test("rejects non-positive, over-max, and non-finite durations", () => {
- expect(isValidFreezeDuration({ seconds: 0 })).toBe(false);
- expect(isValidFreezeDuration({ seconds: -1 })).toBe(false);
- expect(isValidFreezeDuration({ seconds: 600.1 })).toBe(false);
- expect(isValidFreezeDuration({ seconds: Number.NaN })).toBe(false);
- expect(isValidFreezeDuration({ seconds: Number.POSITIVE_INFINITY })).toBe(
- false,
- );
- });
- });
-
- describe("requestDuration", () => {
- test("opens the dialog and resolves with the confirmed seconds", async () => {
- const promise = useFreezeFrameStore.getState().requestDuration();
- expect(useFreezeFrameStore.getState().isOpen).toBe(true);
-
- useFreezeFrameStore.getState().confirmDuration({ seconds: 5 });
-
- await expect(promise).resolves.toBe(5);
- expect(useFreezeFrameStore.getState().isOpen).toBe(false);
- });
-
- test("remembers the confirmed duration as the last-used value", async () => {
- const promise = useFreezeFrameStore.getState().requestDuration();
- useFreezeFrameStore.getState().confirmDuration({ seconds: 5 });
- await promise;
-
- expect(useFreezeFrameStore.getState().lastDurationSeconds).toBe(5);
- });
-
- test("resolves null on cancel and keeps the last-used duration", async () => {
- useFreezeFrameStore.setState({ lastDurationSeconds: 7 });
-
- const promise = useFreezeFrameStore.getState().requestDuration();
- expect(useFreezeFrameStore.getState().isOpen).toBe(true);
-
- useFreezeFrameStore.getState().cancelDuration();
-
- await expect(promise).resolves.toBeNull();
- expect(useFreezeFrameStore.getState().isOpen).toBe(false);
- expect(useFreezeFrameStore.getState().lastDurationSeconds).toBe(7);
- });
-
- test("ignores an invalid confirm and leaves the dialog open", () => {
- useFreezeFrameStore.getState().requestDuration();
-
- useFreezeFrameStore.getState().confirmDuration({ seconds: 0 });
- useFreezeFrameStore.getState().confirmDuration({ seconds: 601 });
-
- expect(useFreezeFrameStore.getState().isOpen).toBe(true);
- expect(useFreezeFrameStore.getState().lastDurationSeconds).toBe(
- DEFAULT_FREEZE_DURATION_SECONDS,
- );
- });
-
- test("a new request supersedes a pending one by cancelling it", async () => {
- const first = useFreezeFrameStore.getState().requestDuration();
- const second = useFreezeFrameStore.getState().requestDuration();
-
- await expect(first).resolves.toBeNull();
- expect(useFreezeFrameStore.getState().isOpen).toBe(true);
-
- useFreezeFrameStore.getState().confirmDuration({ seconds: 2 });
- await expect(second).resolves.toBe(2);
- });
- });
-});
diff --git a/apps/web/src/actions/components/freeze-frame-dialog.tsx b/apps/web/src/actions/components/freeze-frame-dialog.tsx
deleted file mode 100644
index a6a7e0ae..00000000
--- a/apps/web/src/actions/components/freeze-frame-dialog.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogBody,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- isValidFreezeDuration,
- MAX_FREEZE_DURATION_SECONDS,
- useFreezeFrameStore,
-} from "@/actions/freeze-frame-store";
-
-export function FreezeFrameDialog() {
- const isOpen = useFreezeFrameStore((state) => state.isOpen);
- const cancelDuration = useFreezeFrameStore((state) => state.cancelDuration);
-
- return (
- <Dialog
- open={isOpen}
- onOpenChange={(open) => {
- if (!open) {
- cancelDuration();
- }
- }}
- >
- <DialogContent className="sm:max-w-md">
- <DialogHeader>
- <DialogTitle>Freeze frame</DialogTitle>
- <DialogDescription>
- How long should the frozen frame stay on screen?
- </DialogDescription>
- </DialogHeader>
- {isOpen && <FreezeFrameForm />}
- </DialogContent>
- </Dialog>
- );
-}
-
-function FreezeFrameForm() {
- const lastDurationSeconds = useFreezeFrameStore(
- (state) => state.lastDurationSeconds,
- );
- const confirmDuration = useFreezeFrameStore((state) => state.confirmDuration);
- const cancelDuration = useFreezeFrameStore((state) => state.cancelDuration);
- const [value, setValue] = useState(String(lastDurationSeconds));
-
- const seconds = Number(value);
- const showError = value.trim() !== "" && !isValidFreezeDuration({ seconds });
- const canConfirm = isValidFreezeDuration({ seconds });
-
- const handleConfirm = () => {
- if (canConfirm) {
- confirmDuration({ seconds });
- }
- };
-
- return (
- <>
- <DialogBody className="gap-3">
- <Label htmlFor="freeze-frame-duration">Duration (seconds)</Label>
- <Input
- id="freeze-frame-duration"
- value={value}
- inputMode="decimal"
- aria-invalid={showError}
- onChange={(e) => setValue(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- e.preventDefault();
- handleConfirm();
- }
- }}
- placeholder="e.g. 2.5"
- />
- {showError && (
- <p className="text-destructive text-xs">
- Enter a duration greater than 0 and at most{" "}
- {MAX_FREEZE_DURATION_SECONDS} seconds.
- </p>
- )}
- </DialogBody>
-
- <DialogFooter>
- <Button
- variant="outline"
- onClick={(e) => {
- e.preventDefault();
- e.stopPropagation();
- cancelDuration();
- }}
- >
- Cancel
- </Button>
- <Button onClick={handleConfirm} disabled={!canConfirm}>
- Freeze
- </Button>
- </DialogFooter>
- </>
- );
-}
diff --git a/apps/web/src/actions/freeze-frame-store.ts b/apps/web/src/actions/freeze-frame-store.ts
deleted file mode 100644
index 17102780..00000000
--- a/apps/web/src/actions/freeze-frame-store.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-"use client";
-
-import { create } from "zustand";
-import { persist } from "zustand/middleware";
-
-export const DEFAULT_FREEZE_DURATION_SECONDS = 3;
-export const MAX_FREEZE_DURATION_SECONDS = 600;
-
-export function isValidFreezeDuration({
- seconds,
-}: {
- seconds: number;
-}): boolean {
- return (
- Number.isFinite(seconds) &&
- seconds > 0 &&
- seconds <= MAX_FREEZE_DURATION_SECONDS
- );
-}
-
-type DurationResolver = ({ seconds }: { seconds: number | null }) => void;
-
-interface FreezeFrameState {
- isOpen: boolean;
- lastDurationSeconds: number;
- resolver: DurationResolver | null;
- requestDuration: () => Promise<number | null>;
- confirmDuration: ({ seconds }: { seconds: number }) => void;
- cancelDuration: () => void;
-}
-
-export const useFreezeFrameStore = create<FreezeFrameState>()(
- persist(
- (set, get) => ({
- isOpen: false,
- lastDurationSeconds: DEFAULT_FREEZE_DURATION_SECONDS,
- resolver: null,
-
- requestDuration: () =>
- new Promise<number | null>((resolve) => {
- // A new invocation supersedes any pending one: resolve it as
- // cancelled so its handler aborts instead of hanging.
- get().resolver?.({ seconds: null });
- set({
- isOpen: true,
- resolver: ({ seconds }) => resolve(seconds),
- });
- }),
- confirmDuration: ({ seconds }) => {
- if (!isValidFreezeDuration({ seconds })) {
- return;
- }
- get().resolver?.({ seconds });
- set({
- isOpen: false,
- resolver: null,
- lastDurationSeconds: seconds,
- });
- },
- cancelDuration: () => {
- get().resolver?.({ seconds: null });
- set({ isOpen: false, resolver: null });
- },
- }),
- {
- name: "freeze-frame-store",
- partialize: (state) => ({
- lastDurationSeconds: state.lastDurationSeconds,
- }),
- },
- ),
-);
diff --git a/apps/web/src/actions/use-editor-actions.ts b/apps/web/src/actions/use-editor-actions.ts
index c99f65e2..ca16e989 100644
--- a/apps/web/src/actions/use-editor-actions.ts
+++ b/apps/web/src/actions/use-editor-actions.ts
@@ -28,7 +28,6 @@ import {
import { useCommittedRef } from "@/hooks/use-committed-ref";
import { toast } from "sonner";
import { generateImageThumbnail } from "@/media/processing";
-import { useFreezeFrameStore } from "@/actions/freeze-frame-store";
import {
AddMediaAssetCommand,
BatchCommand,
@@ -47,6 +46,8 @@ import {
import type { VoiceOverRecordingResult } from "@/voiceover/recorder";
import { useVoiceOverStore } from "@/voiceover/voiceover-store";
+const FREEZE_DURATION_SECONDS = 3;
+
export function useEditorActions() {
const editor = useEditor();
const { selectedElements, setElementSelection } = useElementSelection();
@@ -421,13 +422,6 @@ export function useEditorActions() {
return;
}
- const freezeDurationSeconds = await useFreezeFrameStore
- .getState()
- .requestDuration();
- if (freezeDurationSeconds === null) {
- return;
- }
-
const blob = await editor.renderer.captureFrame();
if (!blob) {
toast.error("Failed to capture frame");
@@ -442,7 +436,7 @@ export function useEditorActions() {
});
const freezeDuration = mediaTimeFromSeconds({
- seconds: freezeDurationSeconds,
+ seconds: FREEZE_DURATION_SECONDS,
});
const addMediaCommand = new AddMediaAssetCommand({
projectId: activeProject.metadata.id,