summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/timeline/timeline-element.tsx
diff options
context:
space:
mode:
authorryu <ryumacodes@gmail.com>2025-08-07 19:37:06 +0500
committerGitHub <noreply@github.com>2025-08-07 19:37:06 +0500
commite71788014d137a74f10420601451ddfbb84701bb (patch)
tree2c08f7fbbc7537b12935e842981830aed976ce87 /apps/web/src/components/editor/timeline/timeline-element.tsx
parent6412158003f295a350fc66800796f30c8f3c534b (diff)
parentd022a78ae2330665e7d763fdc6e831a71f3b9ea1 (diff)
downloadopencut-e71788014d137a74f10420601451ddfbb84701bb.tar.gz
Merge branch 'main' into feat/timeline-return-to-start
Diffstat (limited to 'apps/web/src/components/editor/timeline/timeline-element.tsx')
-rw-r--r--apps/web/src/components/editor/timeline/timeline-element.tsx189
1 files changed, 79 insertions, 110 deletions
diff --git a/apps/web/src/components/editor/timeline/timeline-element.tsx b/apps/web/src/components/editor/timeline/timeline-element.tsx
index f581bbf0..b26a14bb 100644
--- a/apps/web/src/components/editor/timeline/timeline-element.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-element.tsx
@@ -1,25 +1,21 @@
"use client";
-import { useState } from "react";
-import { Button } from "../../ui/button";
import {
- MoreVertical,
Scissors,
Trash2,
- SplitSquareHorizontal,
- Music,
- ChevronRight,
- ChevronLeft,
- Type,
Copy,
RefreshCw,
+ EyeOff,
+ Eye,
+ Volume2,
+ VolumeX,
} from "lucide-react";
import { useMediaStore } from "@/stores/media-store";
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,
@@ -27,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,
@@ -60,30 +46,27 @@ export function TimelineElement({
removeElementFromTrackWithRipple,
dragState,
splitElement,
- splitAndKeepLeft,
- splitAndKeepRight,
- separateAudio,
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,
- 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;
@@ -141,6 +124,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") {
@@ -177,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>
);
}
@@ -196,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>
@@ -330,9 +266,9 @@ 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)}
onMouseDown={handleElementMouseDown}
onContextMenu={(e) =>
@@ -343,14 +279,24 @@ 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
- 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")}
/>
</>
@@ -358,11 +304,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"}