refactor: store media relative to project, add storage for timeline data, and other things

This commit is contained in:
Maze Winther
2025-07-07 19:06:36 +02:00
parent 11c0b89bd1
commit bd0c7f2206
13 changed files with 573 additions and 514 deletions

View File

@ -24,9 +24,11 @@ import {
SelectValue,
} from "@/components/ui/select";
import { DraggableMediaItem } from "@/components/ui/draggable-item";
import { useProjectStore } from "@/stores/project-store";
export function MediaView() {
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
const { activeProject } = useProjectStore();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [progress, setProgress] = useState(0);
@ -35,6 +37,11 @@ export function MediaView() {
const processFiles = async (files: FileList | File[]) => {
if (!files || files.length === 0) return;
if (!activeProject) {
toast.error("No active project");
return;
}
setIsProcessing(true);
setProgress(0);
try {
@ -44,7 +51,7 @@ export function MediaView() {
);
// Add each processed media item to the store
for (const item of processedItems) {
await addMediaItem(item);
await addMediaItem(activeProject.id, item);
}
} catch (error) {
// Show error toast if processing fails
@ -73,6 +80,11 @@ export function MediaView() {
// Remove a media item from the store
e.stopPropagation();
if (!activeProject) {
toast.error("No active project");
return;
}
// Remove elements automatically when delete media
const { tracks, removeTrack } = useTimelineStore.getState();
tracks.forEach((track) => {
@ -92,7 +104,7 @@ export function MediaView() {
removeTrack(track.id);
}
});
await removeMediaItem(id);
await removeMediaItem(activeProject.id, id);
};
const formatDuration = (duration: number) => {

View File

@ -13,11 +13,12 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from "../ui/context-menu";
import {
import {
TimelineTrack,
sortTracksByOrder,
ensureMainTrack,
getMainTrack
getMainTrack,
canElementGoOnTrack,
} from "@/types/timeline";
import { usePlaybackStore } from "@/stores/playback-store";
import type {
@ -572,7 +573,9 @@ export function TimelineTrackContent({
// dropPosition === "on" but track is not text type
// Insert above main track if main track exists, otherwise at top
if (mainTrack) {
const mainTrackIndex = tracks.findIndex(t => t.id === mainTrack.id);
const mainTrackIndex = tracks.findIndex(
(t) => t.id === mainTrack.id
);
insertIndex = mainTrackIndex;
} else {
insertIndex = 0; // Top of timeline
@ -648,9 +651,11 @@ export function TimelineTrackContent({
const isVideoOrImage =
dragData.type === "video" || dragData.type === "image";
const isAudio = dragData.type === "audio";
const isCompatible =
(track.type === "media" && isVideoOrImage) ||
(track.type === "audio" && isAudio);
const isCompatible = isVideoOrImage
? canElementGoOnTrack("media", track.type)
: isAudio
? canElementGoOnTrack("media", track.type)
: false;
let targetTrack = tracks.find((t) => t.id === targetTrackId);
@ -662,7 +667,7 @@ export function TimelineTrackContent({
if (isVideoOrImage) {
// For video/image, check if we need a main track or additional media track
const mainTrack = getMainTrack(tracks);
if (!mainTrack) {
// No main track exists, create it
const updatedTracks = ensureMainTrack(tracks);
@ -672,22 +677,32 @@ export function TimelineTrackContent({
targetTrack = newMainTrack;
} else {
// Main track was created but somehow has elements, create new media track
const mainTrackIndex = updatedTracks.findIndex(t => t.id === newMainTrack?.id);
const mainTrackIndex = updatedTracks.findIndex(
(t) => t.id === newMainTrack?.id
);
targetTrackId = insertTrackAt("media", mainTrackIndex);
const updatedTracksAfterInsert = useTimelineStore.getState().tracks;
const newTargetTrack = updatedTracksAfterInsert.find(t => t.id === targetTrackId);
const updatedTracksAfterInsert =
useTimelineStore.getState().tracks;
const newTargetTrack = updatedTracksAfterInsert.find(
(t) => t.id === targetTrackId
);
if (!newTargetTrack) return;
targetTrack = newTargetTrack;
}
} else if (mainTrack.elements.length === 0 && dropPosition === "on") {
} else if (
mainTrack.elements.length === 0 &&
dropPosition === "on"
) {
// Main track exists and is empty, use it
targetTrackId = mainTrack.id;
targetTrack = mainTrack;
} else {
// Create new media track above main track
const mainTrackIndex = tracks.findIndex(t => t.id === mainTrack.id);
const mainTrackIndex = tracks.findIndex(
(t) => t.id === mainTrack.id
);
let insertIndex: number;
if (dropPosition === "above") {
insertIndex = currentTrackIndex;
} else if (dropPosition === "below") {
@ -696,10 +711,12 @@ export function TimelineTrackContent({
// Insert above main track
insertIndex = mainTrackIndex;
}
targetTrackId = insertTrackAt("media", insertIndex);
const updatedTracks = useTimelineStore.getState().tracks;
const newTargetTrack = updatedTracks.find(t => t.id === targetTrackId);
const newTargetTrack = updatedTracks.find(
(t) => t.id === targetTrackId
);
if (!newTargetTrack) return;
targetTrack = newTargetTrack;
}
@ -707,7 +724,7 @@ export function TimelineTrackContent({
// Audio tracks go at the bottom
const mainTrack = getMainTrack(tracks);
let insertIndex: number;
if (dropPosition === "above") {
insertIndex = currentTrackIndex;
} else if (dropPosition === "below") {
@ -715,16 +732,20 @@ export function TimelineTrackContent({
} else {
// Insert after main track (bottom area)
if (mainTrack) {
const mainTrackIndex = tracks.findIndex(t => t.id === mainTrack.id);
const mainTrackIndex = tracks.findIndex(
(t) => t.id === mainTrack.id
);
insertIndex = mainTrackIndex + 1;
} else {
insertIndex = tracks.length; // Bottom of timeline
}
}
targetTrackId = insertTrackAt("audio", insertIndex);
const updatedTracks = useTimelineStore.getState().tracks;
const newTargetTrack = updatedTracks.find(t => t.id === targetTrackId);
const newTargetTrack = updatedTracks.find(
(t) => t.id === targetTrackId
);
if (!newTargetTrack) return;
targetTrack = newTargetTrack;
}
@ -805,7 +826,7 @@ export function TimelineTrackContent({
? wouldOverlap
? "Cannot drop - would overlap"
: "Drop element here"
: "Drop media here"}
: ""}
</div>
) : (
<>

View File

@ -31,6 +31,7 @@ import {
import { useTimelineStore } from "@/stores/timeline-store";
import { useMediaStore } from "@/stores/media-store";
import { usePlaybackStore } from "@/stores/playback-store";
import { useProjectStore } from "@/stores/project-store";
import { processMediaFiles } from "@/lib/media-processing";
import { toast } from "sonner";
import { useState, useRef, useEffect, useCallback } from "react";
@ -65,6 +66,7 @@ export function Timeline() {
redo,
} = useTimelineStore();
const { mediaItems, addMediaItem } = useMediaStore();
const { activeProject } = useProjectStore();
const {
currentTime,
duration,
@ -377,6 +379,11 @@ export function Timeline() {
}
} else if (e.dataTransfer.files?.length > 0) {
// Handle file drops by creating new tracks
if (!activeProject) {
toast.error("No active project");
return;
}
setIsProcessing(true);
setProgress(0);
try {
@ -385,7 +392,7 @@ export function Timeline() {
(p) => setProgress(p)
);
for (const processedItem of processedItems) {
await addMediaItem(processedItem);
await addMediaItem(activeProject.id, processedItem);
const currentMediaItems = useMediaStore.getState().mediaItems;
const addedItem = currentMediaItems.find(
(item) =>

View File

@ -36,7 +36,6 @@ export function StorageProvider({ children }: StorageProviderProps) {
});
const loadAllProjects = useProjectStore((state) => state.loadAllProjects);
const loadAllMedia = useMediaStore((state) => state.loadAllMedia);
useEffect(() => {
const initializeStorage = async () => {
@ -52,8 +51,8 @@ export function StorageProvider({ children }: StorageProviderProps) {
);
}
// Load saved data in parallel
await Promise.all([loadAllProjects(), loadAllMedia()]);
// Load saved projects (media will be loaded when a project is loaded)
await loadAllProjects();
setStatus({
isInitialized: true,
@ -73,7 +72,7 @@ export function StorageProvider({ children }: StorageProviderProps) {
};
initializeStorage();
}, [loadAllProjects, loadAllMedia]);
}, [loadAllProjects]);
return (
<StorageContext.Provider value={status}>{children}</StorageContext.Provider>