refactor: centralize track colors and timeline constants
This commit is contained in:
@ -19,6 +19,10 @@ import AudioWaveform from "./audio-waveform";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { TimelineElementProps, TrackType } from "@/types/timeline";
|
import { TimelineElementProps, TrackType } from "@/types/timeline";
|
||||||
import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize";
|
import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize";
|
||||||
|
import {
|
||||||
|
getTrackElementClasses,
|
||||||
|
TIMELINE_CONSTANTS,
|
||||||
|
} from "@/lib/timeline-constants";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -67,7 +71,10 @@ export function TimelineElement({
|
|||||||
|
|
||||||
const effectiveDuration =
|
const effectiveDuration =
|
||||||
element.duration - element.trimStart - element.trimEnd;
|
element.duration - element.trimStart - element.trimEnd;
|
||||||
const elementWidth = Math.max(80, effectiveDuration * 50 * zoomLevel);
|
const elementWidth = Math.max(
|
||||||
|
TIMELINE_CONSTANTS.ELEMENT_MIN_WIDTH,
|
||||||
|
effectiveDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel
|
||||||
|
);
|
||||||
|
|
||||||
// Use real-time position during drag, otherwise use stored position
|
// Use real-time position during drag, otherwise use stored position
|
||||||
const isBeingDragged = dragState.elementId === element.id;
|
const isBeingDragged = dragState.elementId === element.id;
|
||||||
@ -77,19 +84,6 @@ export function TimelineElement({
|
|||||||
: element.startTime;
|
: element.startTime;
|
||||||
const elementLeft = elementStartTime * 50 * zoomLevel;
|
const elementLeft = elementStartTime * 50 * zoomLevel;
|
||||||
|
|
||||||
const getTrackColor = (type: TrackType) => {
|
|
||||||
switch (type) {
|
|
||||||
case "media":
|
|
||||||
return "bg-blue-500/20 border-blue-500/30";
|
|
||||||
case "text":
|
|
||||||
return "bg-purple-500/20 border-purple-500/30";
|
|
||||||
case "audio":
|
|
||||||
return "bg-green-500/20 border-green-500/30";
|
|
||||||
default:
|
|
||||||
return "bg-gray-500/20 border-gray-500/30";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteElement = () => {
|
const handleDeleteElement = () => {
|
||||||
removeElementFromTrack(track.id, element.id);
|
removeElementFromTrack(track.id, element.id);
|
||||||
setElementMenuOpen(false);
|
setElementMenuOpen(false);
|
||||||
@ -271,7 +265,7 @@ export function TimelineElement({
|
|||||||
onMouseLeave={resizing ? handleResizeEnd : undefined}
|
onMouseLeave={resizing ? handleResizeEnd : undefined}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`relative h-full rounded-[0.15rem] cursor-pointer overflow-hidden ${getTrackColor(
|
className={`relative h-full rounded-[0.15rem] cursor-pointer overflow-hidden ${getTrackElementClasses(
|
||||||
track.type
|
track.type
|
||||||
)} ${isSelected ? "border-b-[0.5px] border-t-[0.5px] border-foreground" : ""} ${
|
)} ${isSelected ? "border-b-[0.5px] border-t-[0.5px] border-foreground" : ""} ${
|
||||||
isBeingDragged ? "z-50" : "z-10"
|
isBeingDragged ? "z-50" : "z-10"
|
||||||
|
@ -25,6 +25,7 @@ import type {
|
|||||||
TimelineElement as TimelineElementType,
|
TimelineElement as TimelineElementType,
|
||||||
DragData,
|
DragData,
|
||||||
} from "@/types/timeline";
|
} from "@/types/timeline";
|
||||||
|
import { TIMELINE_CONSTANTS } from "@/lib/timeline-constants";
|
||||||
|
|
||||||
export function TimelineTrackContent({
|
export function TimelineTrackContent({
|
||||||
track,
|
track,
|
||||||
@ -82,7 +83,10 @@ export function TimelineTrackContent({
|
|||||||
|
|
||||||
const timelineRect = timelineRef.current.getBoundingClientRect();
|
const timelineRect = timelineRef.current.getBoundingClientRect();
|
||||||
const mouseX = e.clientX - timelineRect.left;
|
const mouseX = e.clientX - timelineRect.left;
|
||||||
const mouseTime = Math.max(0, mouseX / (50 * zoomLevel));
|
const mouseTime = Math.max(
|
||||||
|
0,
|
||||||
|
mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel)
|
||||||
|
);
|
||||||
const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime);
|
const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime);
|
||||||
const snappedTime = Math.round(adjustedTime * 10) / 10;
|
const snappedTime = Math.round(adjustedTime * 10) / 10;
|
||||||
|
|
||||||
@ -212,7 +216,8 @@ export function TimelineTrackContent({
|
|||||||
const elementElement = e.currentTarget as HTMLElement;
|
const elementElement = e.currentTarget as HTMLElement;
|
||||||
const elementRect = elementElement.getBoundingClientRect();
|
const elementRect = elementElement.getBoundingClientRect();
|
||||||
const clickOffsetX = e.clientX - elementRect.left;
|
const clickOffsetX = e.clientX - elementRect.left;
|
||||||
const clickOffsetTime = clickOffsetX / (50 * zoomLevel);
|
const clickOffsetTime =
|
||||||
|
clickOffsetX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
|
||||||
|
|
||||||
startDragAction(
|
startDragAction(
|
||||||
element.id,
|
element.id,
|
||||||
@ -278,7 +283,7 @@ export function TimelineTrackContent({
|
|||||||
if (trackContainer) {
|
if (trackContainer) {
|
||||||
const rect = trackContainer.getBoundingClientRect();
|
const rect = trackContainer.getBoundingClientRect();
|
||||||
const mouseX = Math.max(0, e.clientX - rect.left);
|
const mouseX = Math.max(0, e.clientX - rect.left);
|
||||||
dropTime = mouseX / (50 * zoomLevel);
|
dropTime = mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for potential overlaps and show appropriate feedback
|
// Check for potential overlaps and show appropriate feedback
|
||||||
@ -453,11 +458,11 @@ export function TimelineTrackContent({
|
|||||||
const rect = trackContainer.getBoundingClientRect();
|
const rect = trackContainer.getBoundingClientRect();
|
||||||
const mouseX = Math.max(0, e.clientX - rect.left);
|
const mouseX = Math.max(0, e.clientX - rect.left);
|
||||||
const mouseY = e.clientY - rect.top; // Get Y position relative to this track
|
const mouseY = e.clientY - rect.top; // Get Y position relative to this track
|
||||||
const newStartTime = mouseX / (50 * zoomLevel);
|
const newStartTime =
|
||||||
|
mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
|
||||||
const snappedTime = Math.round(newStartTime * 10) / 10;
|
const snappedTime = Math.round(newStartTime * 10) / 10;
|
||||||
|
|
||||||
// Calculate drop position relative to tracks
|
// Calculate drop position relative to tracks
|
||||||
const TRACK_HEIGHT = 60;
|
|
||||||
const currentTrackIndex = tracks.findIndex((t) => t.id === track.id);
|
const currentTrackIndex = tracks.findIndex((t) => t.id === track.id);
|
||||||
|
|
||||||
// Determine drop zone within the track (top 20px, middle 20px, bottom 20px)
|
// Determine drop zone within the track (top 20px, middle 20px, bottom 20px)
|
||||||
@ -619,7 +624,7 @@ export function TimelineTrackContent({
|
|||||||
type: "text",
|
type: "text",
|
||||||
name: dragData.name || "Text",
|
name: dragData.name || "Text",
|
||||||
content: dragData.content || "Default Text",
|
content: dragData.content || "Default Text",
|
||||||
duration: 5,
|
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
||||||
startTime: snappedTime,
|
startTime: snappedTime,
|
||||||
trimStart: 0,
|
trimStart: 0,
|
||||||
trimEnd: 0,
|
trimEnd: 0,
|
||||||
|
@ -44,6 +44,10 @@ import {
|
|||||||
} from "../ui/select";
|
} from "../ui/select";
|
||||||
import { TimelineTrackContent } from "./timeline-track";
|
import { TimelineTrackContent } from "./timeline-track";
|
||||||
import type { DragData } from "@/types/timeline";
|
import type { DragData } from "@/types/timeline";
|
||||||
|
import {
|
||||||
|
getTrackLabelColor,
|
||||||
|
TIMELINE_CONSTANTS,
|
||||||
|
} from "@/lib/timeline-constants";
|
||||||
|
|
||||||
export function Timeline() {
|
export function Timeline() {
|
||||||
// Timeline shows all tracks (video, audio, effects) and their elements.
|
// Timeline shows all tracks (video, audio, effects) and their elements.
|
||||||
@ -105,8 +109,8 @@ export function Timeline() {
|
|||||||
|
|
||||||
// Dynamic timeline width calculation based on playhead position and duration
|
// Dynamic timeline width calculation based on playhead position and duration
|
||||||
const dynamicTimelineWidth = Math.max(
|
const dynamicTimelineWidth = Math.max(
|
||||||
(duration || 0) * 50 * zoomLevel, // Base width from duration
|
(duration || 0) * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel, // Base width from duration
|
||||||
(currentTime + 30) * 50 * zoomLevel, // Width to show current time + 30 seconds buffer
|
(currentTime + 30) * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel, // Width to show current time + 30 seconds buffer
|
||||||
timelineRef.current?.clientWidth || 1000 // Minimum width
|
timelineRef.current?.clientWidth || 1000 // Minimum width
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -236,10 +240,11 @@ export function Timeline() {
|
|||||||
let newSelection: { trackId: string; elementId: string }[] = [];
|
let newSelection: { trackId: string; elementId: string }[] = [];
|
||||||
tracks.forEach((track, trackIdx) => {
|
tracks.forEach((track, trackIdx) => {
|
||||||
track.elements.forEach((element) => {
|
track.elements.forEach((element) => {
|
||||||
const clipLeft = element.startTime * 50 * zoomLevel;
|
const clipLeft =
|
||||||
const clipTop = trackIdx * 60;
|
element.startTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
||||||
const clipBottom = clipTop + 60;
|
const clipTop = trackIdx * TIMELINE_CONSTANTS.TRACK_HEIGHT;
|
||||||
const clipRight = clipLeft + 60;
|
const clipBottom = clipTop + TIMELINE_CONSTANTS.TRACK_HEIGHT;
|
||||||
|
const clipRight = clipLeft + TIMELINE_CONSTANTS.TRACK_HEIGHT;
|
||||||
if (
|
if (
|
||||||
bx1 < clipRight &&
|
bx1 < clipRight &&
|
||||||
bx2 > clipLeft &&
|
bx2 > clipLeft &&
|
||||||
@ -334,7 +339,7 @@ export function Timeline() {
|
|||||||
type: "text",
|
type: "text",
|
||||||
name: dragData.name || "Text",
|
name: dragData.name || "Text",
|
||||||
content: dragData.content || "Default Text",
|
content: dragData.content || "Default Text",
|
||||||
duration: 5,
|
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
trimStart: 0,
|
trimStart: 0,
|
||||||
trimEnd: 0,
|
trimEnd: 0,
|
||||||
@ -721,7 +726,8 @@ export function Timeline() {
|
|||||||
"[data-radix-scroll-area-viewport]"
|
"[data-radix-scroll-area-viewport]"
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
if (!rulerViewport || !tracksViewport) return;
|
if (!rulerViewport || !tracksViewport) return;
|
||||||
const playheadPx = playheadPosition * 50 * zoomLevel;
|
const playheadPx =
|
||||||
|
playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
||||||
const viewportWidth = rulerViewport.clientWidth;
|
const viewportWidth = rulerViewport.clientWidth;
|
||||||
const scrollMin = 0;
|
const scrollMin = 0;
|
||||||
const scrollMax = rulerViewport.scrollWidth - viewportWidth;
|
const scrollMax = rulerViewport.scrollWidth - viewportWidth;
|
||||||
@ -792,7 +798,7 @@ export function Timeline() {
|
|||||||
type: "media",
|
type: "media",
|
||||||
mediaId: "test",
|
mediaId: "test",
|
||||||
name: "Test Clip",
|
name: "Test Clip",
|
||||||
duration: 5,
|
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
trimStart: 0,
|
trimStart: 0,
|
||||||
trimEnd: 0,
|
trimEnd: 0,
|
||||||
@ -927,7 +933,8 @@ export function Timeline() {
|
|||||||
{(() => {
|
{(() => {
|
||||||
// Calculate appropriate time interval based on zoom level
|
// Calculate appropriate time interval based on zoom level
|
||||||
const getTimeInterval = (zoom: number) => {
|
const getTimeInterval = (zoom: number) => {
|
||||||
const pixelsPerSecond = 50 * zoom;
|
const pixelsPerSecond =
|
||||||
|
TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoom;
|
||||||
if (pixelsPerSecond >= 200) return 0.1; // Every 0.1s when very zoomed in
|
if (pixelsPerSecond >= 200) return 0.1; // Every 0.1s when very zoomed in
|
||||||
if (pixelsPerSecond >= 100) return 0.5; // Every 0.5s when zoomed in
|
if (pixelsPerSecond >= 100) return 0.5; // Every 0.5s when zoomed in
|
||||||
if (pixelsPerSecond >= 50) return 1; // Every 1s at normal zoom
|
if (pixelsPerSecond >= 50) return 1; // Every 1s at normal zoom
|
||||||
@ -955,7 +962,9 @@ export function Timeline() {
|
|||||||
? "border-l border-muted-foreground/40"
|
? "border-l border-muted-foreground/40"
|
||||||
: "border-l border-muted-foreground/20"
|
: "border-l border-muted-foreground/20"
|
||||||
}`}
|
}`}
|
||||||
style={{ left: `${time * 50 * zoomLevel}px` }}
|
style={{
|
||||||
|
left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`absolute top-1 left-1 text-xs ${
|
className={`absolute top-1 left-1 text-xs ${
|
||||||
@ -991,7 +1000,9 @@ export function Timeline() {
|
|||||||
{/* Playhead in ruler (scrubbable) */}
|
{/* Playhead in ruler (scrubbable) */}
|
||||||
<div
|
<div
|
||||||
className="playhead absolute top-0 bottom-0 w-0.5 bg-red-500 pointer-events-auto z-50 cursor-col-resize"
|
className="playhead absolute top-0 bottom-0 w-0.5 bg-red-500 pointer-events-auto z-50 cursor-col-resize"
|
||||||
style={{ left: `${playheadPosition * 50 * zoomLevel}px` }}
|
style={{
|
||||||
|
left: `${playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
|
||||||
|
}}
|
||||||
onMouseDown={handlePlayheadMouseDown}
|
onMouseDown={handlePlayheadMouseDown}
|
||||||
>
|
>
|
||||||
<div className="absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 bg-red-500 rounded-full border-2 border-white shadow-sm" />
|
<div className="absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 bg-red-500 rounded-full border-2 border-white shadow-sm" />
|
||||||
@ -1014,13 +1025,7 @@ export function Timeline() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center flex-1 min-w-0">
|
<div className="flex items-center flex-1 min-w-0">
|
||||||
<div
|
<div
|
||||||
className={`w-3 h-3 rounded-full flex-shrink-0 ${
|
className={`w-3 h-3 rounded-full flex-shrink-0 ${getTrackLabelColor(track.type)}`}
|
||||||
track.type === "media"
|
|
||||||
? "bg-blue-500"
|
|
||||||
: track.type === "audio"
|
|
||||||
? "bg-green-500"
|
|
||||||
: "bg-purple-500"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
<span className="ml-2 text-sm font-medium truncate">
|
<span className="ml-2 text-sm font-medium truncate">
|
||||||
{track.name}
|
{track.name}
|
||||||
@ -1043,7 +1048,7 @@ export function Timeline() {
|
|||||||
<div
|
<div
|
||||||
className="relative flex-1"
|
className="relative flex-1"
|
||||||
style={{
|
style={{
|
||||||
height: `${Math.max(200, Math.min(800, tracks.length * 60))}px`,
|
height: `${Math.max(200, Math.min(800, tracks.length * TIMELINE_CONSTANTS.TRACK_HEIGHT))}px`,
|
||||||
width: `${dynamicTimelineWidth}px`,
|
width: `${dynamicTimelineWidth}px`,
|
||||||
}}
|
}}
|
||||||
onClick={handleTimelineClick}
|
onClick={handleTimelineClick}
|
||||||
@ -1059,8 +1064,8 @@ export function Timeline() {
|
|||||||
<div
|
<div
|
||||||
className="absolute left-0 right-0 border-b border-muted/30"
|
className="absolute left-0 right-0 border-b border-muted/30"
|
||||||
style={{
|
style={{
|
||||||
top: `${index * 60}px`,
|
top: `${index * TIMELINE_CONSTANTS.TRACK_HEIGHT}px`,
|
||||||
height: "60px",
|
height: `${TIMELINE_CONSTANTS.TRACK_HEIGHT}px`,
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
// If clicking empty area (not on a element), deselect all elements
|
// If clicking empty area (not on a element), deselect all elements
|
||||||
@ -1092,8 +1097,8 @@ export function Timeline() {
|
|||||||
<div
|
<div
|
||||||
className="absolute top-0 w-0.5 bg-red-500 pointer-events-auto z-50 cursor-col"
|
className="absolute top-0 w-0.5 bg-red-500 pointer-events-auto z-50 cursor-col"
|
||||||
style={{
|
style={{
|
||||||
left: `${playheadPosition * 50 * zoomLevel}px`,
|
left: `${playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
|
||||||
height: `${tracks.length * 60}px`,
|
height: `${tracks.length * TIMELINE_CONSTANTS.TRACK_HEIGHT}px`,
|
||||||
}}
|
}}
|
||||||
onMouseDown={handlePlayheadMouseDown}
|
onMouseDown={handlePlayheadMouseDown}
|
||||||
/>
|
/>
|
||||||
|
48
apps/web/src/lib/timeline-constants.ts
Normal file
48
apps/web/src/lib/timeline-constants.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { TrackType } from "@/types/timeline";
|
||||||
|
|
||||||
|
// Track color definitions
|
||||||
|
export const TRACK_COLORS = {
|
||||||
|
media: {
|
||||||
|
solid: "bg-blue-500",
|
||||||
|
background: "bg-blue-500/20",
|
||||||
|
border: "border-blue-500/30",
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
solid: "bg-purple-500",
|
||||||
|
background: "bg-purple-500/20",
|
||||||
|
border: "border-purple-500/30",
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
solid: "bg-green-500",
|
||||||
|
background: "bg-green-500/20",
|
||||||
|
border: "border-green-500/30",
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
solid: "bg-gray-500",
|
||||||
|
background: "bg-gray-500/20",
|
||||||
|
border: "border-gray-500/30",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
export function getTrackColors(type: TrackType) {
|
||||||
|
return TRACK_COLORS[type] || TRACK_COLORS.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTrackElementClasses(type: TrackType) {
|
||||||
|
const colors = getTrackColors(type);
|
||||||
|
return `${colors.background} ${colors.border}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTrackLabelColor(type: TrackType) {
|
||||||
|
return getTrackColors(type).solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other timeline constants
|
||||||
|
export const TIMELINE_CONSTANTS = {
|
||||||
|
ELEMENT_MIN_WIDTH: 80,
|
||||||
|
PIXELS_PER_SECOND: 50,
|
||||||
|
TRACK_HEIGHT: 60,
|
||||||
|
DEFAULT_TEXT_DURATION: 5,
|
||||||
|
ZOOM_LEVELS: [0.25, 0.5, 1, 1.5, 2, 3, 4],
|
||||||
|
} as const;
|
Reference in New Issue
Block a user