From 3e89d2998588f8e35c524e523ba2ee853f0f6475 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 20 Apr 2026 11:31:17 +0200 Subject: refactor: restructure files to their domains, new preview overlay system, and dep graph --- apps/web/src/stores/assets-panel-store.tsx | 124 ---------- apps/web/src/stores/editor-store.ts | 24 -- apps/web/src/stores/keybindings-store.ts | 251 --------------------- .../web/src/stores/keybindings/migrations/index.ts | 32 --- .../src/stores/keybindings/migrations/v2-to-v3.ts | 26 --- .../src/stores/keybindings/migrations/v3-to-v4.ts | 25 -- .../src/stores/keybindings/migrations/v4-to-v5.ts | 17 -- .../src/stores/keybindings/migrations/v5-to-v6.ts | 17 -- .../src/stores/keybindings/migrations/v6-to-v7.ts | 19 -- apps/web/src/stores/panel-store.ts | 93 -------- apps/web/src/stores/preview-store.ts | 112 --------- apps/web/src/stores/properties-store.ts | 19 -- apps/web/src/stores/sounds-store.ts | 251 --------------------- apps/web/src/stores/stickers-store.ts | 238 ------------------- apps/web/src/stores/timeline-store.ts | 57 ----- 15 files changed, 1305 deletions(-) delete mode 100644 apps/web/src/stores/assets-panel-store.tsx delete mode 100644 apps/web/src/stores/editor-store.ts delete mode 100644 apps/web/src/stores/keybindings-store.ts delete mode 100644 apps/web/src/stores/keybindings/migrations/index.ts delete mode 100644 apps/web/src/stores/keybindings/migrations/v2-to-v3.ts delete mode 100644 apps/web/src/stores/keybindings/migrations/v3-to-v4.ts delete mode 100644 apps/web/src/stores/keybindings/migrations/v4-to-v5.ts delete mode 100644 apps/web/src/stores/keybindings/migrations/v5-to-v6.ts delete mode 100644 apps/web/src/stores/keybindings/migrations/v6-to-v7.ts delete mode 100644 apps/web/src/stores/panel-store.ts delete mode 100644 apps/web/src/stores/preview-store.ts delete mode 100644 apps/web/src/stores/properties-store.ts delete mode 100644 apps/web/src/stores/sounds-store.ts delete mode 100644 apps/web/src/stores/stickers-store.ts delete mode 100644 apps/web/src/stores/timeline-store.ts (limited to 'apps/web/src/stores') diff --git a/apps/web/src/stores/assets-panel-store.tsx b/apps/web/src/stores/assets-panel-store.tsx deleted file mode 100644 index 1539d305..00000000 --- a/apps/web/src/stores/assets-panel-store.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import type { ElementType } from "react"; -import { create } from "zustand"; -import { persist } from "zustand/middleware"; -import { - ArrowRightDoubleIcon, - ClosedCaptionIcon, - Folder03Icon, - Happy01Icon, - HeadphonesIcon, - MagicWand05Icon, - TextIcon, - Settings01Icon, - SlidersHorizontalIcon, - ColorsIcon, -} from "@hugeicons/core-free-icons"; -import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react"; - -export const TAB_KEYS = [ - "media", - "sounds", - "text", - "stickers", - "effects", - "transitions", - "captions", - "adjustment", - "settings", -] as const; - -export type Tab = (typeof TAB_KEYS)[number]; - -const createHugeiconsIcon = - ({ icon }: { icon: IconSvgElement }) => - ({ className }: { className?: string }) => ( - - ); - -export const tabs = { - media: { - icon: createHugeiconsIcon({ icon: Folder03Icon }), - label: "Media", - }, - sounds: { - icon: createHugeiconsIcon({ icon: HeadphonesIcon }), - label: "Sounds", - }, - text: { - icon: createHugeiconsIcon({ icon: TextIcon }), - label: "Text", - }, - stickers: { - icon: createHugeiconsIcon({ icon: Happy01Icon }), - label: "Stickers", - }, - effects: { - icon: createHugeiconsIcon({ icon: MagicWand05Icon }), - label: "Effects", - }, - transitions: { - icon: createHugeiconsIcon({ icon: ArrowRightDoubleIcon }), - label: "Transitions", - }, - captions: { - icon: createHugeiconsIcon({ icon: ClosedCaptionIcon }), - label: "Captions", - }, - adjustment: { - icon: createHugeiconsIcon({ icon: SlidersHorizontalIcon }), - label: "Adjustment", - }, - settings: { - icon: createHugeiconsIcon({ icon: Settings01Icon }), - label: "Settings", - }, -} satisfies Record< - Tab, - { icon: ElementType<{ className?: string }>; label: string } ->; - -export type MediaViewMode = "grid" | "list"; -export type MediaSortKey = "name" | "type" | "duration" | "size"; -export type MediaSortOrder = "asc" | "desc"; - -interface AssetsPanelStore { - activeTab: Tab; - setActiveTab: (tab: Tab) => void; - highlightMediaId: string | null; - requestRevealMedia: (mediaId: string) => void; - clearHighlight: () => void; - - /* Media */ - mediaViewMode: MediaViewMode; - setMediaViewMode: (mode: MediaViewMode) => void; - mediaSortBy: MediaSortKey; - mediaSortOrder: MediaSortOrder; - setMediaSort: (key: MediaSortKey, order: MediaSortOrder) => void; -} - -export const useAssetsPanelStore = create()( - persist( - (set) => ({ - activeTab: "media", - setActiveTab: (tab) => set({ activeTab: tab }), - highlightMediaId: null, - requestRevealMedia: (mediaId) => - set({ activeTab: "media", highlightMediaId: mediaId }), - clearHighlight: () => set({ highlightMediaId: null }), - mediaViewMode: "grid", - setMediaViewMode: (mode) => set({ mediaViewMode: mode }), - mediaSortBy: "name", - mediaSortOrder: "asc", - setMediaSort: (key, order) => - set({ mediaSortBy: key, mediaSortOrder: order }), - }), - { - name: "assets-panel", - partialize: (state) => ({ - mediaViewMode: state.mediaViewMode, - mediaSortBy: state.mediaSortBy, - mediaSortOrder: state.mediaSortOrder, - }), - }, - ), -); diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts deleted file mode 100644 index 07cb433e..00000000 --- a/apps/web/src/stores/editor-store.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { create } from "zustand"; -import { DEFAULT_CANVAS_PRESETS } from "@/lib/canvas/sizes"; -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 deleted file mode 100644 index ff77e0b4..00000000 --- a/apps/web/src/stores/keybindings-store.ts +++ /dev/null @@ -1,251 +0,0 @@ -"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) => void; - closeOverlay: (overlayId: 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) => - set((s) => { - const openOverlayIds = s.openOverlayIds.includes(overlayId) - ? s.openOverlayIds - : [...s.openOverlayIds, overlayId]; - const nextOverlayDepth = openOverlayIds.length; - return { - openOverlayIds, - overlayDepth: nextOverlayDepth, - }; - }), - closeOverlay: (overlayId) => - set((s) => { - const openOverlayIds = s.openOverlayIds.filter( - (id) => id !== overlayId, - ); - const nextOverlayDepth = openOverlayIds.length; - return { - openOverlayIds, - overlayDepth: nextOverlayDepth, - }; - }), - setLoadingProject: (loading) => { - 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) => { - 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/index.ts b/apps/web/src/stores/keybindings/migrations/index.ts deleted file mode 100644 index 71fd81a6..00000000 --- a/apps/web/src/stores/keybindings/migrations/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { v2ToV3 } from "./v2-to-v3"; -import { v3ToV4 } from "./v3-to-v4"; -import { v4ToV5 } from "./v4-to-v5"; -import { v5ToV6 } from "./v5-to-v6"; -import { v6ToV7 } from "./v6-to-v7"; - -type MigrationFn = ({ state }: { state: unknown }) => unknown; - -const migrations: Record = { - 2: v2ToV3, - 3: v3ToV4, - 4: v4ToV5, - 5: v5ToV6, - 6: v6ToV7, -}; - -export const CURRENT_VERSION = 7; - -export function runMigrations({ - state, - fromVersion, -}: { - state: unknown; - fromVersion: number; -}): unknown { - let current = state; - for (let version = fromVersion; version < CURRENT_VERSION; version++) { - const migrate = migrations[version]; - if (migrate) current = migrate({ state: current }); - } - return current; -} diff --git a/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts b/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts deleted file mode 100644 index 6ebff92c..00000000 --- a/apps/web/src/stores/keybindings/migrations/v2-to-v3.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 0f9b35da..00000000 --- a/apps/web/src/stores/keybindings/migrations/v3-to-v4.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 880df1dc..00000000 --- a/apps/web/src/stores/keybindings/migrations/v4-to-v5.ts +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index ebabb7c0..00000000 --- a/apps/web/src/stores/keybindings/migrations/v5-to-v6.ts +++ /dev/null @@ -1,17 +0,0 @@ -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/keybindings/migrations/v6-to-v7.ts b/apps/web/src/stores/keybindings/migrations/v6-to-v7.ts deleted file mode 100644 index 8c5fd96c..00000000 --- a/apps/web/src/stores/keybindings/migrations/v6-to-v7.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { KeybindingConfig } from "@/lib/actions/keybinding"; - -interface V6State { - keybindings: KeybindingConfig; - isCustomized: boolean; -} - -export function v6ToV7({ state }: { state: unknown }): unknown { - const v6 = state as V6State; - const keybindings = { ...v6.keybindings }; - - for (const key of Object.keys(keybindings) as Array) { - if (keybindings[key] === ("split-element" as never)) { - keybindings[key] = "split"; - } - } - - return { ...v6, keybindings }; -} diff --git a/apps/web/src/stores/panel-store.ts b/apps/web/src/stores/panel-store.ts deleted file mode 100644 index 757a37a6..00000000 --- a/apps/web/src/stores/panel-store.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; -import { PANEL_CONFIG } from "@/lib/panels/layout"; - -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 deleted file mode 100644 index b5957cb5..00000000 --- a/apps/web/src/stores/preview-store.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; -import { isGuideId, type GuideId } from "@/lib/guides"; -import { DEFAULT_GRID_CONFIG } from "@/lib/guides/grid"; -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/properties-store.ts b/apps/web/src/stores/properties-store.ts deleted file mode 100644 index f45f176d..00000000 --- a/apps/web/src/stores/properties-store.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { create } from "zustand"; - -interface ClipEffectsTarget { - elementId: string; - trackId: string; -} - -interface PropertiesState { - clipEffectsTarget: ClipEffectsTarget | null; - openClipEffects: ({ elementId, trackId }: ClipEffectsTarget) => void; - closeClipEffects: () => void; -} - -export const usePropertiesStore = create()((set) => ({ - clipEffectsTarget: null, - openClipEffects: ({ elementId, trackId }) => - set({ clipEffectsTarget: { elementId, trackId } }), - closeClipEffects: () => set({ clipEffectsTarget: null }), -})); diff --git a/apps/web/src/stores/sounds-store.ts b/apps/web/src/stores/sounds-store.ts deleted file mode 100644 index b4b5772b..00000000 --- a/apps/web/src/stores/sounds-store.ts +++ /dev/null @@ -1,251 +0,0 @@ -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 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 element = buildLibraryAudioElement({ - sourceUrl: audioUrl, - name: sound.name, - duration: sound.duration, - startTime: currentTime, - buffer, - }); - - editor.timeline.insertElement({ - placement: { mode: "auto", trackType: "audio" }, - 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/stickers-store.ts b/apps/web/src/stores/stickers-store.ts deleted file mode 100644 index 191050dd..00000000 --- a/apps/web/src/stores/stickers-store.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; -import { - browseAll, - browseCategory, - searchAll, - searchStickers as searchStickersFromProviders, -} from "@/lib/stickers"; -import type { StickerBrowseResult, StickerSearchResult } from "@/lib/stickers"; -import { STICKER_CATEGORIES } from "@/lib/stickers/categories"; -import type { StickerCategory } from "@/lib/stickers/types"; -import { registerDefaultStickerProviders } from "@/lib/stickers/providers"; -import { stickersRegistry } from "@/lib/stickers/registry"; -import { parseStickerId } from "@/lib/stickers/sticker-id"; - -const MAX_RECENT_STICKERS = 50; -let browseRequestVersion = 0; - -function isValidStickerId(value: unknown): value is string { - if (typeof value !== "string") { - return false; - } - - try { - const parsed = parseStickerId({ stickerId: value }); - return stickersRegistry.has(parsed.providerId); - } catch { - return false; - } -} - -function sanitizeRecentStickers({ - recentStickers, -}: { - recentStickers: unknown; -}): string[] { - registerDefaultStickerProviders({}); - - if (!Array.isArray(recentStickers)) { - return []; - } - - const sanitized: string[] = []; - for (const stickerId of recentStickers) { - if (!isValidStickerId(stickerId)) { - continue; - } - if (sanitized.includes(stickerId)) { - continue; - } - sanitized.push(stickerId); - if (sanitized.length >= MAX_RECENT_STICKERS) { - break; - } - } - - return sanitized; -} - -type ViewMode = "search" | "browse"; - -interface StickersStore { - searchQuery: string; - selectedCategory: StickerCategory; - viewMode: ViewMode; - searchResults: StickerSearchResult | null; - browseContent: StickerBrowseResult | null; - recentStickers: string[]; - isSearching: boolean; - isBrowsing: boolean; - - setSearchQuery: ({ query }: { query: string }) => void; - setSelectedCategory: ({ category }: { category: StickerCategory }) => void; - searchStickers: ({ query }: { query: string }) => Promise; - browseStickers: () => Promise; - addToRecentStickers: ({ stickerId }: { stickerId: string }) => void; - clearRecentStickers: () => void; -} - -export const useStickersStore = create()( - persist( - (set, get) => ({ - searchQuery: "", - selectedCategory: "all", - viewMode: "browse", - - searchResults: null, - browseContent: null, - recentStickers: [], - - isSearching: false, - isBrowsing: false, - - setSearchQuery: ({ query }) => set({ searchQuery: query }), - - setSelectedCategory: ({ category }) => { - set({ - selectedCategory: category in STICKER_CATEGORIES ? category : "all", - browseContent: null, - }); - - const query = get().searchQuery.trim(); - if (query) { - void get().searchStickers({ query }); - return; - } - - void get().browseStickers(); - }, - - searchStickers: async ({ query }: { query: string }) => { - const trimmedQuery = query.trim(); - if (!trimmedQuery) { - set({ searchResults: null, viewMode: "browse" }); - await get().browseStickers(); - return; - } - - const category = get().selectedCategory; - const selectedCategory = - category in STICKER_CATEGORIES ? category : "all"; - - set({ isSearching: true, viewMode: "search" }); - try { - if (selectedCategory === "all") { - const browseContent = await searchAll({ query: trimmedQuery }); - set({ browseContent, searchResults: null }); - } else { - const results = await searchStickersFromProviders({ - query: trimmedQuery, - category: selectedCategory, - limit: 100, - }); - set({ searchResults: results }); - } - } catch (error) { - console.error("Search failed:", error); - set({ searchResults: null }); - } finally { - set({ isSearching: false }); - } - }, - - browseStickers: async () => { - const version = ++browseRequestVersion; - const category = get().selectedCategory; - const selectedCategory = - category in STICKER_CATEGORIES ? category : "all"; - - set({ isBrowsing: true, viewMode: "browse" }); - try { - const browseContent = - selectedCategory === "all" - ? await browseAll({ - recentStickers: get().recentStickers, - }) - : await browseCategory({ - category: selectedCategory, - }); - - if (version !== browseRequestVersion) return; - set({ browseContent }); - } catch (error) { - if (version !== browseRequestVersion) return; - console.error("Browse failed:", error); - set({ browseContent: null }); - } finally { - if (version === browseRequestVersion) { - set({ isBrowsing: false }); - } - } - }, - - addToRecentStickers: ({ stickerId }: { stickerId: string }) => { - const sanitizedStickerIds = sanitizeRecentStickers({ - recentStickers: [stickerId], - }); - if (sanitizedStickerIds.length === 0) { - return; - } - - set((state) => { - const recent = [ - sanitizedStickerIds[0], - ...state.recentStickers.filter((s) => s !== sanitizedStickerIds[0]), - ]; - return { - recentStickers: recent.slice(0, MAX_RECENT_STICKERS), - }; - }); - - if (get().viewMode === "browse" && get().selectedCategory === "all") { - void get().browseStickers(); - } - }, - - clearRecentStickers: () => { - set({ recentStickers: [] }); - - if (get().viewMode === "browse" && get().selectedCategory === "all") { - void get().browseStickers(); - } - }, - }), - { - name: "stickers-settings", - version: 1, - migrate: (persistedState) => { - if ( - typeof persistedState === "object" && - persistedState !== null && - "selectedCategory" in persistedState - ) { - const typedState = persistedState as { - selectedCategory?: string; - recentStickers?: string[]; - }; - const category = typedState.selectedCategory ?? "all"; - return { - ...typedState, - selectedCategory: - category in STICKER_CATEGORIES - ? (category as StickerCategory) - : "all", - recentStickers: sanitizeRecentStickers({ - recentStickers: typedState.recentStickers ?? [], - }), - }; - } - return persistedState; - }, - partialize: (state) => ({ - selectedCategory: state.selectedCategory, - recentStickers: state.recentStickers, - }), - }, - ), -); diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts deleted file mode 100644 index b7ddb885..00000000 --- a/apps/web/src/stores/timeline-store.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * UI state for the timeline - * For core logic, use EditorCore instead. - */ - -import { create } from "zustand"; -import { persist } from "zustand/middleware"; - -interface TimelineStore { - snappingEnabled: boolean; - toggleSnapping: () => void; - rippleEditingEnabled: boolean; - toggleRippleEditing: () => void; - expandedElementIds: Set; - toggleElementExpanded: (elementId: string) => void; -} - -export const useTimelineStore = create()( - persist( - (set) => ({ - snappingEnabled: true, - - toggleSnapping: () => { - set((state) => ({ snappingEnabled: !state.snappingEnabled })); - }, - - rippleEditingEnabled: false, - - toggleRippleEditing: () => { - set((state) => ({ - rippleEditingEnabled: !state.rippleEditingEnabled, - })); - }, - - expandedElementIds: new Set(), - - toggleElementExpanded: (elementId) => { - set((state) => { - const next = new Set(state.expandedElementIds); - if (next.has(elementId)) { - next.delete(elementId); - } else { - next.add(elementId); - } - return { expandedElementIds: next }; - }); - }, - }), - { - name: "timeline-store", - partialize: (state) => ({ - snappingEnabled: state.snappingEnabled, - rippleEditingEnabled: state.rippleEditingEnabled, - }), - }, - ), -); -- cgit v1.3.1