diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-08-01 19:57:26 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-08-01 19:57:26 +0200 |
| commit | 68da487f304018b6e8d06459def8fb45514ab7d4 (patch) | |
| tree | b20bd61558254b358e298554ecde005f498b17b3 /apps/web/src/components/editor/timeline | |
| parent | 09cb8c8d984e3e82f60e28b352508c876423fbdf (diff) | |
| download | opencut-68da487f304018b6e8d06459def8fb45514ab7d4.tar.gz | |
style: complete re-design of whole editor
Diffstat (limited to 'apps/web/src/components/editor/timeline')
3 files changed, 62 insertions, 147 deletions
diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx index 85d6a11e..8f41fbf3 100644 --- a/apps/web/src/components/editor/timeline/index.tsx +++ b/apps/web/src/components/editor/timeline/index.tsx @@ -20,6 +20,9 @@ import { ZoomIn, ZoomOut, Bookmark, + Eye, + MicOff, + Mic, } from "lucide-react"; import { Tooltip, @@ -544,7 +547,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 shrink-0 bg-panel border-r flex items-center justify-between px-3 py-2"> + <div className="w-28 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"> . @@ -652,14 +655,14 @@ export function Timeline() { ); }).filter(Boolean); })()} - + {/* Bookmark markers */} {(() => { const { activeProject } = useProjectStore.getState(); if (!activeProject?.bookmarks?.length) return null; - + return activeProject.bookmarks.map((bookmarkTime, i) => ( - <div + <div key={`bookmark-${i}`} className="absolute top-0 h-10 w-0.5 !bg-primary cursor-pointer" style={{ @@ -687,7 +690,7 @@ export function Timeline() { {tracks.length > 0 && ( <div ref={trackLabelsRef} - className="w-48 shrink-0 border-r border-black overflow-y-auto z-200 bg-panel" + className="w-28 shrink-0 border-r overflow-y-auto z-100 bg-panel" data-track-labels > <ScrollArea className="w-full h-full" ref={trackLabelsScrollRef}> @@ -695,17 +698,24 @@ export function Timeline() { {tracks.map((track) => ( <div key={track.id} - className="flex items-center px-3 border-b border-muted/30 group bg-foreground/5" + className="flex items-center px-3 group" style={{ height: `${getTrackHeight(track.type)}px` }} > - <div className="flex items-center flex-1 min-w-0"> + <div className="flex items-center justify-end flex-1 min-w-0 gap-2"> + {track.muted ? ( + <MicOff + className="h-4 w-4 text-destructive cursor-pointer" + onClick={() => toggleTrackMute(track.id)} + /> + ) : ( + <Mic + className="h-4 w-4 text-muted-foreground cursor-pointer" + onClick={() => toggleTrackMute(track.id)} + /> + )} + <Eye className="h-4 w-4 text-muted-foreground" /> <TrackIcon track={track} /> </div> - {track.muted && ( - <span className="ml-2 text-xs text-red-500 font-semibold shrink-0"> - Muted - </span> - )} </div> ))} </div> @@ -760,7 +770,7 @@ export function Timeline() { <ContextMenu key={track.id}> <ContextMenuTrigger asChild> <div - className="absolute left-0 right-0 border-b border-muted/30 py-[0.05rem]" + className="absolute left-0 right-0" style={{ top: `${getCumulativeHeightBefore( tracks, @@ -798,23 +808,6 @@ 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> ))} @@ -1000,11 +993,11 @@ 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 ( @@ -1142,7 +1135,9 @@ function TimelineToolbar({ <Tooltip> <TooltipTrigger asChild> <Button variant="text" size="icon" onClick={handleToggleBookmark}> - <Bookmark className={`h-4 w-4 ${currentBookmarked ? "fill-primary text-primary" : ""}`} /> + <Bookmark + className={`h-4 w-4 ${currentBookmarked ? "fill-primary text-primary" : ""}`} + /> </Button> </TooltipTrigger> <TooltipContent> diff --git a/apps/web/src/components/editor/timeline/timeline-element.tsx b/apps/web/src/components/editor/timeline/timeline-element.tsx index c7ed833e..b26a14bb 100644 --- a/apps/web/src/components/editor/timeline/timeline-element.tsx +++ b/apps/web/src/components/editor/timeline/timeline-element.tsx @@ -1,16 +1,8 @@ "use client"; -import { useState } from "react"; -import { Button } from "../../ui/button"; import { - MoreVertical, Scissors, Trash2, - SplitSquareHorizontal, - Music, - ChevronRight, - ChevronLeft, - Type, Copy, RefreshCw, EyeOff, @@ -23,7 +15,7 @@ import { useTimelineStore } from "@/stores/timeline-store"; import { usePlaybackStore } from "@/stores/playback-store"; import AudioWaveform from "../audio-waveform"; import { toast } from "sonner"; -import { TimelineElementProps, TrackType } from "@/types/timeline"; +import { TimelineElementProps } from "@/types/timeline"; import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize"; import { getTrackElementClasses, @@ -31,16 +23,6 @@ import { getTrackHeight, } from "@/constants/timeline-constants"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, -} from "../../ui/dropdown-menu"; -import { ContextMenu, ContextMenuContent, ContextMenuItem, @@ -64,9 +46,6 @@ export function TimelineElement({ removeElementFromTrackWithRipple, dragState, splitElement, - splitAndKeepLeft, - splitAndKeepRight, - separateAudio, addElementToTrack, replaceElementMedia, rippleEditingEnabled, @@ -74,27 +53,20 @@ export function TimelineElement({ } = 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, - handleResizeStart, - handleResizeMove, - handleResizeEnd, - } = useTimelineElementResize({ - element, - track, - zoomLevel, - onUpdateTrim: updateElementTrim, - onUpdateDuration: updateElementDuration, - }); + const { resizing, handleResizeStart, handleResizeMove, handleResizeEnd } = + useTimelineElementResize({ + element, + track, + zoomLevel, + onUpdateTrim: updateElementTrim, + onUpdateDuration: updateElementDuration, + }); const effectiveDuration = element.duration - element.trimStart - element.trimEnd; @@ -193,9 +165,7 @@ export function TimelineElement({ if (element.type === "text") { return ( <div className="w-full h-full flex items-center justify-start pl-2"> - <span className="text-xs text-foreground/80 truncate"> - {element.content} - </span> + <span className="text-xs text-white truncate">{element.content}</span> </div> ); } @@ -212,86 +182,36 @@ export function TimelineElement({ const TILE_ASPECT_RATIO = 16 / 9; - if (mediaItem.type === "image") { + if ( + mediaItem.type === "image" || + (mediaItem.type === "video" && mediaItem.thumbnailUrl) + ) { // Calculate tile size based on 16:9 aspect ratio const trackHeight = getTrackHeight(track.type); - const tileHeight = trackHeight - 8; // Account for padding + const tileHeight = trackHeight; 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 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 - 8; // Match image padding - const tileWidth = tileHeight * TILE_ASPECT_RATIO; + const imageUrl = + mediaItem.type === "image" ? mediaItem.url : mediaItem.thumbnailUrl; + const isImage = mediaItem.type === "image"; return ( <div className="w-full h-full flex items-center justify-center"> - <div className="bg-[#004D52] py-3 w-full h-full relative"> - {/* Background with tiled thumbnails */} + <div + className={`w-full h-full relative ${ + isSelected ? "bg-primary" : "bg-transparent" + }`} + > <div - className="absolute top-3 bottom-3 left-0 right-0" + className={`absolute top-[0.15rem] bottom-[0.15rem] left-0 right-0`} style={{ - backgroundImage: mediaItem.thumbnailUrl - ? `url(${mediaItem.thumbnailUrl})` - : "none", + backgroundImage: imageUrl ? `url(${imageUrl})` : "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 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", - }} + aria-label={`Tiled ${isImage ? "background" : "thumbnail"} of ${mediaItem.name}`} /> </div> </div> @@ -346,7 +266,7 @@ export function TimelineElement({ <div className={`relative h-full rounded-[0.15rem] cursor-pointer overflow-hidden ${getTrackElementClasses( track.type - )} ${isSelected ? "border-b-[0.5px] border-t-[0.5px] border-foreground" : ""} ${ + )} ${isSelected ? "" : ""} ${ isBeingDragged ? "z-50" : "z-10" } ${element.hidden ? "opacity-50" : ""}`} onClick={(e) => onElementClick && onElementClick(e, element)} @@ -372,11 +292,11 @@ export function TimelineElement({ {isSelected && ( <> <div - className="absolute left-0 top-0 bottom-0 w-1 cursor-w-resize bg-foreground z-50" + className="absolute left-0 top-0 bottom-0 w-[0.2rem] cursor-w-resize bg-primary z-50" onMouseDown={(e) => handleResizeStart(e, element.id, "left")} /> <div - className="absolute right-0 top-0 bottom-0 w-1 cursor-e-resize bg-foreground z-50" + className="absolute right-0 top-0 bottom-0 w-[0.2rem] cursor-e-resize bg-primary z-50" onMouseDown={(e) => handleResizeStart(e, element.id, "right")} /> </> @@ -407,8 +327,8 @@ export function TimelineElement({ ? "Unmute" : "Mute" : element.hidden - ? "Show" - : "Hide"}{" "} + ? "Show" + : "Hide"}{" "} {element.type === "text" ? "text" : "clip"} </span> </ContextMenuItem> diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx index ea70419b..695843a4 100644 --- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx +++ b/apps/web/src/components/editor/timeline/timeline-playhead.tsx @@ -70,7 +70,7 @@ export function TimelinePlayhead({ // Use timeline container height minus a few pixels for breathing room const timelineContainerHeight = timelineRef.current?.offsetHeight || 400; - const totalHeight = timelineContainerHeight - 8; // 8px padding from edges + const totalHeight = timelineContainerHeight - 4; // Get dynamic track labels width, fallback to 0 if no tracks or no ref const trackLabelsWidth = @@ -137,12 +137,12 @@ export function TimelinePlayhead({ > {/* The playhead line spanning full height */} <div - className={`absolute left-0 w-0.5 cursor-col-resize h-full ${isSnappingToPlayhead ? "bg-primary" : "bg-foreground"}`} + className={`absolute left-0 w-0.5 cursor-col-resize h-full ${isSnappingToPlayhead ? "bg-foreground" : "bg-foreground"}`} /> {/* 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-xs ${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-foreground border-foreground" : "bg-foreground border-foreground/50"}`} /> </div> ); |
