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.ts137
-rw-r--r--apps/web/src/stores/panel-store.ts212
-rw-r--r--apps/web/src/stores/playback-store.ts18
-rw-r--r--apps/web/src/stores/project-store.ts55
-rw-r--r--apps/web/src/stores/timeline-store.ts113
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",