diff options
Diffstat (limited to 'apps/web/src/components/editor/timeline')
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> ); |
