summaryrefslogtreecommitdiff
path: root/apps/web/src/stores
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/stores')
-rw-r--r--apps/web/src/stores/editor-store.ts18
-rw-r--r--apps/web/src/stores/media-store.ts6
-rw-r--r--apps/web/src/stores/project-store.ts87
-rw-r--r--apps/web/src/stores/timeline-store.ts167
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;