feat: implement mute video tracks

This commit is contained in:
Maze Winther
2025-08-13 00:52:19 +02:00
parent a97eb26fa4
commit 71b6718337
4 changed files with 55 additions and 86 deletions
@@ -235,7 +235,7 @@ export function PreviewPanel() {
const activeElements: ActiveElement[] = []; const activeElements: ActiveElement[] = [];
// Iterate tracks from bottom to top so topmost track renders last (on top) // Iterate tracks from bottom to top so topmost track renders last (on top)
;[...tracks].reverse().forEach((track) => { [...tracks].reverse().forEach((track) => {
track.elements.forEach((element) => { track.elements.forEach((element) => {
if (element.hidden) return; if (element.hidden) return;
const elementStart = element.startTime; const elementStart = element.startTime;
@@ -436,7 +436,7 @@ export function PreviewPanel() {
trimStart={element.trimStart} trimStart={element.trimStart}
trimEnd={element.trimEnd} trimEnd={element.trimEnd}
clipDuration={element.duration} clipDuration={element.duration}
trackMuted={elementData.track.muted} trackMuted={element.muted || elementData.track.muted}
/> />
</div> </div>
); );
@@ -462,14 +462,18 @@ export function PreviewPanel() {
// Audio elements (no visual representation) // Audio elements (no visual representation)
if (mediaItem.type === "audio") { if (mediaItem.type === "audio") {
return ( return (
<div key={element.id} className="absolute inset-0" style={{ pointerEvents: "none" }}> <div
key={element.id}
className="absolute inset-0"
style={{ pointerEvents: "none" }}
>
<AudioPlayer <AudioPlayer
src={mediaItem.url!} src={mediaItem.url!}
clipStartTime={element.startTime} clipStartTime={element.startTime}
trimStart={element.trimStart} trimStart={element.trimStart}
trimEnd={element.trimEnd} trimEnd={element.trimEnd}
clipDuration={element.duration} clipDuration={element.duration}
trackMuted={elementData.track.muted} trackMuted={element.muted || elementData.track.muted}
/> />
</div> </div>
); );
@@ -50,6 +50,7 @@ export function TimelineElement({
replaceElementMedia, replaceElementMedia,
rippleEditingEnabled, rippleEditingEnabled,
toggleElementHidden, toggleElementHidden,
toggleElementMuted,
} = useTimelineStore(); } = useTimelineStore();
const { currentTime } = usePlaybackStore(); const { currentTime } = usePlaybackStore();
@@ -57,7 +58,7 @@ export function TimelineElement({
element.type === "media" element.type === "media"
? mediaItems.find((item) => item.id === element.mediaId) ? mediaItems.find((item) => item.id === element.mediaId)
: null; : null;
const isAudio = mediaItem?.type === "audio"; const hasAudio = mediaItem?.type === "audio" || mediaItem?.type === "video";
const { resizing, handleResizeStart, handleResizeMove, handleResizeEnd } = const { resizing, handleResizeStart, handleResizeMove, handleResizeEnd } =
useTimelineElementResize({ useTimelineElementResize({
@@ -124,8 +125,12 @@ export function TimelineElement({
} }
}; };
const handleToggleElementHidden = (e: React.MouseEvent) => { const handleToggleElementContext = (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
if (hasAudio && element.type === "media") {
toggleElementMuted(track.id, element.id);
return;
}
toggleElementHidden(track.id, element.id); toggleElementHidden(track.id, element.id);
}; };
@@ -246,6 +251,8 @@ export function TimelineElement({
} }
}; };
const isMuted = element.type === "media" ? element.muted === true : false;
return ( return (
<ContextMenu> <ContextMenu>
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
@@ -279,9 +286,9 @@ export function TimelineElement({
{renderElementContent()} {renderElementContent()}
</div> </div>
{element.hidden && ( {(hasAudio ? isMuted : element.hidden) && (
<div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center pointer-events-none"> <div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center pointer-events-none">
{isAudio ? ( {hasAudio ? (
<VolumeX className="h-6 w-6 text-white" /> <VolumeX className="h-6 w-6 text-white" />
) : ( ) : (
<EyeOff className="h-6 w-6 text-white" /> <EyeOff className="h-6 w-6 text-white" />
@@ -309,9 +316,9 @@ export function TimelineElement({
<Scissors className="h-4 w-4 mr-2" /> <Scissors className="h-4 w-4 mr-2" />
Split at playhead Split at playhead
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem onClick={handleToggleElementHidden}> <ContextMenuItem onClick={handleToggleElementContext}>
{isAudio ? ( {hasAudio ? (
element.hidden ? ( isMuted ? (
<Volume2 className="h-4 w-4 mr-2" /> <Volume2 className="h-4 w-4 mr-2" />
) : ( ) : (
<VolumeX className="h-4 w-4 mr-2" /> <VolumeX className="h-4 w-4 mr-2" />
@@ -322,8 +329,8 @@ export function TimelineElement({
<EyeOff className="h-4 w-4 mr-2" /> <EyeOff className="h-4 w-4 mr-2" />
)} )}
<span> <span>
{isAudio {hasAudio
? element.hidden ? isMuted
? "Unmute" ? "Unmute"
: "Mute" : "Mute"
: element.hidden : element.hidden
+30 -73
View File
@@ -29,10 +29,10 @@ const getElementNameWithSuffix = (
): string => { ): string => {
// Remove existing suffixes to prevent accumulation // Remove existing suffixes to prevent accumulation
const baseName = originalName const baseName = originalName
.replace(/ \(left\)$/, "") .replace(/ \(left\)$/i, "")
.replace(/ \(right\)$/, "") .replace(/ \(right\)$/i, "")
.replace(/ \(audio\)$/, "") .replace(/ \(audio\)$/i, "")
.replace(/ \(split \d+\)$/, ""); .replace(/ \(split \d+\)$/i, "");
return `${baseName} (${suffix})`; return `${baseName} (${suffix})`;
}; };
@@ -126,6 +126,7 @@ interface TimelineStore {
) => void; ) => void;
toggleTrackMute: (trackId: string) => void; toggleTrackMute: (trackId: string) => void;
toggleElementHidden: (trackId: string, elementId: string) => void; toggleElementHidden: (trackId: string, elementId: string) => void;
toggleElementMuted: (trackId: string, elementId: string) => void;
// Split operations for elements // Split operations for elements
splitElement: ( splitElement: (
@@ -322,7 +323,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
addTrack: (type) => { addTrack: (type) => {
get().pushHistory(); get().pushHistory();
// Generate proper track name based on type
const trackName = const trackName =
type === "media" type === "media"
? "Media Track" ? "Media Track"
@@ -347,7 +347,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
insertTrackAt: (type, index) => { insertTrackAt: (type, index) => {
get().pushHistory(); get().pushHistory();
// Generate proper track name based on type
const trackName = const trackName =
type === "media" type === "media"
? "Media Track" ? "Media Track"
@@ -392,13 +391,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
get().pushHistory(); 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) => ({ const occupiedRanges = trackToRemove.elements.map((element) => ({
startTime: element.startTime, startTime: element.startTime,
endTime: endTime:
@@ -406,10 +398,8 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
(element.duration - element.trimStart - element.trimEnd), (element.duration - element.trimStart - element.trimEnd),
})); }));
// Sort ranges by start time
occupiedRanges.sort((a, b) => a.startTime - b.startTime); occupiedRanges.sort((a, b) => a.startTime - b.startTime);
// Merge overlapping ranges to get consolidated gaps
const mergedRanges: Array<{ const mergedRanges: Array<{
startTime: number; startTime: number;
endTime: number; endTime: number;
@@ -426,11 +416,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
} else { } else {
const lastRange = mergedRanges[mergedRanges.length - 1]; const lastRange = mergedRanges[mergedRanges.length - 1];
if (range.startTime <= lastRange.endTime) { if (range.startTime <= lastRange.endTime) {
// Overlapping or adjacent ranges, merge them
lastRange.endTime = Math.max(lastRange.endTime, range.endTime); lastRange.endTime = Math.max(lastRange.endTime, range.endTime);
lastRange.duration = lastRange.endTime - lastRange.startTime; lastRange.duration = lastRange.endTime - lastRange.startTime;
} else { } else {
// Non-overlapping range, add as new
mergedRanges.push({ mergedRanges.push({
startTime: range.startTime, startTime: range.startTime,
endTime: range.endTime, endTime: range.endTime,
@@ -440,17 +428,14 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
} }
} }
// Remove the track and apply ripple effects to remaining tracks
const updatedTracks = _tracks const updatedTracks = _tracks
.filter((track) => track.id !== trackId) .filter((track) => track.id !== trackId)
.map((track) => { .map((track) => {
const updatedElements = track.elements.map((element) => { const updatedElements = track.elements.map((element) => {
let newStartTime = element.startTime; 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--) { for (let i = mergedRanges.length - 1; i >= 0; i--) {
const gap = mergedRanges[i]; const gap = mergedRanges[i];
// If this element starts after the gap, shift it left by the gap duration
if (newStartTime >= gap.endTime) { if (newStartTime >= gap.endTime) {
newStartTime -= gap.duration; newStartTime -= gap.duration;
} }
@@ -462,7 +447,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
}; };
}); });
// Check for overlaps and resolve them if necessary
const hasOverlaps = checkElementOverlaps(updatedElements); const hasOverlaps = checkElementOverlaps(updatedElements);
if (hasOverlaps) { if (hasOverlaps) {
const resolvedElements = resolveElementOverlaps(updatedElements); const resolvedElements = resolveElementOverlaps(updatedElements);
@@ -478,33 +462,28 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
addElementToTrack: (trackId, elementData) => { addElementToTrack: (trackId, elementData) => {
get().pushHistory(); get().pushHistory();
// Validate element type matches track type
const track = get()._tracks.find((t) => t.id === trackId); const track = get()._tracks.find((t) => t.id === trackId);
if (!track) { if (!track) {
console.error("Track not found:", trackId); console.error("Track not found:", trackId);
return; return;
} }
// Use utility function for validation
const validation = validateElementTrackCompatibility(elementData, track); const validation = validateElementTrackCompatibility(elementData, track);
if (!validation.isValid) { if (!validation.isValid) {
console.error(validation.errorMessage); console.error(validation.errorMessage);
return; return;
} }
// For media elements, validate mediaId exists
if (elementData.type === "media" && !elementData.mediaId) { if (elementData.type === "media" && !elementData.mediaId) {
console.error("Media element must have mediaId"); console.error("Media element must have mediaId");
return; return;
} }
// For text elements, validate required text properties
if (elementData.type === "text" && !elementData.content) { if (elementData.type === "text" && !elementData.content) {
console.error("Text element must have content"); console.error("Text element must have content");
return; return;
} }
// Check if this is the first element being added to the timeline
const currentState = get(); const currentState = get();
const totalElementsInTimeline = currentState._tracks.reduce( const totalElementsInTimeline = currentState._tracks.reduce(
(total, track) => total + track.elements.length, (total, track) => total + track.elements.length,
@@ -518,10 +497,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
startTime: elementData.startTime || 0, startTime: elementData.startTime || 0,
trimStart: 0, trimStart: 0,
trimEnd: 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") { if (isFirstElement && newElement.type === "media") {
const mediaStore = useMediaStore.getState(); const mediaStore = useMediaStore.getState();
const mediaItem = mediaStore.mediaItems.find( const mediaItem = mediaStore.mediaItems.find(
@@ -539,7 +517,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
); );
} }
// Set project FPS from the first video element
if (mediaItem && mediaItem.type === "video" && mediaItem.fps) { if (mediaItem && mediaItem.type === "video" && mediaItem.fps) {
const projectStore = useProjectStore.getState(); const projectStore = useProjectStore.getState();
if (projectStore.activeProject) { if (projectStore.activeProject) {
@@ -591,7 +568,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
const { _tracks, rippleEditingEnabled } = get(); const { _tracks, rippleEditingEnabled } = get();
if (!rippleEditingEnabled) { if (!rippleEditingEnabled) {
// If ripple editing is disabled, use regular removal
get().removeElementFromTrack(trackId, elementId, pushHistory); get().removeElementFromTrack(trackId, elementId, pushHistory);
return; return;
} }
@@ -608,15 +584,12 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
element.duration - element.trimStart - element.trimEnd; element.duration - element.trimStart - element.trimEnd;
const elementEndTime = elementStartTime + elementDuration; const elementEndTime = elementStartTime + elementDuration;
// Remove the element and shift all elements that come after it
const updatedTracks = _tracks const updatedTracks = _tracks
.map((currentTrack) => { .map((currentTrack) => {
// Only apply ripple effects to the same track unless multi-track ripple is enabled
const shouldApplyRipple = currentTrack.id === trackId; const shouldApplyRipple = currentTrack.id === trackId;
const updatedElements = currentTrack.elements const updatedElements = currentTrack.elements
.filter((currentElement) => { .filter((currentElement) => {
// Remove the target element
if ( if (
currentElement.id === elementId && currentElement.id === elementId &&
currentTrack.id === trackId currentTrack.id === trackId
@@ -626,12 +599,10 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
return true; return true;
}) })
.map((currentElement) => { .map((currentElement) => {
// Only apply ripple effects if we should process this track
if (!shouldApplyRipple) { if (!shouldApplyRipple) {
return currentElement; return currentElement;
} }
// Shift elements that start after the removed element
if (currentElement.startTime >= elementEndTime) { if (currentElement.startTime >= elementEndTime) {
return { return {
...currentElement, ...currentElement,
@@ -644,10 +615,8 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
return currentElement; return currentElement;
}); });
// Check for overlaps and resolve them if necessary
const hasOverlaps = checkElementOverlaps(updatedElements); const hasOverlaps = checkElementOverlaps(updatedElements);
if (hasOverlaps) { if (hasOverlaps) {
// Resolve overlaps by adjusting element positions
const resolvedElements = resolveElementOverlaps(updatedElements); const resolvedElements = resolveElementOverlaps(updatedElements);
return { ...currentTrack, elements: resolvedElements }; return { ...currentTrack, elements: resolvedElements };
} }
@@ -670,7 +639,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
if (!elementToMove || !toTrack) return; if (!elementToMove || !toTrack) return;
// Validate element type compatibility with target track
const validation = validateElementTrackCompatibility( const validation = validateElementTrackCompatibility(
elementToMove, elementToMove,
toTrack toTrack
@@ -776,7 +744,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
const { _tracks, rippleEditingEnabled } = get(); const { _tracks, rippleEditingEnabled } = get();
if (!rippleEditingEnabled) { if (!rippleEditingEnabled) {
// If ripple editing is disabled, use regular update
get().updateElementStartTime(trackId, elementId, newStartTime); get().updateElementStartTime(trackId, elementId, newStartTime);
return; return;
} }
@@ -796,9 +763,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
newStartTime + (element.duration - element.trimStart - element.trimEnd); newStartTime + (element.duration - element.trimStart - element.trimEnd);
const timeDelta = newStartTime - oldStartTime; const timeDelta = newStartTime - oldStartTime;
// Update tracks based on multi-track ripple setting
const updatedTracks = _tracks.map((currentTrack) => { 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 shouldApplyRipple = currentTrack.id === trackId;
const updatedElements = currentTrack.elements.map((currentElement) => { const updatedElements = currentTrack.elements.map((currentElement) => {
@@ -806,12 +771,10 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
return { ...currentElement, startTime: Math.max(0, newStartTime) }; return { ...currentElement, startTime: Math.max(0, newStartTime) };
} }
// Only apply ripple effects if we should process this track
if (!shouldApplyRipple) { if (!shouldApplyRipple) {
return currentElement; return currentElement;
} }
// For ripple editing, we need to move elements that come after the moved element
const currentElementStart = currentElement.startTime; const currentElementStart = currentElement.startTime;
const currentElementEnd = const currentElementEnd =
currentElement.startTime + currentElement.startTime +
@@ -819,19 +782,14 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
currentElement.trimStart - currentElement.trimStart -
currentElement.trimEnd); currentElement.trimEnd);
// If moving element to the right (positive delta)
if (timeDelta > 0) { if (timeDelta > 0) {
// Move elements that start after the original position of the moved element
if (currentElementStart >= oldEndTime) { if (currentElementStart >= oldEndTime) {
return { return {
...currentElement, ...currentElement,
startTime: currentElementStart + timeDelta, startTime: currentElementStart + timeDelta,
}; };
} }
} } else if (timeDelta < 0) {
// If moving element to the left (negative delta)
else if (timeDelta < 0) {
// Move elements that start after the new position of the moved element
if ( if (
currentElementStart >= newEndTime && currentElementStart >= newEndTime &&
currentElementStart >= oldStartTime currentElementStart >= oldStartTime
@@ -846,10 +804,8 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
return currentElement; return currentElement;
}); });
// Check for overlaps and resolve them if necessary
const hasOverlaps = checkElementOverlaps(updatedElements); const hasOverlaps = checkElementOverlaps(updatedElements);
if (hasOverlaps) { if (hasOverlaps) {
// Resolve overlaps by adjusting element positions
const resolvedElements = resolveElementOverlaps(updatedElements); const resolvedElements = resolveElementOverlaps(updatedElements);
return { ...currentTrack, elements: resolvedElements }; 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) => { updateTextElement: (trackId, elementId, updates) => {
get().pushHistory(); get().pushHistory();
updateTracksAndSave( updateTracksAndSave(
@@ -1050,12 +1024,10 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
get().pushHistory(); get().pushHistory();
// Find existing audio track or prepare to create one
const existingAudioTrack = _tracks.find((t) => t.type === "audio"); const existingAudioTrack = _tracks.find((t) => t.type === "audio");
const audioElementId = generateUUID(); const audioElementId = generateUUID();
if (existingAudioTrack) { if (existingAudioTrack) {
// Add audio element to existing audio track
updateTracksAndSave( updateTracksAndSave(
get()._tracks.map((track) => get()._tracks.map((track) =>
track.id === existingAudioTrack.id track.id === existingAudioTrack.id
@@ -1074,7 +1046,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
) )
); );
} else { } else {
// Create new audio track with the audio element in a single atomic update
const newAudioTrack: TimelineTrack = { const newAudioTrack: TimelineTrack = {
id: generateUUID(), id: generateUUID(),
name: "Audio Track", name: "Audio Track",
@@ -1124,7 +1095,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
return { success: false, error: "No active project found" }; return { success: false, error: "No active project found" };
} }
// Import required media processing functions
const { const {
getFileType, getFileType,
getImageDimensions, getImageDimensions,
@@ -1141,7 +1111,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
}; };
} }
// Process the new media file
const mediaData: any = { const mediaData: any = {
name: newFile.name, name: newFile.name,
type: fileType, type: fileType,
@@ -1150,7 +1119,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
}; };
try { try {
// Get media-specific metadata
if (fileType === "image") { if (fileType === "image") {
const { width, height } = await getImageDimensions(newFile); const { width, height } = await getImageDimensions(newFile);
mediaData.width = width; mediaData.width = width;
@@ -1175,7 +1143,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
}; };
} }
// Add new media item to store
try { try {
await mediaStore.addMediaItem( await mediaStore.addMediaItem(
projectStore.activeProject.id, 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( const newMediaItem = mediaStore.mediaItems.find(
(item) => item.file === newFile (item) => item.file === newFile
); );
@@ -1202,7 +1168,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
get().pushHistory(); get().pushHistory();
// Update the timeline element to reference the new media
updateTracksAndSave( updateTracksAndSave(
_tracks.map((track) => _tracks.map((track) =>
track.id === trackId track.id === trackId
@@ -1214,7 +1179,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
...c, ...c,
mediaId: newMediaItem.id, mediaId: newMediaItem.id,
name: newMediaItem.name, name: newMediaItem.name,
// Update duration if the new media has a different duration
duration: newMediaItem.duration || c.duration, duration: newMediaItem.duration || c.duration,
} }
: c : c
@@ -1355,22 +1319,18 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
}); });
}, },
// Persistence methods
loadProjectTimeline: async (projectId) => { loadProjectTimeline: async (projectId) => {
try { try {
const tracks = await storageService.loadTimeline(projectId); const tracks = await storageService.loadTimeline(projectId);
if (tracks) { if (tracks) {
updateTracks(tracks); updateTracks(tracks);
} else { } else {
// No timeline saved yet, initialize with default
const defaultTracks = ensureMainTrack([]); const defaultTracks = ensureMainTrack([]);
updateTracks(defaultTracks); updateTracks(defaultTracks);
} }
// Clear history when loading a project
set({ history: [], redoStack: [] }); set({ history: [], redoStack: [] });
} catch (error) { } catch (error) {
console.error("Failed to load timeline:", error); console.error("Failed to load timeline:", error);
// Initialize with default on error
const defaultTracks = ensureMainTrack([]); const defaultTracks = ensureMainTrack([]);
updateTracks(defaultTracks); updateTracks(defaultTracks);
set({ history: [], redoStack: [] }); set({ history: [], redoStack: [] });
@@ -1429,8 +1389,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
}, },
findOrCreateTrack: (trackType) => { findOrCreateTrack: (trackType) => {
// Always create new text track to allow multiple text elements
// Insert text tracks at the top
if (trackType === "text") { if (trackType === "text") {
return get().insertTrackAt(trackType, 0); return get().insertTrackAt(trackType, 0);
} }
@@ -1448,10 +1406,8 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
const duration = const duration =
item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION; item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION;
// Get all tracks of the right type
const tracks = get()._tracks.filter((t) => t.type === trackType); const tracks = get()._tracks.filter((t) => t.type === trackType);
// Try to find a track with no overlap
let targetTrackId = null; let targetTrackId = null;
for (const track of tracks) { for (const track of tracks) {
if (!get().checkElementOverlap(track.id, currentTime, duration)) { 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) { if (!targetTrackId) {
targetTrackId = get().addTrack(trackType); targetTrackId = get().addTrack(trackType);
} }
@@ -1473,12 +1428,13 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
startTime: currentTime, startTime: currentTime,
trimStart: 0, trimStart: 0,
trimEnd: 0, trimEnd: 0,
muted: false,
}); });
return true; return true;
}, },
addTextAtTime: (item, currentTime = 0) => { 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, { get().addElementToTrack(targetTrackId, {
type: "text", type: "text",
@@ -1516,12 +1472,13 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
startTime: 0, startTime: 0,
trimStart: 0, trimStart: 0,
trimEnd: 0, trimEnd: 0,
muted: false,
}); });
return true; return true;
}, },
addTextToNewTrack: (item) => { 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, { get().addElementToTrack(targetTrackId, {
type: "text", type: "text",
+1
View File
@@ -18,6 +18,7 @@ interface BaseTimelineElement {
export interface MediaElement extends BaseTimelineElement { export interface MediaElement extends BaseTimelineElement {
type: "media"; type: "media";
mediaId: string; mediaId: string;
muted?: boolean;
} }
// Text element with embedded text data // Text element with embedded text data