summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/timeline
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components/editor/timeline')
-rw-r--r--apps/web/src/components/editor/timeline/index.tsx83
-rw-r--r--apps/web/src/components/editor/timeline/timeline-element.tsx53
-rw-r--r--apps/web/src/components/editor/timeline/timeline-playhead.tsx4
3 files changed, 126 insertions, 14 deletions
diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx
index 7943af06..85d6a11e 100644
--- a/apps/web/src/components/editor/timeline/index.tsx
+++ b/apps/web/src/components/editor/timeline/index.tsx
@@ -19,6 +19,7 @@ import {
Link,
ZoomIn,
ZoomOut,
+ Bookmark,
} from "lucide-react";
import {
Tooltip,
@@ -543,7 +544,7 @@ export function Timeline() {
{/* Timeline Header with Ruler */}
<div className="flex bg-panel sticky top-0 z-10">
{/* Track Labels Header */}
- <div className="w-48 flex-shrink-0 bg-panel border-r flex items-center justify-between px-3 py-2">
+ <div className="w-48 shrink-0 bg-panel border-r flex items-center justify-between px-3 py-2">
{/* Empty space */}
<span className="text-sm font-medium text-muted-foreground opacity-0">
.
@@ -552,7 +553,7 @@ export function Timeline() {
{/* Timeline Ruler */}
<div
- className="flex-1 relative overflow-hidden h-4"
+ className="flex-1 relative overflow-hidden h-10"
onWheel={(e) => {
// Check if this is horizontal scrolling - if so, don't handle it here
if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
@@ -567,7 +568,7 @@ export function Timeline() {
<ScrollArea className="w-full" ref={rulerScrollRef}>
<div
ref={rulerRef}
- className="relative h-4 select-none cursor-default"
+ className="relative h-10 select-none cursor-default"
style={{
width: `${dynamicTimelineWidth}px`,
}}
@@ -601,7 +602,7 @@ export function Timeline() {
return (
<div
key={i}
- className={`absolute top-0 bottom-0 ${
+ className={`absolute top-0 h-4 ${
isMainMarker
? "border-l border-muted-foreground/40"
: "border-l border-muted-foreground/20"
@@ -651,6 +652,30 @@ export function Timeline() {
);
}).filter(Boolean);
})()}
+
+ {/* Bookmark markers */}
+ {(() => {
+ const { activeProject } = useProjectStore.getState();
+ if (!activeProject?.bookmarks?.length) return null;
+
+ return activeProject.bookmarks.map((bookmarkTime, i) => (
+ <div
+ key={`bookmark-${i}`}
+ className="absolute top-0 h-10 w-0.5 !bg-primary cursor-pointer"
+ style={{
+ left: `${bookmarkTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
+ }}
+ onClick={(e) => {
+ e.stopPropagation();
+ usePlaybackStore.getState().seek(bookmarkTime);
+ }}
+ >
+ <div className="absolute top-[-1px] left-[-5px] text-primary">
+ <Bookmark className="h-3 w-3 fill-primary" />
+ </div>
+ </div>
+ ));
+ })()}
</div>
</ScrollArea>
</div>
@@ -662,7 +687,7 @@ export function Timeline() {
{tracks.length > 0 && (
<div
ref={trackLabelsRef}
- className="w-48 flex-shrink-0 border-r border-black overflow-y-auto z-[200] bg-panel"
+ className="w-48 shrink-0 border-r border-black overflow-y-auto z-200 bg-panel"
data-track-labels
>
<ScrollArea className="w-full h-full" ref={trackLabelsScrollRef}>
@@ -677,7 +702,7 @@ export function Timeline() {
<TrackIcon track={track} />
</div>
{track.muted && (
- <span className="ml-2 text-xs text-red-500 font-semibold flex-shrink-0">
+ <span className="ml-2 text-xs text-red-500 font-semibold shrink-0">
Muted
</span>
)}
@@ -761,7 +786,7 @@ export function Timeline() {
/>
</div>
</ContextMenuTrigger>
- <ContextMenuContent className="z-[200]">
+ <ContextMenuContent className="z-200">
<ContextMenuItem
onClick={(e) => {
e.stopPropagation();
@@ -773,6 +798,23 @@ export function Timeline() {
<ContextMenuItem onClick={(e) => e.stopPropagation()}>
Track settings (soon)
</ContextMenuItem>
+ {activeProject?.bookmarks?.length && activeProject.bookmarks.length > 0 && (
+ <>
+ <ContextMenuItem disabled>Bookmarks</ContextMenuItem>
+ {activeProject.bookmarks.map((bookmarkTime, i) => (
+ <ContextMenuItem
+ key={`bookmark-menu-${i}`}
+ onClick={(e) => {
+ e.stopPropagation();
+ seek(bookmarkTime);
+ }}
+ >
+ <Bookmark className="h-3 w-3 mr-2 inline-block" />
+ {bookmarkTime.toFixed(1)}s
+ </ContextMenuItem>
+ ))}
+ </>
+ )}
</ContextMenuContent>
</ContextMenu>
))}
@@ -791,13 +833,13 @@ function TrackIcon({ track }: { track: TimelineTrack }) {
return (
<>
{track.type === "media" && (
- <Video className="w-4 h-4 flex-shrink-0 text-muted-foreground" />
+ <Video className="w-4 h-4 shrink-0 text-muted-foreground" />
)}
{track.type === "text" && (
- <TypeIcon className="w-4 h-4 flex-shrink-0 text-muted-foreground" />
+ <TypeIcon className="w-4 h-4 shrink-0 text-muted-foreground" />
)}
{track.type === "audio" && (
- <Music className="w-4 h-4 flex-shrink-0 text-muted-foreground" />
+ <Music className="w-4 h-4 shrink-0 text-muted-foreground" />
)}
</>
);
@@ -828,6 +870,7 @@ function TimelineToolbar({
toggleRippleEditing,
} = useTimelineStore();
const { currentTime, duration, isPlaying, toggle } = usePlaybackStore();
+ const { toggleBookmark, isBookmarked } = useProjectStore();
// Action handlers
const handleSplitSelected = () => {
@@ -957,6 +1000,13 @@ function TimelineToolbar({
const handleZoomSliderChange = (values: number[]) => {
setZoomLevel(values[0]);
};
+
+ const handleToggleBookmark = async () => {
+ await toggleBookmark(currentTime);
+ };
+
+ // Check if the current time is bookmarked
+ const currentBookmarked = isBookmarked(currentTime);
return (
<div className="border-b flex items-center justify-between px-2 py-1">
<div className="flex items-center gap-1 w-full">
@@ -1088,6 +1138,17 @@ function TimelineToolbar({
</TooltipTrigger>
<TooltipContent>Delete element (Delete)</TooltipContent>
</Tooltip>
+ <div className="w-px h-6 bg-border mx-1" />
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="text" size="icon" onClick={handleToggleBookmark}>
+ <Bookmark className={`h-4 w-4 ${currentBookmarked ? "fill-primary text-primary" : ""}`} />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>
+ {currentBookmarked ? "Remove bookmark" : "Add bookmark"}
+ </TooltipContent>
+ </Tooltip>
</TooltipProvider>
</div>
<div className="flex items-center gap-1">
@@ -1121,6 +1182,8 @@ function TimelineToolbar({
</TooltipContent>
</Tooltip>
</TooltipProvider>
+
+ <div className="h-6 w-px bg-border mx-1" />
<div className="flex items-center gap-1">
<Button variant="text" size="icon" onClick={handleZoomOut}>
<ZoomOut className="h-4 w-4" />
diff --git a/apps/web/src/components/editor/timeline/timeline-element.tsx b/apps/web/src/components/editor/timeline/timeline-element.tsx
index f581bbf0..c7ed833e 100644
--- a/apps/web/src/components/editor/timeline/timeline-element.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-element.tsx
@@ -13,6 +13,10 @@ import {
Type,
Copy,
RefreshCw,
+ EyeOff,
+ Eye,
+ Volume2,
+ VolumeX,
} from "lucide-react";
import { useMediaStore } from "@/stores/media-store";
import { useTimelineStore } from "@/stores/timeline-store";
@@ -66,11 +70,18 @@ export function TimelineElement({
addElementToTrack,
replaceElementMedia,
rippleEditingEnabled,
+ toggleElementHidden,
} = useTimelineStore();
const { currentTime } = usePlaybackStore();
const [elementMenuOpen, setElementMenuOpen] = useState(false);
+ const mediaItem =
+ element.type === "media"
+ ? mediaItems.find((item) => item.id === element.mediaId)
+ : null;
+ const isAudio = mediaItem?.type === "audio";
+
const {
resizing,
isResizing,
@@ -141,6 +152,11 @@ export function TimelineElement({
}
};
+ const handleToggleElementHidden = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ toggleElementHidden(track.id, element.id);
+ };
+
const handleReplaceClip = (e: React.MouseEvent) => {
e.stopPropagation();
if (element.type !== "media") {
@@ -332,7 +348,7 @@ export function TimelineElement({
track.type
)} ${isSelected ? "border-b-[0.5px] border-t-[0.5px] border-foreground" : ""} ${
isBeingDragged ? "z-50" : "z-10"
- }`}
+ } ${element.hidden ? "opacity-50" : ""}`}
onClick={(e) => onElementClick && onElementClick(e, element)}
onMouseDown={handleElementMouseDown}
onContextMenu={(e) =>
@@ -343,6 +359,16 @@ export function TimelineElement({
{renderElementContent()}
</div>
+ {element.hidden && (
+ <div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center pointer-events-none">
+ {isAudio ? (
+ <VolumeX className="h-6 w-6 text-white" />
+ ) : (
+ <EyeOff className="h-6 w-6 text-white" />
+ )}
+ </div>
+ )}
+
{isSelected && (
<>
<div
@@ -358,11 +384,34 @@ export function TimelineElement({
</div>
</div>
</ContextMenuTrigger>
- <ContextMenuContent className="z-[200]">
+ <ContextMenuContent className="z-200">
<ContextMenuItem onClick={handleElementSplitContext}>
<Scissors className="h-4 w-4 mr-2" />
Split at playhead
</ContextMenuItem>
+ <ContextMenuItem onClick={handleToggleElementHidden}>
+ {isAudio ? (
+ element.hidden ? (
+ <Volume2 className="h-4 w-4 mr-2" />
+ ) : (
+ <VolumeX className="h-4 w-4 mr-2" />
+ )
+ ) : element.hidden ? (
+ <Eye className="h-4 w-4 mr-2" />
+ ) : (
+ <EyeOff className="h-4 w-4 mr-2" />
+ )}
+ <span>
+ {isAudio
+ ? element.hidden
+ ? "Unmute"
+ : "Mute"
+ : element.hidden
+ ? "Show"
+ : "Hide"}{" "}
+ {element.type === "text" ? "text" : "clip"}
+ </span>
+ </ContextMenuItem>
<ContextMenuItem onClick={handleElementDuplicateContext}>
<Copy className="h-4 w-4 mr-2" />
Duplicate {element.type === "text" ? "text" : "clip"}
diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx
index b33166c0..ea70419b 100644
--- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-playhead.tsx
@@ -126,7 +126,7 @@ export function TimelinePlayhead({
return (
<div
ref={playheadRef}
- className="absolute pointer-events-auto z-[150]"
+ className="absolute pointer-events-auto z-150"
style={{
left: `${leftPosition}px`,
top: 0,
@@ -142,7 +142,7 @@ export function TimelinePlayhead({
{/* Playhead dot indicator at the top (in ruler area) */}
<div
- className={`absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 rounded-full border-2 shadow-sm ${isSnappingToPlayhead ? "bg-primary border-primary" : "bg-foreground border-foreground"}`}
+ className={`absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 rounded-full border-2 shadow-xs ${isSnappingToPlayhead ? "bg-primary border-primary" : "bg-foreground border-foreground"}`}
/>
</div>
);