summaryrefslogtreecommitdiff
path: root/apps/web/src/actions/freeze-frame-store.ts
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-27 01:21:02 +0200
committercashmere <cashmere@autistici.org>2026-07-27 01:21:02 +0200
commit2bbada7d2c5c00c5f844b6916c08bc4a9ea8cb25 (patch)
tree2fdd617e9d51f7293e56b0e9b947f00643aeb8cd /apps/web/src/actions/freeze-frame-store.ts
parentf8fdff7f0c1b8efdef9093c932e28e427799d6ef (diff)
downloadopencut-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/freeze-frame-store.ts')
-rw-r--r--apps/web/src/actions/freeze-frame-store.ts72
1 files changed, 72 insertions, 0 deletions
diff --git a/apps/web/src/actions/freeze-frame-store.ts b/apps/web/src/actions/freeze-frame-store.ts
new file mode 100644
index 00000000..17102780
--- /dev/null
+++ b/apps/web/src/actions/freeze-frame-store.ts
@@ -0,0 +1,72 @@
+"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,
+ }),
+ },
+ ),
+);