Files
OpenCut/legacy/media-store.ts
T
2026-01-15 11:05:17 +01:00

225 lines
6.0 KiB
TypeScript

import { create } from "zustand";
import { storageService } from "@/lib/storage/storage-service";
import { useTimelineStore } from "../apps/web/src/stores/timeline-store";
import { generateUUID } from "@/lib/utils";
import { MediaType, MediaAsset } from "@/types/assets";
import { videoCache } from "@/lib/video-cache";
interface MediaStore {
mediaFiles: MediaAsset[];
isLoading: boolean;
// Actions
addMediaFile: (
projectId: string,
file: Omit<MediaAsset, "id">,
) => Promise<void>;
removeMediaFile: (projectId: string, id: string) => Promise<void>;
loadProjectMedia: (projectId: string) => Promise<void>;
clearProjectMedia: (projectId: string) => Promise<void>;
clearAllMedia: () => void;
}
// 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 get media duration
export const getMediaDuration = (file: File): Promise<number> => {
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();
});
};
export const getMediaAspectRatio = (item: MediaAsset): number => {
if (item.width && item.height) {
return item.width / item.height;
}
return 16 / 9; // Default aspect ratio
};
export const useMediaStore = create<MediaStore>((set, get) => ({
mediaFiles: [],
isLoading: false,
addMediaFile: async (projectId, file) => {
const newItem: MediaAsset = {
...file,
id: generateUUID(),
};
// Add to local state immediately for UI responsiveness
set((state) => ({
mediaFiles: [...state.mediaFiles, newItem],
}));
// Save to persistent storage in background
try {
await storageService.saveMediaFile({ projectId, mediaItem: newItem });
} catch (error) {
console.error("Failed to save media item:", error);
// Remove from local state if save failed
set((state) => ({
mediaFiles: state.mediaFiles.filter((media) => media.id !== newItem.id),
}));
}
},
removeMediaFile: async (projectId: string, id: string) => {
const state = get();
const item = state.mediaFiles.find((media) => media.id === id);
videoCache.clearVideo(id);
// Cleanup object URLs to prevent memory leaks
if (item?.url) {
URL.revokeObjectURL(item.url);
if (item.thumbnailUrl) {
URL.revokeObjectURL(item.thumbnailUrl);
}
}
// 1) Remove from local state immediately
set((state) => ({
mediaFiles: state.mediaFiles.filter((media) => media.id !== id),
}));
// 2) Cascade into the timeline: remove any elements using this media ID
const timeline = useTimelineStore.getState();
const { tracks, deleteSelected, setSelectedElements } = timeline;
// Find all elements that reference this media
const elementsToRemove: Array<{ trackId: string; elementId: string }> = [];
for (const track of tracks) {
for (const el of track.elements) {
if (el.type === "media" && el.mediaId === id) {
elementsToRemove.push({ trackId: track.id, elementId: el.id });
}
}
}
// If there are elements to remove, use unified delete function
if (elementsToRemove.length > 0) {
setSelectedElements(elementsToRemove);
deleteSelected();
}
// 3) Remove from persistent storage
try {
await storageService.deleteMediaFile({ projectId, id });
} catch (error) {
console.error("Failed to delete media item:", error);
}
},
loadProjectMedia: async (projectId) => {
set({ isLoading: true });
try {
const mediaItems = await storageService.loadAllMediaFiles({ projectId });
set({ mediaFiles: 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.mediaFiles.forEach((item) => {
if (item.url) {
URL.revokeObjectURL(item.url);
}
if (item.thumbnailUrl) {
URL.revokeObjectURL(item.thumbnailUrl);
}
});
// Clear local state
set({ mediaFiles: [] });
// Clear persistent storage
try {
const mediaIds = state.mediaFiles.map((item) => item.id);
await Promise.all(
mediaIds.map((id) => storageService.deleteMediaFile({ projectId, id })),
);
} catch (error) {
console.error("Failed to clear media items from storage:", error);
}
},
clearAllMedia: () => {
const state = get();
videoCache.clearAll();
// Cleanup all object URLs
state.mediaFiles.forEach((item) => {
if (item.url) {
URL.revokeObjectURL(item.url);
}
if (item.thumbnailUrl) {
URL.revokeObjectURL(item.thumbnailUrl);
}
});
// Clear local state
set({ mediaFiles: [] });
},
}));