mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: add TimelineToolbar back
This commit is contained in:
@@ -64,23 +64,11 @@ export function Timeline() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
tracks,
|
tracks,
|
||||||
addTrack,
|
|
||||||
addElementToTrack,
|
|
||||||
removeElementFromTrack,
|
|
||||||
removeElementFromTrackWithRipple,
|
|
||||||
getTotalDuration,
|
getTotalDuration,
|
||||||
selectedElements,
|
|
||||||
clearSelectedElements,
|
clearSelectedElements,
|
||||||
setSelectedElements,
|
|
||||||
splitElement,
|
|
||||||
splitAndKeepLeft,
|
|
||||||
splitAndKeepRight,
|
|
||||||
toggleTrackMute,
|
|
||||||
separateAudio,
|
|
||||||
snappingEnabled,
|
snappingEnabled,
|
||||||
toggleSnapping,
|
setSelectedElements,
|
||||||
rippleEditingEnabled,
|
toggleTrackMute,
|
||||||
toggleRippleEditing,
|
|
||||||
dragState,
|
dragState,
|
||||||
} = useTimelineStore();
|
} = useTimelineStore();
|
||||||
const { mediaItems, addMediaItem } = useMediaStore();
|
const { mediaItems, addMediaItem } = useMediaStore();
|
||||||
@@ -430,139 +418,6 @@ export function Timeline() {
|
|||||||
onDrop: handleDrop,
|
onDrop: handleDrop,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Action handlers for toolbar
|
|
||||||
const handleSplitSelected = () => {
|
|
||||||
if (selectedElements.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let splitCount = 0;
|
|
||||||
selectedElements.forEach(({ trackId, elementId }) => {
|
|
||||||
const track = tracks.find((t) => t.id === trackId);
|
|
||||||
const element = track?.elements.find((c) => c.id === elementId);
|
|
||||||
if (element && track) {
|
|
||||||
const effectiveStart = element.startTime;
|
|
||||||
const effectiveEnd =
|
|
||||||
element.startTime +
|
|
||||||
(element.duration - element.trimStart - element.trimEnd);
|
|
||||||
|
|
||||||
if (currentTime > effectiveStart && currentTime < effectiveEnd) {
|
|
||||||
const newElementId = splitElement(trackId, elementId, currentTime);
|
|
||||||
if (newElementId) splitCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (splitCount === 0) {
|
|
||||||
toast.error("Playhead must be within selected elements to split");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDuplicateSelected = () => {
|
|
||||||
if (selectedElements.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const canDuplicate = selectedElements.length === 1;
|
|
||||||
if (!canDuplicate) return;
|
|
||||||
|
|
||||||
const newSelections: { trackId: string; elementId: string }[] = [];
|
|
||||||
|
|
||||||
selectedElements.forEach(({ trackId, elementId }) => {
|
|
||||||
const track = tracks.find((t) => t.id === trackId);
|
|
||||||
const element = track?.elements.find((el) => el.id === elementId);
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
const newStartTime =
|
|
||||||
element.startTime +
|
|
||||||
(element.duration - element.trimStart - element.trimEnd) +
|
|
||||||
0.1;
|
|
||||||
|
|
||||||
// Create element without id (will be generated by store)
|
|
||||||
const { id, ...elementWithoutId } = element;
|
|
||||||
|
|
||||||
addElementToTrack(trackId, {
|
|
||||||
...elementWithoutId,
|
|
||||||
startTime: newStartTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
// We can't predict the new id, so just clear selection for now
|
|
||||||
// TODO: addElementToTrack could return the new element id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
clearSelectedElements();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFreezeSelected = () => {
|
|
||||||
toast.info("Freeze frame functionality coming soon!");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSplitAndKeepLeft = () => {
|
|
||||||
if (selectedElements.length !== 1) {
|
|
||||||
toast.error("Select exactly one element");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { trackId, elementId } = selectedElements[0];
|
|
||||||
const track = tracks.find((t) => t.id === trackId);
|
|
||||||
const element = track?.elements.find((c) => c.id === elementId);
|
|
||||||
if (!element) return;
|
|
||||||
const effectiveStart = element.startTime;
|
|
||||||
const effectiveEnd =
|
|
||||||
element.startTime +
|
|
||||||
(element.duration - element.trimStart - element.trimEnd);
|
|
||||||
if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
|
|
||||||
toast.error("Playhead must be within selected element");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
splitAndKeepLeft(trackId, elementId, currentTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSplitAndKeepRight = () => {
|
|
||||||
if (selectedElements.length !== 1) {
|
|
||||||
toast.error("Select exactly one element");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { trackId, elementId } = selectedElements[0];
|
|
||||||
const track = tracks.find((t) => t.id === trackId);
|
|
||||||
const element = track?.elements.find((c) => c.id === elementId);
|
|
||||||
if (!element) return;
|
|
||||||
const effectiveStart = element.startTime;
|
|
||||||
const effectiveEnd =
|
|
||||||
element.startTime +
|
|
||||||
(element.duration - element.trimStart - element.trimEnd);
|
|
||||||
if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
|
|
||||||
toast.error("Playhead must be within selected element");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
splitAndKeepRight(trackId, elementId, currentTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSeparateAudio = () => {
|
|
||||||
if (selectedElements.length !== 1) {
|
|
||||||
toast.error("Select exactly one media element to separate audio");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { trackId, elementId } = selectedElements[0];
|
|
||||||
const track = tracks.find((t) => t.id === trackId);
|
|
||||||
if (!track || track.type !== "media") {
|
|
||||||
toast.error("Select a media element to separate audio");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
separateAudio(trackId, elementId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteSelected = () => {
|
|
||||||
if (selectedElements.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedElements.forEach(({ trackId, elementId }) => {
|
|
||||||
if (rippleEditingEnabled) {
|
|
||||||
removeElementFromTrackWithRipple(trackId, elementId);
|
|
||||||
} else {
|
|
||||||
removeElementFromTrack(trackId, elementId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
clearSelectedElements();
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Scroll synchronization effect ---
|
// --- Scroll synchronization effect ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const rulerViewport = rulerScrollRef.current?.querySelector(
|
const rulerViewport = rulerScrollRef.current?.querySelector(
|
||||||
@@ -649,192 +504,7 @@ export function Timeline() {
|
|||||||
onMouseEnter={() => setIsInTimeline(true)}
|
onMouseEnter={() => setIsInTimeline(true)}
|
||||||
onMouseLeave={() => setIsInTimeline(false)}
|
onMouseLeave={() => setIsInTimeline(false)}
|
||||||
>
|
>
|
||||||
{/* Toolbar */}
|
<TimelineToolbar />
|
||||||
<div className="border-b flex items-center justify-between px-2 py-1">
|
|
||||||
<div className="flex items-center gap-1 w-full">
|
|
||||||
<TooltipProvider delayDuration={500}>
|
|
||||||
{/* Play/Pause Button */}
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="icon"
|
|
||||||
onClick={toggle}
|
|
||||||
className="mr-2"
|
|
||||||
>
|
|
||||||
{isPlaying ? (
|
|
||||||
<Pause className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Play className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
{isPlaying ? "Pause (Space)" : "Play (Space)"}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<div className="w-px h-6 bg-border mx-1" />
|
|
||||||
{/* Time Display */}
|
|
||||||
<div
|
|
||||||
className="text-xs text-muted-foreground font-mono px-2"
|
|
||||||
style={{ minWidth: "18ch", textAlign: "center" }}
|
|
||||||
>
|
|
||||||
{currentTime.toFixed(1)}s / {duration.toFixed(1)}s
|
|
||||||
</div>
|
|
||||||
{/* Test Clip Button - for debugging */}
|
|
||||||
{tracks.length === 0 && (
|
|
||||||
<>
|
|
||||||
<div className="w-px h-6 bg-border mx-1" />
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
const trackId = addTrack("media");
|
|
||||||
addElementToTrack(trackId, {
|
|
||||||
type: "media",
|
|
||||||
mediaId: "test",
|
|
||||||
name: "Test Clip",
|
|
||||||
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
|
||||||
startTime: 0,
|
|
||||||
trimStart: 0,
|
|
||||||
trimEnd: 0,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
Add Test Clip
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
Add a test clip to try playback
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="w-px h-6 bg-border mx-1" />
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleSplitSelected}
|
|
||||||
>
|
|
||||||
<Scissors className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Split element (Ctrl+S)</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleSplitAndKeepLeft}
|
|
||||||
>
|
|
||||||
<ArrowLeftToLine className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Split and keep left (Ctrl+Q)</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleSplitAndKeepRight}
|
|
||||||
>
|
|
||||||
<ArrowRightToLine className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Split and keep right (Ctrl+W)</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleSeparateAudio}
|
|
||||||
>
|
|
||||||
<SplitSquareHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Separate audio (Ctrl+D)</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleDuplicateSelected}
|
|
||||||
>
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Duplicate element (Ctrl+D)</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleFreezeSelected}
|
|
||||||
>
|
|
||||||
<Snowflake className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Freeze frame (F)</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleDeleteSelected}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Delete element (Delete)</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<TooltipProvider delayDuration={500}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button variant="text" size="icon" onClick={toggleSnapping}>
|
|
||||||
{snappingEnabled ? (
|
|
||||||
<LockOpen className="h-4 w-4 text-primary" />
|
|
||||||
) : (
|
|
||||||
<Lock className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Auto snapping</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size="icon"
|
|
||||||
onClick={toggleRippleEditing}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
className={`h-4 w-4 ${rippleEditingEnabled ? "text-primary" : ""}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
{rippleEditingEnabled
|
|
||||||
? "Disable Ripple Editing"
|
|
||||||
: "Enable Ripple Editing"}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timeline Container */}
|
{/* Timeline Container */}
|
||||||
<div
|
<div
|
||||||
@@ -1107,3 +777,307 @@ function TrackIcon({ track }: { track: TimelineTrack }) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TimelineToolbar() {
|
||||||
|
const {
|
||||||
|
tracks,
|
||||||
|
addTrack,
|
||||||
|
addElementToTrack,
|
||||||
|
removeElementFromTrack,
|
||||||
|
removeElementFromTrackWithRipple,
|
||||||
|
selectedElements,
|
||||||
|
clearSelectedElements,
|
||||||
|
splitElement,
|
||||||
|
splitAndKeepLeft,
|
||||||
|
splitAndKeepRight,
|
||||||
|
separateAudio,
|
||||||
|
snappingEnabled,
|
||||||
|
toggleSnapping,
|
||||||
|
rippleEditingEnabled,
|
||||||
|
toggleRippleEditing,
|
||||||
|
} = useTimelineStore();
|
||||||
|
const { currentTime, duration, isPlaying, toggle } = usePlaybackStore();
|
||||||
|
const { activeProject } = useProjectStore();
|
||||||
|
const { mediaItems, addMediaItem } = useMediaStore();
|
||||||
|
|
||||||
|
// Action handlers
|
||||||
|
const handleSplitSelected = () => {
|
||||||
|
if (selectedElements.length === 0) return;
|
||||||
|
let splitCount = 0;
|
||||||
|
selectedElements.forEach(({ trackId, elementId }) => {
|
||||||
|
const track = tracks.find((t) => t.id === trackId);
|
||||||
|
const element = track?.elements.find((c) => c.id === elementId);
|
||||||
|
if (element && track) {
|
||||||
|
const effectiveStart = element.startTime;
|
||||||
|
const effectiveEnd =
|
||||||
|
element.startTime +
|
||||||
|
(element.duration - element.trimStart - element.trimEnd);
|
||||||
|
if (currentTime > effectiveStart && currentTime < effectiveEnd) {
|
||||||
|
const newElementId = splitElement(trackId, elementId, currentTime);
|
||||||
|
if (newElementId) splitCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (splitCount === 0) {
|
||||||
|
toast.error("Playhead must be within selected elements to split");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDuplicateSelected = () => {
|
||||||
|
if (selectedElements.length === 0) return;
|
||||||
|
const canDuplicate = selectedElements.length === 1;
|
||||||
|
if (!canDuplicate) return;
|
||||||
|
|
||||||
|
selectedElements.forEach(({ trackId, elementId }) => {
|
||||||
|
const track = tracks.find((t) => t.id === trackId);
|
||||||
|
const element = track?.elements.find((el) => el.id === elementId);
|
||||||
|
if (element) {
|
||||||
|
const newStartTime =
|
||||||
|
element.startTime +
|
||||||
|
(element.duration - element.trimStart - element.trimEnd) +
|
||||||
|
0.1;
|
||||||
|
const { id, ...elementWithoutId } = element;
|
||||||
|
addElementToTrack(trackId, {
|
||||||
|
...elementWithoutId,
|
||||||
|
startTime: newStartTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clearSelectedElements();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFreezeSelected = () => {
|
||||||
|
toast.info("Freeze frame functionality coming soon!");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSplitAndKeepLeft = () => {
|
||||||
|
if (selectedElements.length !== 1) {
|
||||||
|
toast.error("Select exactly one element");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { trackId, elementId } = selectedElements[0];
|
||||||
|
const track = tracks.find((t) => t.id === trackId);
|
||||||
|
const element = track?.elements.find((c) => c.id === elementId);
|
||||||
|
if (!element) return;
|
||||||
|
const effectiveStart = element.startTime;
|
||||||
|
const effectiveEnd =
|
||||||
|
element.startTime +
|
||||||
|
(element.duration - element.trimStart - element.trimEnd);
|
||||||
|
if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
|
||||||
|
toast.error("Playhead must be within selected element");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
splitAndKeepLeft(trackId, elementId, currentTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSplitAndKeepRight = () => {
|
||||||
|
if (selectedElements.length !== 1) {
|
||||||
|
toast.error("Select exactly one element");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { trackId, elementId } = selectedElements[0];
|
||||||
|
const track = tracks.find((t) => t.id === trackId);
|
||||||
|
const element = track?.elements.find((c) => c.id === elementId);
|
||||||
|
if (!element) return;
|
||||||
|
const effectiveStart = element.startTime;
|
||||||
|
const effectiveEnd =
|
||||||
|
element.startTime +
|
||||||
|
(element.duration - element.trimStart - element.trimEnd);
|
||||||
|
if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
|
||||||
|
toast.error("Playhead must be within selected element");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
splitAndKeepRight(trackId, elementId, currentTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSeparateAudio = () => {
|
||||||
|
if (selectedElements.length !== 1) {
|
||||||
|
toast.error("Select exactly one media element to separate audio");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { trackId, elementId } = selectedElements[0];
|
||||||
|
const track = tracks.find((t) => t.id === trackId);
|
||||||
|
if (!track || track.type !== "media") {
|
||||||
|
toast.error("Select a media element to separate audio");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
separateAudio(trackId, elementId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteSelected = () => {
|
||||||
|
if (selectedElements.length === 0) return;
|
||||||
|
selectedElements.forEach(({ trackId, elementId }) => {
|
||||||
|
if (rippleEditingEnabled) {
|
||||||
|
removeElementFromTrackWithRipple(trackId, elementId);
|
||||||
|
} else {
|
||||||
|
removeElementFromTrack(trackId, elementId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clearSelectedElements();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="border-b flex items-center justify-between px-2 py-1">
|
||||||
|
<div className="flex items-center gap-1 w-full">
|
||||||
|
<TooltipProvider delayDuration={500}>
|
||||||
|
{/* Play/Pause Button */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggle}
|
||||||
|
className="mr-2"
|
||||||
|
>
|
||||||
|
{isPlaying ? (
|
||||||
|
<Pause className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{isPlaying ? "Pause (Space)" : "Play (Space)"}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
|
{/* Time Display */}
|
||||||
|
<div
|
||||||
|
className="text-xs text-muted-foreground font-mono px-2"
|
||||||
|
style={{ minWidth: "18ch", textAlign: "center" }}
|
||||||
|
>
|
||||||
|
{currentTime.toFixed(1)}s / {duration.toFixed(1)}s
|
||||||
|
</div>
|
||||||
|
{/* Test Clip Button - for debugging */}
|
||||||
|
{tracks.length === 0 && (
|
||||||
|
<>
|
||||||
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const trackId = addTrack("media");
|
||||||
|
addElementToTrack(trackId, {
|
||||||
|
type: "media",
|
||||||
|
mediaId: "test",
|
||||||
|
name: "Test Clip",
|
||||||
|
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
||||||
|
startTime: 0,
|
||||||
|
trimStart: 0,
|
||||||
|
trimEnd: 0,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
Add Test Clip
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Add a test clip to try playback</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="text" size="icon" onClick={handleSplitSelected}>
|
||||||
|
<Scissors className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Split element (Ctrl+S)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleSplitAndKeepLeft}
|
||||||
|
>
|
||||||
|
<ArrowLeftToLine className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Split and keep left (Ctrl+Q)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleSplitAndKeepRight}
|
||||||
|
>
|
||||||
|
<ArrowRightToLine className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Split and keep right (Ctrl+W)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="text" size="icon" onClick={handleSeparateAudio}>
|
||||||
|
<SplitSquareHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Separate audio (Ctrl+D)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleDuplicateSelected}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Duplicate element (Ctrl+D)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="text" size="icon" onClick={handleFreezeSelected}>
|
||||||
|
<Snowflake className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Freeze frame (F)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="text" size="icon" onClick={handleDeleteSelected}>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Delete element (Delete)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<TooltipProvider delayDuration={500}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="text" size="icon" onClick={toggleSnapping}>
|
||||||
|
{snappingEnabled ? (
|
||||||
|
<LockOpen className="h-4 w-4 text-primary" />
|
||||||
|
) : (
|
||||||
|
<Lock className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Auto snapping</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="text" size="icon" onClick={toggleRippleEditing}>
|
||||||
|
<Link
|
||||||
|
className={`h-4 w-4 ${rippleEditingEnabled ? "text-primary" : ""}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{rippleEditingEnabled
|
||||||
|
? "Disable Ripple Editing"
|
||||||
|
: "Enable Ripple Editing"}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user