diff --git a/apps/web/src/components/editor/media-panel/views/settings.tsx b/apps/web/src/components/editor/media-panel/views/settings.tsx index 3bce1753..b6b79b59 100644 --- a/apps/web/src/components/editor/media-panel/views/settings.tsx +++ b/apps/web/src/components/editor/media-panel/views/settings.tsx @@ -57,14 +57,18 @@ function ProjectSettingsTabs() { } function ProjectInfoView() { - const { activeProject, updateProjectFps } = useProjectStore(); - const { canvasPresets, setCanvasSize } = useEditorStore(); + const { activeProject, updateProjectFps, updateCanvasSize } = + useProjectStore(); + const { canvasPresets } = useEditorStore(); const { getDisplayName } = useAspectRatio(); const handleAspectRatioChange = (value: string) => { const preset = canvasPresets.find((p) => p.name === value); if (preset) { - setCanvasSize({ width: preset.width, height: preset.height }); + updateCanvasSize( + { width: preset.width, height: preset.height }, + "preset" + ); } }; diff --git a/apps/web/src/lib/editor-utils.ts b/apps/web/src/lib/editor-utils.ts new file mode 100644 index 00000000..f041aa5e --- /dev/null +++ b/apps/web/src/lib/editor-utils.ts @@ -0,0 +1,46 @@ +import { CanvasSize } from "@/types/editor"; + +const DEFAULT_CANVAS_PRESETS = [ + { name: "16:9", width: 1920, height: 1080 }, + { name: "9:16", width: 1080, height: 1920 }, + { name: "1:1", width: 1080, height: 1080 }, + { name: "4:3", width: 1440, height: 1080 }, +]; + +/** + * Helper function to find the best matching canvas preset for an aspect ratio + * @param aspectRatio The target aspect ratio to match + * @returns The best matching canvas size + */ +export function findBestCanvasPreset(aspectRatio: number): CanvasSize { + // Calculate aspect ratio for each preset and find the closest match + let bestMatch = DEFAULT_CANVAS_PRESETS[0]; // Default to 16:9 HD + let smallestDifference = Math.abs( + aspectRatio - bestMatch.width / bestMatch.height + ); + + for (const preset of DEFAULT_CANVAS_PRESETS) { + const presetAspectRatio = preset.width / preset.height; + const difference = Math.abs(aspectRatio - presetAspectRatio); + + if (difference < smallestDifference) { + smallestDifference = difference; + bestMatch = preset; + } + } + + // If the difference is still significant (> 0.1), create a custom size + // based on the media aspect ratio with a reasonable resolution + const bestAspectRatio = bestMatch.width / bestMatch.height; + if (Math.abs(aspectRatio - bestAspectRatio) > 0.1) { + // Create custom dimensions based on the aspect ratio + if (aspectRatio > 1) { + // Landscape - use 1920 width + return { width: 1920, height: Math.round(1920 / aspectRatio) }; + } + // Portrait or square - use 1080 height + return { width: Math.round(1080 * aspectRatio), height: 1080 }; + } + + return { width: bestMatch.width, height: bestMatch.height }; +} diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts index a9c8f2ca..9923aa87 100644 --- a/apps/web/src/stores/editor-store.ts +++ b/apps/web/src/stores/editor-store.ts @@ -1,7 +1,7 @@ import { create } from "zustand"; -import { CanvasSize, CanvasPreset } from "@/types/editor"; +import { persist } from "zustand/middleware"; +import { CanvasPreset } from "@/types/editor"; -type CanvasMode = "preset" | "original" | "custom"; export type PlatformLayout = "tiktok"; export const PLATFORM_LAYOUTS: Record = { @@ -17,9 +17,7 @@ interface EditorState { isInitializing: boolean; isPanelsReady: boolean; - // Canvas/Project settings - canvasSize: CanvasSize; - canvasMode: CanvasMode; + // Editor UI settings canvasPresets: CanvasPreset[]; layoutGuide: LayoutGuideSettings; @@ -27,9 +25,6 @@ interface EditorState { setInitializing: (loading: boolean) => void; setPanelsReady: (ready: boolean) => void; initializeApp: () => Promise; - setCanvasSize: (size: CanvasSize) => void; - setCanvasSizeToOriginal: (aspectRatio: number) => void; - setCanvasSizeFromAspectRatio: (aspectRatio: number) => void; setLayoutGuide: (settings: Partial) => void; toggleLayoutGuide: (platform: PlatformLayout) => void; } @@ -41,96 +36,56 @@ const DEFAULT_CANVAS_PRESETS: CanvasPreset[] = [ { name: "4:3", width: 1440, height: 1080 }, ]; -// Helper function to find the best matching canvas preset for an aspect ratio -const findBestCanvasPreset = (aspectRatio: number): CanvasSize => { - // Calculate aspect ratio for each preset and find the closest match - let bestMatch = DEFAULT_CANVAS_PRESETS[0]; // Default to 16:9 HD - let smallestDifference = Math.abs( - aspectRatio - bestMatch.width / bestMatch.height - ); - - for (const preset of DEFAULT_CANVAS_PRESETS) { - const presetAspectRatio = preset.width / preset.height; - const difference = Math.abs(aspectRatio - presetAspectRatio); - - if (difference < smallestDifference) { - smallestDifference = difference; - bestMatch = preset; - } - } - - // If the difference is still significant (> 0.1), create a custom size - // based on the media aspect ratio with a reasonable resolution - const bestAspectRatio = bestMatch.width / bestMatch.height; - if (Math.abs(aspectRatio - bestAspectRatio) > 0.1) { - // Create custom dimensions based on the aspect ratio - if (aspectRatio > 1) { - // Landscape - use 1920 width - return { width: 1920, height: Math.round(1920 / aspectRatio) }; - } - // Portrait or square - use 1080 height - return { width: Math.round(1080 * aspectRatio), height: 1080 }; - } - - return { width: bestMatch.width, height: bestMatch.height }; -}; - -export const useEditorStore = create((set, get) => ({ - // Initial states - isInitializing: true, - isPanelsReady: false, - canvasSize: { width: 1920, height: 1080 }, // Default 16:9 HD - canvasMode: "preset" as CanvasMode, - canvasPresets: DEFAULT_CANVAS_PRESETS, - layoutGuide: { - platform: null, - }, - - // Actions - setInitializing: (loading) => { - set({ isInitializing: loading }); - }, - - setPanelsReady: (ready) => { - set({ isPanelsReady: ready }); - }, - - initializeApp: async () => { - console.log("Initializing video editor..."); - set({ isInitializing: true, isPanelsReady: false }); - - set({ isPanelsReady: true, isInitializing: false }); - console.log("Video editor ready"); - }, - - setCanvasSize: (size) => { - set({ canvasSize: size, canvasMode: "preset" }); - }, - - setCanvasSizeToOriginal: (aspectRatio) => { - const newCanvasSize = findBestCanvasPreset(aspectRatio); - set({ canvasSize: newCanvasSize, canvasMode: "original" }); - }, - - setCanvasSizeFromAspectRatio: (aspectRatio) => { - const newCanvasSize = findBestCanvasPreset(aspectRatio); - set({ canvasSize: newCanvasSize, canvasMode: "custom" }); - }, - - setLayoutGuide: (settings) => { - set((state) => ({ +export const useEditorStore = create()( + persist( + (set) => ({ + // Initial states + isInitializing: true, + isPanelsReady: false, + canvasPresets: DEFAULT_CANVAS_PRESETS, layoutGuide: { - ...state.layoutGuide, - ...settings, + platform: null, }, - })); - }, - toggleLayoutGuide: (platform) => { - set((state) => ({ - layoutGuide: { - platform: state.layoutGuide.platform === platform ? null : platform, + // Actions + setInitializing: (loading) => { + set({ isInitializing: loading }); }, - })); - }, -})); + + setPanelsReady: (ready) => { + set({ isPanelsReady: ready }); + }, + + initializeApp: async () => { + console.log("Initializing video editor..."); + set({ isInitializing: true, isPanelsReady: false }); + + set({ isPanelsReady: true, isInitializing: false }); + console.log("Video editor ready"); + }, + + setLayoutGuide: (settings) => { + set((state) => ({ + layoutGuide: { + ...state.layoutGuide, + ...settings, + }, + })); + }, + + toggleLayoutGuide: (platform) => { + set((state) => ({ + layoutGuide: { + platform: state.layoutGuide.platform === platform ? null : platform, + }, + })); + }, + }), + { + name: "editor-settings", + partialize: (state) => ({ + layoutGuide: state.layoutGuide, + }), + } + ) +); diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 5c617dff..db6d014a 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -5,6 +5,22 @@ import { toast } from "sonner"; import { useMediaStore } from "./media-store"; import { useTimelineStore } from "./timeline-store"; import { generateUUID } from "@/lib/utils"; +import { CanvasSize, CanvasMode } from "@/types/editor"; + +const DEFAULT_PROJECT: TProject = { + id: generateUUID(), + name: "Untitled", + thumbnail: "", + createdAt: new Date(), + updatedAt: new Date(), + backgroundColor: "#000000", + backgroundType: "color", + blurIntensity: 8, + bookmarks: [], + fps: 30, + canvasSize: { width: 1920, height: 1080 }, + canvasMode: "preset", +}; interface ProjectStore { activeProject: TProject | null; @@ -28,6 +44,7 @@ interface ProjectStore { options?: { backgroundColor?: string; blurIntensity?: number } ) => Promise; updateProjectFps: (fps: number) => Promise; + updateCanvasSize: (size: CanvasSize, mode: CanvasMode) => Promise; // Bookmark methods toggleBookmark: (time: number) => Promise; @@ -144,18 +161,7 @@ export const useProjectStore = create((set, get) => ({ }, createNewProject: async (name: string) => { - const newProject: TProject = { - id: generateUUID(), - name, - thumbnail: "", - createdAt: new Date(), - updatedAt: new Date(), - backgroundColor: "#000000", - backgroundType: "color", - blurIntensity: 8, - bookmarks: [], - fps: 30, - }; + const newProject: TProject = { ...DEFAULT_PROJECT, name }; set({ activeProject: newProject }); @@ -428,6 +434,29 @@ export const useProjectStore = create((set, get) => ({ } }, + updateCanvasSize: async (size: CanvasSize, mode: CanvasMode) => { + const { activeProject } = get(); + if (!activeProject) return; + + const updatedProject = { + ...activeProject, + canvasSize: size, + canvasMode: mode, + updatedAt: new Date(), + }; + + try { + await storageService.saveProject(updatedProject); + set({ activeProject: updatedProject }); + await get().loadAllProjects(); // Refresh the list + } catch (error) { + console.error("Failed to update canvas size:", error); + toast.error("Failed to update canvas size", { + description: "Please try again", + }); + } + }, + getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => { const { savedProjects } = get(); diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index 71ddedca..ed2ed612 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -10,17 +10,16 @@ import { ensureMainTrack, validateElementTrackCompatibility, } from "@/types/timeline"; -import { useEditorStore } from "./editor-store"; import { useMediaStore, getMediaAspectRatio, type MediaItem, } from "./media-store"; +import { findBestCanvasPreset } from "@/lib/editor-utils"; import { storageService } from "@/lib/storage/storage-service"; import { useProjectStore } from "./project-store"; import { generateUUID } from "@/lib/utils"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { toast } from "sonner"; import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline"; // Helper function to manage element naming with suffixes @@ -533,9 +532,10 @@ export const useTimelineStore = create((set, get) => { mediaItem && (mediaItem.type === "image" || mediaItem.type === "video") ) { - const editorStore = useEditorStore.getState(); - editorStore.setCanvasSizeFromAspectRatio( - getMediaAspectRatio(mediaItem) + const projectStore = useProjectStore.getState(); + projectStore.updateCanvasSize( + findBestCanvasPreset(getMediaAspectRatio(mediaItem)), + "original" ); } diff --git a/apps/web/src/types/editor.ts b/apps/web/src/types/editor.ts index 12b474fd..6cbc6a6a 100644 --- a/apps/web/src/types/editor.ts +++ b/apps/web/src/types/editor.ts @@ -1,5 +1,7 @@ export type BackgroundType = "blur" | "mirror" | "color"; +export type CanvasMode = "preset" | "original" | "custom"; + export interface CanvasSize { width: number; height: number; diff --git a/apps/web/src/types/project.ts b/apps/web/src/types/project.ts index f52cba73..bf510942 100644 --- a/apps/web/src/types/project.ts +++ b/apps/web/src/types/project.ts @@ -1,3 +1,5 @@ +import { CanvasSize } from "./editor"; + export interface TProject { id: string; name: string; @@ -10,4 +12,12 @@ export interface TProject { blurIntensity?: number; // in pixels (4, 8, 18) fps?: number; bookmarks?: number[]; + canvasSize: CanvasSize; + /** + * Tracks how the canvas size was set: + * - "preset": User selected a standard preset (e.g. 16:9, 9:16) + * - "original": Using the first media item's dimensions + * - "custom": User set a custom aspect ratio or dimensions + */ + canvasMode: "preset" | "original" | "custom"; }