mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
283 lines
8.7 KiB
TypeScript
283 lines
8.7 KiB
TypeScript
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";
|
|
|
|
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;
|
|
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>;
|
|
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 }));
|
|
},
|
|
|
|
// 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) =>
|
|
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 }),
|
|
|
|
// Pagination actions
|
|
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,
|
|
}),
|
|
|
|
// Saved sounds actions
|
|
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: SoundEffect) => {
|
|
try {
|
|
await storageService.saveSoundEffect(soundEffect);
|
|
|
|
// Refresh saved sounds
|
|
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: number) => {
|
|
try {
|
|
await storageService.removeSavedSound(soundId);
|
|
|
|
// Update local state immediately
|
|
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: number) => {
|
|
const { savedSounds } = get();
|
|
return savedSounds.some((sound) => sound.id === soundId);
|
|
},
|
|
|
|
toggleSavedSound: async (soundEffect: SoundEffect) => {
|
|
const { isSoundSaved, saveSoundEffect, removeSavedSound } = get();
|
|
|
|
if (isSoundSaved(soundEffect.id)) {
|
|
await removeSavedSound(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 activeProject = useProjectStore.getState().activeProject;
|
|
if (!activeProject) {
|
|
toast.error("No active project");
|
|
return false;
|
|
}
|
|
|
|
const audioUrl = sound.previewUrl;
|
|
if (!audioUrl) {
|
|
toast.error("Sound file not available");
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
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",
|
|
});
|
|
|
|
await useMediaStore.getState().addMediaItem(activeProject.id, {
|
|
name: sound.name,
|
|
type: "audio",
|
|
file,
|
|
duration: sound.duration,
|
|
url: URL.createObjectURL(file),
|
|
});
|
|
|
|
const mediaItem = useMediaStore
|
|
.getState()
|
|
.mediaItems.find((item) => item.file === file);
|
|
if (!mediaItem) throw new Error("Failed to create media item");
|
|
|
|
const success = useTimelineStore
|
|
.getState()
|
|
.addMediaAtTime(mediaItem, usePlaybackStore.getState().currentTime);
|
|
|
|
if (success) {
|
|
return true;
|
|
}
|
|
throw new Error("Failed to add to timeline - check for overlaps");
|
|
} 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;
|
|
}
|
|
},
|
|
}));
|