mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
cleanup
This commit is contained in:
@@ -24,7 +24,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { DraggableMediaItem } from "@/components/ui/draggable-item";
|
import { DraggableMediaItem } from "@/components/ui/draggable-item";
|
||||||
import { useProjectStore } from "@/stores/project-store";
|
import { useProjectStore } from "@/stores/project-store";
|
||||||
import { addMediaToTimeline } from "@/lib/timeline-utils";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
|
|
||||||
export function MediaView() {
|
export function MediaView() {
|
||||||
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
|
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
|
||||||
@@ -289,7 +289,11 @@ export function MediaView() {
|
|||||||
name: item.name,
|
name: item.name,
|
||||||
}}
|
}}
|
||||||
showPlusOnDrag={false}
|
showPlusOnDrag={false}
|
||||||
onAddToTimeline={(currentTime) => addMediaToTimeline(item, currentTime)}
|
onAddToTimeline={(currentTime) =>
|
||||||
|
useTimelineStore
|
||||||
|
.getState()
|
||||||
|
.addMediaAtTime(item, currentTime)
|
||||||
|
}
|
||||||
rounded={false}
|
rounded={false}
|
||||||
/>
|
/>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DraggableMediaItem } from "@/components/ui/draggable-item";
|
import { DraggableMediaItem } from "@/components/ui/draggable-item";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
import { addTextToTimeline } from "@/lib/timeline-utils";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
import { type TextElement } from "@/types/timeline";
|
import { type TextElement } from "@/types/timeline";
|
||||||
|
|
||||||
let textData: TextElement = {
|
let textData: TextElement = {
|
||||||
@@ -43,7 +43,9 @@ export function TextView() {
|
|||||||
content: textData.content,
|
content: textData.content,
|
||||||
}}
|
}}
|
||||||
aspectRatio={1}
|
aspectRatio={1}
|
||||||
onAddToTimeline={(currentTime) => addTextToTimeline(textData, currentTime)}
|
onAddToTimeline={(currentTime) =>
|
||||||
|
useTimelineStore.getState().addTextAtTime(textData, currentTime)
|
||||||
|
}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import { useSelectionBox } from "@/hooks/use-selection-box";
|
|||||||
import { SnapIndicator } from "./snap-indicator";
|
import { SnapIndicator } from "./snap-indicator";
|
||||||
import { SnapPoint } from "@/hooks/use-timeline-snapping";
|
import { SnapPoint } from "@/hooks/use-timeline-snapping";
|
||||||
import type { DragData, TimelineTrack } from "@/types/timeline";
|
import type { DragData, TimelineTrack } from "@/types/timeline";
|
||||||
import { addTextToNewTrack, addMediaToNewTrack } from "@/lib/timeline-utils";
|
|
||||||
import {
|
import {
|
||||||
getTrackHeight,
|
getTrackHeight,
|
||||||
getCumulativeHeightBefore,
|
getCumulativeHeightBefore,
|
||||||
@@ -229,7 +228,7 @@ export function Timeline() {
|
|||||||
Math.min(
|
Math.min(
|
||||||
duration,
|
duration,
|
||||||
(mouseX + scrollLeft) /
|
(mouseX + scrollLeft) /
|
||||||
(TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel)
|
(TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -377,7 +376,7 @@ export function Timeline() {
|
|||||||
|
|
||||||
if (dragData.type === "text") {
|
if (dragData.type === "text") {
|
||||||
// Always create new text track to avoid overlaps
|
// Always create new text track to avoid overlaps
|
||||||
addTextToNewTrack(dragData);
|
useTimelineStore.getState().addTextToNewTrack(dragData);
|
||||||
} else {
|
} else {
|
||||||
// Handle media items
|
// Handle media items
|
||||||
const mediaItem = mediaItems.find((item) => item.id === dragData.id);
|
const mediaItem = mediaItems.find((item) => item.id === dragData.id);
|
||||||
@@ -386,7 +385,7 @@ export function Timeline() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addMediaToNewTrack(mediaItem);
|
useTimelineStore.getState().addMediaToNewTrack(mediaItem);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error parsing dropped item data:", error);
|
console.error("Error parsing dropped item data:", error);
|
||||||
@@ -414,7 +413,7 @@ export function Timeline() {
|
|||||||
item.name === processedItem.name && item.url === processedItem.url
|
item.name === processedItem.name && item.url === processedItem.url
|
||||||
);
|
);
|
||||||
if (addedItem) {
|
if (addedItem) {
|
||||||
addMediaToNewTrack(addedItem);
|
useTimelineStore.getState().addMediaToNewTrack(addedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -902,19 +901,21 @@ export function Timeline() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={`absolute top-0 bottom-0 ${isMainMarker
|
className={`absolute top-0 bottom-0 ${
|
||||||
|
isMainMarker
|
||||||
? "border-l border-muted-foreground/40"
|
? "border-l border-muted-foreground/40"
|
||||||
: "border-l border-muted-foreground/20"
|
: "border-l border-muted-foreground/20"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
|
left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`absolute top-1 left-1 text-[0.6rem] ${isMainMarker
|
className={`absolute top-1 left-1 text-[0.6rem] ${
|
||||||
|
isMainMarker
|
||||||
? "text-muted-foreground font-medium"
|
? "text-muted-foreground font-medium"
|
||||||
: "text-muted-foreground/70"
|
: "text-muted-foreground/70"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
const formatTime = (seconds: number) => {
|
const formatTime = (seconds: number) => {
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
|
||||||
import { type MediaItem } from "@/stores/media-store";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
|
||||||
import { DragData, TextElement } from "@/types/timeline";
|
|
||||||
|
|
||||||
|
|
||||||
const findOrCreateTrack = (trackType: "media" | "audio" | "text") => {
|
|
||||||
const timelineStore = useTimelineStore.getState();
|
|
||||||
|
|
||||||
// Always create new text track to allow multiple text elements
|
|
||||||
if (trackType === "text") {
|
|
||||||
return timelineStore.addTrack(trackType);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingTrack = timelineStore.tracks.find(track => track.type === trackType);
|
|
||||||
return existingTrack ? existingTrack.id : timelineStore.addTrack(trackType);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const checkOverlap = (trackId: string, startTime: number, duration: number, excludeElementId?: string) => {
|
|
||||||
const timelineStore = useTimelineStore.getState();
|
|
||||||
const targetTrack = timelineStore.tracks.find(track => track.id === trackId);
|
|
||||||
|
|
||||||
if (!targetTrack) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const elementEnd = startTime + duration;
|
|
||||||
|
|
||||||
return targetTrack.elements.some((existingElement) => {
|
|
||||||
if (excludeElementId && existingElement.id === excludeElementId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingStart = existingElement.startTime;
|
|
||||||
const existingEnd = existingElement.startTime +
|
|
||||||
(existingElement.duration - existingElement.trimStart - existingElement.trimEnd);
|
|
||||||
|
|
||||||
return startTime < existingEnd && elementEnd > existingStart;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addMediaElement = (trackId: string, item: MediaItem, startTime: number) => {
|
|
||||||
const timelineStore = useTimelineStore.getState();
|
|
||||||
|
|
||||||
timelineStore.addElementToTrack(trackId, {
|
|
||||||
type: "media",
|
|
||||||
mediaId: item.id,
|
|
||||||
name: item.name,
|
|
||||||
duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
|
|
||||||
startTime,
|
|
||||||
trimStart: 0,
|
|
||||||
trimEnd: 0,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addTextElement = (trackId: string, item: TextElement | DragData, startTime: number) => {
|
|
||||||
const timelineStore = useTimelineStore.getState();
|
|
||||||
|
|
||||||
timelineStore.addElementToTrack(trackId, {
|
|
||||||
type: "text",
|
|
||||||
name: item.name,
|
|
||||||
content: ("content" in item ? item.content : "Default Text"),
|
|
||||||
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
|
||||||
startTime,
|
|
||||||
trimStart: ("trimStart" in item ? item.trimStart : 0),
|
|
||||||
trimEnd: ("trimEnd" in item ? item.trimEnd : 0),
|
|
||||||
fontSize: ("fontSize" in item ? item.fontSize : 48),
|
|
||||||
fontFamily: ("fontFamily" in item ? item.fontFamily : "Arial"),
|
|
||||||
color: ("color" in item ? item.color : "#ffffff"),
|
|
||||||
backgroundColor: ("backgroundColor" in item ? item.backgroundColor : "transparent"),
|
|
||||||
textAlign: ("textAlign" in item ? item.textAlign : "center"),
|
|
||||||
fontWeight: ("fontWeight" in item ? item.fontWeight : "normal"),
|
|
||||||
fontStyle: ("fontStyle" in item ? item.fontStyle : "normal"),
|
|
||||||
textDecoration: ("textDecoration" in item ? item.textDecoration : "none"),
|
|
||||||
x: ("x" in item ? item.x : 0),
|
|
||||||
y: ("y" in item ? item.y : 0),
|
|
||||||
rotation: ("rotation" in item ? item.rotation : 0),
|
|
||||||
opacity: ("opacity" in item && item.opacity !== undefined ? item.opacity : 1),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Adds a media item to the timeline at the specified time
|
|
||||||
export const addMediaToTimeline = (item: MediaItem, currentTime: number = 0) => {
|
|
||||||
const trackType = item.type === "audio" ? "audio" : "media";
|
|
||||||
const targetTrackId = findOrCreateTrack(trackType);
|
|
||||||
|
|
||||||
const duration = item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION;
|
|
||||||
|
|
||||||
if (checkOverlap(targetTrackId, currentTime, duration)) {
|
|
||||||
toast.error("Cannot place element here - it would overlap with existing elements");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addMediaElement(targetTrackId, item, currentTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Adds a text item to the timeline at the specified time
|
|
||||||
export const addTextToTimeline = (item: TextElement, currentTime: number = 0) => {
|
|
||||||
const targetTrackId = findOrCreateTrack("text");
|
|
||||||
addTextElement(targetTrackId, item, currentTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Adds a media item to timeline
|
|
||||||
export const addMediaToNewTrack = (item: MediaItem) => {
|
|
||||||
const trackType = item.type === "audio" ? "audio" : "media";
|
|
||||||
const targetTrackId = findOrCreateTrack(trackType);
|
|
||||||
addMediaElement(targetTrackId, item, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Adds a text item to timeline
|
|
||||||
export const addTextToNewTrack = (item: TextElement | DragData) => {
|
|
||||||
const targetTrackId = findOrCreateTrack("text");
|
|
||||||
addTextElement(targetTrackId, item, 0);
|
|
||||||
};
|
|
||||||
@@ -5,15 +5,22 @@ import {
|
|||||||
CreateTimelineElement,
|
CreateTimelineElement,
|
||||||
TimelineTrack,
|
TimelineTrack,
|
||||||
TextElement,
|
TextElement,
|
||||||
|
DragData,
|
||||||
sortTracksByOrder,
|
sortTracksByOrder,
|
||||||
ensureMainTrack,
|
ensureMainTrack,
|
||||||
validateElementTrackCompatibility,
|
validateElementTrackCompatibility,
|
||||||
} from "@/types/timeline";
|
} from "@/types/timeline";
|
||||||
import { useEditorStore } from "./editor-store";
|
import { useEditorStore } from "./editor-store";
|
||||||
import { useMediaStore, getMediaAspectRatio } from "./media-store";
|
import {
|
||||||
|
useMediaStore,
|
||||||
|
getMediaAspectRatio,
|
||||||
|
type MediaItem,
|
||||||
|
} from "./media-store";
|
||||||
import { storageService } from "@/lib/storage/storage-service";
|
import { storageService } from "@/lib/storage/storage-service";
|
||||||
import { useProjectStore } from "./project-store";
|
import { useProjectStore } from "./project-store";
|
||||||
import { generateUUID } from "@/lib/utils";
|
import { generateUUID } from "@/lib/utils";
|
||||||
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
// Helper function to manage element naming with suffixes
|
// Helper function to manage element naming with suffixes
|
||||||
const getElementNameWithSuffix = (
|
const getElementNameWithSuffix = (
|
||||||
@@ -166,6 +173,17 @@ interface TimelineStore {
|
|||||||
>
|
>
|
||||||
>
|
>
|
||||||
) => void;
|
) => void;
|
||||||
|
checkElementOverlap: (
|
||||||
|
trackId: string,
|
||||||
|
startTime: number,
|
||||||
|
duration: number,
|
||||||
|
excludeElementId?: string
|
||||||
|
) => boolean;
|
||||||
|
findOrCreateTrack: (trackType: TrackType) => string;
|
||||||
|
addMediaAtTime: (item: MediaItem, currentTime?: number) => boolean;
|
||||||
|
addTextAtTime: (item: TextElement, currentTime?: number) => boolean;
|
||||||
|
addMediaToNewTrack: (item: MediaItem) => boolean;
|
||||||
|
addTextToNewTrack: (item: TextElement | DragData) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTimelineStore = create<TimelineStore>((set, get) => {
|
export const useTimelineStore = create<TimelineStore>((set, get) => {
|
||||||
@@ -963,5 +981,149 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||||||
toggleSnapping: () => {
|
toggleSnapping: () => {
|
||||||
set((state) => ({ snappingEnabled: !state.snappingEnabled }));
|
set((state) => ({ snappingEnabled: !state.snappingEnabled }));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
checkElementOverlap: (trackId, startTime, duration, excludeElementId) => {
|
||||||
|
const track = get()._tracks.find((t) => t.id === trackId);
|
||||||
|
if (!track) return false;
|
||||||
|
|
||||||
|
const overlap = track.elements.some((element) => {
|
||||||
|
const elementEnd =
|
||||||
|
element.startTime +
|
||||||
|
element.duration -
|
||||||
|
element.trimStart -
|
||||||
|
element.trimEnd;
|
||||||
|
|
||||||
|
if (element.id === excludeElementId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
(startTime >= element.startTime && startTime < elementEnd) ||
|
||||||
|
(startTime + duration > element.startTime &&
|
||||||
|
startTime + duration <= elementEnd) ||
|
||||||
|
(startTime < element.startTime && startTime + duration > elementEnd)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return overlap;
|
||||||
|
},
|
||||||
|
|
||||||
|
findOrCreateTrack: (trackType) => {
|
||||||
|
// Always create new text track to allow multiple text elements
|
||||||
|
if (trackType === "text") {
|
||||||
|
return get().addTrack(trackType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingTrack = get()._tracks.find((t) => t.type === trackType);
|
||||||
|
if (existingTrack) {
|
||||||
|
return existingTrack.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return get().addTrack(trackType);
|
||||||
|
},
|
||||||
|
|
||||||
|
addMediaAtTime: (item, currentTime = 0) => {
|
||||||
|
const trackType = item.type === "audio" ? "audio" : "media";
|
||||||
|
const targetTrackId = get().findOrCreateTrack(trackType);
|
||||||
|
|
||||||
|
const duration =
|
||||||
|
item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION;
|
||||||
|
|
||||||
|
if (get().checkElementOverlap(targetTrackId, currentTime, duration)) {
|
||||||
|
toast.error(
|
||||||
|
"Cannot place element here - it would overlap with existing elements"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get().addElementToTrack(targetTrackId, {
|
||||||
|
type: "media",
|
||||||
|
mediaId: item.id,
|
||||||
|
name: item.name,
|
||||||
|
duration,
|
||||||
|
startTime: currentTime,
|
||||||
|
trimStart: 0,
|
||||||
|
trimEnd: 0,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
addTextAtTime: (item, currentTime = 0) => {
|
||||||
|
const targetTrackId = get().addTrack("text"); // Always create new text track to allow multiple text elements
|
||||||
|
|
||||||
|
get().addElementToTrack(targetTrackId, {
|
||||||
|
type: "text",
|
||||||
|
name: item.name || "Text",
|
||||||
|
content: item.content || "Default Text",
|
||||||
|
duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
||||||
|
startTime: currentTime,
|
||||||
|
trimStart: 0,
|
||||||
|
trimEnd: 0,
|
||||||
|
fontSize: item.fontSize || 48,
|
||||||
|
fontFamily: item.fontFamily || "Arial",
|
||||||
|
color: item.color || "#ffffff",
|
||||||
|
backgroundColor: item.backgroundColor || "transparent",
|
||||||
|
textAlign: item.textAlign || "center",
|
||||||
|
fontWeight: item.fontWeight || "normal",
|
||||||
|
fontStyle: item.fontStyle || "normal",
|
||||||
|
textDecoration: item.textDecoration || "none",
|
||||||
|
x: item.x || 0,
|
||||||
|
y: item.y || 0,
|
||||||
|
rotation: item.rotation || 0,
|
||||||
|
opacity: item.opacity !== undefined ? item.opacity : 1,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
addMediaToNewTrack: (item) => {
|
||||||
|
const trackType = item.type === "audio" ? "audio" : "media";
|
||||||
|
const targetTrackId = get().findOrCreateTrack(trackType);
|
||||||
|
|
||||||
|
get().addElementToTrack(targetTrackId, {
|
||||||
|
type: "media",
|
||||||
|
mediaId: item.id,
|
||||||
|
name: item.name,
|
||||||
|
duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
|
||||||
|
startTime: 0,
|
||||||
|
trimStart: 0,
|
||||||
|
trimEnd: 0,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
addTextToNewTrack: (item) => {
|
||||||
|
const targetTrackId = get().addTrack("text"); // Always create new text track to allow multiple text elements
|
||||||
|
|
||||||
|
get().addElementToTrack(targetTrackId, {
|
||||||
|
type: "text",
|
||||||
|
name: item.name || "Text",
|
||||||
|
content:
|
||||||
|
("content" in item ? item.content : "Default Text") || "Default Text",
|
||||||
|
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
||||||
|
startTime: 0,
|
||||||
|
trimStart: 0,
|
||||||
|
trimEnd: 0,
|
||||||
|
fontSize: ("fontSize" in item ? item.fontSize : 48) || 48,
|
||||||
|
fontFamily:
|
||||||
|
("fontFamily" in item ? item.fontFamily : "Arial") || "Arial",
|
||||||
|
color: ("color" in item ? item.color : "#ffffff") || "#ffffff",
|
||||||
|
backgroundColor:
|
||||||
|
("backgroundColor" in item ? item.backgroundColor : "transparent") ||
|
||||||
|
"transparent",
|
||||||
|
textAlign:
|
||||||
|
("textAlign" in item ? item.textAlign : "center") || "center",
|
||||||
|
fontWeight:
|
||||||
|
("fontWeight" in item ? item.fontWeight : "normal") || "normal",
|
||||||
|
fontStyle:
|
||||||
|
("fontStyle" in item ? item.fontStyle : "normal") || "normal",
|
||||||
|
textDecoration:
|
||||||
|
("textDecoration" in item ? item.textDecoration : "none") || "none",
|
||||||
|
x: ("x" in item ? item.x : 0) || 0,
|
||||||
|
y: ("y" in item ? item.y : 0) || 0,
|
||||||
|
rotation: ("rotation" in item ? item.rotation : 0) || 0,
|
||||||
|
opacity:
|
||||||
|
"opacity" in item && item.opacity !== undefined ? item.opacity : 1,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user