lots of stuff

This commit is contained in:
Maze Winther
2026-01-23 15:40:01 +01:00
parent b1701e1b0e
commit c6deceaa89
292 changed files with 28825 additions and 26457 deletions
+77 -77
View File
@@ -1,97 +1,97 @@
import {
CaptionsIcon,
ArrowLeftRightIcon,
SparklesIcon,
StickerIcon,
MusicIcon,
VideoIcon,
BlendIcon,
SlidersHorizontalIcon,
LucideIcon,
TypeIcon,
SettingsIcon,
CaptionsIcon,
ArrowLeftRightIcon,
SparklesIcon,
StickerIcon,
MusicIcon,
VideoIcon,
BlendIcon,
SlidersHorizontalIcon,
type LucideIcon,
TypeIcon,
SettingsIcon,
} from "lucide-react";
import { create } from "zustand";
export const TAB_KEYS = [
"media",
"sounds",
"text",
"stickers",
"effects",
"transitions",
"captions",
"filters",
"adjustment",
"settings",
"media",
"sounds",
"text",
"stickers",
"effects",
"transitions",
"captions",
"filters",
"adjustment",
"settings",
] as const;
export type Tab = (typeof TAB_KEYS)[number];
export const tabs = {
media: {
icon: VideoIcon,
label: "Media",
},
sounds: {
icon: MusicIcon,
label: "Sounds",
},
text: {
icon: TypeIcon,
label: "Text",
},
stickers: {
icon: StickerIcon,
label: "Stickers",
},
effects: {
icon: SparklesIcon,
label: "Effects",
},
transitions: {
icon: ArrowLeftRightIcon,
label: "Transitions",
},
captions: {
icon: CaptionsIcon,
label: "Captions",
},
filters: {
icon: BlendIcon,
label: "Filters",
},
adjustment: {
icon: SlidersHorizontalIcon,
label: "Adjustment",
},
settings: {
icon: SettingsIcon,
label: "Settings",
},
media: {
icon: VideoIcon,
label: "Media",
},
sounds: {
icon: MusicIcon,
label: "Sounds",
},
text: {
icon: TypeIcon,
label: "Text",
},
stickers: {
icon: StickerIcon,
label: "Stickers",
},
effects: {
icon: SparklesIcon,
label: "Effects",
},
transitions: {
icon: ArrowLeftRightIcon,
label: "Transitions",
},
captions: {
icon: CaptionsIcon,
label: "Captions",
},
filters: {
icon: BlendIcon,
label: "Filters",
},
adjustment: {
icon: SlidersHorizontalIcon,
label: "Adjustment",
},
settings: {
icon: SettingsIcon,
label: "Settings",
},
} satisfies Record<Tab, { icon: LucideIcon; label: string }>;
type MediaViewMode = "grid" | "list";
interface AssetsPanelStore {
activeTab: Tab;
setActiveTab: (tab: Tab) => void;
highlightMediaId: string | null;
requestRevealMedia: (mediaId: string) => void;
clearHighlight: () => void;
activeTab: Tab;
setActiveTab: (tab: Tab) => void;
highlightMediaId: string | null;
requestRevealMedia: (mediaId: string) => void;
clearHighlight: () => void;
/* Media */
mediaViewMode: MediaViewMode;
setMediaViewMode: (mode: MediaViewMode) => void;
/* Media */
mediaViewMode: MediaViewMode;
setMediaViewMode: (mode: MediaViewMode) => void;
}
export const useAssetsPanelStore = create<AssetsPanelStore>((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 }),
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 }),
}));
+52 -52
View File
@@ -2,69 +2,69 @@ import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { TPlatformLayout } from "@/types/editor";
import { DEFAULT_CANVAS_PRESETS } from "@/constants/project-constants";
import { TCanvasSize } from "@/types/project";
import type { TCanvasSize } from "@/types/project";
interface LayoutGuideSettings {
platform: TPlatformLayout | null;
platform: TPlatformLayout | null;
}
interface EditorState {
isInitializing: boolean;
isPanelsReady: boolean;
canvasPresets: TCanvasSize[];
layoutGuide: LayoutGuideSettings;
setInitializing: (loading: boolean) => void;
setPanelsReady: (ready: boolean) => void;
initializeApp: () => Promise<void>;
setLayoutGuide: (settings: Partial<LayoutGuideSettings>) => void;
toggleLayoutGuide: (platform: TPlatformLayout) => void;
isInitializing: boolean;
isPanelsReady: boolean;
canvasPresets: TCanvasSize[];
layoutGuide: LayoutGuideSettings;
setInitializing: (loading: boolean) => void;
setPanelsReady: (ready: boolean) => void;
initializeApp: () => Promise<void>;
setLayoutGuide: (settings: Partial<LayoutGuideSettings>) => void;
toggleLayoutGuide: (platform: TPlatformLayout) => void;
}
export const useEditorStore = create<EditorState>()(
persist(
(set) => ({
isInitializing: true,
isPanelsReady: false,
canvasPresets: DEFAULT_CANVAS_PRESETS,
layoutGuide: {
platform: null,
},
setInitializing: (loading) => {
set({ isInitializing: loading });
},
persist(
(set) => ({
isInitializing: true,
isPanelsReady: false,
canvasPresets: DEFAULT_CANVAS_PRESETS,
layoutGuide: {
platform: null,
},
setInitializing: (loading) => {
set({ isInitializing: loading });
},
setPanelsReady: (ready) => {
set({ isPanelsReady: ready });
},
setPanelsReady: (ready) => {
set({ isPanelsReady: ready });
},
initializeApp: async () => {
set({ isInitializing: true, isPanelsReady: false });
initializeApp: async () => {
set({ isInitializing: true, isPanelsReady: false });
set({ isPanelsReady: true, isInitializing: false });
},
set({ isPanelsReady: true, isInitializing: false });
},
setLayoutGuide: (settings) => {
set((state) => ({
layoutGuide: {
...state.layoutGuide,
...settings,
},
}));
},
setLayoutGuide: (settings) => {
set((state) => ({
layoutGuide: {
...state.layoutGuide,
...settings,
},
}));
},
toggleLayoutGuide: (platform) => {
set((state) => ({
layoutGuide: {
platform: state.layoutGuide.platform === platform ? null : platform,
},
}));
},
}),
{
name: "editor-settings",
partialize: (state) => ({
layoutGuide: state.layoutGuide,
}),
},
),
toggleLayoutGuide: (platform) => {
set((state) => ({
layoutGuide: {
platform: state.layoutGuide.platform === platform ? null : platform,
},
}));
},
}),
{
name: "editor-settings",
partialize: (state) => ({
layoutGuide: state.layoutGuide,
}),
},
),
);
+184 -184
View File
@@ -6,243 +6,243 @@ import type { TActionWithOptionalArgs } from "@/lib/actions";
import { getDefaultShortcuts } from "@/lib/actions";
import { isTypableDOMElement } from "@/utils/browser";
import { isAppleDevice } from "@/utils/platform";
import { KeybindingConfig, ShortcutKey } from "@/types/keybinding";
import type { KeybindingConfig, ShortcutKey } from "@/types/keybinding";
export const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
export interface KeybindingConflict {
key: ShortcutKey;
existingAction: TActionWithOptionalArgs;
newAction: TActionWithOptionalArgs;
key: ShortcutKey;
existingAction: TActionWithOptionalArgs;
newAction: TActionWithOptionalArgs;
}
interface KeybindingsState {
keybindings: KeybindingConfig;
isCustomized: boolean;
keybindingsEnabled: boolean;
isRecording: boolean;
keybindings: KeybindingConfig;
isCustomized: boolean;
keybindingsEnabled: boolean;
isRecording: boolean;
updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => void;
removeKeybinding: (key: ShortcutKey) => void;
resetToDefaults: () => void;
importKeybindings: (config: KeybindingConfig) => void;
exportKeybindings: () => KeybindingConfig;
enableKeybindings: () => void;
disableKeybindings: () => void;
setIsRecording: (isRecording: boolean) => void;
validateKeybinding: (
key: ShortcutKey,
action: TActionWithOptionalArgs,
) => KeybindingConflict | null;
getKeybindingsForAction: (action: TActionWithOptionalArgs) => ShortcutKey[];
updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => void;
removeKeybinding: (key: ShortcutKey) => void;
resetToDefaults: () => void;
importKeybindings: (config: KeybindingConfig) => void;
exportKeybindings: () => KeybindingConfig;
enableKeybindings: () => void;
disableKeybindings: () => void;
setIsRecording: (isRecording: boolean) => void;
validateKeybinding: (
key: ShortcutKey,
action: TActionWithOptionalArgs,
) => KeybindingConflict | null;
getKeybindingsForAction: (action: TActionWithOptionalArgs) => ShortcutKey[];
// Utility
getKeybindingString: (ev: KeyboardEvent) => ShortcutKey | null;
// Utility
getKeybindingString: (ev: KeyboardEvent) => ShortcutKey | null;
}
function isDOMElement(el: EventTarget | null): el is HTMLElement {
return !!el && (el instanceof Element || el instanceof HTMLElement);
return !!el && (el instanceof Element || el instanceof HTMLElement);
}
export const useKeybindingsStore = create<KeybindingsState>()(
persist(
(set, get) => ({
keybindings: { ...defaultKeybindings },
isCustomized: false,
keybindingsEnabled: true,
isRecording: false,
persist(
(set, get) => ({
keybindings: { ...defaultKeybindings },
isCustomized: false,
keybindingsEnabled: true,
isRecording: false,
updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => {
set((state) => {
const newKeybindings = { ...state.keybindings };
newKeybindings[key] = action;
updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => {
set((state) => {
const newKeybindings = { ...state.keybindings };
newKeybindings[key] = action;
return {
keybindings: newKeybindings,
isCustomized: true,
};
});
},
return {
keybindings: newKeybindings,
isCustomized: true,
};
});
},
removeKeybinding: (key: ShortcutKey) => {
set((state) => {
const newKeybindings = { ...state.keybindings };
delete newKeybindings[key];
removeKeybinding: (key: ShortcutKey) => {
set((state) => {
const newKeybindings = { ...state.keybindings };
delete newKeybindings[key];
return {
keybindings: newKeybindings,
isCustomized: true,
};
});
},
return {
keybindings: newKeybindings,
isCustomized: true,
};
});
},
resetToDefaults: () => {
set({
keybindings: { ...defaultKeybindings },
isCustomized: false,
});
},
resetToDefaults: () => {
set({
keybindings: { ...defaultKeybindings },
isCustomized: false,
});
},
enableKeybindings: () => {
set({ keybindingsEnabled: true });
},
enableKeybindings: () => {
set({ keybindingsEnabled: true });
},
disableKeybindings: () => {
set({ keybindingsEnabled: false });
},
disableKeybindings: () => {
set({ keybindingsEnabled: false });
},
importKeybindings: (config: KeybindingConfig) => {
// Validate all keys and actions
for (const [key, action] of Object.entries(config)) {
// Validate the key format
if (typeof key !== "string" || key.length === 0) {
throw new Error(`Invalid key format: ${key}`);
}
}
set({
keybindings: { ...config },
isCustomized: true,
});
},
importKeybindings: (config: KeybindingConfig) => {
// Validate all keys and actions
for (const [key, action] of Object.entries(config)) {
// Validate the key format
if (typeof key !== "string" || key.length === 0) {
throw new Error(`Invalid key format: ${key}`);
}
}
set({
keybindings: { ...config },
isCustomized: true,
});
},
exportKeybindings: () => {
return get().keybindings;
},
exportKeybindings: () => {
return get().keybindings;
},
validateKeybinding: (
key: ShortcutKey,
action: TActionWithOptionalArgs,
) => {
const { keybindings } = get();
const existingAction = keybindings[key];
validateKeybinding: (
key: ShortcutKey,
action: TActionWithOptionalArgs,
) => {
const { keybindings } = get();
const existingAction = keybindings[key];
if (existingAction && existingAction !== action) {
return {
key,
existingAction,
newAction: action,
};
}
if (existingAction && existingAction !== action) {
return {
key,
existingAction,
newAction: action,
};
}
return null;
},
setIsRecording: (isRecording: boolean) => {
set({ isRecording });
},
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[];
},
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: 2,
},
),
getKeybindingString: (ev: KeyboardEvent) => {
return generateKeybindingString(ev) as ShortcutKey | null;
},
}),
{
name: "opencut-keybindings",
version: 2,
},
),
);
// Utility functions
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
const target = ev.target;
const target = ev.target;
// We may or may not have a modifier key
const modifierKey = getActiveModifier(ev);
// We may or may not have a modifier key
const modifierKey = getActiveModifier(ev);
// We will always have a non-modifier key
const key = getPressedKey(ev);
if (!key) return null;
// We will always have a non-modifier key
const key = getPressedKey(ev);
if (!key) return null;
// All key combos backed by modifiers are valid shortcuts (whether currently typing or not)
if (modifierKey) {
// If the modifier is shift and the target is an input, we ignore
if (
modifierKey === "shift" &&
isDOMElement(target) &&
isTypableDOMElement({ element: target as HTMLElement })
) {
return null;
}
// All key combos backed by modifiers are valid shortcuts (whether currently typing or not)
if (modifierKey) {
// If the modifier is shift and the target is an input, we ignore
if (
modifierKey === "shift" &&
isDOMElement(target) &&
isTypableDOMElement({ element: target as HTMLElement })
) {
return null;
}
return `${modifierKey}+${key}` as ShortcutKey;
}
return `${modifierKey}+${key}` as ShortcutKey;
}
// no modifier key here then we do not do anything while on input
if (
isDOMElement(target) &&
isTypableDOMElement({ element: target as HTMLElement })
)
return null;
// no modifier key here then we do not do anything while on input
if (
isDOMElement(target) &&
isTypableDOMElement({ element: target as HTMLElement })
)
return null;
// single key while not input
return `${key}` as ShortcutKey;
// single key while not input
return `${key}` as ShortcutKey;
}
function getPressedKey(ev: KeyboardEvent): string | null {
// Sometimes the property code is not available on the KeyboardEvent object
const key = (ev.key ?? "").toLowerCase();
const code = ev.code ?? "";
// Sometimes the property code is not available on the KeyboardEvent object
const key = (ev.key ?? "").toLowerCase();
const code = ev.code ?? "";
if (code === "Space" || key === " " || key === "spacebar" || key === "space")
return "space";
if (code === "Space" || key === " " || key === "spacebar" || key === "space")
return "space";
// Check arrow keys
if (key.startsWith("arrow")) {
return key.slice(5);
}
// Check arrow keys
if (key.startsWith("arrow")) {
return key.slice(5);
}
// Check for special keys
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";
// Check for special keys
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";
// Check letter keys
if (code.startsWith("Key")) {
const letter = code.slice(3).toLowerCase();
if (letter.length === 1 && letter >= "a" && letter <= "z") {
return letter;
}
}
// Check letter keys
if (code.startsWith("Key")) {
const letter = code.slice(3).toLowerCase();
if (letter.length === 1 && letter >= "a" && letter <= "z") {
return letter;
}
}
// Check number keys using physical position for AZERTY support
if (code.startsWith("Digit")) {
const digit = code.slice(5);
if (digit.length === 1 && digit >= "0" && digit <= "9") {
return digit;
}
}
// Check number keys using physical position for AZERTY support
if (code.startsWith("Digit")) {
const digit = code.slice(5);
if (digit.length === 1 && digit >= "0" && digit <= "9") {
return digit;
}
}
// Fallback for other layouts
const isDigit = key.length === 1 && key >= "0" && key <= "9";
if (isDigit) return key;
// Fallback for other layouts
const isDigit = key.length === 1 && key >= "0" && key <= "9";
if (isDigit) return key;
// Check if slash, period or enter
if (key === "/" || key === "." || key === "enter") return key;
// Check if slash, period or enter
if (key === "/" || key === "." || key === "enter") return key;
// If no other cases match, this is not a valid key
return null;
// If no other cases match, this is not a valid key
return null;
}
function getActiveModifier(ev: KeyboardEvent): string | null {
const modifierKeys = {
ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
alt: ev.altKey,
shift: ev.shiftKey,
};
const modifierKeys = {
ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
alt: ev.altKey,
shift: ev.shiftKey,
};
// active modifier: ctrl | alt | ctrl+alt | ctrl+shift | ctrl+alt+shift | alt+shift
// modiferKeys object's keys are sorted to match the above order
const activeModifier = Object.keys(modifierKeys)
.filter((key) => modifierKeys[key as keyof typeof modifierKeys])
.join("+");
// active modifier: ctrl | alt | ctrl+alt | ctrl+shift | ctrl+alt+shift | alt+shift
// modiferKeys object's keys are sorted to match the above order
const activeModifier = Object.keys(modifierKeys)
.filter((key) => modifierKeys[key as keyof typeof modifierKeys])
.join("+");
return activeModifier === "" ? null : activeModifier;
return activeModifier === "" ? null : activeModifier;
}
+76 -76
View File
@@ -3,91 +3,91 @@ 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;
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;
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;
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) return { panels: { ...PANEL_CONFIG.panels } };
if (state.panels && typeof state.panels === "object") {
return {
panels: {
...PANEL_CONFIG.panels,
...state.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,
}),
},
),
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,
}),
},
),
);
+224 -224
View File
@@ -6,256 +6,256 @@ 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;
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>;
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,
topSoundEffects: [],
isLoading: false,
error: null,
hasLoaded: false,
showCommercialOnly: true,
toggleCommercialFilter: () => {
set((state) => ({ showCommercialOnly: !state.showCommercialOnly }));
},
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,
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 }),
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],
})),
appendSearchResults: ({ results }) =>
set((state) => ({
searchResults: [...state.searchResults, ...results],
})),
appendTopSounds: ({ results }) =>
set((state) => ({
topSoundEffects: [...state.topSoundEffects, ...results],
})),
appendTopSounds: ({ results }) =>
set((state) => ({
topSoundEffects: [...state.topSoundEffects, ...results],
})),
resetPagination: () =>
set({
currentPage: 1,
hasNextPage: false,
totalCount: 0,
isLoadingMore: false,
}),
resetPagination: () =>
set({
currentPage: 1,
hasNextPage: false,
totalCount: 0,
isLoadingMore: false,
}),
loadSavedSounds: async () => {
if (get().isSavedSoundsLoaded) return;
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);
}
},
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 });
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);
}
},
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 });
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);
}
},
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);
},
isSoundSaved: ({ soundId }) => {
const { savedSounds } = get();
return savedSounds.some((sound) => sound.id === soundId);
},
toggleSavedSound: async ({ soundEffect }) => {
const { isSoundSaved, saveSoundEffect, removeSavedSound } = get();
toggleSavedSound: async ({ soundEffect }) => {
const { isSoundSaved, saveSoundEffect, removeSavedSound } = get();
if (isSoundSaved({ soundId: soundEffect.id })) {
await removeSavedSound({ soundId: soundEffect.id });
} else {
await saveSoundEffect({ soundEffect });
}
},
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);
}
},
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;
}
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();
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 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 arrayBuffer = await response.arrayBuffer();
const audioContext = new AudioContext();
const buffer = await audioContext.decodeAudioData(arrayBuffer);
let audioTrack = tracks.find((t) => t.type === "audio");
let trackId: string;
const audioTrack = tracks.find((t) => t.type === "audio");
let trackId: string;
if (audioTrack) {
trackId = audioTrack.id;
} else {
trackId = editor.timeline.addTrack({ type: "audio" });
}
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,
});
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;
}
},
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;
}
},
}));
+139 -139
View File
@@ -1,11 +1,11 @@
import { create } from "zustand";
import {
getCollections,
getCollection,
searchIcons,
type IconSet,
type CollectionInfo,
type IconSearchResult,
getCollections,
getCollection,
searchIcons,
type IconSet,
type CollectionInfo,
type IconSearchResult,
} from "@/lib/iconify-api";
import { EditorCore } from "@/core";
import { buildStickerElement } from "@/lib/timeline/element-utils";
@@ -15,162 +15,162 @@ import type { StickerCategory } from "@/types/stickers";
type ViewMode = "search" | "browse" | "collection";
interface StickersStore {
searchQuery: string;
selectedCategory: StickerCategory;
selectedCollection: string | null;
viewMode: ViewMode;
collections: Record<string, IconSet>;
currentCollection: CollectionInfo | null;
searchResults: IconSearchResult | null;
recentStickers: string[];
isLoadingCollections: boolean;
isLoadingCollection: boolean;
isSearching: boolean;
addingSticker: string | null;
searchQuery: string;
selectedCategory: StickerCategory;
selectedCollection: string | null;
viewMode: ViewMode;
collections: Record<string, IconSet>;
currentCollection: CollectionInfo | null;
searchResults: IconSearchResult | null;
recentStickers: string[];
isLoadingCollections: boolean;
isLoadingCollection: boolean;
isSearching: boolean;
addingSticker: string | null;
setSearchQuery: ({ query }: { query: string }) => void;
setSelectedCategory: ({ category }: { category: StickerCategory }) => void;
setSelectedCollection: ({
collection,
}: {
collection: string | null;
}) => void;
setViewMode: ({ mode }: { mode: ViewMode }) => void;
loadCollections: () => Promise<void>;
loadCollection: ({ prefix }: { prefix: string }) => Promise<void>;
searchStickers: ({ query }: { query: string }) => Promise<void>;
addStickerToTimeline: ({ iconName }: { iconName: string }) => void;
addToRecentStickers: ({ iconName }: { iconName: string }) => void;
clearRecentStickers: () => void;
setSearchQuery: ({ query }: { query: string }) => void;
setSelectedCategory: ({ category }: { category: StickerCategory }) => void;
setSelectedCollection: ({
collection,
}: {
collection: string | null;
}) => void;
setViewMode: ({ mode }: { mode: ViewMode }) => void;
loadCollections: () => Promise<void>;
loadCollection: ({ prefix }: { prefix: string }) => Promise<void>;
searchStickers: ({ query }: { query: string }) => Promise<void>;
addStickerToTimeline: ({ iconName }: { iconName: string }) => void;
addToRecentStickers: ({ iconName }: { iconName: string }) => void;
clearRecentStickers: () => void;
}
const MAX_RECENT_STICKERS = 50;
export const useStickersStore = create<StickersStore>((set, get) => ({
searchQuery: "",
selectedCategory: "all",
selectedCollection: null,
viewMode: "browse",
searchQuery: "",
selectedCategory: "all",
selectedCollection: null,
viewMode: "browse",
collections: {},
currentCollection: null,
searchResults: null,
recentStickers: [],
collections: {},
currentCollection: null,
searchResults: null,
recentStickers: [],
isLoadingCollections: false,
isLoadingCollection: false,
isSearching: false,
addingSticker: null,
isLoadingCollections: false,
isLoadingCollection: false,
isSearching: false,
addingSticker: null,
setSearchQuery: ({ query }) => set({ searchQuery: query }),
setSearchQuery: ({ query }) => set({ searchQuery: query }),
setSelectedCategory: ({ category }) =>
set({
selectedCategory: category,
viewMode: "browse",
selectedCollection: null,
currentCollection: null,
}),
setSelectedCategory: ({ category }) =>
set({
selectedCategory: category,
viewMode: "browse",
selectedCollection: null,
currentCollection: null,
}),
setSelectedCollection: ({ collection }) => {
set({
selectedCollection: collection,
viewMode: collection ? "collection" : "browse",
currentCollection: null,
});
setSelectedCollection: ({ collection }) => {
set({
selectedCollection: collection,
viewMode: collection ? "collection" : "browse",
currentCollection: null,
});
if (collection) {
get().loadCollection({ prefix: collection });
}
},
if (collection) {
get().loadCollection({ prefix: collection });
}
},
setViewMode: ({ mode }) => set({ viewMode: mode }),
setViewMode: ({ mode }) => set({ viewMode: mode }),
loadCollections: async () => {
set({ isLoadingCollections: true });
try {
const collections = await getCollections();
set({ collections });
} catch (error) {
console.error("Failed to load collections:", error);
} finally {
set({ isLoadingCollections: false });
}
},
loadCollections: async () => {
set({ isLoadingCollections: true });
try {
const collections = await getCollections();
set({ collections });
} catch (error) {
console.error("Failed to load collections:", error);
} finally {
set({ isLoadingCollections: false });
}
},
loadCollection: async ({ prefix }: { prefix: string }) => {
set({ isLoadingCollection: true });
try {
const collection = await getCollection(prefix);
set({ currentCollection: collection });
} catch (error) {
console.error(`Failed to load collection ${prefix}:`, error);
set({ currentCollection: null });
} finally {
set({ isLoadingCollection: false });
}
},
loadCollection: async ({ prefix }: { prefix: string }) => {
set({ isLoadingCollection: true });
try {
const collection = await getCollection(prefix);
set({ currentCollection: collection });
} catch (error) {
console.error(`Failed to load collection ${prefix}:`, error);
set({ currentCollection: null });
} finally {
set({ isLoadingCollection: false });
}
},
searchStickers: async ({ query }: { query: string }) => {
if (!query.trim()) {
set({ searchResults: null, viewMode: "browse" });
return;
}
searchStickers: async ({ query }: { query: string }) => {
if (!query.trim()) {
set({ searchResults: null, viewMode: "browse" });
return;
}
const { selectedCategory } = get();
const { selectedCategory } = get();
set({ isSearching: true, viewMode: "search" });
try {
const category = STICKER_CATEGORY_CONFIG[selectedCategory];
const results = await searchIcons(query, 100, undefined, category);
set({ searchResults: results });
} catch (error) {
console.error("Search failed:", error);
set({ searchResults: null });
} finally {
set({ isSearching: false });
}
},
set({ isSearching: true, viewMode: "search" });
try {
const category = STICKER_CATEGORY_CONFIG[selectedCategory];
const results = await searchIcons(query, 100, undefined, category);
set({ searchResults: results });
} catch (error) {
console.error("Search failed:", error);
set({ searchResults: null });
} finally {
set({ isSearching: false });
}
},
addStickerToTimeline: ({ iconName }: { iconName: string }) => {
set({ addingSticker: iconName });
try {
const editor = EditorCore.getInstance();
const currentTime = editor.playback.getCurrentTime();
const tracks = editor.timeline.getTracks();
addStickerToTimeline: ({ iconName }: { iconName: string }) => {
set({ addingSticker: iconName });
try {
const editor = EditorCore.getInstance();
const currentTime = editor.playback.getCurrentTime();
const tracks = editor.timeline.getTracks();
let stickerTrack = tracks.find((t) => t.type === "sticker");
let trackId: string;
const stickerTrack = tracks.find((t) => t.type === "sticker");
let trackId: string;
if (stickerTrack) {
trackId = stickerTrack.id;
} else {
trackId = editor.timeline.addTrack({ type: "sticker" });
}
if (stickerTrack) {
trackId = stickerTrack.id;
} else {
trackId = editor.timeline.addTrack({ type: "sticker" });
}
const element = buildStickerElement({ iconName, startTime: currentTime });
editor.timeline.insertElement({
placement: { mode: "explicit", trackId },
element,
});
const element = buildStickerElement({ iconName, startTime: currentTime });
editor.timeline.insertElement({
placement: { mode: "explicit", trackId },
element,
});
get().addToRecentStickers({ iconName });
} finally {
set({ addingSticker: null });
}
},
get().addToRecentStickers({ iconName });
} finally {
set({ addingSticker: null });
}
},
addToRecentStickers: ({ iconName }: { iconName: string }) => {
set((state) => {
const recent = [
iconName,
...state.recentStickers.filter((s) => s !== iconName),
];
return {
recentStickers: recent.slice(0, MAX_RECENT_STICKERS),
};
});
},
addToRecentStickers: ({ iconName }: { iconName: string }) => {
set((state) => {
const recent = [
iconName,
...state.recentStickers.filter((s) => s !== iconName),
];
return {
recentStickers: recent.slice(0, MAX_RECENT_STICKERS),
};
});
},
clearRecentStickers: () => set({ recentStickers: [] }),
clearRecentStickers: () => set({ recentStickers: [] }),
}));
+14 -14
View File
@@ -4,30 +4,30 @@ import { persist } from "zustand/middleware";
export type TextPropertiesTab = "text" | "transform";
export interface TextPropertiesTabMeta {
value: TextPropertiesTab;
label: string;
value: TextPropertiesTab;
label: string;
}
export const TEXT_PROPERTIES_TABS: ReadonlyArray<TextPropertiesTabMeta> = [
{ value: "text", label: "Text" },
{ value: "transform", label: "Transform" },
{ value: "text", label: "Text" },
{ value: "transform", label: "Transform" },
] as const;
export function isTextPropertiesTab(value: string): value is TextPropertiesTab {
return TEXT_PROPERTIES_TABS.some((t) => t.value === value);
return TEXT_PROPERTIES_TABS.some((t) => t.value === value);
}
interface TextPropertiesState {
activeTab: TextPropertiesTab;
setActiveTab: (tab: TextPropertiesTab) => void;
activeTab: TextPropertiesTab;
setActiveTab: (tab: TextPropertiesTab) => void;
}
export const useTextPropertiesStore = create<TextPropertiesState>()(
persist(
(set) => ({
activeTab: "text",
setActiveTab: (tab) => set({ activeTab: tab }),
}),
{ name: "text-properties" },
),
persist(
(set) => ({
activeTab: "text",
setActiveTab: (tab) => set({ activeTab: tab }),
}),
{ name: "text-properties" },
),
);
+36 -36
View File
@@ -7,50 +7,50 @@ import { create } from "zustand";
import type { ClipboardItem } from "@/types/timeline";
interface TimelineStore {
selectedElements: { trackId: string; elementId: string }[];
setSelectedElements: ({
elements,
}: {
elements: { trackId: string; elementId: string }[];
}) => void;
snappingEnabled: boolean;
toggleSnapping: () => void;
rippleEditingEnabled: boolean;
toggleRippleEditing: () => void;
clipboard: {
items: ClipboardItem[];
} | null;
setClipboard: (
clipboard: {
items: ClipboardItem[];
} | null,
) => void;
selectedElements: { trackId: string; elementId: string }[];
setSelectedElements: ({
elements,
}: {
elements: { trackId: string; elementId: string }[];
}) => void;
snappingEnabled: boolean;
toggleSnapping: () => void;
rippleEditingEnabled: boolean;
toggleRippleEditing: () => void;
clipboard: {
items: ClipboardItem[];
} | null;
setClipboard: (
clipboard: {
items: ClipboardItem[];
} | null,
) => void;
}
export const useTimelineStore = create<TimelineStore>((set) => ({
selectedElements: [],
selectedElements: [],
setSelectedElements: ({ elements }) => {
set({ selectedElements: elements });
},
setSelectedElements: ({ elements }) => {
set({ selectedElements: elements });
},
snappingEnabled: true,
snappingEnabled: true,
toggleSnapping: () => {
set((state) => ({ snappingEnabled: !state.snappingEnabled }));
},
toggleSnapping: () => {
set((state) => ({ snappingEnabled: !state.snappingEnabled }));
},
rippleEditingEnabled: false,
rippleEditingEnabled: false,
toggleRippleEditing: () => {
set((state) => ({
rippleEditingEnabled: !state.rippleEditingEnabled,
}));
},
toggleRippleEditing: () => {
set((state) => ({
rippleEditingEnabled: !state.rippleEditingEnabled,
}));
},
clipboard: null,
clipboard: null,
setClipboard: (clipboard) => {
set({ clipboard });
},
setClipboard: (clipboard) => {
set({ clipboard });
},
}));