diff options
Diffstat (limited to 'apps/web/src/stores')
| -rw-r--r-- | apps/web/src/stores/assets-panel-store.tsx | 124 | ||||
| -rw-r--r-- | apps/web/src/stores/editor-store.ts | 24 | ||||
| -rw-r--r-- | apps/web/src/stores/keybindings-store.ts | 251 | ||||
| -rw-r--r-- | apps/web/src/stores/keybindings/migrations/index.ts | 32 | ||||
| -rw-r--r-- | apps/web/src/stores/keybindings/migrations/v2-to-v3.ts | 26 | ||||
| -rw-r--r-- | apps/web/src/stores/keybindings/migrations/v3-to-v4.ts | 25 | ||||
| -rw-r--r-- | apps/web/src/stores/keybindings/migrations/v4-to-v5.ts | 17 | ||||
| -rw-r--r-- | apps/web/src/stores/keybindings/migrations/v5-to-v6.ts | 17 | ||||
| -rw-r--r-- | apps/web/src/stores/keybindings/migrations/v6-to-v7.ts | 19 | ||||
| -rw-r--r-- | apps/web/src/stores/panel-store.ts | 93 | ||||
| -rw-r--r-- | apps/web/src/stores/preview-store.ts | 112 | ||||
| -rw-r--r-- | apps/web/src/stores/properties-store.ts | 19 | ||||
| -rw-r--r-- | apps/web/src/stores/sounds-store.ts | 251 | ||||
| -rw-r--r-- | apps/web/src/stores/stickers-store.ts | 238 | ||||
| -rw-r--r-- | apps/web/src/stores/timeline-store.ts | 57 |
15 files changed, 0 insertions, 1305 deletions
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 }) => ( - <HugeiconsIcon icon={icon} className={className} /> - ); - -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<AssetsPanelStore>()( - 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<void>;
-}
-
-export const useEditorStore = create<EditorState>()((set) => ({
- isInitializing: true,
- isPanelsReady: false,
- canvasPresets: DEFAULT_CANVAS_PRESETS,
- setInitializing: (loading) => set({ isInitializing: loading }),
- setPanelsReady: (ready) => set({ isPanelsReady: ready }),
- initializeApp: async () => {
- set({ isInitializing: true, isPanelsReady: false });
- set({ isPanelsReady: true, isInitializing: false });
- },
-}));
diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts 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<KeybindingsState>()( - 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<number, MigrationFn> = { - 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<string, string> = {
- "split-selected": "split",
- "split-selected-left": "split-left",
- "split-selected-right": "split-right",
- };
-
- const migrated = { ...v2.keybindings };
- for (const [key, action] of Object.entries(migrated)) {
- if (action && renames[action]) {
- migrated[key as ShortcutKey] = renames[action] as TActionWithOptionalArgs;
- }
- }
-
- return { ...v2, keybindings: migrated };
-}
diff --git a/apps/web/src/stores/keybindings/migrations/v3-to-v4.ts b/apps/web/src/stores/keybindings/migrations/v3-to-v4.ts 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<string, string> = {
- "paste-selected": "paste-copied",
- };
-
- const migrated = { ...v3.keybindings };
- for (const [key, action] of Object.entries(migrated)) {
- if (action && renames[action]) {
- migrated[key as ShortcutKey] = renames[action] as TActionWithOptionalArgs;
- }
- }
-
- return { ...v3, keybindings: migrated };
-}
diff --git a/apps/web/src/stores/keybindings/migrations/v4-to-v5.ts b/apps/web/src/stores/keybindings/migrations/v4-to-v5.ts 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<keyof KeybindingConfig>) { - 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<PanelSizes>) => void;
- resetPanels: () => void;
-}
-
-export const usePanelStore = create<PanelState>()(
- persist(
- (set) => ({
- ...PANEL_CONFIG,
- setPanel: (panel, size) =>
- set((state) => ({
- panels: {
- ...state.panels,
- [panel]: size,
- },
- })),
- setPanels: (sizes) =>
- set((state) => ({
- panels: {
- ...state.panels,
- ...sizes,
- },
- })),
- resetPanels: () => set({ ...PANEL_CONFIG }),
- }),
- {
- name: "panel-sizes",
- version: 2,
- migrate: (persistedState) => {
- const state = persistedState as
- | {
- panels?: Partial<PanelSizes> | null;
- toolsPanel?: number;
- previewPanel?: number;
- propertiesPanel?: number;
- mainContent?: number;
- timeline?: number;
- tools?: number;
- preview?: number;
- properties?: number;
- }
- | undefined
- | null;
-
- if (!state) return { panels: { ...PANEL_CONFIG.panels } };
-
- if (state.panels && typeof state.panels === "object") {
- return {
- panels: {
- ...PANEL_CONFIG.panels,
- ...state.panels,
- },
- };
- }
-
- return {
- panels: {
- tools: state.tools ?? state.toolsPanel ?? PANEL_CONFIG.panels.tools,
- preview:
- state.preview ??
- state.previewPanel ??
- PANEL_CONFIG.panels.preview,
- properties:
- state.properties ??
- state.propertiesPanel ??
- PANEL_CONFIG.panels.properties,
- mainContent: state.mainContent ?? PANEL_CONFIG.panels.mainContent,
- timeline: state.timeline ?? PANEL_CONFIG.panels.timeline,
- },
- };
- },
- partialize: (state) => ({
- panels: state.panels,
- }),
- },
- ),
-);
diff --git a/apps/web/src/stores/preview-store.ts b/apps/web/src/stores/preview-store.ts 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<GridConfig>) => void;
- setOverlayVisibility: ({
- overlay,
- isVisible,
- }: {
- overlay: keyof PreviewOverlaysState;
- isVisible: boolean;
- }) => void;
- toggleOverlayVisibility: ({
- overlay,
- }: {
- overlay: keyof PreviewOverlaysState;
- }) => void;
-}
-
-const DEFAULT_PREVIEW_OVERLAYS: PreviewOverlaysState = {
- bookmarks: true,
-};
-
-function getPersistedActiveGuide(
- state: PersistedPreviewState | undefined,
-): GuideId | null {
- const persistedGuide =
- state?.activeGuide ?? state?.layoutGuide?.platform ?? null;
-
- if (typeof persistedGuide !== "string") {
- return null;
- }
-
- return isGuideId(persistedGuide) ? persistedGuide : null;
-}
-
-export const usePreviewStore = create<PreviewState>()(
- persist(
- (set) => ({
- activeGuide: null,
- overlays: DEFAULT_PREVIEW_OVERLAYS,
- gridConfig: DEFAULT_GRID_CONFIG,
- toggleGuide: (guideId) => {
- set((state) => ({
- activeGuide: state.activeGuide === guideId ? null : guideId,
- }));
- },
- setGridConfig: (config) => {
- set((state) => ({
- gridConfig: { ...state.gridConfig, ...config },
- }));
- },
- setOverlayVisibility: ({ overlay, isVisible }) => {
- set((state) => ({
- overlays: {
- ...state.overlays,
- [overlay]: isVisible,
- },
- }));
- },
- toggleOverlayVisibility: ({ overlay }) => {
- set((state) => ({
- overlays: {
- ...state.overlays,
- [overlay]: !state.overlays[overlay],
- },
- }));
- },
- }),
- {
- name: "preview-settings",
- version: 4,
- migrate: (persistedState) => {
- const state = persistedState as PersistedPreviewState | undefined;
-
- return {
- activeGuide: getPersistedActiveGuide(state),
- overlays: state?.overlays ?? DEFAULT_PREVIEW_OVERLAYS,
- gridConfig: {
- rows: state?.gridConfig?.rows ?? DEFAULT_GRID_CONFIG.rows,
- cols: state?.gridConfig?.cols ?? DEFAULT_GRID_CONFIG.cols,
- },
- };
- },
- partialize: (state) => ({
- activeGuide: state.activeGuide,
- overlays: state.overlays,
- gridConfig: state.gridConfig,
- }),
- },
- ),
-);
diff --git a/apps/web/src/stores/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<PropertiesState>()((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<boolean>; - setTopSoundEffects: ({ sounds }: { sounds: SoundEffect[] }) => void; - setLoading: ({ loading }: { loading: boolean }) => void; - setError: ({ error }: { error: string | null }) => void; - setHasLoaded: ({ loaded }: { loaded: boolean }) => void; - setSearchQuery: ({ query }: { query: string }) => void; - setSearchResults: ({ results }: { results: SoundEffect[] }) => void; - setSearching: ({ searching }: { searching: boolean }) => void; - setSearchError: ({ error }: { error: string | null }) => void; - setLastSearchQuery: ({ query }: { query: string }) => void; - setScrollPosition: ({ position }: { position: number }) => void; - setCurrentPage: ({ page }: { page: number }) => void; - setHasNextPage: ({ hasNext }: { hasNext: boolean }) => void; - setTotalCount: ({ count }: { count: number }) => void; - setLoadingMore: ({ loading }: { loading: boolean }) => void; - appendSearchResults: ({ results }: { results: SoundEffect[] }) => void; - appendTopSounds: ({ results }: { results: SoundEffect[] }) => void; - resetPagination: () => void; - loadSavedSounds: () => Promise<void>; - saveSoundEffect: ({ - soundEffect, - }: { - soundEffect: SoundEffect; - }) => Promise<void>; - removeSavedSound: ({ soundId }: { soundId: number }) => Promise<void>; - isSoundSaved: ({ soundId }: { soundId: number }) => boolean; - toggleSavedSound: ({ - soundEffect, - }: { - soundEffect: SoundEffect; - }) => Promise<void>; - clearSavedSounds: () => Promise<void>; -} - -export const useSoundsStore = create<SoundsStore>((set, get) => ({ - topSoundEffects: [], - isLoading: false, - error: null, - hasLoaded: false, - showCommercialOnly: true, - - toggleCommercialFilter: () => { - set((state) => ({ showCommercialOnly: !state.showCommercialOnly })); - }, - - searchQuery: "", - searchResults: [], - isSearching: false, - searchError: null, - lastSearchQuery: "", - scrollPosition: 0, - currentPage: 1, - hasNextPage: false, - totalCount: 0, - isLoadingMore: false, - savedSounds: [], - isSavedSoundsLoaded: false, - isLoadingSavedSounds: false, - savedSoundsError: null, - - setTopSoundEffects: ({ sounds }) => set({ topSoundEffects: sounds }), - setLoading: ({ loading }) => set({ isLoading: loading }), - setError: ({ error }) => set({ error }), - setHasLoaded: ({ loaded }) => set({ hasLoaded: loaded }), - setSearchQuery: ({ query }) => set({ searchQuery: query }), - setSearchResults: ({ results }) => - set({ searchResults: results, currentPage: 1 }), - setSearching: ({ searching }) => set({ isSearching: searching }), - setSearchError: ({ error }) => set({ searchError: error }), - setLastSearchQuery: ({ query }) => set({ lastSearchQuery: query }), - setScrollPosition: ({ position }) => set({ scrollPosition: position }), - setCurrentPage: ({ page }) => set({ currentPage: page }), - setHasNextPage: ({ hasNext }) => set({ hasNextPage: hasNext }), - setTotalCount: ({ count }) => set({ totalCount: count }), - setLoadingMore: ({ loading }) => set({ isLoadingMore: loading }), - - appendSearchResults: ({ results }) => - set((state) => ({ - searchResults: [...state.searchResults, ...results], - })), - - appendTopSounds: ({ results }) => - set((state) => ({ - topSoundEffects: [...state.topSoundEffects, ...results], - })), - - resetPagination: () => - set({ - currentPage: 1, - hasNextPage: false, - totalCount: 0, - isLoadingMore: false, - }), - - loadSavedSounds: async () => { - if (get().isSavedSoundsLoaded) return; - - try { - set({ isLoadingSavedSounds: true, savedSoundsError: null }); - const savedSoundsData = await storageService.loadSavedSounds(); - set({ - savedSounds: savedSoundsData.sounds, - isSavedSoundsLoaded: true, - isLoadingSavedSounds: false, - }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Failed to load saved sounds"; - set({ - savedSoundsError: errorMessage, - isLoadingSavedSounds: false, - }); - console.error("Failed to load saved sounds:", error); - } - }, - - saveSoundEffect: async ({ soundEffect }) => { - try { - await storageService.saveSoundEffect({ soundEffect }); - - const savedSoundsData = await storageService.loadSavedSounds(); - set({ savedSounds: savedSoundsData.sounds }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Failed to save sound"; - set({ savedSoundsError: errorMessage }); - toast.error("Failed to save sound"); - console.error("Failed to save sound:", error); - } - }, - - removeSavedSound: async ({ soundId }) => { - try { - await storageService.removeSavedSound({ soundId }); - - set((state) => ({ - savedSounds: state.savedSounds.filter((sound) => sound.id !== soundId), - })); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Failed to remove sound"; - set({ savedSoundsError: errorMessage }); - toast.error("Failed to remove sound"); - console.error("Failed to remove sound:", error); - } - }, - - isSoundSaved: ({ soundId }) => { - const { savedSounds } = get(); - return savedSounds.some((sound) => sound.id === soundId); - }, - - toggleSavedSound: async ({ soundEffect }) => { - const { isSoundSaved, saveSoundEffect, removeSavedSound } = get(); - - if (isSoundSaved({ soundId: soundEffect.id })) { - await removeSavedSound({ soundId: soundEffect.id }); - } else { - await saveSoundEffect({ soundEffect }); - } - }, - - clearSavedSounds: async () => { - try { - await storageService.clearSavedSounds(); - set({ - savedSounds: [], - savedSoundsError: null, - }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Failed to clear saved sounds"; - set({ savedSoundsError: errorMessage }); - toast.error("Failed to clear saved sounds"); - console.error("Failed to clear saved sounds:", error); - } - }, - - addSoundToTimeline: async ({ sound }) => { - const audioUrl = sound.previewUrl; - if (!audioUrl) { - toast.error("Sound file not available"); - return false; - } - - try { - const editor = EditorCore.getInstance(); - const currentTime = editor.playback.getCurrentTime(); - - const 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<void>; - browseStickers: () => Promise<void>; - addToRecentStickers: ({ stickerId }: { stickerId: string }) => void; - clearRecentStickers: () => void; -} - -export const useStickersStore = create<StickersStore>()( - 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<string>; - toggleElementExpanded: (elementId: string) => void; -} - -export const useTimelineStore = create<TimelineStore>()( - persist( - (set) => ({ - snappingEnabled: true, - - toggleSnapping: () => { - set((state) => ({ snappingEnabled: !state.snappingEnabled })); - }, - - rippleEditingEnabled: false, - - toggleRippleEditing: () => { - set((state) => ({ - rippleEditingEnabled: !state.rippleEditingEnabled, - })); - }, - - expandedElementIds: new Set<string>(), - - 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, - }), - }, - ), -); |
