summaryrefslogtreecommitdiff
path: root/apps/web/src/stores
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/stores')
-rw-r--r--apps/web/src/stores/editor-store.ts48
-rw-r--r--apps/web/src/stores/keybindings-store.ts694
-rw-r--r--apps/web/src/stores/keybindings/migrations/v2-to-v3.ts52
-rw-r--r--apps/web/src/stores/keybindings/migrations/v3-to-v4.ts50
-rw-r--r--apps/web/src/stores/keybindings/migrations/v4-to-v5.ts34
-rw-r--r--apps/web/src/stores/keybindings/migrations/v5-to-v6.ts34
-rw-r--r--apps/web/src/stores/panel-store.ts186
-rw-r--r--apps/web/src/stores/preview-store.ts224
-rw-r--r--apps/web/src/stores/sounds-store.ts522
-rw-r--r--apps/web/src/stores/timeline-store.ts112
10 files changed, 978 insertions, 978 deletions
diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts
index 96009928..0eb3c93e 100644
--- a/apps/web/src/stores/editor-store.ts
+++ b/apps/web/src/stores/editor-store.ts
@@ -1,24 +1,24 @@
-import { create } from "zustand";
-import { DEFAULT_CANVAS_PRESETS } from "@/constants/project-constants";
-import type { TCanvasSize } from "@/lib/project/types";
-
-interface EditorState {
- isInitializing: boolean;
- isPanelsReady: boolean;
- canvasPresets: TCanvasSize[];
- setInitializing: (loading: boolean) => void;
- setPanelsReady: (ready: boolean) => void;
- initializeApp: () => Promise<void>;
-}
-
-export const useEditorStore = create<EditorState>()((set) => ({
- isInitializing: true,
- isPanelsReady: false,
- canvasPresets: DEFAULT_CANVAS_PRESETS,
- setInitializing: (loading) => set({ isInitializing: loading }),
- setPanelsReady: (ready) => set({ isPanelsReady: ready }),
- initializeApp: async () => {
- set({ isInitializing: true, isPanelsReady: false });
- set({ isPanelsReady: true, isInitializing: false });
- },
-}));
+import { create } from "zustand";
+import { DEFAULT_CANVAS_PRESETS } from "@/constants/project-constants";
+import type { TCanvasSize } from "@/lib/project/types";
+
+interface EditorState {
+ isInitializing: boolean;
+ isPanelsReady: boolean;
+ canvasPresets: TCanvasSize[];
+ setInitializing: (loading: boolean) => void;
+ setPanelsReady: (ready: boolean) => void;
+ initializeApp: () => Promise<void>;
+}
+
+export const useEditorStore = create<EditorState>()((set) => ({
+ isInitializing: true,
+ isPanelsReady: false,
+ canvasPresets: DEFAULT_CANVAS_PRESETS,
+ setInitializing: (loading) => set({ isInitializing: loading }),
+ setPanelsReady: (ready) => set({ isPanelsReady: ready }),
+ initializeApp: async () => {
+ set({ isInitializing: true, isPanelsReady: false });
+ set({ isPanelsReady: true, isInitializing: false });
+ },
+}));
diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts
index b4cf2703..630b287b 100644
--- a/apps/web/src/stores/keybindings-store.ts
+++ b/apps/web/src/stores/keybindings-store.ts
@@ -1,347 +1,347 @@
-"use client";
-
-import { create } from "zustand";
-import { persist } from "zustand/middleware";
-import type { TActionWithOptionalArgs } from "@/lib/actions";
-import { getDefaultShortcuts } from "@/lib/actions";
-import { isTypableDOMElement } from "@/utils/browser";
-import { isAppleDevice } from "@/utils/platform";
-import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
-import { runMigrations, CURRENT_VERSION } from "./keybindings/migrations";
-
-const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
-
-export interface KeybindingConflict {
- key: ShortcutKey;
- existingAction: TActionWithOptionalArgs;
- newAction: TActionWithOptionalArgs;
-}
-
-interface KeybindingsState {
- keybindings: KeybindingConfig;
- isCustomized: boolean;
- overlayDepth: number;
- openOverlayIds: string[];
- isLoadingProject: boolean;
- isRecording: boolean;
-
- updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => void;
- removeKeybinding: (key: ShortcutKey) => void;
- resetToDefaults: () => void;
- importKeybindings: (config: KeybindingConfig) => void;
- exportKeybindings: () => KeybindingConfig;
- openOverlay: (overlayId: string, source: string) => void;
- closeOverlay: (overlayId: string, source: string) => void;
- setLoadingProject: (loading: boolean) => void;
- setIsRecording: (isRecording: boolean) => void;
- validateKeybinding: (
- key: ShortcutKey,
- action: TActionWithOptionalArgs,
- ) => KeybindingConflict | null;
- getKeybindingsForAction: (action: TActionWithOptionalArgs) => ShortcutKey[];
- getKeybindingString: (ev: KeyboardEvent) => ShortcutKey | null;
-}
-
-function isDOMElement(element: EventTarget | null): element is HTMLElement {
- return element instanceof HTMLElement;
-}
-
-export const useKeybindingsStore = create<KeybindingsState>()(
- persist(
- (set, get) => ({
- keybindings: { ...defaultKeybindings },
- isCustomized: false,
- overlayDepth: 0,
- openOverlayIds: [],
- isLoadingProject: false,
- isRecording: false,
-
- openOverlay: (overlayId, source) =>
- set((s) => {
- const openOverlayIds = s.openOverlayIds.includes(overlayId)
- ? s.openOverlayIds
- : [...s.openOverlayIds, overlayId];
- const nextOverlayDepth = openOverlayIds.length;
- // #region agent log
- fetch(
- "http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "X-Debug-Session-Id": "3997d9",
- },
- body: JSON.stringify({
- sessionId: "3997d9",
- runId: "initial",
- hypothesisId: "H2",
- location: "keybindings-store.ts:openOverlay",
- message: "Overlay depth incremented",
- data: {
- source,
- overlayId,
- overlayDepth: s.overlayDepth,
- nextOverlayDepth,
- openOverlayIds,
- },
- timestamp: Date.now(),
- }),
- },
- ).catch(() => {});
- // #endregion
- return {
- openOverlayIds,
- overlayDepth: nextOverlayDepth,
- };
- }),
- closeOverlay: (overlayId, source) =>
- set((s) => {
- const openOverlayIds = s.openOverlayIds.filter(
- (id) => id !== overlayId,
- );
- const nextOverlayDepth = openOverlayIds.length;
- // #region agent log
- fetch(
- "http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "X-Debug-Session-Id": "3997d9",
- },
- body: JSON.stringify({
- sessionId: "3997d9",
- runId: "initial",
- hypothesisId: "H2",
- location: "keybindings-store.ts:closeOverlay",
- message: "Overlay depth decremented",
- data: {
- source,
- overlayId,
- overlayDepth: s.overlayDepth,
- nextOverlayDepth,
- openOverlayIds,
- },
- timestamp: Date.now(),
- }),
- },
- ).catch(() => {});
- // #endregion
- return {
- openOverlayIds,
- overlayDepth: nextOverlayDepth,
- };
- }),
- setLoadingProject: (loading) => {
- // #region agent log
- fetch(
- "http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "X-Debug-Session-Id": "3997d9",
- },
- body: JSON.stringify({
- sessionId: "3997d9",
- runId: "initial",
- hypothesisId: "H2",
- location: "keybindings-store.ts:setLoadingProject",
- message: "Loading gate updated",
- data: { loading },
- timestamp: Date.now(),
- }),
- },
- ).catch(() => {});
- // #endregion
- set({ isLoadingProject: loading });
- },
-
- updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => {
- set((state) => {
- const newKeybindings = { ...state.keybindings };
- newKeybindings[key] = action;
-
- return {
- keybindings: newKeybindings,
- isCustomized: true,
- };
- });
- },
-
- removeKeybinding: (key: ShortcutKey) => {
- set((state) => {
- const newKeybindings = { ...state.keybindings };
- delete newKeybindings[key];
-
- return {
- keybindings: newKeybindings,
- isCustomized: true,
- };
- });
- },
-
- resetToDefaults: () => {
- set({
- keybindings: { ...defaultKeybindings },
- isCustomized: false,
- });
- },
-
- importKeybindings: (config: KeybindingConfig) => {
- for (const [key] of Object.entries(config)) {
- if (typeof key !== "string" || key.length === 0) {
- throw new Error(`Invalid key format: ${key}`);
- }
- }
- set({
- keybindings: { ...config },
- isCustomized: true,
- });
- },
-
- exportKeybindings: () => {
- return get().keybindings;
- },
-
- validateKeybinding: (
- key: ShortcutKey,
- action: TActionWithOptionalArgs,
- ) => {
- const { keybindings } = get();
- const existingAction = keybindings[key];
-
- if (existingAction && existingAction !== action) {
- return {
- key,
- existingAction,
- newAction: action,
- };
- }
-
- return null;
- },
- setIsRecording: (isRecording: boolean) => {
- // #region agent log
- fetch(
- "http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "X-Debug-Session-Id": "3997d9",
- },
- body: JSON.stringify({
- sessionId: "3997d9",
- runId: "initial",
- hypothesisId: "H2",
- location: "keybindings-store.ts:setIsRecording",
- message: "Recording gate updated",
- data: { isRecording },
- timestamp: Date.now(),
- }),
- },
- ).catch(() => {});
- // #endregion
- set({ isRecording });
- },
-
- getKeybindingsForAction: (action: TActionWithOptionalArgs) => {
- const { keybindings } = get();
- return Object.keys(keybindings).filter(
- (key) => keybindings[key as ShortcutKey] === action,
- ) as ShortcutKey[];
- },
-
- getKeybindingString: (ev: KeyboardEvent) => {
- return generateKeybindingString(ev) as ShortcutKey | null;
- },
- }),
- {
- name: "opencut-keybindings",
- version: CURRENT_VERSION,
- partialize: (state) => ({
- keybindings: state.keybindings,
- isCustomized: state.isCustomized,
- }),
- migrate: (persisted, version) =>
- runMigrations({ state: persisted, fromVersion: version }),
- },
- ),
-);
-
-function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
- const target = ev.target;
- const modifierKey = getActiveModifier(ev);
- const key = getPressedKey(ev);
- if (!key) return null;
-
- if (modifierKey) {
- if (
- modifierKey === "shift" &&
- isDOMElement(target) &&
- isTypableDOMElement({ element: target as HTMLElement })
- ) {
- return null;
- }
-
- return `${modifierKey}+${key}` as ShortcutKey;
- }
-
- if (
- isDOMElement(target) &&
- isTypableDOMElement({ element: target as HTMLElement })
- )
- return null;
-
- return `${key}` as ShortcutKey;
-}
-
-function getPressedKey(ev: KeyboardEvent): string | null {
- const key = (ev.key ?? "").toLowerCase();
- const code = ev.code ?? "";
-
- if (code === "Space" || key === " " || key === "spacebar" || key === "space")
- return "space";
-
- if (key.startsWith("arrow")) return key.slice(5);
-
- if (key === "escape") return "escape";
- if (key === "tab") return "tab";
- if (key === "home") return "home";
- if (key === "end") return "end";
- if (key === "delete") return "delete";
- if (key === "backspace") return "backspace";
-
- if (code.startsWith("Key")) {
- const letter = code.slice(3).toLowerCase();
- if (letter.length === 1 && letter >= "a" && letter <= "z") return letter;
- }
-
- // Use physical key position for AZERTY and other non-QWERTY layouts
- if (code.startsWith("Digit")) {
- const digit = code.slice(5);
- if (digit.length === 1 && digit >= "0" && digit <= "9") return digit;
- }
-
- const isDigit = key.length === 1 && key >= "0" && key <= "9";
- if (isDigit) return key;
-
- if (key === "/" || key === "." || key === "enter") return key;
-
- return null;
-}
-
-function getActiveModifier(ev: KeyboardEvent): string | null {
- const modifierKeys = {
- ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
- alt: ev.altKey,
- shift: ev.shiftKey,
- };
-
- const activeModifier = Object.keys(modifierKeys)
- .filter((key) => modifierKeys[key as keyof typeof modifierKeys])
- .join("+");
-
- return activeModifier === "" ? null : activeModifier;
-}
+"use client";
+
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import type { TActionWithOptionalArgs } from "@/lib/actions";
+import { getDefaultShortcuts } from "@/lib/actions";
+import { isTypableDOMElement } from "@/utils/browser";
+import { isAppleDevice } from "@/utils/platform";
+import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
+import { runMigrations, CURRENT_VERSION } from "./keybindings/migrations";
+
+const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
+
+export interface KeybindingConflict {
+ key: ShortcutKey;
+ existingAction: TActionWithOptionalArgs;
+ newAction: TActionWithOptionalArgs;
+}
+
+interface KeybindingsState {
+ keybindings: KeybindingConfig;
+ isCustomized: boolean;
+ overlayDepth: number;
+ openOverlayIds: string[];
+ isLoadingProject: boolean;
+ isRecording: boolean;
+
+ updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => void;
+ removeKeybinding: (key: ShortcutKey) => void;
+ resetToDefaults: () => void;
+ importKeybindings: (config: KeybindingConfig) => void;
+ exportKeybindings: () => KeybindingConfig;
+ openOverlay: (overlayId: string, source: string) => void;
+ closeOverlay: (overlayId: string, source: string) => void;
+ setLoadingProject: (loading: boolean) => void;
+ setIsRecording: (isRecording: boolean) => void;
+ validateKeybinding: (
+ key: ShortcutKey,
+ action: TActionWithOptionalArgs,
+ ) => KeybindingConflict | null;
+ getKeybindingsForAction: (action: TActionWithOptionalArgs) => ShortcutKey[];
+ getKeybindingString: (ev: KeyboardEvent) => ShortcutKey | null;
+}
+
+function isDOMElement(element: EventTarget | null): element is HTMLElement {
+ return element instanceof HTMLElement;
+}
+
+export const useKeybindingsStore = create<KeybindingsState>()(
+ persist(
+ (set, get) => ({
+ keybindings: { ...defaultKeybindings },
+ isCustomized: false,
+ overlayDepth: 0,
+ openOverlayIds: [],
+ isLoadingProject: false,
+ isRecording: false,
+
+ openOverlay: (overlayId, source) =>
+ set((s) => {
+ const openOverlayIds = s.openOverlayIds.includes(overlayId)
+ ? s.openOverlayIds
+ : [...s.openOverlayIds, overlayId];
+ const nextOverlayDepth = openOverlayIds.length;
+ // #region agent log
+ fetch(
+ "http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Debug-Session-Id": "3997d9",
+ },
+ body: JSON.stringify({
+ sessionId: "3997d9",
+ runId: "initial",
+ hypothesisId: "H2",
+ location: "keybindings-store.ts:openOverlay",
+ message: "Overlay depth incremented",
+ data: {
+ source,
+ overlayId,
+ overlayDepth: s.overlayDepth,
+ nextOverlayDepth,
+ openOverlayIds,
+ },
+ timestamp: Date.now(),
+ }),
+ },
+ ).catch(() => {});
+ // #endregion
+ return {
+ openOverlayIds,
+ overlayDepth: nextOverlayDepth,
+ };
+ }),
+ closeOverlay: (overlayId, source) =>
+ set((s) => {
+ const openOverlayIds = s.openOverlayIds.filter(
+ (id) => id !== overlayId,
+ );
+ const nextOverlayDepth = openOverlayIds.length;
+ // #region agent log
+ fetch(
+ "http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Debug-Session-Id": "3997d9",
+ },
+ body: JSON.stringify({
+ sessionId: "3997d9",
+ runId: "initial",
+ hypothesisId: "H2",
+ location: "keybindings-store.ts:closeOverlay",
+ message: "Overlay depth decremented",
+ data: {
+ source,
+ overlayId,
+ overlayDepth: s.overlayDepth,
+ nextOverlayDepth,
+ openOverlayIds,
+ },
+ timestamp: Date.now(),
+ }),
+ },
+ ).catch(() => {});
+ // #endregion
+ return {
+ openOverlayIds,
+ overlayDepth: nextOverlayDepth,
+ };
+ }),
+ setLoadingProject: (loading) => {
+ // #region agent log
+ fetch(
+ "http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Debug-Session-Id": "3997d9",
+ },
+ body: JSON.stringify({
+ sessionId: "3997d9",
+ runId: "initial",
+ hypothesisId: "H2",
+ location: "keybindings-store.ts:setLoadingProject",
+ message: "Loading gate updated",
+ data: { loading },
+ timestamp: Date.now(),
+ }),
+ },
+ ).catch(() => {});
+ // #endregion
+ set({ isLoadingProject: loading });
+ },
+
+ updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => {
+ set((state) => {
+ const newKeybindings = { ...state.keybindings };
+ newKeybindings[key] = action;
+
+ return {
+ keybindings: newKeybindings,
+ isCustomized: true,
+ };
+ });
+ },
+
+ removeKeybinding: (key: ShortcutKey) => {
+ set((state) => {
+ const newKeybindings = { ...state.keybindings };
+ delete newKeybindings[key];
+
+ return {
+ keybindings: newKeybindings,
+ isCustomized: true,
+ };
+ });
+ },
+
+ resetToDefaults: () => {
+ set({
+ keybindings: { ...defaultKeybindings },
+ isCustomized: false,
+ });
+ },
+
+ importKeybindings: (config: KeybindingConfig) => {
+ for (const [key] of Object.entries(config)) {
+ if (typeof key !== "string" || key.length === 0) {
+ throw new Error(`Invalid key format: ${key}`);
+ }
+ }
+ set({
+ keybindings: { ...config },
+ isCustomized: true,
+ });
+ },
+
+ exportKeybindings: () => {
+ return get().keybindings;
+ },
+
+ validateKeybinding: (
+ key: ShortcutKey,
+ action: TActionWithOptionalArgs,
+ ) => {
+ const { keybindings } = get();
+ const existingAction = keybindings[key];
+
+ if (existingAction && existingAction !== action) {
+ return {
+ key,
+ existingAction,
+ newAction: action,
+ };
+ }
+
+ return null;
+ },
+ setIsRecording: (isRecording: boolean) => {
+ // #region agent log
+ fetch(
+ "http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Debug-Session-Id": "3997d9",
+ },
+ body: JSON.stringify({
+ sessionId: "3997d9",
+ runId: "initial",
+ hypothesisId: "H2",
+ location: "keybindings-store.ts:setIsRecording",
+ message: "Recording gate updated",
+ data: { isRecording },
+ timestamp: Date.now(),
+ }),
+ },
+ ).catch(() => {});
+ // #endregion
+ set({ isRecording });
+ },
+
+ getKeybindingsForAction: (action: TActionWithOptionalArgs) => {
+ const { keybindings } = get();
+ return Object.keys(keybindings).filter(
+ (key) => keybindings[key as ShortcutKey] === action,
+ ) as ShortcutKey[];
+ },
+
+ getKeybindingString: (ev: KeyboardEvent) => {
+ return generateKeybindingString(ev) as ShortcutKey | null;
+ },
+ }),
+ {
+ name: "opencut-keybindings",
+ version: CURRENT_VERSION,
+ partialize: (state) => ({
+ keybindings: state.keybindings,
+ isCustomized: state.isCustomized,
+ }),
+ migrate: (persisted, version) =>
+ runMigrations({ state: persisted, fromVersion: version }),
+ },
+ ),
+);
+
+function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
+ const target = ev.target;
+ const modifierKey = getActiveModifier(ev);
+ const key = getPressedKey(ev);
+ if (!key) return null;
+
+ if (modifierKey) {
+ if (
+ modifierKey === "shift" &&
+ isDOMElement(target) &&
+ isTypableDOMElement({ element: target as HTMLElement })
+ ) {
+ return null;
+ }
+
+ return `${modifierKey}+${key}` as ShortcutKey;
+ }
+
+ if (
+ isDOMElement(target) &&
+ isTypableDOMElement({ element: target as HTMLElement })
+ )
+ return null;
+
+ return `${key}` as ShortcutKey;
+}
+
+function getPressedKey(ev: KeyboardEvent): string | null {
+ const key = (ev.key ?? "").toLowerCase();
+ const code = ev.code ?? "";
+
+ if (code === "Space" || key === " " || key === "spacebar" || key === "space")
+ return "space";
+
+ if (key.startsWith("arrow")) return key.slice(5);
+
+ if (key === "escape") return "escape";
+ if (key === "tab") return "tab";
+ if (key === "home") return "home";
+ if (key === "end") return "end";
+ if (key === "delete") return "delete";
+ if (key === "backspace") return "backspace";
+
+ if (code.startsWith("Key")) {
+ const letter = code.slice(3).toLowerCase();
+ if (letter.length === 1 && letter >= "a" && letter <= "z") return letter;
+ }
+
+ // Use physical key position for AZERTY and other non-QWERTY layouts
+ if (code.startsWith("Digit")) {
+ const digit = code.slice(5);
+ if (digit.length === 1 && digit >= "0" && digit <= "9") return digit;
+ }
+
+ const isDigit = key.length === 1 && key >= "0" && key <= "9";
+ if (isDigit) return key;
+
+ if (key === "/" || key === "." || key === "enter") return key;
+
+ return null;
+}
+
+function getActiveModifier(ev: KeyboardEvent): string | null {
+ const modifierKeys = {
+ ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
+ alt: ev.altKey,
+ shift: ev.shiftKey,
+ };
+
+ const activeModifier = Object.keys(modifierKeys)
+ .filter((key) => modifierKeys[key as keyof typeof modifierKeys])
+ .join("+");
+
+ return activeModifier === "" ? null : activeModifier;
+}
diff --git a/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts b/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts
index da0df588..6ebff92c 100644
--- a/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts
+++ b/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts
@@ -1,26 +1,26 @@
-import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
-import type { TActionWithOptionalArgs } from "@/lib/actions";
-
-interface V2State {
- keybindings: KeybindingConfig;
- isCustomized: boolean;
-}
-
-export function v2ToV3({ state }: { state: unknown }): unknown {
- const v2 = state as V2State;
-
- const renames: Record<string, string> = {
- "split-selected": "split",
- "split-selected-left": "split-left",
- "split-selected-right": "split-right",
- };
-
- const migrated = { ...v2.keybindings };
- for (const [key, action] of Object.entries(migrated)) {
- if (action && renames[action]) {
- migrated[key as ShortcutKey] = renames[action] as TActionWithOptionalArgs;
- }
- }
-
- return { ...v2, keybindings: migrated };
-}
+import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
+import type { TActionWithOptionalArgs } from "@/lib/actions";
+
+interface V2State {
+ keybindings: KeybindingConfig;
+ isCustomized: boolean;
+}
+
+export function v2ToV3({ state }: { state: unknown }): unknown {
+ const v2 = state as V2State;
+
+ const renames: Record<string, string> = {
+ "split-selected": "split",
+ "split-selected-left": "split-left",
+ "split-selected-right": "split-right",
+ };
+
+ const migrated = { ...v2.keybindings };
+ for (const [key, action] of Object.entries(migrated)) {
+ if (action && renames[action]) {
+ migrated[key as ShortcutKey] = renames[action] as TActionWithOptionalArgs;
+ }
+ }
+
+ return { ...v2, keybindings: migrated };
+}
diff --git a/apps/web/src/stores/keybindings/migrations/v3-to-v4.ts b/apps/web/src/stores/keybindings/migrations/v3-to-v4.ts
index be3b4a64..0f9b35da 100644
--- a/apps/web/src/stores/keybindings/migrations/v3-to-v4.ts
+++ b/apps/web/src/stores/keybindings/migrations/v3-to-v4.ts
@@ -1,25 +1,25 @@
-import type { TActionWithOptionalArgs } from "@/lib/actions";
-import type { ShortcutKey } from "@/lib/actions/keybinding";
-import type { KeybindingConfig } from "@/lib/actions/keybinding";
-
-interface V3State {
- keybindings: KeybindingConfig;
- isCustomized: boolean;
-}
-
-export function v3ToV4({ state }: { state: unknown }): unknown {
- const v3 = state as V3State;
-
- const renames: Record<string, string> = {
- "paste-selected": "paste-copied",
- };
-
- const migrated = { ...v3.keybindings };
- for (const [key, action] of Object.entries(migrated)) {
- if (action && renames[action]) {
- migrated[key as ShortcutKey] = renames[action] as TActionWithOptionalArgs;
- }
- }
-
- return { ...v3, keybindings: migrated };
-}
+import type { TActionWithOptionalArgs } from "@/lib/actions";
+import type { ShortcutKey } from "@/lib/actions/keybinding";
+import type { KeybindingConfig } from "@/lib/actions/keybinding";
+
+interface V3State {
+ keybindings: KeybindingConfig;
+ isCustomized: boolean;
+}
+
+export function v3ToV4({ state }: { state: unknown }): unknown {
+ const v3 = state as V3State;
+
+ const renames: Record<string, string> = {
+ "paste-selected": "paste-copied",
+ };
+
+ const migrated = { ...v3.keybindings };
+ for (const [key, action] of Object.entries(migrated)) {
+ if (action && renames[action]) {
+ migrated[key as ShortcutKey] = renames[action] as TActionWithOptionalArgs;
+ }
+ }
+
+ return { ...v3, keybindings: migrated };
+}
diff --git a/apps/web/src/stores/keybindings/migrations/v4-to-v5.ts b/apps/web/src/stores/keybindings/migrations/v4-to-v5.ts
index 4e23e0f4..880df1dc 100644
--- a/apps/web/src/stores/keybindings/migrations/v4-to-v5.ts
+++ b/apps/web/src/stores/keybindings/migrations/v4-to-v5.ts
@@ -1,17 +1,17 @@
-import type { KeybindingConfig } from "@/lib/actions/keybinding";
-
-interface V4State {
- keybindings: KeybindingConfig;
- isCustomized: boolean;
-}
-
-export function v4ToV5({ state }: { state: unknown }): unknown {
- const v4 = state as V4State;
- const keybindings = { ...v4.keybindings };
-
- if (!keybindings.escape) {
- keybindings.escape = "deselect-all";
- }
-
- return { ...v4, keybindings };
-}
+import type { KeybindingConfig } from "@/lib/actions/keybinding";
+
+interface V4State {
+ keybindings: KeybindingConfig;
+ isCustomized: boolean;
+}
+
+export function v4ToV5({ state }: { state: unknown }): unknown {
+ const v4 = state as V4State;
+ const keybindings = { ...v4.keybindings };
+
+ if (!keybindings.escape) {
+ keybindings.escape = "deselect-all";
+ }
+
+ return { ...v4, keybindings };
+}
diff --git a/apps/web/src/stores/keybindings/migrations/v5-to-v6.ts b/apps/web/src/stores/keybindings/migrations/v5-to-v6.ts
index 3545815d..ebabb7c0 100644
--- a/apps/web/src/stores/keybindings/migrations/v5-to-v6.ts
+++ b/apps/web/src/stores/keybindings/migrations/v5-to-v6.ts
@@ -1,17 +1,17 @@
-import type { KeybindingConfig } from "@/lib/actions/keybinding";
-
-interface V5State {
- keybindings: KeybindingConfig;
- isCustomized: boolean;
-}
-
-export function v5ToV6({ state }: { state: unknown }): unknown {
- const v5 = state as V5State;
- const keybindings = { ...v5.keybindings };
-
- if (keybindings.escape === "deselect-all") {
- keybindings.escape = "cancel-interaction";
- }
-
- return { ...v5, keybindings };
-}
+import type { KeybindingConfig } from "@/lib/actions/keybinding";
+
+interface V5State {
+ keybindings: KeybindingConfig;
+ isCustomized: boolean;
+}
+
+export function v5ToV6({ state }: { state: unknown }): unknown {
+ const v5 = state as V5State;
+ const keybindings = { ...v5.keybindings };
+
+ if (keybindings.escape === "deselect-all") {
+ keybindings.escape = "cancel-interaction";
+ }
+
+ return { ...v5, keybindings };
+}
diff --git a/apps/web/src/stores/panel-store.ts b/apps/web/src/stores/panel-store.ts
index 5d2621a4..6f22b6b4 100644
--- a/apps/web/src/stores/panel-store.ts
+++ b/apps/web/src/stores/panel-store.ts
@@ -1,93 +1,93 @@
-import { create } from "zustand";
-import { persist } from "zustand/middleware";
-import { PANEL_CONFIG } from "@/constants/editor-constants";
-
-export interface PanelSizes {
- tools: number;
- preview: number;
- properties: number;
- mainContent: number;
- timeline: number;
-}
-
-export type PanelId = keyof PanelSizes;
-
-interface PanelState {
- panels: PanelSizes;
- setPanel: (panel: PanelId, size: number) => void;
- setPanels: (sizes: Partial<PanelSizes>) => void;
- resetPanels: () => void;
-}
-
-export const usePanelStore = create<PanelState>()(
- persist(
- (set) => ({
- ...PANEL_CONFIG,
- setPanel: (panel, size) =>
- set((state) => ({
- panels: {
- ...state.panels,
- [panel]: size,
- },
- })),
- setPanels: (sizes) =>
- set((state) => ({
- panels: {
- ...state.panels,
- ...sizes,
- },
- })),
- resetPanels: () => set({ ...PANEL_CONFIG }),
- }),
- {
- name: "panel-sizes",
- version: 2,
- migrate: (persistedState) => {
- const state = persistedState as
- | {
- panels?: Partial<PanelSizes> | null;
- toolsPanel?: number;
- previewPanel?: number;
- propertiesPanel?: number;
- mainContent?: number;
- timeline?: number;
- tools?: number;
- preview?: number;
- properties?: number;
- }
- | undefined
- | null;
-
- if (!state) return { panels: { ...PANEL_CONFIG.panels } };
-
- if (state.panels && typeof state.panels === "object") {
- return {
- panels: {
- ...PANEL_CONFIG.panels,
- ...state.panels,
- },
- };
- }
-
- return {
- panels: {
- tools: state.tools ?? state.toolsPanel ?? PANEL_CONFIG.panels.tools,
- preview:
- state.preview ??
- state.previewPanel ??
- PANEL_CONFIG.panels.preview,
- properties:
- state.properties ??
- state.propertiesPanel ??
- PANEL_CONFIG.panels.properties,
- mainContent: state.mainContent ?? PANEL_CONFIG.panels.mainContent,
- timeline: state.timeline ?? PANEL_CONFIG.panels.timeline,
- },
- };
- },
- partialize: (state) => ({
- panels: state.panels,
- }),
- },
- ),
-);
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import { PANEL_CONFIG } from "@/constants/editor-constants";
+
+export interface PanelSizes {
+ tools: number;
+ preview: number;
+ properties: number;
+ mainContent: number;
+ timeline: number;
+}
+
+export type PanelId = keyof PanelSizes;
+
+interface PanelState {
+ panels: PanelSizes;
+ setPanel: (panel: PanelId, size: number) => void;
+ setPanels: (sizes: Partial<PanelSizes>) => void;
+ resetPanels: () => void;
+}
+
+export const usePanelStore = create<PanelState>()(
+ persist(
+ (set) => ({
+ ...PANEL_CONFIG,
+ setPanel: (panel, size) =>
+ set((state) => ({
+ panels: {
+ ...state.panels,
+ [panel]: size,
+ },
+ })),
+ setPanels: (sizes) =>
+ set((state) => ({
+ panels: {
+ ...state.panels,
+ ...sizes,
+ },
+ })),
+ resetPanels: () => set({ ...PANEL_CONFIG }),
+ }),
+ {
+ name: "panel-sizes",
+ version: 2,
+ migrate: (persistedState) => {
+ const state = persistedState as
+ | {
+ panels?: Partial<PanelSizes> | null;
+ toolsPanel?: number;
+ previewPanel?: number;
+ propertiesPanel?: number;
+ mainContent?: number;
+ timeline?: number;
+ tools?: number;
+ preview?: number;
+ properties?: number;
+ }
+ | undefined
+ | null;
+
+ if (!state) return { panels: { ...PANEL_CONFIG.panels } };
+
+ if (state.panels && typeof state.panels === "object") {
+ return {
+ panels: {
+ ...PANEL_CONFIG.panels,
+ ...state.panels,
+ },
+ };
+ }
+
+ return {
+ panels: {
+ tools: state.tools ?? state.toolsPanel ?? PANEL_CONFIG.panels.tools,
+ preview:
+ state.preview ??
+ state.previewPanel ??
+ PANEL_CONFIG.panels.preview,
+ properties:
+ state.properties ??
+ state.propertiesPanel ??
+ PANEL_CONFIG.panels.properties,
+ mainContent: state.mainContent ?? PANEL_CONFIG.panels.mainContent,
+ timeline: state.timeline ?? PANEL_CONFIG.panels.timeline,
+ },
+ };
+ },
+ partialize: (state) => ({
+ panels: state.panels,
+ }),
+ },
+ ),
+);
diff --git a/apps/web/src/stores/preview-store.ts b/apps/web/src/stores/preview-store.ts
index cd6488b3..d0489e3b 100644
--- a/apps/web/src/stores/preview-store.ts
+++ b/apps/web/src/stores/preview-store.ts
@@ -1,112 +1,112 @@
-import { create } from "zustand";
-import { persist } from "zustand/middleware";
-import { isGuideId, type GuideId } from "@/lib/guides";
-import { DEFAULT_GRID_CONFIG } from "@/constants/guide-constants";
-import type { GridConfig } from "@/lib/guides/types";
-
-interface PreviewOverlaysState {
- bookmarks: boolean;
-}
-
-interface PersistedPreviewState {
- activeGuide?: string | null;
- layoutGuide?: {
- platform?: string | null;
- };
- overlays?: PreviewOverlaysState;
- gridConfig?: GridConfig;
-}
-
-interface PreviewState {
- activeGuide: GuideId | null;
- overlays: PreviewOverlaysState;
- gridConfig: GridConfig;
- toggleGuide: (guideId: GuideId) => void;
- setGridConfig: (config: Partial<GridConfig>) => void;
- setOverlayVisibility: ({
- overlay,
- isVisible,
- }: {
- overlay: keyof PreviewOverlaysState;
- isVisible: boolean;
- }) => void;
- toggleOverlayVisibility: ({
- overlay,
- }: {
- overlay: keyof PreviewOverlaysState;
- }) => void;
-}
-
-const DEFAULT_PREVIEW_OVERLAYS: PreviewOverlaysState = {
- bookmarks: true,
-};
-
-function getPersistedActiveGuide(
- state: PersistedPreviewState | undefined,
-): GuideId | null {
- const persistedGuide =
- state?.activeGuide ?? state?.layoutGuide?.platform ?? null;
-
- if (typeof persistedGuide !== "string") {
- return null;
- }
-
- return isGuideId(persistedGuide) ? persistedGuide : null;
-}
-
-export const usePreviewStore = create<PreviewState>()(
- persist(
- (set) => ({
- activeGuide: null,
- overlays: DEFAULT_PREVIEW_OVERLAYS,
- gridConfig: DEFAULT_GRID_CONFIG,
- toggleGuide: (guideId) => {
- set((state) => ({
- activeGuide: state.activeGuide === guideId ? null : guideId,
- }));
- },
- setGridConfig: (config) => {
- set((state) => ({
- gridConfig: { ...state.gridConfig, ...config },
- }));
- },
- setOverlayVisibility: ({ overlay, isVisible }) => {
- set((state) => ({
- overlays: {
- ...state.overlays,
- [overlay]: isVisible,
- },
- }));
- },
- toggleOverlayVisibility: ({ overlay }) => {
- set((state) => ({
- overlays: {
- ...state.overlays,
- [overlay]: !state.overlays[overlay],
- },
- }));
- },
- }),
- {
- name: "preview-settings",
- version: 4,
- migrate: (persistedState) => {
- const state = persistedState as PersistedPreviewState | undefined;
-
- return {
- activeGuide: getPersistedActiveGuide(state),
- overlays: state?.overlays ?? DEFAULT_PREVIEW_OVERLAYS,
- gridConfig: {
- rows: state?.gridConfig?.rows ?? DEFAULT_GRID_CONFIG.rows,
- cols: state?.gridConfig?.cols ?? DEFAULT_GRID_CONFIG.cols,
- },
- };
- },
- partialize: (state) => ({
- activeGuide: state.activeGuide,
- overlays: state.overlays,
- gridConfig: state.gridConfig,
- }),
- },
- ),
-);
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import { isGuideId, type GuideId } from "@/lib/guides";
+import { DEFAULT_GRID_CONFIG } from "@/constants/guide-constants";
+import type { GridConfig } from "@/lib/guides/types";
+
+interface PreviewOverlaysState {
+ bookmarks: boolean;
+}
+
+interface PersistedPreviewState {
+ activeGuide?: string | null;
+ layoutGuide?: {
+ platform?: string | null;
+ };
+ overlays?: PreviewOverlaysState;
+ gridConfig?: GridConfig;
+}
+
+interface PreviewState {
+ activeGuide: GuideId | null;
+ overlays: PreviewOverlaysState;
+ gridConfig: GridConfig;
+ toggleGuide: (guideId: GuideId) => void;
+ setGridConfig: (config: Partial<GridConfig>) => void;
+ setOverlayVisibility: ({
+ overlay,
+ isVisible,
+ }: {
+ overlay: keyof PreviewOverlaysState;
+ isVisible: boolean;
+ }) => void;
+ toggleOverlayVisibility: ({
+ overlay,
+ }: {
+ overlay: keyof PreviewOverlaysState;
+ }) => void;
+}
+
+const DEFAULT_PREVIEW_OVERLAYS: PreviewOverlaysState = {
+ bookmarks: true,
+};
+
+function getPersistedActiveGuide(
+ state: PersistedPreviewState | undefined,
+): GuideId | null {
+ const persistedGuide =
+ state?.activeGuide ?? state?.layoutGuide?.platform ?? null;
+
+ if (typeof persistedGuide !== "string") {
+ return null;
+ }
+
+ return isGuideId(persistedGuide) ? persistedGuide : null;
+}
+
+export const usePreviewStore = create<PreviewState>()(
+ persist(
+ (set) => ({
+ activeGuide: null,
+ overlays: DEFAULT_PREVIEW_OVERLAYS,
+ gridConfig: DEFAULT_GRID_CONFIG,
+ toggleGuide: (guideId) => {
+ set((state) => ({
+ activeGuide: state.activeGuide === guideId ? null : guideId,
+ }));
+ },
+ setGridConfig: (config) => {
+ set((state) => ({
+ gridConfig: { ...state.gridConfig, ...config },
+ }));
+ },
+ setOverlayVisibility: ({ overlay, isVisible }) => {
+ set((state) => ({
+ overlays: {
+ ...state.overlays,
+ [overlay]: isVisible,
+ },
+ }));
+ },
+ toggleOverlayVisibility: ({ overlay }) => {
+ set((state) => ({
+ overlays: {
+ ...state.overlays,
+ [overlay]: !state.overlays[overlay],
+ },
+ }));
+ },
+ }),
+ {
+ name: "preview-settings",
+ version: 4,
+ migrate: (persistedState) => {
+ const state = persistedState as PersistedPreviewState | undefined;
+
+ return {
+ activeGuide: getPersistedActiveGuide(state),
+ overlays: state?.overlays ?? DEFAULT_PREVIEW_OVERLAYS,
+ gridConfig: {
+ rows: state?.gridConfig?.rows ?? DEFAULT_GRID_CONFIG.rows,
+ cols: state?.gridConfig?.cols ?? DEFAULT_GRID_CONFIG.cols,
+ },
+ };
+ },
+ partialize: (state) => ({
+ activeGuide: state.activeGuide,
+ overlays: state.overlays,
+ gridConfig: state.gridConfig,
+ }),
+ },
+ ),
+);
diff --git a/apps/web/src/stores/sounds-store.ts b/apps/web/src/stores/sounds-store.ts
index 1c58bd69..b32d77bc 100644
--- a/apps/web/src/stores/sounds-store.ts
+++ b/apps/web/src/stores/sounds-store.ts
@@ -1,261 +1,261 @@
-import { create } from "zustand";
-import type { SoundEffect, SavedSound } from "@/lib/sounds/types";
-import { storageService } from "@/services/storage/service";
-import { toast } from "sonner";
-import { EditorCore } from "@/core";
-import { buildLibraryAudioElement } from "@/lib/timeline/element-utils";
-
-interface SoundsStore {
- topSoundEffects: SoundEffect[];
- isLoading: boolean;
- error: string | null;
- hasLoaded: boolean;
- showCommercialOnly: boolean;
- toggleCommercialFilter: () => void;
- searchQuery: string;
- searchResults: SoundEffect[];
- isSearching: boolean;
- searchError: string | null;
- lastSearchQuery: string;
- scrollPosition: number;
- currentPage: number;
- hasNextPage: boolean;
- totalCount: number;
- isLoadingMore: boolean;
- savedSounds: SavedSound[];
- isSavedSoundsLoaded: boolean;
- isLoadingSavedSounds: boolean;
- savedSoundsError: string | null;
-
- addSoundToTimeline: ({ sound }: { sound: SoundEffect }) => Promise<boolean>;
- setTopSoundEffects: ({ sounds }: { sounds: SoundEffect[] }) => void;
- setLoading: ({ loading }: { loading: boolean }) => void;
- setError: ({ error }: { error: string | null }) => void;
- setHasLoaded: ({ loaded }: { loaded: boolean }) => void;
- setSearchQuery: ({ query }: { query: string }) => void;
- setSearchResults: ({ results }: { results: SoundEffect[] }) => void;
- setSearching: ({ searching }: { searching: boolean }) => void;
- setSearchError: ({ error }: { error: string | null }) => void;
- setLastSearchQuery: ({ query }: { query: string }) => void;
- setScrollPosition: ({ position }: { position: number }) => void;
- setCurrentPage: ({ page }: { page: number }) => void;
- setHasNextPage: ({ hasNext }: { hasNext: boolean }) => void;
- setTotalCount: ({ count }: { count: number }) => void;
- setLoadingMore: ({ loading }: { loading: boolean }) => void;
- appendSearchResults: ({ results }: { results: SoundEffect[] }) => void;
- appendTopSounds: ({ results }: { results: SoundEffect[] }) => void;
- resetPagination: () => void;
- loadSavedSounds: () => Promise<void>;
- saveSoundEffect: ({
- soundEffect,
- }: {
- soundEffect: SoundEffect;
- }) => Promise<void>;
- removeSavedSound: ({ soundId }: { soundId: number }) => Promise<void>;
- isSoundSaved: ({ soundId }: { soundId: number }) => boolean;
- toggleSavedSound: ({
- soundEffect,
- }: {
- soundEffect: SoundEffect;
- }) => Promise<void>;
- clearSavedSounds: () => Promise<void>;
-}
-
-export const useSoundsStore = create<SoundsStore>((set, get) => ({
- topSoundEffects: [],
- isLoading: false,
- error: null,
- hasLoaded: false,
- showCommercialOnly: true,
-
- toggleCommercialFilter: () => {
- set((state) => ({ showCommercialOnly: !state.showCommercialOnly }));
- },
-
- searchQuery: "",
- searchResults: [],
- isSearching: false,
- searchError: null,
- lastSearchQuery: "",
- scrollPosition: 0,
- currentPage: 1,
- hasNextPage: false,
- totalCount: 0,
- isLoadingMore: false,
- savedSounds: [],
- isSavedSoundsLoaded: false,
- isLoadingSavedSounds: false,
- savedSoundsError: null,
-
- setTopSoundEffects: ({ sounds }) => set({ topSoundEffects: sounds }),
- setLoading: ({ loading }) => set({ isLoading: loading }),
- setError: ({ error }) => set({ error }),
- setHasLoaded: ({ loaded }) => set({ hasLoaded: loaded }),
- setSearchQuery: ({ query }) => set({ searchQuery: query }),
- setSearchResults: ({ results }) =>
- set({ searchResults: results, currentPage: 1 }),
- setSearching: ({ searching }) => set({ isSearching: searching }),
- setSearchError: ({ error }) => set({ searchError: error }),
- setLastSearchQuery: ({ query }) => set({ lastSearchQuery: query }),
- setScrollPosition: ({ position }) => set({ scrollPosition: position }),
- setCurrentPage: ({ page }) => set({ currentPage: page }),
- setHasNextPage: ({ hasNext }) => set({ hasNextPage: hasNext }),
- setTotalCount: ({ count }) => set({ totalCount: count }),
- setLoadingMore: ({ loading }) => set({ isLoadingMore: loading }),
-
- appendSearchResults: ({ results }) =>
- set((state) => ({
- searchResults: [...state.searchResults, ...results],
- })),
-
- appendTopSounds: ({ results }) =>
- set((state) => ({
- topSoundEffects: [...state.topSoundEffects, ...results],
- })),
-
- resetPagination: () =>
- set({
- currentPage: 1,
- hasNextPage: false,
- totalCount: 0,
- isLoadingMore: false,
- }),
-
- loadSavedSounds: async () => {
- if (get().isSavedSoundsLoaded) return;
-
- try {
- set({ isLoadingSavedSounds: true, savedSoundsError: null });
- const savedSoundsData = await storageService.loadSavedSounds();
- set({
- savedSounds: savedSoundsData.sounds,
- isSavedSoundsLoaded: true,
- isLoadingSavedSounds: false,
- });
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Failed to load saved sounds";
- set({
- savedSoundsError: errorMessage,
- isLoadingSavedSounds: false,
- });
- console.error("Failed to load saved sounds:", error);
- }
- },
-
- saveSoundEffect: async ({ soundEffect }) => {
- try {
- await storageService.saveSoundEffect({ soundEffect });
-
- const savedSoundsData = await storageService.loadSavedSounds();
- set({ savedSounds: savedSoundsData.sounds });
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Failed to save sound";
- set({ savedSoundsError: errorMessage });
- toast.error("Failed to save sound");
- console.error("Failed to save sound:", error);
- }
- },
-
- removeSavedSound: async ({ soundId }) => {
- try {
- await storageService.removeSavedSound({ soundId });
-
- set((state) => ({
- savedSounds: state.savedSounds.filter((sound) => sound.id !== soundId),
- }));
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Failed to remove sound";
- set({ savedSoundsError: errorMessage });
- toast.error("Failed to remove sound");
- console.error("Failed to remove sound:", error);
- }
- },
-
- isSoundSaved: ({ soundId }) => {
- const { savedSounds } = get();
- return savedSounds.some((sound) => sound.id === soundId);
- },
-
- toggleSavedSound: async ({ soundEffect }) => {
- const { isSoundSaved, saveSoundEffect, removeSavedSound } = get();
-
- if (isSoundSaved({ soundId: soundEffect.id })) {
- await removeSavedSound({ soundId: soundEffect.id });
- } else {
- await saveSoundEffect({ soundEffect });
- }
- },
-
- clearSavedSounds: async () => {
- try {
- await storageService.clearSavedSounds();
- set({
- savedSounds: [],
- savedSoundsError: null,
- });
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Failed to clear saved sounds";
- set({ savedSoundsError: errorMessage });
- toast.error("Failed to clear saved sounds");
- console.error("Failed to clear saved sounds:", error);
- }
- },
-
- addSoundToTimeline: async ({ sound }) => {
- const audioUrl = sound.previewUrl;
- if (!audioUrl) {
- toast.error("Sound file not available");
- return false;
- }
-
- try {
- const editor = EditorCore.getInstance();
- const currentTime = editor.playback.getCurrentTime();
- const tracks = editor.timeline.getTracks();
-
- const response = await fetch(audioUrl);
- if (!response.ok)
- throw new Error(`Failed to download audio: ${response.statusText}`);
-
- const arrayBuffer = await response.arrayBuffer();
- const audioContext = new AudioContext();
- const buffer = await audioContext.decodeAudioData(arrayBuffer);
-
- const audioTrack = tracks.find((t) => t.type === "audio");
- let trackId: string;
-
- if (audioTrack) {
- trackId = audioTrack.id;
- } else {
- trackId = editor.timeline.addTrack({ type: "audio" });
- }
-
- const element = buildLibraryAudioElement({
- sourceUrl: audioUrl,
- name: sound.name,
- duration: sound.duration,
- startTime: currentTime,
- buffer,
- });
-
- editor.timeline.insertElement({
- placement: { mode: "explicit", trackId },
- element,
- });
- return true;
- } catch (error) {
- console.error("Failed to add sound to timeline:", error);
- toast.error(
- error instanceof Error
- ? error.message
- : "Failed to add sound to timeline",
- { id: `sound-${sound.id}` },
- );
- return false;
- }
- },
-}));
+import { create } from "zustand";
+import type { SoundEffect, SavedSound } from "@/lib/sounds/types";
+import { storageService } from "@/services/storage/service";
+import { toast } from "sonner";
+import { EditorCore } from "@/core";
+import { buildLibraryAudioElement } from "@/lib/timeline/element-utils";
+
+interface SoundsStore {
+ topSoundEffects: SoundEffect[];
+ isLoading: boolean;
+ error: string | null;
+ hasLoaded: boolean;
+ showCommercialOnly: boolean;
+ toggleCommercialFilter: () => void;
+ searchQuery: string;
+ searchResults: SoundEffect[];
+ isSearching: boolean;
+ searchError: string | null;
+ lastSearchQuery: string;
+ scrollPosition: number;
+ currentPage: number;
+ hasNextPage: boolean;
+ totalCount: number;
+ isLoadingMore: boolean;
+ savedSounds: SavedSound[];
+ isSavedSoundsLoaded: boolean;
+ isLoadingSavedSounds: boolean;
+ savedSoundsError: string | null;
+
+ addSoundToTimeline: ({ sound }: { sound: SoundEffect }) => Promise<boolean>;
+ setTopSoundEffects: ({ sounds }: { sounds: SoundEffect[] }) => void;
+ setLoading: ({ loading }: { loading: boolean }) => void;
+ setError: ({ error }: { error: string | null }) => void;
+ setHasLoaded: ({ loaded }: { loaded: boolean }) => void;
+ setSearchQuery: ({ query }: { query: string }) => void;
+ setSearchResults: ({ results }: { results: SoundEffect[] }) => void;
+ setSearching: ({ searching }: { searching: boolean }) => void;
+ setSearchError: ({ error }: { error: string | null }) => void;
+ setLastSearchQuery: ({ query }: { query: string }) => void;
+ setScrollPosition: ({ position }: { position: number }) => void;
+ setCurrentPage: ({ page }: { page: number }) => void;
+ setHasNextPage: ({ hasNext }: { hasNext: boolean }) => void;
+ setTotalCount: ({ count }: { count: number }) => void;
+ setLoadingMore: ({ loading }: { loading: boolean }) => void;
+ appendSearchResults: ({ results }: { results: SoundEffect[] }) => void;
+ appendTopSounds: ({ results }: { results: SoundEffect[] }) => void;
+ resetPagination: () => void;
+ loadSavedSounds: () => Promise<void>;
+ saveSoundEffect: ({
+ soundEffect,
+ }: {
+ soundEffect: SoundEffect;
+ }) => Promise<void>;
+ removeSavedSound: ({ soundId }: { soundId: number }) => Promise<void>;
+ isSoundSaved: ({ soundId }: { soundId: number }) => boolean;
+ toggleSavedSound: ({
+ soundEffect,
+ }: {
+ soundEffect: SoundEffect;
+ }) => Promise<void>;
+ clearSavedSounds: () => Promise<void>;
+}
+
+export const useSoundsStore = create<SoundsStore>((set, get) => ({
+ topSoundEffects: [],
+ isLoading: false,
+ error: null,
+ hasLoaded: false,
+ showCommercialOnly: true,
+
+ toggleCommercialFilter: () => {
+ set((state) => ({ showCommercialOnly: !state.showCommercialOnly }));
+ },
+
+ searchQuery: "",
+ searchResults: [],
+ isSearching: false,
+ searchError: null,
+ lastSearchQuery: "",
+ scrollPosition: 0,
+ currentPage: 1,
+ hasNextPage: false,
+ totalCount: 0,
+ isLoadingMore: false,
+ savedSounds: [],
+ isSavedSoundsLoaded: false,
+ isLoadingSavedSounds: false,
+ savedSoundsError: null,
+
+ setTopSoundEffects: ({ sounds }) => set({ topSoundEffects: sounds }),
+ setLoading: ({ loading }) => set({ isLoading: loading }),
+ setError: ({ error }) => set({ error }),
+ setHasLoaded: ({ loaded }) => set({ hasLoaded: loaded }),
+ setSearchQuery: ({ query }) => set({ searchQuery: query }),
+ setSearchResults: ({ results }) =>
+ set({ searchResults: results, currentPage: 1 }),
+ setSearching: ({ searching }) => set({ isSearching: searching }),
+ setSearchError: ({ error }) => set({ searchError: error }),
+ setLastSearchQuery: ({ query }) => set({ lastSearchQuery: query }),
+ setScrollPosition: ({ position }) => set({ scrollPosition: position }),
+ setCurrentPage: ({ page }) => set({ currentPage: page }),
+ setHasNextPage: ({ hasNext }) => set({ hasNextPage: hasNext }),
+ setTotalCount: ({ count }) => set({ totalCount: count }),
+ setLoadingMore: ({ loading }) => set({ isLoadingMore: loading }),
+
+ appendSearchResults: ({ results }) =>
+ set((state) => ({
+ searchResults: [...state.searchResults, ...results],
+ })),
+
+ appendTopSounds: ({ results }) =>
+ set((state) => ({
+ topSoundEffects: [...state.topSoundEffects, ...results],
+ })),
+
+ resetPagination: () =>
+ set({
+ currentPage: 1,
+ hasNextPage: false,
+ totalCount: 0,
+ isLoadingMore: false,
+ }),
+
+ loadSavedSounds: async () => {
+ if (get().isSavedSoundsLoaded) return;
+
+ try {
+ set({ isLoadingSavedSounds: true, savedSoundsError: null });
+ const savedSoundsData = await storageService.loadSavedSounds();
+ set({
+ savedSounds: savedSoundsData.sounds,
+ isSavedSoundsLoaded: true,
+ isLoadingSavedSounds: false,
+ });
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Failed to load saved sounds";
+ set({
+ savedSoundsError: errorMessage,
+ isLoadingSavedSounds: false,
+ });
+ console.error("Failed to load saved sounds:", error);
+ }
+ },
+
+ saveSoundEffect: async ({ soundEffect }) => {
+ try {
+ await storageService.saveSoundEffect({ soundEffect });
+
+ const savedSoundsData = await storageService.loadSavedSounds();
+ set({ savedSounds: savedSoundsData.sounds });
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Failed to save sound";
+ set({ savedSoundsError: errorMessage });
+ toast.error("Failed to save sound");
+ console.error("Failed to save sound:", error);
+ }
+ },
+
+ removeSavedSound: async ({ soundId }) => {
+ try {
+ await storageService.removeSavedSound({ soundId });
+
+ set((state) => ({
+ savedSounds: state.savedSounds.filter((sound) => sound.id !== soundId),
+ }));
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Failed to remove sound";
+ set({ savedSoundsError: errorMessage });
+ toast.error("Failed to remove sound");
+ console.error("Failed to remove sound:", error);
+ }
+ },
+
+ isSoundSaved: ({ soundId }) => {
+ const { savedSounds } = get();
+ return savedSounds.some((sound) => sound.id === soundId);
+ },
+
+ toggleSavedSound: async ({ soundEffect }) => {
+ const { isSoundSaved, saveSoundEffect, removeSavedSound } = get();
+
+ if (isSoundSaved({ soundId: soundEffect.id })) {
+ await removeSavedSound({ soundId: soundEffect.id });
+ } else {
+ await saveSoundEffect({ soundEffect });
+ }
+ },
+
+ clearSavedSounds: async () => {
+ try {
+ await storageService.clearSavedSounds();
+ set({
+ savedSounds: [],
+ savedSoundsError: null,
+ });
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Failed to clear saved sounds";
+ set({ savedSoundsError: errorMessage });
+ toast.error("Failed to clear saved sounds");
+ console.error("Failed to clear saved sounds:", error);
+ }
+ },
+
+ addSoundToTimeline: async ({ sound }) => {
+ const audioUrl = sound.previewUrl;
+ if (!audioUrl) {
+ toast.error("Sound file not available");
+ return false;
+ }
+
+ try {
+ const editor = EditorCore.getInstance();
+ const currentTime = editor.playback.getCurrentTime();
+ const tracks = editor.timeline.getTracks();
+
+ const response = await fetch(audioUrl);
+ if (!response.ok)
+ throw new Error(`Failed to download audio: ${response.statusText}`);
+
+ const arrayBuffer = await response.arrayBuffer();
+ const audioContext = new AudioContext();
+ const buffer = await audioContext.decodeAudioData(arrayBuffer);
+
+ const audioTrack = tracks.find((t) => t.type === "audio");
+ let trackId: string;
+
+ if (audioTrack) {
+ trackId = audioTrack.id;
+ } else {
+ trackId = editor.timeline.addTrack({ type: "audio" });
+ }
+
+ const element = buildLibraryAudioElement({
+ sourceUrl: audioUrl,
+ name: sound.name,
+ duration: sound.duration,
+ startTime: currentTime,
+ buffer,
+ });
+
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId },
+ element,
+ });
+ return true;
+ } catch (error) {
+ console.error("Failed to add sound to timeline:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to add sound to timeline",
+ { id: `sound-${sound.id}` },
+ );
+ return false;
+ }
+ },
+}));
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts
index e4c8ec89..c8428355 100644
--- a/apps/web/src/stores/timeline-store.ts
+++ b/apps/web/src/stores/timeline-store.ts
@@ -1,56 +1,56 @@
-/**
- * UI state for the timeline
- * For core logic, use EditorCore instead.
- */
-
-import { create } from "zustand";
-import { persist } from "zustand/middleware";
-import type { ClipboardItem } from "@/lib/timeline";
-
-interface TimelineStore {
- snappingEnabled: boolean;
- toggleSnapping: () => void;
- rippleEditingEnabled: boolean;
- toggleRippleEditing: () => void;
- clipboard: {
- items: ClipboardItem[];
- } | null;
- setClipboard: (
- clipboard: {
- items: ClipboardItem[];
- } | null,
- ) => void;
-}
-
-export const useTimelineStore = create<TimelineStore>()(
- persist(
- (set) => ({
- snappingEnabled: true,
-
- toggleSnapping: () => {
- set((state) => ({ snappingEnabled: !state.snappingEnabled }));
- },
-
- rippleEditingEnabled: false,
-
- toggleRippleEditing: () => {
- set((state) => ({
- rippleEditingEnabled: !state.rippleEditingEnabled,
- }));
- },
-
- clipboard: null,
-
- setClipboard: (clipboard) => {
- set({ clipboard });
- },
- }),
- {
- name: "timeline-store",
- partialize: (state) => ({
- snappingEnabled: state.snappingEnabled,
- rippleEditingEnabled: state.rippleEditingEnabled,
- }),
- },
- ),
-);
+/**
+ * UI state for the timeline
+ * For core logic, use EditorCore instead.
+ */
+
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import type { ClipboardItem } from "@/lib/timeline";
+
+interface TimelineStore {
+ snappingEnabled: boolean;
+ toggleSnapping: () => void;
+ rippleEditingEnabled: boolean;
+ toggleRippleEditing: () => void;
+ clipboard: {
+ items: ClipboardItem[];
+ } | null;
+ setClipboard: (
+ clipboard: {
+ items: ClipboardItem[];
+ } | null,
+ ) => void;
+}
+
+export const useTimelineStore = create<TimelineStore>()(
+ persist(
+ (set) => ({
+ snappingEnabled: true,
+
+ toggleSnapping: () => {
+ set((state) => ({ snappingEnabled: !state.snappingEnabled }));
+ },
+
+ rippleEditingEnabled: false,
+
+ toggleRippleEditing: () => {
+ set((state) => ({
+ rippleEditingEnabled: !state.rippleEditingEnabled,
+ }));
+ },
+
+ clipboard: null,
+
+ setClipboard: (clipboard) => {
+ set({ clipboard });
+ },
+ }),
+ {
+ name: "timeline-store",
+ partialize: (state) => ({
+ snappingEnabled: state.snappingEnabled,
+ rippleEditingEnabled: state.rippleEditingEnabled,
+ }),
+ },
+ ),
+);