mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
stuff
This commit is contained in:
@@ -1,22 +1,23 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { CanvasPreset, PlatformLayout } from "@/types/editor";
|
||||
import { DEFAULT_CANVAS_PRESETS } from "@/constants/editor-constants";
|
||||
import type { TPlatformLayout } from "@/types/editor";
|
||||
import { DEFAULT_CANVAS_PRESETS } from "@/constants/project-constants";
|
||||
import { TCanvasSize } from "@/types/project";
|
||||
|
||||
interface LayoutGuideSettings {
|
||||
platform: PlatformLayout | null;
|
||||
platform: TPlatformLayout | null;
|
||||
}
|
||||
|
||||
interface EditorState {
|
||||
isInitializing: boolean;
|
||||
isPanelsReady: boolean;
|
||||
canvasPresets: CanvasPreset[];
|
||||
canvasPresets: TCanvasSize[];
|
||||
layoutGuide: LayoutGuideSettings;
|
||||
setInitializing: (loading: boolean) => void;
|
||||
setPanelsReady: (ready: boolean) => void;
|
||||
initializeApp: () => Promise<void>;
|
||||
setLayoutGuide: (settings: Partial<LayoutGuideSettings>) => void;
|
||||
toggleLayoutGuide: (platform: PlatformLayout) => void;
|
||||
toggleLayoutGuide: (platform: TPlatformLayout) => void;
|
||||
}
|
||||
|
||||
export const useEditorStore = create<EditorState>()(
|
||||
|
||||
@@ -2,40 +2,17 @@
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { ActionWithOptionalArgs } from "@/constants/action-constants";
|
||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
||||
import { getDefaultShortcuts } from "@/lib/actions";
|
||||
import { isAppleDevice, isTypableDOMElement } from "@/lib/utils";
|
||||
import { KeybindingConfig, ShortcutKey } from "@/types/keybinding";
|
||||
|
||||
// Default keybindings configuration
|
||||
export const defaultKeybindings: KeybindingConfig = {
|
||||
space: "toggle-play",
|
||||
j: "seek-backward",
|
||||
k: "toggle-play",
|
||||
l: "seek-forward",
|
||||
left: "frame-step-backward",
|
||||
right: "frame-step-forward",
|
||||
"shift+left": "jump-backward",
|
||||
"shift+right": "jump-forward",
|
||||
home: "goto-start",
|
||||
enter: "goto-start",
|
||||
end: "goto-end",
|
||||
s: "split-element",
|
||||
n: "toggle-snapping",
|
||||
"ctrl+a": "select-all",
|
||||
"ctrl+d": "duplicate-selected",
|
||||
"ctrl+c": "copy-selected",
|
||||
"ctrl+v": "paste-selected",
|
||||
"ctrl+z": "undo",
|
||||
"ctrl+shift+z": "redo",
|
||||
"ctrl+y": "redo",
|
||||
delete: "delete-selected",
|
||||
backspace: "delete-selected",
|
||||
};
|
||||
export const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
|
||||
|
||||
export interface KeybindingConflict {
|
||||
key: ShortcutKey;
|
||||
existingAction: ActionWithOptionalArgs;
|
||||
newAction: ActionWithOptionalArgs;
|
||||
existingAction: TActionWithOptionalArgs;
|
||||
newAction: TActionWithOptionalArgs;
|
||||
}
|
||||
|
||||
interface KeybindingsState {
|
||||
@@ -44,8 +21,7 @@ interface KeybindingsState {
|
||||
keybindingsEnabled: boolean;
|
||||
isRecording: boolean;
|
||||
|
||||
// Actions
|
||||
updateKeybinding: (key: ShortcutKey, action: ActionWithOptionalArgs) => void;
|
||||
updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => void;
|
||||
removeKeybinding: (key: ShortcutKey) => void;
|
||||
resetToDefaults: () => void;
|
||||
importKeybindings: (config: KeybindingConfig) => void;
|
||||
@@ -53,13 +29,11 @@ interface KeybindingsState {
|
||||
enableKeybindings: () => void;
|
||||
disableKeybindings: () => void;
|
||||
setIsRecording: (isRecording: boolean) => void;
|
||||
|
||||
// Validation
|
||||
validateKeybinding: (
|
||||
key: ShortcutKey,
|
||||
action: ActionWithOptionalArgs,
|
||||
action: TActionWithOptionalArgs,
|
||||
) => KeybindingConflict | null;
|
||||
getKeybindingsForAction: (action: ActionWithOptionalArgs) => ShortcutKey[];
|
||||
getKeybindingsForAction: (action: TActionWithOptionalArgs) => ShortcutKey[];
|
||||
|
||||
// Utility
|
||||
getKeybindingString: (ev: KeyboardEvent) => ShortcutKey | null;
|
||||
@@ -77,7 +51,7 @@ export const useKeybindingsStore = create<KeybindingsState>()(
|
||||
keybindingsEnabled: true,
|
||||
isRecording: false,
|
||||
|
||||
updateKeybinding: (key: ShortcutKey, action: ActionWithOptionalArgs) => {
|
||||
updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => {
|
||||
set((state) => {
|
||||
const newKeybindings = { ...state.keybindings };
|
||||
newKeybindings[key] = action;
|
||||
@@ -136,7 +110,7 @@ export const useKeybindingsStore = create<KeybindingsState>()(
|
||||
|
||||
validateKeybinding: (
|
||||
key: ShortcutKey,
|
||||
action: ActionWithOptionalArgs,
|
||||
action: TActionWithOptionalArgs,
|
||||
) => {
|
||||
const { keybindings } = get();
|
||||
const existingAction = keybindings[key];
|
||||
@@ -155,7 +129,7 @@ export const useKeybindingsStore = create<KeybindingsState>()(
|
||||
set({ isRecording });
|
||||
},
|
||||
|
||||
getKeybindingsForAction: (action: ActionWithOptionalArgs) => {
|
||||
getKeybindingsForAction: (action: TActionWithOptionalArgs) => {
|
||||
const { keybindings } = get();
|
||||
return Object.keys(keybindings).filter(
|
||||
(key) => keybindings[key as ShortcutKey] === action,
|
||||
@@ -190,7 +164,7 @@ function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
||||
if (
|
||||
modifierKey === "shift" &&
|
||||
isDOMElement(target) &&
|
||||
isTypableDOMElement(target as HTMLElement)
|
||||
isTypableDOMElement({ element: target as HTMLElement })
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -199,7 +173,10 @@ function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
||||
}
|
||||
|
||||
// no modifier key here then we do not do anything while on input
|
||||
if (isDOMElement(target) && isTypableDOMElement(target as HTMLElement))
|
||||
if (
|
||||
isDOMElement(target) &&
|
||||
isTypableDOMElement({ element: target as HTMLElement })
|
||||
)
|
||||
return null;
|
||||
|
||||
// single key while not input
|
||||
|
||||
@@ -2,72 +2,54 @@ import { create } from "zustand";
|
||||
import type { SoundEffect, SavedSound } from "@/types/sounds";
|
||||
import { storageService } from "@/lib/storage/storage-service";
|
||||
import { toast } from "sonner";
|
||||
import { useMediaStore } from "./media-store";
|
||||
import { useTimelineStore } from "./timeline-store";
|
||||
import { useProjectStore } from "./project-store";
|
||||
import { usePlaybackStore } from "./playback-store";
|
||||
import { EditorCore } from "@/core";
|
||||
import { buildLibraryAudioElement } from "@/lib/timeline/element-utils";
|
||||
|
||||
interface SoundsStore {
|
||||
topSoundEffects: SoundEffect[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
hasLoaded: boolean;
|
||||
|
||||
// Filter state
|
||||
showCommercialOnly: boolean;
|
||||
toggleCommercialFilter: () => void;
|
||||
|
||||
// Search state
|
||||
searchQuery: string;
|
||||
searchResults: SoundEffect[];
|
||||
isSearching: boolean;
|
||||
searchError: string | null;
|
||||
lastSearchQuery: string;
|
||||
scrollPosition: number;
|
||||
|
||||
// Pagination state
|
||||
currentPage: number;
|
||||
hasNextPage: boolean;
|
||||
totalCount: number;
|
||||
isLoadingMore: boolean;
|
||||
|
||||
// Saved sounds state
|
||||
savedSounds: SavedSound[];
|
||||
isSavedSoundsLoaded: boolean;
|
||||
isLoadingSavedSounds: boolean;
|
||||
savedSoundsError: string | null;
|
||||
|
||||
// Timeline integration
|
||||
addSoundToTimeline: (sound: SoundEffect) => Promise<boolean>;
|
||||
|
||||
setTopSoundEffects: (sounds: SoundEffect[]) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setHasLoaded: (loaded: boolean) => void;
|
||||
|
||||
// Search actions
|
||||
setSearchQuery: (query: string) => void;
|
||||
setSearchResults: (results: SoundEffect[]) => void;
|
||||
setSearching: (searching: boolean) => void;
|
||||
setSearchError: (error: string | null) => void;
|
||||
setLastSearchQuery: (query: string) => void;
|
||||
setScrollPosition: (position: number) => void;
|
||||
|
||||
// Pagination actions
|
||||
setCurrentPage: (page: number) => void;
|
||||
setHasNextPage: (hasNext: boolean) => void;
|
||||
setTotalCount: (count: number) => void;
|
||||
setLoadingMore: (loading: boolean) => void;
|
||||
appendSearchResults: (results: SoundEffect[]) => void;
|
||||
appendTopSounds: (results: SoundEffect[]) => 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;
|
||||
|
||||
// Saved sounds actions
|
||||
loadSavedSounds: () => Promise<void>;
|
||||
saveSoundEffect: (soundEffect: SoundEffect) => Promise<void>;
|
||||
removeSavedSound: (soundId: number) => Promise<void>;
|
||||
isSoundSaved: (soundId: number) => boolean;
|
||||
toggleSavedSound: (soundEffect: SoundEffect) => 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>;
|
||||
}
|
||||
|
||||
@@ -82,53 +64,47 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
||||
set((state) => ({ showCommercialOnly: !state.showCommercialOnly }));
|
||||
},
|
||||
|
||||
// Search state
|
||||
searchQuery: "",
|
||||
searchResults: [],
|
||||
isSearching: false,
|
||||
searchError: null,
|
||||
lastSearchQuery: "",
|
||||
scrollPosition: 0,
|
||||
|
||||
// Pagination state
|
||||
currentPage: 1,
|
||||
hasNextPage: false,
|
||||
totalCount: 0,
|
||||
isLoadingMore: false,
|
||||
|
||||
// Saved sounds state
|
||||
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 }),
|
||||
|
||||
// Search actions
|
||||
setSearchQuery: (query) => set({ searchQuery: query }),
|
||||
setSearchResults: (results) =>
|
||||
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 }),
|
||||
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 }),
|
||||
|
||||
// Pagination actions
|
||||
setCurrentPage: (page) => set({ currentPage: page }),
|
||||
setHasNextPage: (hasNext) => set({ hasNextPage: hasNext }),
|
||||
setTotalCount: (count) => set({ totalCount: count }),
|
||||
setLoadingMore: (loading) => set({ isLoadingMore: loading }),
|
||||
appendSearchResults: (results) =>
|
||||
appendSearchResults: ({ results }) =>
|
||||
set((state) => ({
|
||||
searchResults: [...state.searchResults, ...results],
|
||||
})),
|
||||
appendTopSounds: (results) =>
|
||||
|
||||
appendTopSounds: ({ results }) =>
|
||||
set((state) => ({
|
||||
topSoundEffects: [...state.topSoundEffects, ...results],
|
||||
})),
|
||||
|
||||
resetPagination: () =>
|
||||
set({
|
||||
currentPage: 1,
|
||||
@@ -137,7 +113,6 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
||||
isLoadingMore: false,
|
||||
}),
|
||||
|
||||
// Saved sounds actions
|
||||
loadSavedSounds: async () => {
|
||||
if (get().isSavedSoundsLoaded) return;
|
||||
|
||||
@@ -160,11 +135,10 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
saveSoundEffect: async (soundEffect: SoundEffect) => {
|
||||
saveSoundEffect: async ({ soundEffect }) => {
|
||||
try {
|
||||
await storageService.saveSoundEffect({ soundEffect });
|
||||
|
||||
// Refresh saved sounds
|
||||
const savedSoundsData = await storageService.loadSavedSounds();
|
||||
set({ savedSounds: savedSoundsData.sounds });
|
||||
} catch (error) {
|
||||
@@ -176,11 +150,10 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
removeSavedSound: async (soundId: number) => {
|
||||
removeSavedSound: async ({ soundId }) => {
|
||||
try {
|
||||
await storageService.removeSavedSound({ soundId });
|
||||
|
||||
// Update local state immediately
|
||||
set((state) => ({
|
||||
savedSounds: state.savedSounds.filter((sound) => sound.id !== soundId),
|
||||
}));
|
||||
@@ -193,18 +166,18 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
isSoundSaved: (soundId: number) => {
|
||||
isSoundSaved: ({ soundId }) => {
|
||||
const { savedSounds } = get();
|
||||
return savedSounds.some((sound) => sound.id === soundId);
|
||||
},
|
||||
|
||||
toggleSavedSound: async (soundEffect: SoundEffect) => {
|
||||
toggleSavedSound: async ({ soundEffect }) => {
|
||||
const { isSoundSaved, saveSoundEffect, removeSavedSound } = get();
|
||||
|
||||
if (isSoundSaved(soundEffect.id)) {
|
||||
await removeSavedSound(soundEffect.id);
|
||||
if (isSoundSaved({ soundId: soundEffect.id })) {
|
||||
await removeSavedSound({ soundId: soundEffect.id });
|
||||
} else {
|
||||
await saveSoundEffect(soundEffect);
|
||||
await saveSoundEffect({ soundEffect });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -224,13 +197,7 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
addSoundToTimeline: async (sound) => {
|
||||
const activeProject = useProjectStore.getState().activeProject;
|
||||
if (!activeProject) {
|
||||
toast.error("No active project");
|
||||
return false;
|
||||
}
|
||||
|
||||
addSoundToTimeline: async ({ sound }) => {
|
||||
const audioUrl = sound.previewUrl;
|
||||
if (!audioUrl) {
|
||||
toast.error("Sound file not available");
|
||||
@@ -238,36 +205,37 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
||||
}
|
||||
|
||||
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 blob = await response.blob();
|
||||
const file = new File([blob], `${sound.name}.mp3`, {
|
||||
type: "audio/mpeg",
|
||||
});
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioContext = new AudioContext();
|
||||
const buffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
|
||||
await useMediaStore.getState().addMediaFile(activeProject.id, {
|
||||
name: sound.name,
|
||||
type: "audio",
|
||||
file,
|
||||
duration: sound.duration,
|
||||
url: URL.createObjectURL(file),
|
||||
});
|
||||
let audioTrack = tracks.find((t) => t.type === "audio");
|
||||
let trackId: string;
|
||||
|
||||
const mediaItem = useMediaStore
|
||||
.getState()
|
||||
.mediaFiles.find((item) => item.file === file);
|
||||
if (!mediaItem) throw new Error("Failed to create media item");
|
||||
|
||||
const success = useTimelineStore
|
||||
.getState()
|
||||
.addElementAtTime(mediaItem, usePlaybackStore.getState().currentTime);
|
||||
|
||||
if (success) {
|
||||
return true;
|
||||
if (audioTrack) {
|
||||
trackId = audioTrack.id;
|
||||
} else {
|
||||
trackId = editor.timeline.addTrack({ type: "audio" });
|
||||
}
|
||||
throw new Error("Failed to add to timeline - check for overlaps");
|
||||
|
||||
const element = buildLibraryAudioElement({
|
||||
sourceUrl: audioUrl,
|
||||
name: sound.name,
|
||||
duration: sound.duration,
|
||||
startTime: currentTime,
|
||||
buffer,
|
||||
});
|
||||
|
||||
editor.timeline.addElementToTrack({ trackId, element });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to add sound to timeline:", error);
|
||||
toast.error(
|
||||
|
||||
@@ -3,27 +3,22 @@ import {
|
||||
getCollections,
|
||||
getCollection,
|
||||
searchIcons,
|
||||
downloadSvgAsText,
|
||||
svgToFile,
|
||||
type IconSet,
|
||||
type CollectionInfo,
|
||||
type IconSearchResult,
|
||||
} from "@/lib/iconify-api";
|
||||
import { useProjectStore } from "@/stores/project-store";
|
||||
import { useMediaStore } from "@/stores/media-store";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
import { usePlaybackStore } from "@/stores/playback-store";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import type { MediaFile } from "@/types/assets";
|
||||
import { EditorCore } from "@/core";
|
||||
import { buildStickerElement } from "@/lib/timeline/element-utils";
|
||||
import { STICKER_CATEGORY_CONFIG } from "@/constants/stickers-constants";
|
||||
import type { StickerCategory } from "@/types/stickers";
|
||||
|
||||
export type StickerCategory = "all" | "general" | "brands" | "emoji";
|
||||
type ViewMode = "search" | "browse" | "collection";
|
||||
|
||||
interface StickersStore {
|
||||
searchQuery: string;
|
||||
selectedCategory: StickerCategory;
|
||||
selectedCollection: string | null;
|
||||
viewMode: "search" | "browse" | "collection";
|
||||
|
||||
viewMode: ViewMode;
|
||||
collections: Record<string, IconSet>;
|
||||
currentCollection: CollectionInfo | null;
|
||||
searchResults: IconSearchResult | null;
|
||||
@@ -31,21 +26,21 @@ interface StickersStore {
|
||||
isLoadingCollections: boolean;
|
||||
isLoadingCollection: boolean;
|
||||
isSearching: boolean;
|
||||
isDownloading: boolean;
|
||||
addingSticker: string | null;
|
||||
|
||||
setSearchQuery: (query: string) => void;
|
||||
setSelectedCategory: (category: StickerCategory) => void;
|
||||
setSelectedCollection: (collection: string | null) => void;
|
||||
setViewMode: (mode: "search" | "browse" | "collection") => 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: string) => Promise<void>;
|
||||
searchStickers: (query: string) => Promise<void>;
|
||||
downloadSticker: (iconName: string) => Promise<File | null>;
|
||||
addStickerToTimeline: (iconName: string) => Promise<void>;
|
||||
|
||||
addToRecentStickers: (iconName: string) => 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;
|
||||
}
|
||||
|
||||
@@ -65,12 +60,11 @@ export const useStickersStore = create<StickersStore>((set, get) => ({
|
||||
isLoadingCollections: false,
|
||||
isLoadingCollection: false,
|
||||
isSearching: false,
|
||||
isDownloading: false,
|
||||
addingSticker: null,
|
||||
|
||||
setSearchQuery: (query) => set({ searchQuery: query }),
|
||||
setSearchQuery: ({ query }) => set({ searchQuery: query }),
|
||||
|
||||
setSelectedCategory: (category) =>
|
||||
setSelectedCategory: ({ category }) =>
|
||||
set({
|
||||
selectedCategory: category,
|
||||
viewMode: "browse",
|
||||
@@ -78,7 +72,7 @@ export const useStickersStore = create<StickersStore>((set, get) => ({
|
||||
currentCollection: null,
|
||||
}),
|
||||
|
||||
setSelectedCollection: (collection) => {
|
||||
setSelectedCollection: ({ collection }) => {
|
||||
set({
|
||||
selectedCollection: collection,
|
||||
viewMode: collection ? "collection" : "browse",
|
||||
@@ -86,11 +80,11 @@ export const useStickersStore = create<StickersStore>((set, get) => ({
|
||||
});
|
||||
|
||||
if (collection) {
|
||||
get().loadCollection(collection);
|
||||
get().loadCollection({ prefix: collection });
|
||||
}
|
||||
},
|
||||
|
||||
setViewMode: (mode) => set({ viewMode: mode }),
|
||||
setViewMode: ({ mode }) => set({ viewMode: mode }),
|
||||
|
||||
loadCollections: async () => {
|
||||
set({ isLoadingCollections: true });
|
||||
@@ -104,7 +98,7 @@ export const useStickersStore = create<StickersStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
loadCollection: async (prefix: string) => {
|
||||
loadCollection: async ({ prefix }: { prefix: string }) => {
|
||||
set({ isLoadingCollection: true });
|
||||
try {
|
||||
const collection = await getCollection(prefix);
|
||||
@@ -117,7 +111,7 @@ export const useStickersStore = create<StickersStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
searchStickers: async (query: string) => {
|
||||
searchStickers: async ({ query }: { query: string }) => {
|
||||
if (!query.trim()) {
|
||||
set({ searchResults: null, viewMode: "browse" });
|
||||
return;
|
||||
@@ -127,18 +121,7 @@ export const useStickersStore = create<StickersStore>((set, get) => ({
|
||||
|
||||
set({ isSearching: true, viewMode: "search" });
|
||||
try {
|
||||
let category: string | undefined;
|
||||
|
||||
if (selectedCategory !== "all") {
|
||||
if (selectedCategory === "general") {
|
||||
category = "General";
|
||||
} else if (selectedCategory === "brands") {
|
||||
category = "Brands / Social";
|
||||
} else if (selectedCategory === "emoji") {
|
||||
category = "Emoji";
|
||||
}
|
||||
}
|
||||
|
||||
const category = STICKER_CATEGORY_CONFIG[selectedCategory];
|
||||
const results = await searchIcons(query, 100, undefined, category);
|
||||
set({ searchResults: results });
|
||||
} catch (error) {
|
||||
@@ -149,73 +132,32 @@ export const useStickersStore = create<StickersStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
downloadSticker: async (iconName: string) => {
|
||||
set({ isDownloading: true });
|
||||
try {
|
||||
const svgText = await downloadSvgAsText(iconName, {
|
||||
width: 200,
|
||||
height: 200,
|
||||
});
|
||||
|
||||
const fileName = `${iconName.replace(":", "-")}.svg`;
|
||||
const file = svgToFile(svgText, fileName);
|
||||
|
||||
get().addToRecentStickers(iconName);
|
||||
|
||||
return file;
|
||||
} catch (error) {
|
||||
console.error(`Failed to download sticker ${iconName}:`, error);
|
||||
return null;
|
||||
} finally {
|
||||
set({ isDownloading: false });
|
||||
}
|
||||
},
|
||||
|
||||
addStickerToTimeline: async (iconName: string) => {
|
||||
addStickerToTimeline: ({ iconName }: { iconName: string }) => {
|
||||
set({ addingSticker: iconName });
|
||||
try {
|
||||
const { activeProject } = useProjectStore.getState();
|
||||
if (!activeProject) {
|
||||
throw new Error("No active project");
|
||||
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;
|
||||
|
||||
if (stickerTrack) {
|
||||
trackId = stickerTrack.id;
|
||||
} else {
|
||||
trackId = editor.timeline.addTrack({ type: "sticker" });
|
||||
}
|
||||
|
||||
const file = await get().downloadSticker(iconName);
|
||||
if (!file) {
|
||||
throw new Error("Failed to download sticker");
|
||||
}
|
||||
const element = buildStickerElement({ iconName, startTime: currentTime });
|
||||
editor.timeline.addElementToTrack({ trackId, element });
|
||||
|
||||
const mediaItem: Omit<MediaFile, "id"> = {
|
||||
name: iconName.replace(":", "-"),
|
||||
type: "image",
|
||||
file,
|
||||
url: URL.createObjectURL(file),
|
||||
width: 200,
|
||||
height: 200,
|
||||
duration: TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION,
|
||||
ephemeral: false,
|
||||
};
|
||||
|
||||
const { addMediaFile } = useMediaStore.getState();
|
||||
await addMediaFile(activeProject.id, mediaItem);
|
||||
|
||||
const added = useMediaStore
|
||||
.getState()
|
||||
.mediaFiles.find(
|
||||
(m) => m.url === mediaItem.url && m.name === mediaItem.name,
|
||||
);
|
||||
if (!added) {
|
||||
throw new Error("Sticker not in media store");
|
||||
}
|
||||
|
||||
const { currentTime } = usePlaybackStore.getState();
|
||||
const { addElementAtTime } = useTimelineStore.getState();
|
||||
addElementAtTime(added, currentTime);
|
||||
get().addToRecentStickers({ iconName });
|
||||
} finally {
|
||||
set({ addingSticker: null });
|
||||
}
|
||||
},
|
||||
|
||||
addToRecentStickers: (iconName: string) => {
|
||||
addToRecentStickers: ({ iconName }: { iconName: string }) => {
|
||||
set((state) => {
|
||||
const recent = [
|
||||
iconName,
|
||||
|
||||
Reference in New Issue
Block a user