feat: so much stuff

This commit is contained in:
Maze Winther
2025-06-22 19:28:03 +02:00
parent e22aa6620c
commit 6ee16f9df8
9 changed files with 1229 additions and 83 deletions

View File

@ -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<MediaItem, "id">) => 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<number> => {
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<number> => {
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<MediaStore>((set, get) => ({
mediaItems: [],
@ -28,8 +138,33 @@ export const useMediaStore = create<MediaStore>((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: [] });
},
}));

View File

@ -18,9 +18,21 @@ interface TimelineStore {
tracks: TimelineTrack[];
// Actions
addTrack: (type: "video" | "audio" | "effects") => void;
addTrack: (type: "video" | "audio" | "effects") => string;
removeTrack: (trackId: string) => void;
addClipToTrack: (trackId: string, clip: Omit<TimelineClip, "id">) => void;
removeClipFromTrack: (trackId: string, clipId: string) => void;
moveClipToTrack: (
fromTrackId: string,
toTrackId: string,
clipId: string,
insertIndex?: number
) => void;
reorderClipInTrack: (
trackId: string,
clipId: string,
newIndex: number
) => void;
}
export const useTimelineStore = create<TimelineStore>((set) => ({
@ -36,6 +48,7 @@ export const useTimelineStore = create<TimelineStore>((set) => ({
set((state) => ({
tracks: [...state.tracks, newTrack],
}));
return newTrack.id;
},
removeTrack: (trackId) => {
@ -58,4 +71,67 @@ export const useTimelineStore = create<TimelineStore>((set) => ({
),
}));
},
removeClipFromTrack: (trackId, clipId) => {
set((state) => ({
tracks: state.tracks.map((track) =>
track.id === trackId
? {
...track,
clips: track.clips.filter((clip) => clip.id !== clipId),
}
: track
),
}));
},
moveClipToTrack: (fromTrackId, toTrackId, clipId, insertIndex) => {
set((state) => {
// Find the clip to move
const fromTrack = state.tracks.find((track) => track.id === fromTrackId);
const clipToMove = fromTrack?.clips.find((clip) => clip.id === clipId);
if (!clipToMove) return state;
return {
tracks: state.tracks.map((track) => {
if (track.id === fromTrackId) {
// Remove clip from source track
return {
...track,
clips: track.clips.filter((clip) => clip.id !== clipId),
};
} else if (track.id === toTrackId) {
// Add clip to destination track
const newClips = [...track.clips];
const index =
insertIndex !== undefined ? insertIndex : newClips.length;
newClips.splice(index, 0, clipToMove);
return {
...track,
clips: newClips,
};
}
return track;
}),
};
});
},
reorderClipInTrack: (trackId, clipId, newIndex) => {
set((state) => ({
tracks: state.tracks.map((track) => {
if (track.id !== trackId) return track;
const clipIndex = track.clips.findIndex((clip) => clip.id === clipId);
if (clipIndex === -1) return track;
const newClips = [...track.clips];
const [movedClip] = newClips.splice(clipIndex, 1);
newClips.splice(newIndex, 0, movedClip);
return { ...track, clips: newClips };
}),
}));
},
}));