From be1ae20d9089e81cb84a8294be76fe680a24b60f Mon Sep 17 00:00:00 2001 From: Hyteq Date: Sun, 20 Jul 2025 11:00:22 +0300 Subject: format --- apps/web/src/stores/media-store.ts | 508 +++++++++++++++++++------------------ 1 file changed, 256 insertions(+), 252 deletions(-) (limited to 'apps/web/src/stores/media-store.ts') diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts index c4ee5f3e..305316c8 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -6,284 +6,288 @@ 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 + 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 + 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; + 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 + 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); - }); + 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 + 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(); - }); + 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(); - }); + 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 + 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); - - // Regenerate thumbnails for video items - const updatedMediaItems = await Promise.all( - mediaItems.map(async (item) => { - if (item.type === "video" && item.file) { - try { - const { thumbnailUrl, width, height } = await generateVideoThumbnail(item.file); - return { - ...item, - thumbnailUrl, - width: width || item.width, - height: height || item.height - }; - } catch (error) { - console.error(`Failed to regenerate thumbnail for video ${item.id}:`, error); - return item; - } - } - return item; - }) - ); - - set({ mediaItems: updatedMediaItems }); - } 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: [] }); - }, + 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); + + // Regenerate thumbnails for video items + const updatedMediaItems = await Promise.all( + mediaItems.map(async (item) => { + if (item.type === "video" && item.file) { + try { + const { thumbnailUrl, width, height } = + await generateVideoThumbnail(item.file); + return { + ...item, + thumbnailUrl, + width: width || item.width, + height: height || item.height, + }; + } catch (error) { + console.error( + `Failed to regenerate thumbnail for video ${item.id}:`, + error, + ); + return item; + } + } + return item; + }), + ); + + set({ mediaItems: updatedMediaItems }); + } 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: [] }); + }, })); -- cgit v1.3.1