diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-07-09 21:03:56 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-07-09 21:03:56 +0200 |
| commit | c02f276303e00f4d8c66d8d1c804c4af9c719afb (patch) | |
| tree | aac3a38fd5f673cc30a9bbe1aef9eb197b430c9e /apps/web/src/stores | |
| parent | dd35c91f395eacf37f96dff8c9cec2340c9bb2b1 (diff) | |
| download | opencut-c02f276303e00f4d8c66d8d1c804c4af9c719afb.tar.gz | |
refactor: fresh properties panel
Diffstat (limited to 'apps/web/src/stores')
| -rw-r--r-- | apps/web/src/stores/editor-store.ts | 18 | ||||
| -rw-r--r-- | apps/web/src/stores/timeline-store.ts | 103 |
2 files changed, 115 insertions, 6 deletions
diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts index 5382c3e2..c24929a2 100644 --- a/apps/web/src/stores/editor-store.ts +++ b/apps/web/src/stores/editor-store.ts @@ -1,6 +1,8 @@ import { create } from "zustand";
import { CanvasSize, CanvasPreset } from "@/types/editor";
+type CanvasMode = "preset" | "original" | "custom";
+
interface EditorState {
// Loading states
isInitializing: boolean;
@@ -8,6 +10,7 @@ interface EditorState { // Canvas/Project settings
canvasSize: CanvasSize;
+ canvasMode: CanvasMode;
canvasPresets: CanvasPreset[];
// Actions
@@ -15,6 +18,7 @@ interface EditorState { setPanelsReady: (ready: boolean) => void;
initializeApp: () => Promise<void>;
setCanvasSize: (size: CanvasSize) => void;
+ setCanvasSizeToOriginal: (aspectRatio: number) => void;
setCanvasSizeFromAspectRatio: (aspectRatio: number) => void;
}
@@ -65,6 +69,7 @@ export const useEditorStore = create<EditorState>((set, get) => ({ isInitializing: true,
isPanelsReady: false,
canvasSize: { width: 1920, height: 1080 }, // Default 16:9 HD
+ canvasMode: "preset" as CanvasMode,
canvasPresets: DEFAULT_CANVAS_PRESETS,
// Actions
@@ -85,15 +90,16 @@ export const useEditorStore = create<EditorState>((set, get) => ({ },
setCanvasSize: (size) => {
- set({ canvasSize: size });
+ set({ canvasSize: size, canvasMode: "preset" });
+ },
+
+ setCanvasSizeToOriginal: (aspectRatio) => {
+ const newCanvasSize = findBestCanvasPreset(aspectRatio);
+ set({ canvasSize: newCanvasSize, canvasMode: "original" });
},
setCanvasSizeFromAspectRatio: (aspectRatio) => {
const newCanvasSize = findBestCanvasPreset(aspectRatio);
- console.log(
- `Setting canvas size based on aspect ratio ${aspectRatio}:`,
- newCanvasSize
- );
- set({ canvasSize: newCanvasSize });
+ set({ canvasSize: newCanvasSize, canvasMode: "custom" });
},
}));
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index 6de24c84..7431e085 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -117,6 +117,13 @@ interface TimelineStore { ) => void; separateAudio: (trackId: string, elementId: string) => string | null; + // Replace media for an element + replaceElementMedia: ( + trackId: string, + elementId: string, + newFile: File + ) => Promise<boolean>; + // Computed values getTotalDuration: () => number; @@ -677,6 +684,102 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { return audioElementId; }, + // Replace media for an element + replaceElementMedia: async (trackId, elementId, newFile) => { + const { _tracks } = get(); + const track = _tracks.find((t) => t.id === trackId); + const element = track?.elements.find((c) => c.id === elementId); + + if (!element || element.type !== "media") return false; + + try { + const mediaStore = useMediaStore.getState(); + const projectStore = useProjectStore.getState(); + + if (!projectStore.activeProject) return false; + + // Import required media processing functions + const { + getFileType, + getImageDimensions, + generateVideoThumbnail, + getMediaDuration, + } = await import("./media-store"); + + const fileType = getFileType(newFile); + if (!fileType) return false; + + // Process the new media file + let mediaData: any = { + name: newFile.name, + type: fileType, + file: newFile, + url: URL.createObjectURL(newFile), + }; + + // Get media-specific metadata + if (fileType === "image") { + const { width, height } = await getImageDimensions(newFile); + mediaData.width = width; + mediaData.height = height; + } else if (fileType === "video") { + const [duration, { thumbnailUrl, width, height }] = await Promise.all( + [getMediaDuration(newFile), generateVideoThumbnail(newFile)] + ); + mediaData.duration = duration; + mediaData.thumbnailUrl = thumbnailUrl; + mediaData.width = width; + mediaData.height = height; + } else if (fileType === "audio") { + mediaData.duration = await getMediaDuration(newFile); + } + + // Add new media item to store + await mediaStore.addMediaItem(projectStore.activeProject.id, mediaData); + + // Find the newly created media item + const newMediaItem = mediaStore.mediaItems.find( + (item) => item.file === newFile + ); + + if (!newMediaItem) return false; + + get().pushHistory(); + + // Update the timeline element to reference the new media + updateTracksAndSave( + _tracks.map((track) => + track.id === trackId + ? { + ...track, + elements: track.elements.map((c) => + c.id === elementId + ? { + ...c, + mediaId: newMediaItem.id, + name: newMediaItem.name, + // Update duration if the new media has a different duration + duration: newMediaItem.duration || c.duration, + } + : c + ), + } + : track + ) + ); + + return true; + } catch (error) { + console.log( + JSON.stringify({ + error: "Failed to replace element media", + details: error, + }) + ); + return false; + } + }, + getTotalDuration: () => { const { _tracks } = get(); if (_tracks.length === 0) return 0; |
