From 6ee16f9df8aeabfef0ca533f9f5b13daab3a3758 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 22 Jun 2025 19:28:03 +0200 Subject: feat: so much stuff --- apps/web/src/stores/media-store.ts | 135 +++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) (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 25d11af8..bdb4d52a 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -4,6 +4,11 @@ export interface MediaItem { id: string; name: string; type: "image" | "video" | "audio"; + file: File; + url: string; // Object URL for preview + thumbnailUrl?: string; // For video thumbnails + duration?: number; // For video/audio duration + aspectRatio: number; // width / height } interface MediaStore { @@ -12,8 +17,113 @@ interface MediaStore { // Actions addMediaItem: (item: Omit) => void; removeMediaItem: (id: string) => void; + clearAllMedia: () => void; } +// Helper function to determine file type +export const getFileType = (file: File): "image" | "video" | "audio" | 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 aspect ratio +export const getImageAspectRatio = (file: File): Promise => { + return new Promise((resolve, reject) => { + const img = new Image(); + + img.addEventListener("load", () => { + const aspectRatio = img.naturalWidth / img.naturalHeight; + resolve(aspectRatio); + 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 aspect ratio +export const generateVideoThumbnail = ( + file: File +): Promise<{ thumbnailUrl: string; aspectRatio: number }> => { + return new Promise((resolve, reject) => { + const video = document.createElement("video"); + const canvas = document.createElement("canvas"); + 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 aspectRatio = video.videoWidth / video.videoHeight; + + resolve({ thumbnailUrl, aspectRatio }); + + // 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 | HTMLAudioElement; + + 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(); + }); +}; + export const useMediaStore = create((set, get) => ({ mediaItems: [], @@ -28,8 +138,33 @@ export const useMediaStore = create((set, get) => ({ }, removeMediaItem: (id) => { + const state = get(); + const item = state.mediaItems.find((item) => item.id === id); + + // Cleanup object URLs to prevent memory leaks + if (item) { + URL.revokeObjectURL(item.url); + if (item.thumbnailUrl) { + URL.revokeObjectURL(item.thumbnailUrl); + } + } + set((state) => ({ mediaItems: state.mediaItems.filter((item) => item.id !== id), })); }, + + clearAllMedia: () => { + const state = get(); + + // Cleanup all object URLs + state.mediaItems.forEach((item) => { + URL.revokeObjectURL(item.url); + if (item.thumbnailUrl) { + URL.revokeObjectURL(item.thumbnailUrl); + } + }); + + set({ mediaItems: [] }); + }, })); -- cgit v1.3.1