import { create } from "zustand"; import { storageService } from "@/lib/storage/storage-service"; import { useTimelineStore } from "./timeline-store"; import { generateUUID } from "@/lib/utils"; export type MediaType = "image" | "video" | "audio"; export interface MediaItem { id: string; name: string; type: MediaType; file: File; url?: string; // Object URL for preview thumbnailUrl?: string; // For video thumbnails duration?: number; // For video/audio duration width?: number; // For video/image width height?: number; // For video/image height fps?: number; // For video frame rate // Text-specific properties content?: string; // Text content fontSize?: number; // Font size fontFamily?: string; // Font family color?: string; // Text color backgroundColor?: string; // Background color textAlign?: "left" | "center" | "right"; // Text alignment } interface MediaStore { mediaItems: MediaItem[]; isLoading: boolean; // Actions - now require projectId addMediaItem: ( projectId: string, item: Omit ) => Promise; removeMediaItem: (projectId: string, id: string) => Promise; loadProjectMedia: (projectId: string) => Promise; clearProjectMedia: (projectId: string) => Promise; clearAllMedia: () => void; // Clear local state only } // Helper function to determine file type export const getFileType = (file: File): MediaType | null => { const { type } = file; if (type.startsWith("image/")) { return "image"; } if (type.startsWith("video/")) { return "video"; } if (type.startsWith("audio/")) { return "audio"; } return null; }; // Helper function to get image dimensions export const getImageDimensions = ( file: File ): Promise<{ width: number; height: number }> => { return new Promise((resolve, reject) => { const img = new window.Image(); img.addEventListener("load", () => { const width = img.naturalWidth; const height = img.naturalHeight; resolve({ width, height }); img.remove(); }); img.addEventListener("error", () => { reject(new Error("Could not load image")); img.remove(); }); img.src = URL.createObjectURL(file); }); }; // Helper function to generate video thumbnail and get dimensions export const generateVideoThumbnail = ( file: File ): Promise<{ thumbnailUrl: string; width: number; height: number }> => { return new Promise((resolve, reject) => { const video = document.createElement("video") as HTMLVideoElement; const canvas = document.createElement("canvas") as HTMLCanvasElement; const ctx = canvas.getContext("2d"); if (!ctx) { reject(new Error("Could not get canvas context")); return; } video.addEventListener("loadedmetadata", () => { canvas.width = video.videoWidth; canvas.height = video.videoHeight; // Seek to 1 second or 10% of duration, whichever is smaller video.currentTime = Math.min(1, video.duration * 0.1); }); video.addEventListener("seeked", () => { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const thumbnailUrl = canvas.toDataURL("image/jpeg", 0.8); const width = video.videoWidth; const height = video.videoHeight; resolve({ thumbnailUrl, width, height }); // Cleanup video.remove(); canvas.remove(); }); video.addEventListener("error", () => { reject(new Error("Could not load video")); video.remove(); canvas.remove(); }); video.src = URL.createObjectURL(file); video.load(); }); }; // Helper function to get media duration export const getMediaDuration = (file: File): Promise => { return new Promise((resolve, reject) => { const element = document.createElement( file.type.startsWith("video/") ? "video" : "audio" ) as HTMLVideoElement; element.addEventListener("loadedmetadata", () => { resolve(element.duration); element.remove(); }); element.addEventListener("error", () => { reject(new Error("Could not load media")); element.remove(); }); element.src = URL.createObjectURL(file); element.load(); }); }; // Helper to get aspect ratio from MediaItem export const getMediaAspectRatio = (item: MediaItem): number => { if (item.width && item.height) { return item.width / item.height; } return 16 / 9; // Default aspect ratio }; export const useMediaStore = create((set, get) => ({ mediaItems: [], isLoading: false, addMediaItem: async (projectId, item) => { const newItem: MediaItem = { ...item, id: generateUUID(), }; // Add to local state immediately for UI responsiveness set((state) => ({ mediaItems: [...state.mediaItems, newItem], })); // Save to persistent storage in background try { await storageService.saveMediaItem(projectId, newItem); } catch (error) { console.error("Failed to save media item:", error); // Remove from local state if save failed set((state) => ({ mediaItems: state.mediaItems.filter((media) => media.id !== newItem.id), })); } }, removeMediaItem: async (projectId, id: string) => { const state = get(); const item = state.mediaItems.find((media) => media.id === id); // Cleanup object URLs to prevent memory leaks if (item && item.url) { URL.revokeObjectURL(item.url); if (item.thumbnailUrl) { URL.revokeObjectURL(item.thumbnailUrl); } } // Remove from local state immediately set((state) => ({ mediaItems: state.mediaItems.filter((media) => media.id !== id), })); // Remove from persistent storage try { await storageService.deleteMediaItem(projectId, id); } catch (error) { console.error("Failed to delete media item:", error); } }, loadProjectMedia: async (projectId) => { set({ isLoading: true }); try { const mediaItems = await storageService.loadAllMediaItems(projectId); set({ mediaItems }); } catch (error) { console.error("Failed to load media items:", error); } finally { set({ isLoading: false }); } }, clearProjectMedia: async (projectId) => { const state = get(); // Cleanup all object URLs state.mediaItems.forEach((item) => { if (item.url) { URL.revokeObjectURL(item.url); } if (item.thumbnailUrl) { URL.revokeObjectURL(item.thumbnailUrl); } }); // Clear local state set({ mediaItems: [] }); // Clear persistent storage try { const mediaIds = state.mediaItems.map((item) => item.id); await Promise.all( mediaIds.map((id) => storageService.deleteMediaItem(projectId, id)) ); } catch (error) { console.error("Failed to clear media items from storage:", error); } }, clearAllMedia: () => { const state = get(); // Cleanup all object URLs state.mediaItems.forEach((item) => { if (item.url) { URL.revokeObjectURL(item.url); } if (item.thumbnailUrl) { URL.revokeObjectURL(item.thumbnailUrl); } }); // Clear local state set({ mediaItems: [] }); }, }));