diff options
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/media-store.ts | 6 | ||||
| -rw-r--r-- | apps/web/src/stores/project-store.ts | 87 | ||||
| -rw-r--r-- | apps/web/src/stores/timeline-store.ts | 167 |
4 files changed, 261 insertions, 17 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/media-store.ts b/apps/web/src/stores/media-store.ts index cb69a7e9..7d4851c2 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; import { storageService } from "@/lib/storage/storage-service"; -import { useProjectStore } from "./project-store"; +import { useTimelineStore } from "./timeline-store"; +import { generateUUID } from "@/lib/utils"; export type MediaType = "image" | "video" | "audio"; @@ -14,6 +15,7 @@ export interface MediaItem { duration?: number; // For video/audio duration width?: number; // For video/image width height?: number; // For video/image height + fps?: number; // For video frame rate // Text-specific properties content?: string; // Text content fontSize?: number; // Font size @@ -161,7 +163,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ addMediaItem: async (projectId, item) => { const newItem: MediaItem = { ...item, - id: crypto.randomUUID(), + id: generateUUID(), }; // Add to local state immediately for UI responsiveness diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 9570f4a0..efb9cc71 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -4,6 +4,7 @@ import { storageService } from "@/lib/storage/storage-service"; import { toast } from "sonner";
import { useMediaStore } from "./media-store";
import { useTimelineStore } from "./timeline-store";
+import { generateUUID } from "@/lib/utils";
interface ProjectStore {
activeProject: TProject | null;
@@ -20,6 +21,12 @@ interface ProjectStore { closeProject: () => void;
renameProject: (projectId: string, name: string) => Promise<void>;
duplicateProject: (projectId: string) => Promise<string>;
+ updateProjectBackground: (backgroundColor: string) => Promise<void>;
+ updateBackgroundType: (
+ type: "color" | "blur",
+ options?: { backgroundColor?: string; blurIntensity?: number }
+ ) => Promise<void>;
+ updateProjectFps: (fps: number) => Promise<void>;
}
export const useProjectStore = create<ProjectStore>((set, get) => ({
@@ -30,11 +37,14 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ createNewProject: async (name: string) => {
const newProject: TProject = {
- id: crypto.randomUUID(),
+ id: generateUUID(),
name,
thumbnail: "",
createdAt: new Date(),
updatedAt: new Date(),
+ backgroundColor: "#000000",
+ backgroundType: "color",
+ blurIntensity: 8,
};
set({ activeProject: newProject });
@@ -215,7 +225,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1;
const newProject: TProject = {
- id: crypto.randomUUID(),
+ id: generateUUID(),
name: `(${nextNumber}) ${baseName}`,
thumbnail: project.thumbnail,
createdAt: new Date(),
@@ -234,4 +244,77 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ throw error;
}
},
+
+ updateProjectBackground: async (backgroundColor: string) => {
+ const { activeProject } = get();
+ if (!activeProject) return;
+
+ const updatedProject = {
+ ...activeProject,
+ backgroundColor,
+ updatedAt: new Date(),
+ };
+
+ try {
+ await storageService.saveProject(updatedProject);
+ set({ activeProject: updatedProject });
+ await get().loadAllProjects(); // Refresh the list
+ } catch (error) {
+ console.error("Failed to update project background:", error);
+ toast.error("Failed to update background", {
+ description: "Please try again",
+ });
+ }
+ },
+
+ updateBackgroundType: async (
+ type: "color" | "blur",
+ options?: { backgroundColor?: string; blurIntensity?: number }
+ ) => {
+ const { activeProject } = get();
+ if (!activeProject) return;
+
+ const updatedProject = {
+ ...activeProject,
+ backgroundType: type,
+ ...(options?.backgroundColor && {
+ backgroundColor: options.backgroundColor,
+ }),
+ ...(options?.blurIntensity && { blurIntensity: options.blurIntensity }),
+ updatedAt: new Date(),
+ };
+
+ try {
+ await storageService.saveProject(updatedProject);
+ set({ activeProject: updatedProject });
+ await get().loadAllProjects(); // Refresh the list
+ } catch (error) {
+ console.error("Failed to update background type:", error);
+ toast.error("Failed to update background", {
+ description: "Please try again",
+ });
+ }
+ },
+
+ updateProjectFps: async (fps: number) => {
+ const { activeProject } = get();
+ if (!activeProject) return;
+
+ const updatedProject = {
+ ...activeProject,
+ fps,
+ updatedAt: new Date(),
+ };
+
+ try {
+ await storageService.saveProject(updatedProject);
+ set({ activeProject: updatedProject });
+ await get().loadAllProjects(); // Refresh the list
+ } catch (error) {
+ console.error("Failed to update project FPS:", error);
+ toast.error("Failed to update project FPS", {
+ description: "Please try again",
+ });
+ }
+ },
}));
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index 6de24c84..37b45b4a 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -4,6 +4,7 @@ import { TimelineElement, CreateTimelineElement, TimelineTrack, + TextElement, sortTracksByOrder, ensureMainTrack, validateElementTrackCompatibility, @@ -12,6 +13,7 @@ import { useEditorStore } from "./editor-store"; import { useMediaStore, getMediaAspectRatio } from "./media-store"; import { storageService } from "@/lib/storage/storage-service"; import { useProjectStore } from "./project-store"; +import { generateUUID } from "@/lib/utils"; // Helper function to manage element naming with suffixes const getElementNameWithSuffix = ( @@ -117,6 +119,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; @@ -129,6 +138,28 @@ interface TimelineStore { loadProjectTimeline: (projectId: string) => Promise<void>; saveProjectTimeline: (projectId: string) => Promise<void>; clearTimeline: () => void; + updateTextElement: ( + trackId: string, + elementId: string, + updates: Partial< + Pick< + TextElement, + | "content" + | "fontSize" + | "fontFamily" + | "color" + | "backgroundColor" + | "textAlign" + | "fontWeight" + | "fontStyle" + | "textDecoration" + | "x" + | "y" + | "rotation" + | "opacity" + > + > + ) => void; } export const useTimelineStore = create<TimelineStore>((set, get) => { @@ -249,7 +280,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { : "Track"; const newTrack: TimelineTrack = { - id: crypto.randomUUID(), + id: generateUUID(), name: trackName, type, elements: [], @@ -274,7 +305,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { : "Track"; const newTrack: TimelineTrack = { - id: crypto.randomUUID(), + id: generateUUID(), name: trackName, type, elements: [], @@ -333,14 +364,14 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { const newElement: TimelineElement = { ...elementData, - id: crypto.randomUUID(), + id: generateUUID(), startTime: elementData.startTime || 0, trimStart: 0, trimEnd: 0, } as TimelineElement; // Type assertion since we trust the caller passes valid data // If this is the first element and it's a media element, automatically set the project canvas size - // to match the media's aspect ratio + // to match the media's aspect ratio and FPS (for videos) if (isFirstElement && newElement.type === "media") { const mediaStore = useMediaStore.getState(); const mediaItem = mediaStore.mediaItems.find( @@ -356,6 +387,14 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { getMediaAspectRatio(mediaItem) ); } + + // Set project FPS from the first video element + if (mediaItem && mediaItem.type === "video" && mediaItem.fps) { + const projectStore = useProjectStore.getState(); + if (projectStore.activeProject) { + projectStore.updateProjectFps(mediaItem.fps); + } + } } updateTracksAndSave( @@ -487,6 +526,24 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { ); }, + updateTextElement: (trackId, elementId, updates) => { + get().pushHistory(); + updateTracksAndSave( + get()._tracks.map((track) => + track.id === trackId + ? { + ...track, + elements: track.elements.map((element) => + element.id === elementId && element.type === "text" + ? { ...element, ...updates } + : element + ), + } + : track + ) + ); + }, + splitElement: (trackId, elementId, splitTime) => { const { _tracks } = get(); const track = _tracks.find((t) => t.id === trackId); @@ -508,7 +565,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { const secondDuration = element.duration - element.trimStart - element.trimEnd - relativeTime; - const secondElementId = crypto.randomUUID(); + const secondElementId = generateUUID(); updateTracksAndSave( get()._tracks.map((track) => @@ -634,7 +691,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { // Find existing audio track or prepare to create one const existingAudioTrack = _tracks.find((t) => t.type === "audio"); - const audioElementId = crypto.randomUUID(); + const audioElementId = generateUUID(); if (existingAudioTrack) { // Add audio element to existing audio track @@ -658,7 +715,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { } else { // Create new audio track with the audio element in a single atomic update const newAudioTrack: TimelineTrack = { - id: crypto.randomUUID(), + id: generateUUID(), name: "Audio Track", type: "audio", elements: [ @@ -677,6 +734,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; |
