From 4d127a038bf768eded68aae3cfa5c1b12241bdac Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Fri, 27 Mar 2026 00:55:15 +0100 Subject: chore: normalize line endings --- apps/web/src/stores/editor-store.ts | 48 +- apps/web/src/stores/keybindings-store.ts | 694 ++++++++++----------- .../src/stores/keybindings/migrations/v2-to-v3.ts | 52 +- .../src/stores/keybindings/migrations/v3-to-v4.ts | 50 +- .../src/stores/keybindings/migrations/v4-to-v5.ts | 34 +- .../src/stores/keybindings/migrations/v5-to-v6.ts | 34 +- apps/web/src/stores/panel-store.ts | 186 +++--- apps/web/src/stores/preview-store.ts | 224 +++---- apps/web/src/stores/sounds-store.ts | 522 ++++++++-------- apps/web/src/stores/timeline-store.ts | 112 ++-- 10 files changed, 978 insertions(+), 978 deletions(-) (limited to 'apps/web/src/stores') 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; -} - -export const useEditorStore = create()((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; +} + +export const useEditorStore = create()((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()( - 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()( + 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 = { - "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 = { + "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 = { - "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 = { + "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) => void; - resetPanels: () => void; -} - -export const usePanelStore = create()( - 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 | 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) => void; + resetPanels: () => void; +} + +export const usePanelStore = create()( + 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 | 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) => 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()( - 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) => 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()( + 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; - 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; - saveSoundEffect: ({ - soundEffect, - }: { - soundEffect: SoundEffect; - }) => Promise; - removeSavedSound: ({ soundId }: { soundId: number }) => Promise; - isSoundSaved: ({ soundId }: { soundId: number }) => boolean; - toggleSavedSound: ({ - soundEffect, - }: { - soundEffect: SoundEffect; - }) => Promise; - clearSavedSounds: () => Promise; -} - -export const useSoundsStore = create((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; + 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; + saveSoundEffect: ({ + soundEffect, + }: { + soundEffect: SoundEffect; + }) => Promise; + removeSavedSound: ({ soundId }: { soundId: number }) => Promise; + isSoundSaved: ({ soundId }: { soundId: number }) => boolean; + toggleSavedSound: ({ + soundEffect, + }: { + soundEffect: SoundEffect; + }) => Promise; + clearSavedSounds: () => Promise; +} + +export const useSoundsStore = create((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()( - 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()( + 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, + }), + }, + ), +); -- cgit v1.3.1