diff options
| author | cashmere <cashmere@autistici.org> | 2026-07-27 01:21:02 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-07-27 01:21:02 +0200 |
| commit | 2bbada7d2c5c00c5f844b6916c08bc4a9ea8cb25 (patch) | |
| tree | 2fdd617e9d51f7293e56b0e9b947f00643aeb8cd /apps/web/src/actions/components | |
| parent | f8fdff7f0c1b8efdef9093c932e28e427799d6ef (diff) | |
| download | opencut-2bbada7d2c5c00c5f844b6916c08bc4a9ea8cb25.tar.gz | |
feat: freeze-duration prompt
Freeze frame now asks for the duration (decimal seconds, 0 < d <= 600)
before capture, via a promise-based zustand store mounted as an
editor-global dialog. Last-used duration persists and prefills. Cancel
aborts before capture with no commands.
Diffstat (limited to 'apps/web/src/actions/components')
| -rw-r--r-- | apps/web/src/actions/components/freeze-frame-dialog.tsx | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/apps/web/src/actions/components/freeze-frame-dialog.tsx b/apps/web/src/actions/components/freeze-frame-dialog.tsx new file mode 100644 index 00000000..a6a7e0ae --- /dev/null +++ b/apps/web/src/actions/components/freeze-frame-dialog.tsx @@ -0,0 +1,109 @@ +"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> + </> + ); +} |
