Merge branch 'staging'
This commit is contained in:
commit
d5c52557df
|
|
@ -1,18 +1,12 @@
|
|||
import { Hero } from "@/components/landing/hero";
|
||||
import { Header } from "@/components/header";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { getWaitlistCount } from "@/lib/waitlist";
|
||||
|
||||
// Force dynamic rendering so waitlist count updates in real-time
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function Home() {
|
||||
const signupCount = await getWaitlistCount();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Hero signupCount={signupCount} />
|
||||
<Hero />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
} from "@/components/ui/select";
|
||||
import { DraggableMediaItem } from "@/components/ui/draggable-item";
|
||||
import { useProjectStore } from "@/stores/project-store";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
|
||||
export function MediaView() {
|
||||
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
|
||||
|
|
@ -288,6 +289,11 @@ export function MediaView() {
|
|||
name: item.name,
|
||||
}}
|
||||
showPlusOnDrag={false}
|
||||
onAddToTimeline={(currentTime) =>
|
||||
useTimelineStore
|
||||
.getState()
|
||||
.addMediaAtTime(item, currentTime)
|
||||
}
|
||||
rounded={false}
|
||||
/>
|
||||
</ContextMenuTrigger>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,30 @@
|
|||
import { DraggableMediaItem } from "@/components/ui/draggable-item";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
import { type TextElement } from "@/types/timeline";
|
||||
|
||||
let textData: TextElement = {
|
||||
id: "default-text",
|
||||
type: "text",
|
||||
name: "Default text",
|
||||
content: "Default text",
|
||||
fontSize: 48,
|
||||
fontFamily: "Arial",
|
||||
color: "#ffffff",
|
||||
backgroundColor: "transparent",
|
||||
textAlign: "center" as const,
|
||||
fontWeight: "normal" as const,
|
||||
fontStyle: "normal" as const,
|
||||
textDecoration: "none" as const,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
||||
startTime: 0,
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
};
|
||||
|
||||
export function TextView() {
|
||||
return (
|
||||
|
|
@ -11,12 +37,15 @@ export function TextView() {
|
|||
</div>
|
||||
}
|
||||
dragData={{
|
||||
id: "default-text",
|
||||
type: "text",
|
||||
name: "Default text",
|
||||
content: "Default text",
|
||||
id: textData.id,
|
||||
type: textData.type,
|
||||
name: textData.name,
|
||||
content: textData.content,
|
||||
}}
|
||||
aspectRatio={1}
|
||||
onAddToTimeline={(currentTime) =>
|
||||
useTimelineStore.getState().addTextAtTime(textData, currentTime)
|
||||
}
|
||||
showLabel={false}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize";
|
|||
import {
|
||||
getTrackElementClasses,
|
||||
TIMELINE_CONSTANTS,
|
||||
getTrackHeight,
|
||||
} from "@/constants/timeline-constants";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -267,35 +268,99 @@ export function TimelineElement({
|
|||
);
|
||||
}
|
||||
|
||||
const TILE_ASPECT_RATIO = 16 / 9;
|
||||
|
||||
if (mediaItem.type === "image") {
|
||||
// Calculate tile size based on 16:9 aspect ratio
|
||||
const trackHeight = getTrackHeight(track.type);
|
||||
const tileHeight = trackHeight - 8; // Account for padding
|
||||
const tileWidth = tileHeight * TILE_ASPECT_RATIO;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="bg-[#004D52] py-3 w-full h-full">
|
||||
<img
|
||||
src={mediaItem.url}
|
||||
alt={mediaItem.name}
|
||||
className="w-full h-full object-cover"
|
||||
draggable={false}
|
||||
<div className="bg-[#004D52] py-3 w-full h-full relative">
|
||||
{/* Background with tiled images */}
|
||||
<div
|
||||
className="absolute top-3 bottom-3 left-0 right-0"
|
||||
style={{
|
||||
backgroundImage: mediaItem.url
|
||||
? `url(${mediaItem.url})`
|
||||
: "none",
|
||||
backgroundRepeat: "repeat-x",
|
||||
backgroundSize: `${tileWidth}px ${tileHeight}px`,
|
||||
backgroundPosition: "left center",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
aria-label={`Tiled background of ${mediaItem.name}`}
|
||||
/>
|
||||
{/* Overlay with vertical borders */}
|
||||
<div
|
||||
className="absolute top-3 bottom-3 left-0 right-0 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: `repeating-linear-gradient(
|
||||
to right,
|
||||
transparent 0px,
|
||||
transparent ${tileWidth - 1}px,
|
||||
rgba(255, 255, 255, 0.6) ${tileWidth - 1}px,
|
||||
rgba(255, 255, 255, 0.6) ${tileWidth}px
|
||||
)`,
|
||||
backgroundPosition: "left center",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const VIDEO_TILE_PADDING = 16;
|
||||
const OVERLAY_SPACE_MULTIPLIER = 1.5;
|
||||
|
||||
if (mediaItem.type === "video" && mediaItem.thumbnailUrl) {
|
||||
const trackHeight = getTrackHeight(track.type);
|
||||
const tileHeight = trackHeight - VIDEO_TILE_PADDING;
|
||||
const tileWidth = tileHeight * TILE_ASPECT_RATIO;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex items-center gap-2">
|
||||
<div className="w-8 h-8 flex-shrink-0">
|
||||
<img
|
||||
src={mediaItem.thumbnailUrl}
|
||||
alt={mediaItem.name}
|
||||
className="w-full h-full object-cover rounded-sm"
|
||||
draggable={false}
|
||||
<div className="flex-1 h-full relative overflow-hidden">
|
||||
{/* Background with tiled thumbnails */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage: mediaItem.thumbnailUrl
|
||||
? `url(${mediaItem.thumbnailUrl})`
|
||||
: "none",
|
||||
backgroundRepeat: "repeat-x",
|
||||
backgroundSize: `${tileWidth}px ${tileHeight}px`,
|
||||
backgroundPosition: "left center",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
aria-label={`Tiled thumbnail of ${mediaItem.name}`}
|
||||
/>
|
||||
{/* Overlay with vertical borders */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: `repeating-linear-gradient(
|
||||
to right,
|
||||
transparent 0px,
|
||||
transparent ${tileWidth - 1}px,
|
||||
rgba(255, 255, 255, 0.6) ${tileWidth - 1}px,
|
||||
rgba(255, 255, 255, 0.6) ${tileWidth}px
|
||||
)`,
|
||||
backgroundPosition: "left center",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-foreground/80 truncate flex-1">
|
||||
{element.name}
|
||||
</span>
|
||||
{elementWidth > tileWidth * OVERLAY_SPACE_MULTIPLIER ? (
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 bg-black/70 text-white text-xs px-2 py-1 rounded pointer-events-none max-w-[40%] truncate">
|
||||
{element.name}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-foreground/80 truncate flex-shrink-0 max-w-[120px]">
|
||||
{element.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
TypeIcon,
|
||||
Magnet,
|
||||
Lock,
|
||||
LockOpen,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
|
|
@ -376,29 +377,7 @@ export function Timeline() {
|
|||
|
||||
if (dragData.type === "text") {
|
||||
// Always create new text track to avoid overlaps
|
||||
const newTrackId = addTrack("text");
|
||||
|
||||
addElementToTrack(newTrackId, {
|
||||
type: "text",
|
||||
name: dragData.name || "Text",
|
||||
content: dragData.content || "Default Text",
|
||||
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
||||
startTime: 0,
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
fontSize: 48,
|
||||
fontFamily: "Arial",
|
||||
color: "#ffffff",
|
||||
backgroundColor: "transparent",
|
||||
textAlign: "center",
|
||||
fontWeight: "normal",
|
||||
fontStyle: "normal",
|
||||
textDecoration: "none",
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
});
|
||||
useTimelineStore.getState().addTextToNewTrack(dragData);
|
||||
} else {
|
||||
// Handle media items
|
||||
const mediaItem = mediaItems.find((item) => item.id === dragData.id);
|
||||
|
|
@ -407,19 +386,7 @@ export function Timeline() {
|
|||
return;
|
||||
}
|
||||
|
||||
const trackType = dragData.type === "audio" ? "audio" : "media";
|
||||
let targetTrack = tracks.find((t) => t.type === trackType);
|
||||
const newTrackId = targetTrack ? targetTrack.id : addTrack(trackType);
|
||||
|
||||
addElementToTrack(newTrackId, {
|
||||
type: "media",
|
||||
mediaId: mediaItem.id,
|
||||
name: mediaItem.name,
|
||||
duration: mediaItem.duration || 5,
|
||||
startTime: 0,
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
});
|
||||
useTimelineStore.getState().addMediaToNewTrack(mediaItem);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing dropped item data:", error);
|
||||
|
|
@ -447,18 +414,7 @@ export function Timeline() {
|
|||
item.name === processedItem.name && item.url === processedItem.url
|
||||
);
|
||||
if (addedItem) {
|
||||
const trackType =
|
||||
processedItem.type === "audio" ? "audio" : "media";
|
||||
const newTrackId = addTrack(trackType);
|
||||
addElementToTrack(newTrackId, {
|
||||
type: "media",
|
||||
mediaId: addedItem.id,
|
||||
name: addedItem.name,
|
||||
duration: addedItem.duration || 5,
|
||||
startTime: 0,
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
});
|
||||
useTimelineStore.getState().addMediaToNewTrack(addedItem);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -853,7 +809,11 @@ export function Timeline() {
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="text" size="icon" onClick={toggleSnapping}>
|
||||
<Lock className="h-4 w-4" />
|
||||
{snappingEnabled ? (
|
||||
<Lock className="h-4 w-4" />
|
||||
) : (
|
||||
<LockOpen className="h-4 w-4 text-primary" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Auto snapping</TooltipContent>
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ import { toast } from "sonner";
|
|||
import Image from "next/image";
|
||||
import { Handlebars } from "./handlebars";
|
||||
|
||||
interface HeroProps {
|
||||
signupCount: number;
|
||||
}
|
||||
|
||||
export function Hero({ signupCount }: HeroProps) {
|
||||
export function Hero() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
|
|
@ -131,18 +127,6 @@ export function Hero({ signupCount }: HeroProps) {
|
|||
</Button>
|
||||
</form>
|
||||
</motion.div>
|
||||
|
||||
{signupCount > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.8, duration: 0.6 }}
|
||||
className="mt-8 inline-flex items-center gap-2 text-sm text-muted-foreground justify-center"
|
||||
>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
<span>{signupCount.toLocaleString()} people already joined</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import { ReactNode, useState, useRef, useEffect } from "react";
|
|||
import { createPortal } from "react-dom";
|
||||
import { Plus } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePlaybackStore } from "@/stores/playback-store";
|
||||
|
||||
export interface DraggableMediaItemProps {
|
||||
name: string;
|
||||
preview: ReactNode;
|
||||
dragData: Record<string, any>;
|
||||
onDragStart?: (e: React.DragEvent) => void;
|
||||
onAddToTimeline?: (currentTime: number) => void;
|
||||
aspectRatio?: number;
|
||||
className?: string;
|
||||
showPlusOnDrag?: boolean;
|
||||
|
|
@ -24,6 +26,7 @@ export function DraggableMediaItem({
|
|||
preview,
|
||||
dragData,
|
||||
onDragStart,
|
||||
onAddToTimeline,
|
||||
aspectRatio = 16 / 9,
|
||||
className = "",
|
||||
showPlusOnDrag = true,
|
||||
|
|
@ -33,6 +36,11 @@ export function DraggableMediaItem({
|
|||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 });
|
||||
const dragRef = useRef<HTMLDivElement>(null);
|
||||
const currentTime = usePlaybackStore((state) => state.currentTime);
|
||||
|
||||
const handleAddToTimeline = () => {
|
||||
onAddToTimeline?.(currentTime);
|
||||
};
|
||||
|
||||
const emptyImg = new window.Image();
|
||||
emptyImg.src =
|
||||
|
|
@ -92,7 +100,10 @@ export function DraggableMediaItem({
|
|||
>
|
||||
{preview}
|
||||
{!isDragging && (
|
||||
<PlusButton className="opacity-0 group-hover:opacity-100" />
|
||||
<PlusButton
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={handleAddToTimeline}
|
||||
/>
|
||||
)}
|
||||
</AspectRatio>
|
||||
{showLabel && (
|
||||
|
|
@ -128,7 +139,7 @@ export function DraggableMediaItem({
|
|||
<div className="w-full h-full [&_img]:w-full [&_img]:h-full [&_img]:object-cover [&_img]:rounded-none">
|
||||
{preview}
|
||||
</div>
|
||||
{showPlusOnDrag && <PlusButton />}
|
||||
{showPlusOnDrag && <PlusButton onClick={handleAddToTimeline} tooltipText="Add to timeline or drag to position" />}
|
||||
</AspectRatio>
|
||||
</div>
|
||||
</div>,
|
||||
|
|
@ -138,11 +149,16 @@ export function DraggableMediaItem({
|
|||
);
|
||||
}
|
||||
|
||||
function PlusButton({ className }: { className?: string }) {
|
||||
function PlusButton({ className, onClick }: { className?: string; onClick?: () => void }) {
|
||||
return (
|
||||
<Button
|
||||
size="icon"
|
||||
className={cn("absolute bottom-2 right-2 size-4", className)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClick?.();
|
||||
}}
|
||||
>
|
||||
<Plus className="!size-3" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-foreground/10 px-3 py-1.5 text-xs text-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-50 overflow-hidden rounded-md bg-foreground px-3 py-1.5 text-xs text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export const TIMELINE_CONSTANTS = {
|
|||
PIXELS_PER_SECOND: 50,
|
||||
TRACK_HEIGHT: 60, // Default fallback
|
||||
DEFAULT_TEXT_DURATION: 5,
|
||||
DEFAULT_IMAGE_DURATION: 5,
|
||||
ZOOM_LEVELS: [0.25, 0.5, 1, 1.5, 2, 3, 4],
|
||||
} as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,22 @@ import {
|
|||
CreateTimelineElement,
|
||||
TimelineTrack,
|
||||
TextElement,
|
||||
DragData,
|
||||
sortTracksByOrder,
|
||||
ensureMainTrack,
|
||||
validateElementTrackCompatibility,
|
||||
} from "@/types/timeline";
|
||||
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 { useProjectStore } from "./project-store";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// Helper function to manage element naming with suffixes
|
||||
const getElementNameWithSuffix = (
|
||||
|
|
@ -166,6 +173,17 @@ interface TimelineStore {
|
|||
>
|
||||
>
|
||||
) => 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) => {
|
||||
|
|
@ -963,5 +981,149 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
toggleSnapping: () => {
|
||||
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;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue