diff options
Diffstat (limited to 'apps/web/src/stores')
| -rw-r--r-- | apps/web/src/stores/editor-store.ts | 137 | ||||
| -rw-r--r-- | apps/web/src/stores/panel-store.ts | 212 | ||||
| -rw-r--r-- | apps/web/src/stores/playback-store.ts | 18 | ||||
| -rw-r--r-- | apps/web/src/stores/project-store.ts | 55 | ||||
| -rw-r--r-- | apps/web/src/stores/timeline-store.ts | 113 |
5 files changed, 351 insertions, 184 deletions
diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts index aac7077e..9923aa87 100644 --- a/apps/web/src/stores/editor-store.ts +++ b/apps/web/src/stores/editor-store.ts @@ -1,25 +1,32 @@ 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<PlatformLayout, string> = { + tiktok: "TikTok", +}; + +interface LayoutGuideSettings { + platform: PlatformLayout | null; +} interface EditorState { // Loading states isInitializing: boolean; isPanelsReady: boolean; - // Canvas/Project settings - canvasSize: CanvasSize; - canvasMode: CanvasMode; + // Editor UI settings canvasPresets: CanvasPreset[]; + layoutGuide: LayoutGuideSettings; // Actions setInitializing: (loading: boolean) => void; setPanelsReady: (ready: boolean) => void; initializeApp: () => Promise<void>; - setCanvasSize: (size: CanvasSize) => void; - setCanvasSizeToOriginal: (aspectRatio: number) => void; - setCanvasSizeFromAspectRatio: (aspectRatio: number) => void; + setLayoutGuide: (settings: Partial<LayoutGuideSettings>) => void; + toggleLayoutGuide: (platform: PlatformLayout) => void; } const DEFAULT_CANVAS_PRESETS: CanvasPreset[] = [ @@ -29,76 +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 - ); +export const useEditorStore = create<EditorState>()( + persist( + (set) => ({ + // Initial states + isInitializing: true, + isPanelsReady: false, + canvasPresets: DEFAULT_CANVAS_PRESETS, + layoutGuide: { + platform: null, + }, - for (const preset of DEFAULT_CANVAS_PRESETS) { - const presetAspectRatio = preset.width / preset.height; - const difference = Math.abs(aspectRatio - presetAspectRatio); + // Actions + setInitializing: (loading) => { + set({ isInitializing: loading }); + }, - if (difference < smallestDifference) { - smallestDifference = difference; - bestMatch = preset; - } - } + setPanelsReady: (ready) => { + set({ isPanelsReady: ready }); + }, - // 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 }; - } + initializeApp: async () => { + console.log("Initializing video editor..."); + set({ isInitializing: true, isPanelsReady: false }); - return { width: bestMatch.width, height: bestMatch.height }; -}; + set({ isPanelsReady: true, isInitializing: false }); + console.log("Video editor ready"); + }, -export const useEditorStore = create<EditorState>((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, + setLayoutGuide: (settings) => { + set((state) => ({ + layoutGuide: { + ...state.layoutGuide, + ...settings, + }, + })); + }, - // 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" }); - }, -})); + 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/panel-store.ts b/apps/web/src/stores/panel-store.ts index ebbce3c0..ae09c13b 100644 --- a/apps/web/src/stores/panel-store.ts +++ b/apps/web/src/stores/panel-store.ts @@ -1,48 +1,222 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -const DEFAULT_PANEL_SIZES = { - toolsPanel: 45, - previewPanel: 75, - propertiesPanel: 20, - mainContent: 70, - timeline: 30, -} as const; +export type PanelPreset = + | "default" + | "media" + | "inspector" + | "vertical-preview"; -interface PanelState { - // Panel sizes as percentages +interface PanelSizes { toolsPanel: number; previewPanel: number; propertiesPanel: number; mainContent: number; timeline: number; +} + +export const PRESET_CONFIGS: Record<PanelPreset, PanelSizes> = { + default: { + toolsPanel: 25, + previewPanel: 50, + propertiesPanel: 25, + mainContent: 70, + timeline: 30, + }, + media: { + toolsPanel: 30, + previewPanel: 45, + propertiesPanel: 25, + mainContent: 100, + timeline: 25, + }, + inspector: { + toolsPanel: 30, + previewPanel: 70, + propertiesPanel: 30, + mainContent: 75, + timeline: 25, + }, + "vertical-preview": { + toolsPanel: 30, + previewPanel: 40, + propertiesPanel: 30, + mainContent: 75, + timeline: 25, + }, +}; + +interface PanelState extends PanelSizes { + activePreset: PanelPreset; + presetCustomSizes: Record<PanelPreset, Partial<PanelSizes>>; + resetCounter: number; mediaViewMode: "grid" | "list"; - // Actions setToolsPanel: (size: number) => void; setPreviewPanel: (size: number) => void; setPropertiesPanel: (size: number) => void; setMainContent: (size: number) => void; setTimeline: (size: number) => void; setMediaViewMode: (mode: "grid" | "list") => void; + + setActivePreset: (preset: PanelPreset) => void; + resetPreset: (preset: PanelPreset) => void; + getCurrentPresetSizes: () => PanelSizes; } export const usePanelStore = create<PanelState>()( persist( - (set) => ({ - // Default sizes - optimized for responsiveness - ...DEFAULT_PANEL_SIZES, + (set, get) => ({ + ...PRESET_CONFIGS.default, + activePreset: "default" as PanelPreset, + presetCustomSizes: { + default: {}, + media: {}, + inspector: {}, + "vertical-preview": {}, + }, + resetCounter: 0, mediaViewMode: "grid" as const, - // Actions - setToolsPanel: (size) => set({ toolsPanel: size }), - setPreviewPanel: (size) => set({ previewPanel: size }), - setPropertiesPanel: (size) => set({ propertiesPanel: size }), - setMainContent: (size) => set({ mainContent: size }), - setTimeline: (size) => set({ timeline: size }), + setToolsPanel: (size) => { + const { activePreset, presetCustomSizes } = get(); + set({ + toolsPanel: size, + presetCustomSizes: { + ...presetCustomSizes, + [activePreset]: { + ...presetCustomSizes[activePreset], + toolsPanel: size, + }, + }, + }); + }, + setPreviewPanel: (size) => { + const { activePreset, presetCustomSizes } = get(); + set({ + previewPanel: size, + presetCustomSizes: { + ...presetCustomSizes, + [activePreset]: { + ...presetCustomSizes[activePreset], + previewPanel: size, + }, + }, + }); + }, + setPropertiesPanel: (size) => { + const { activePreset, presetCustomSizes } = get(); + set({ + propertiesPanel: size, + presetCustomSizes: { + ...presetCustomSizes, + [activePreset]: { + ...presetCustomSizes[activePreset], + propertiesPanel: size, + }, + }, + }); + }, + setMainContent: (size) => { + const { activePreset, presetCustomSizes } = get(); + set({ + mainContent: size, + presetCustomSizes: { + ...presetCustomSizes, + [activePreset]: { + ...presetCustomSizes[activePreset], + mainContent: size, + }, + }, + }); + }, + setTimeline: (size) => { + const { activePreset, presetCustomSizes } = get(); + set({ + timeline: size, + presetCustomSizes: { + ...presetCustomSizes, + [activePreset]: { + ...presetCustomSizes[activePreset], + timeline: size, + }, + }, + }); + }, setMediaViewMode: (mode) => set({ mediaViewMode: mode }), + + setActivePreset: (preset) => { + const { + activePreset: currentPreset, + presetCustomSizes, + toolsPanel, + previewPanel, + propertiesPanel, + mainContent, + timeline, + } = get(); + + const updatedPresetCustomSizes = { + ...presetCustomSizes, + [currentPreset]: { + toolsPanel, + previewPanel, + propertiesPanel, + mainContent, + timeline, + }, + }; + + const defaultSizes = PRESET_CONFIGS[preset]; + const customSizes = updatedPresetCustomSizes[preset] || {}; + const finalSizes = { ...defaultSizes, ...customSizes } as PanelSizes; + + set({ + activePreset: preset, + presetCustomSizes: updatedPresetCustomSizes, + ...finalSizes, + }); + }, + + resetPreset: (preset) => { + const { presetCustomSizes, activePreset, resetCounter } = get(); + const defaultSizes = PRESET_CONFIGS[preset]; + + const newPresetCustomSizes = { + ...presetCustomSizes, + [preset]: {}, + }; + + const updates: Partial<PanelState> = { + presetCustomSizes: newPresetCustomSizes, + resetCounter: resetCounter + 1, + }; + + if (preset === activePreset) { + Object.assign(updates, defaultSizes); + } + + set(updates); + }, + + getCurrentPresetSizes: () => { + const { + toolsPanel, + previewPanel, + propertiesPanel, + mainContent, + timeline, + } = get(); + return { + toolsPanel, + previewPanel, + propertiesPanel, + mainContent, + timeline, + }; + }, }), { name: "panel-sizes", diff --git a/apps/web/src/stores/playback-store.ts b/apps/web/src/stores/playback-store.ts index feea0f46..a92cb530 100644 --- a/apps/web/src/stores/playback-store.ts +++ b/apps/web/src/stores/playback-store.ts @@ -82,6 +82,24 @@ export const usePlaybackStore = create<PlaybackStore>((set, get) => ({ speed: 1.0, play: () => { + const state = get(); + + const actualContentDuration = useTimelineStore + .getState() + .getTotalDuration(); + const effectiveDuration = + actualContentDuration > 0 ? actualContentDuration : state.duration; + + if (effectiveDuration > 0) { + const fps = useProjectStore.getState().activeProject?.fps ?? 30; + const frameOffset = 1 / fps; + const endThreshold = Math.max(0, effectiveDuration - frameOffset); + + if (state.currentTime >= endThreshold) { + get().seek(0); + } + } + set({ isPlaying: true }); startTimer(get); }, diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 5c617dff..259533bf 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -5,6 +5,24 @@ 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"; + +export const DEFAULT_CANVAS_SIZE: CanvasSize = { width: 1920, height: 1080 }; + +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: DEFAULT_CANVAS_SIZE, + canvasMode: "preset", +}; interface ProjectStore { activeProject: TProject | null; @@ -28,6 +46,7 @@ interface ProjectStore { options?: { backgroundColor?: string; blurIntensity?: number } ) => Promise<void>; updateProjectFps: (fps: number) => Promise<void>; + updateCanvasSize: (size: CanvasSize, mode: CanvasMode) => Promise<void>; // Bookmark methods toggleBookmark: (time: number) => Promise<void>; @@ -144,18 +163,7 @@ export const useProjectStore = create<ProjectStore>((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 +436,29 @@ export const useProjectStore = create<ProjectStore>((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..3eada6de 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 @@ -30,10 +29,10 @@ const getElementNameWithSuffix = ( ): string => { // Remove existing suffixes to prevent accumulation const baseName = originalName - .replace(/ \(left\)$/, "") - .replace(/ \(right\)$/, "") - .replace(/ \(audio\)$/, "") - .replace(/ \(split \d+\)$/, ""); + .replace(/ \(left\)$/i, "") + .replace(/ \(right\)$/i, "") + .replace(/ \(audio\)$/i, "") + .replace(/ \(split \d+\)$/i, ""); return `${baseName} (${suffix})`; }; @@ -127,6 +126,7 @@ interface TimelineStore { ) => void; toggleTrackMute: (trackId: string) => void; toggleElementHidden: (trackId: string, elementId: string) => void; + toggleElementMuted: (trackId: string, elementId: string) => void; // Split operations for elements splitElement: ( @@ -323,7 +323,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { addTrack: (type) => { get().pushHistory(); - // Generate proper track name based on type const trackName = type === "media" ? "Media Track" @@ -348,7 +347,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { insertTrackAt: (type, index) => { get().pushHistory(); - // Generate proper track name based on type const trackName = type === "media" ? "Media Track" @@ -393,13 +391,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { get().pushHistory(); - // If track has no elements, just remove it normally - if (trackToRemove.elements.length === 0) { - updateTracksAndSave(_tracks.filter((track) => track.id !== trackId)); - return; - } - - // Find all the time ranges occupied by elements in the track being removed const occupiedRanges = trackToRemove.elements.map((element) => ({ startTime: element.startTime, endTime: @@ -407,10 +398,8 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { (element.duration - element.trimStart - element.trimEnd), })); - // Sort ranges by start time occupiedRanges.sort((a, b) => a.startTime - b.startTime); - // Merge overlapping ranges to get consolidated gaps const mergedRanges: Array<{ startTime: number; endTime: number; @@ -427,11 +416,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { } else { const lastRange = mergedRanges[mergedRanges.length - 1]; if (range.startTime <= lastRange.endTime) { - // Overlapping or adjacent ranges, merge them lastRange.endTime = Math.max(lastRange.endTime, range.endTime); lastRange.duration = lastRange.endTime - lastRange.startTime; } else { - // Non-overlapping range, add as new mergedRanges.push({ startTime: range.startTime, endTime: range.endTime, @@ -441,17 +428,14 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { } } - // Remove the track and apply ripple effects to remaining tracks const updatedTracks = _tracks .filter((track) => track.id !== trackId) .map((track) => { const updatedElements = track.elements.map((element) => { let newStartTime = element.startTime; - // Process gaps from right to left (latest to earliest) to avoid cumulative shifts for (let i = mergedRanges.length - 1; i >= 0; i--) { const gap = mergedRanges[i]; - // If this element starts after the gap, shift it left by the gap duration if (newStartTime >= gap.endTime) { newStartTime -= gap.duration; } @@ -463,7 +447,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }; }); - // Check for overlaps and resolve them if necessary const hasOverlaps = checkElementOverlaps(updatedElements); if (hasOverlaps) { const resolvedElements = resolveElementOverlaps(updatedElements); @@ -479,33 +462,28 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { addElementToTrack: (trackId, elementData) => { get().pushHistory(); - // Validate element type matches track type const track = get()._tracks.find((t) => t.id === trackId); if (!track) { console.error("Track not found:", trackId); return; } - // Use utility function for validation const validation = validateElementTrackCompatibility(elementData, track); if (!validation.isValid) { console.error(validation.errorMessage); return; } - // For media elements, validate mediaId exists if (elementData.type === "media" && !elementData.mediaId) { console.error("Media element must have mediaId"); return; } - // For text elements, validate required text properties if (elementData.type === "text" && !elementData.content) { console.error("Text element must have content"); return; } - // Check if this is the first element being added to the timeline const currentState = get(); const totalElementsInTimeline = currentState._tracks.reduce( (total, track) => total + track.elements.length, @@ -519,10 +497,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { startTime: elementData.startTime || 0, trimStart: 0, trimEnd: 0, - } as TimelineElement; // Type assertion since we trust the caller passes valid data + ...(elementData.type === "media" ? { muted: false } : {}), + } as TimelineElement; - // 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 and FPS (for videos) if (isFirstElement && newElement.type === "media") { const mediaStore = useMediaStore.getState(); const mediaItem = mediaStore.mediaItems.find( @@ -533,13 +510,13 @@ export const useTimelineStore = create<TimelineStore>((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" ); } - // Set project FPS from the first video element if (mediaItem && mediaItem.type === "video" && mediaItem.fps) { const projectStore = useProjectStore.getState(); if (projectStore.activeProject) { @@ -591,7 +568,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { const { _tracks, rippleEditingEnabled } = get(); if (!rippleEditingEnabled) { - // If ripple editing is disabled, use regular removal get().removeElementFromTrack(trackId, elementId, pushHistory); return; } @@ -608,15 +584,12 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { element.duration - element.trimStart - element.trimEnd; const elementEndTime = elementStartTime + elementDuration; - // Remove the element and shift all elements that come after it const updatedTracks = _tracks .map((currentTrack) => { - // Only apply ripple effects to the same track unless multi-track ripple is enabled const shouldApplyRipple = currentTrack.id === trackId; const updatedElements = currentTrack.elements .filter((currentElement) => { - // Remove the target element if ( currentElement.id === elementId && currentTrack.id === trackId @@ -626,12 +599,10 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { return true; }) .map((currentElement) => { - // Only apply ripple effects if we should process this track if (!shouldApplyRipple) { return currentElement; } - // Shift elements that start after the removed element if (currentElement.startTime >= elementEndTime) { return { ...currentElement, @@ -644,10 +615,8 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { return currentElement; }); - // Check for overlaps and resolve them if necessary const hasOverlaps = checkElementOverlaps(updatedElements); if (hasOverlaps) { - // Resolve overlaps by adjusting element positions const resolvedElements = resolveElementOverlaps(updatedElements); return { ...currentTrack, elements: resolvedElements }; } @@ -670,7 +639,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { if (!elementToMove || !toTrack) return; - // Validate element type compatibility with target track const validation = validateElementTrackCompatibility( elementToMove, toTrack @@ -776,7 +744,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { const { _tracks, rippleEditingEnabled } = get(); if (!rippleEditingEnabled) { - // If ripple editing is disabled, use regular update get().updateElementStartTime(trackId, elementId, newStartTime); return; } @@ -796,9 +763,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { newStartTime + (element.duration - element.trimStart - element.trimEnd); const timeDelta = newStartTime - oldStartTime; - // Update tracks based on multi-track ripple setting const updatedTracks = _tracks.map((currentTrack) => { - // Only apply ripple effects to the same track unless multi-track ripple is enabled const shouldApplyRipple = currentTrack.id === trackId; const updatedElements = currentTrack.elements.map((currentElement) => { @@ -806,12 +771,10 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { return { ...currentElement, startTime: Math.max(0, newStartTime) }; } - // Only apply ripple effects if we should process this track if (!shouldApplyRipple) { return currentElement; } - // For ripple editing, we need to move elements that come after the moved element const currentElementStart = currentElement.startTime; const currentElementEnd = currentElement.startTime + @@ -819,19 +782,14 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { currentElement.trimStart - currentElement.trimEnd); - // If moving element to the right (positive delta) if (timeDelta > 0) { - // Move elements that start after the original position of the moved element if (currentElementStart >= oldEndTime) { return { ...currentElement, startTime: currentElementStart + timeDelta, }; } - } - // If moving element to the left (negative delta) - else if (timeDelta < 0) { - // Move elements that start after the new position of the moved element + } else if (timeDelta < 0) { if ( currentElementStart >= newEndTime && currentElementStart >= oldStartTime @@ -846,10 +804,8 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { return currentElement; }); - // Check for overlaps and resolve them if necessary const hasOverlaps = checkElementOverlaps(updatedElements); if (hasOverlaps) { - // Resolve overlaps by adjusting element positions const resolvedElements = resolveElementOverlaps(updatedElements); return { ...currentTrack, elements: resolvedElements }; } @@ -887,6 +843,24 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { ); }, + toggleElementMuted: (trackId, elementId) => { + get().pushHistory(); + updateTracksAndSave( + get()._tracks.map((track) => + track.id === trackId + ? { + ...track, + elements: track.elements.map((element) => + element.id === elementId && element.type === "media" + ? { ...element, muted: !element.muted } + : element + ), + } + : track + ) + ); + }, + updateTextElement: (trackId, elementId, updates) => { get().pushHistory(); updateTracksAndSave( @@ -1050,12 +1024,10 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { get().pushHistory(); - // Find existing audio track or prepare to create one const existingAudioTrack = _tracks.find((t) => t.type === "audio"); const audioElementId = generateUUID(); if (existingAudioTrack) { - // Add audio element to existing audio track updateTracksAndSave( get()._tracks.map((track) => track.id === existingAudioTrack.id @@ -1074,7 +1046,6 @@ 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: generateUUID(), name: "Audio Track", @@ -1124,7 +1095,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { return { success: false, error: "No active project found" }; } - // Import required media processing functions const { getFileType, getImageDimensions, @@ -1141,7 +1111,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }; } - // Process the new media file const mediaData: any = { name: newFile.name, type: fileType, @@ -1150,7 +1119,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }; try { - // Get media-specific metadata if (fileType === "image") { const { width, height } = await getImageDimensions(newFile); mediaData.width = width; @@ -1175,7 +1143,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }; } - // Add new media item to store try { await mediaStore.addMediaItem( projectStore.activeProject.id, @@ -1188,7 +1155,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }; } - // Find the newly created media item const newMediaItem = mediaStore.mediaItems.find( (item) => item.file === newFile ); @@ -1202,7 +1168,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { get().pushHistory(); - // Update the timeline element to reference the new media updateTracksAndSave( _tracks.map((track) => track.id === trackId @@ -1214,7 +1179,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { ...c, mediaId: newMediaItem.id, name: newMediaItem.name, - // Update duration if the new media has a different duration duration: newMediaItem.duration || c.duration, } : c @@ -1355,22 +1319,18 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }); }, - // Persistence methods loadProjectTimeline: async (projectId) => { try { const tracks = await storageService.loadTimeline(projectId); if (tracks) { updateTracks(tracks); } else { - // No timeline saved yet, initialize with default const defaultTracks = ensureMainTrack([]); updateTracks(defaultTracks); } - // Clear history when loading a project set({ history: [], redoStack: [] }); } catch (error) { console.error("Failed to load timeline:", error); - // Initialize with default on error const defaultTracks = ensureMainTrack([]); updateTracks(defaultTracks); set({ history: [], redoStack: [] }); @@ -1429,8 +1389,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }, findOrCreateTrack: (trackType) => { - // Always create new text track to allow multiple text elements - // Insert text tracks at the top if (trackType === "text") { return get().insertTrackAt(trackType, 0); } @@ -1448,10 +1406,8 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { const duration = item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION; - // Get all tracks of the right type const tracks = get()._tracks.filter((t) => t.type === trackType); - // Try to find a track with no overlap let targetTrackId = null; for (const track of tracks) { if (!get().checkElementOverlap(track.id, currentTime, duration)) { @@ -1460,7 +1416,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { } } - // If no free track found, create a new one if (!targetTrackId) { targetTrackId = get().addTrack(trackType); } @@ -1473,12 +1428,13 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { startTime: currentTime, trimStart: 0, trimEnd: 0, + muted: false, }); return true; }, addTextAtTime: (item, currentTime = 0) => { - const targetTrackId = get().insertTrackAt("text", 0); // Always create new text track at the top + const targetTrackId = get().insertTrackAt("text", 0); get().addElementToTrack(targetTrackId, { type: "text", @@ -1516,12 +1472,13 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { startTime: 0, trimStart: 0, trimEnd: 0, + muted: false, }); return true; }, addTextToNewTrack: (item) => { - const targetTrackId = get().insertTrackAt("text", 0); // Always create new text track at the top + const targetTrackId = get().insertTrackAt("text", 0); get().addElementToTrack(targetTrackId, { type: "text", |
