Skip to content

Commit f3b7f0e

Browse files
committed
Add timeline stamps
Draws a canvas above the timeline with ticks spaced at a fixed interval. At certain major ticks the respective time is rendered as well. This should help the user orient themselves on the timeline.
1 parent ca19996 commit f3b7f0e

File tree

3 files changed

+234
-29
lines changed

3 files changed

+234
-29
lines changed

src/main/SubtitleTimeline.tsx

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef, useState } from "react";
1+
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
22
import { css } from "@emotion/react";
33
import { SegmentsList as CuttingSegmentsList, Waveforms } from "./Timeline";
44
import {
@@ -27,6 +27,7 @@ import { useTranslation } from "react-i18next";
2727
import { useHotkeys } from "react-hotkeys-hook";
2828
import { KEYMAP } from "../globalKeys";
2929
import { shallowEqual } from "react-redux";
30+
import TimelineStamps from "./TimelineStamps";
3031

3132
/**
3233
* Copy-paste of the timeline in Video.tsx, so that we can make some small adjustments,
@@ -57,13 +58,24 @@ const SubtitleTimeline: React.FC = () => {
5758
paddingRight: "50%",
5859
});
5960

61+
// Vars for timelineStamps
62+
const [scrollLeft, setScrollLeft] = useState(0);
63+
const [visibleWidth, setVisibleWidth] = useState(0);
64+
const paddingOffset = visibleWidth / 2;
65+
const virtualScrollLeft = scrollLeft - paddingOffset;
66+
6067
const setCurrentlyAtToClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
6168
const rect = e.currentTarget.getBoundingClientRect();
6269
const offsetX = e.clientX - rect.left;
6370
dispatch(setClickTriggered(true));
6471
dispatch(setCurrentlyAt((offsetX / widthMiniTimeline) * (duration)));
6572
};
6673

74+
// Make sure visibleWidth is set so canvas is drawn on first render
75+
useLayoutEffect(() => {
76+
updateScrollMetrics();
77+
}, [width, duration]);
78+
6779
// Apply horizonal scrolling when scrolled from somewhere else
6880
useEffect(() => {
6981
if (currentlyAt !== undefined && refTop.current) {
@@ -114,6 +126,15 @@ const SubtitleTimeline: React.FC = () => {
114126
{}, [currentlyAt],
115127
);
116128

129+
const updateScrollMetrics = () => {
130+
if (!refTop.current) {
131+
return;
132+
}
133+
const el = refTop.current;
134+
setScrollLeft(el.scrollLeft);
135+
setVisibleWidth(el.clientWidth);
136+
};
137+
117138
// Callback for the scroll container
118139
const onEndScroll = (e: ScrollEvent) => {
119140
// If scrolled by user
@@ -135,7 +156,6 @@ const SubtitleTimeline: React.FC = () => {
135156
const subtitleTimelineStyle = css({
136157
position: "relative",
137158
width: "100%",
138-
height: "250px",
139159
paddingBottom: "15px",
140160
});
141161

@@ -146,25 +166,31 @@ const SubtitleTimeline: React.FC = () => {
146166
css={{
147167
position: "absolute",
148168
width: "2px",
149-
height: "200px",
169+
height: "222px",
150170
...(refTop.current) && { left: (refTop.current.clientWidth / 2) },
151-
top: "10px",
152171
background: `${theme.text}`,
153172
zIndex: 100,
154173
}}
155174
/>
175+
{/* Time codes above the timeline */}
176+
<TimelineStamps
177+
durationMs={duration}
178+
zoomedWidth={width}
179+
scrollLeft={virtualScrollLeft}
180+
visibleWidth={visibleWidth}
181+
height={20}
182+
/>
156183
{/* Scrollable timeline container. Has width of parent*/}
157184
<ScrollContainer innerRef={refTop} css={{ overflow: "hidden", width: "100%", height: "215px" }}
158185
vertical={false}
159186
horizontal={true}
160187
onEndScroll={onEndScroll}
188+
onScroll={updateScrollMetrics}
161189
// dom elements with this id in the container will not trigger scrolling when dragged
162190
ignoreElements={".prevent-drag-scroll"}
163191
>
164192
{/* Container. Overflows. Width based on parent times zoom level*/}
165193
<div ref={ref} css={timelineStyle}>
166-
{/* Fake padding. TODO: Figure out a better way to pad absolutely positioned elements*/}
167-
<div css={{ height: "10px" }} />
168194
<TimelineSubtitleSegmentsList timelineWidth={width} />
169195
<div css={{ position: "relative", height: "100px" }} >
170196
<Waveforms timelineHeight={120} />

src/main/Timeline.tsx

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { debounce } from "lodash";
2-
import React, { useState, useRef, useEffect, RefObject } from "react";
2+
import React, { useState, useRef, useEffect, RefObject, useLayoutEffect } from "react";
33

44
import Draggable, { DraggableEventHandler } from "react-draggable";
55

@@ -42,6 +42,7 @@ import {
4242
selectActiveSegmentIndex as chapterSelectActiveSegmentIndex,
4343
moveCut as chapterMoveCut,
4444
} from "../redux/chapterSlice";
45+
import TimelineStamps from "./TimelineStamps";
4546

4647
/**
4748
* A container for visualizing the cutting of the video, as well as for controlling
@@ -81,11 +82,16 @@ const Timeline: React.FC<{
8182
const { ref, width = 1 } = useResizeObserver<HTMLDivElement>();
8283
const scrollContainerRef = useRef<HTMLElement>(null);
8384
const { width: scrollContainerWidth = 1 } = useResizeObserver<HTMLElement>({ ref: scrollContainerRef });
84-
const topOffset = 20;
8585

8686
const currentlyScrolling = useRef(false);
8787
const zoomCenter = useRef(0);
8888

89+
// Vars for timelineStamps
90+
const timelineStampsHeight = 20;
91+
const waveformHeight = timelineHeight - timelineStampsHeight;
92+
const [scrollLeft, setScrollLeft] = useState(0);
93+
const [visibleWidth, setVisibleWidth] = useState(0);
94+
8995
const updateScroll = () => {
9096
if (currentlyScrolling.current) {
9197
currentlyScrolling.current = false;
@@ -98,6 +104,16 @@ const Timeline: React.FC<{
98104
const scrubberVisible = scrollLeft <= scrubberPosition && scrubberPosition <= scrollLeft + clientWidth;
99105

100106
zoomCenter.current = (scrubberVisible ? scrubberPosition : centerPosition) / width;
107+
108+
};
109+
110+
const updateScrollMetrics = () => {
111+
if (!scrollContainerRef.current) {
112+
return;
113+
}
114+
const el = scrollContainerRef.current;
115+
setScrollLeft(el.scrollLeft);
116+
setVisibleWidth(el.clientWidth);
101117
};
102118

103119
const displayPercentage = (durationInSeconds / displayDuration);
@@ -106,6 +122,11 @@ const Timeline: React.FC<{
106122
};
107123
const zoomedWidth = getWaveformWidth(scrollContainerWidth);
108124

125+
// Make sure visibleWidth is set so canvas is drawn on first render
126+
useLayoutEffect(() => {
127+
updateScrollMetrics();
128+
}, [width, duration]);
129+
109130
// eslint-disable-next-line react-hooks/exhaustive-deps
110131
useEffect(updateScroll, [currentlyAt, timelineZoom, width, scrollContainerWidth]);
111132

@@ -124,7 +145,6 @@ const Timeline: React.FC<{
124145
position: "relative", // Need to set position for Draggable bounds to work
125146
height: timelineHeight + "px",
126147
width: `${zoomedWidth}px`, // Width modified by zoom
127-
top: `${topOffset}px`,
128148
});
129149

130150
// Update the current time based on the position clicked on the timeline
@@ -136,17 +156,31 @@ const Timeline: React.FC<{
136156
};
137157

138158
return (
139-
<ScrollContainer
140-
innerRef={scrollContainerRef}
141-
css={{ overflowY: "hidden", width: "100%", height: `${timelineHeight + topOffset}px` }}
142-
vertical={false}
143-
horizontal={true}
144-
// dom elements with this id in the container will not trigger scrolling when dragged
145-
ignoreElements={".prevent-drag-scroll"}
146-
hideScrollbars={false} // ScrollContainer hides scrollbars per default
147-
onEndScroll={updateScroll}
148-
>
149-
<CuttingActionsContextMenu>
159+
<CuttingActionsContextMenu>
160+
<div css={css({ position: "absolute" })}>
161+
<TimelineStamps
162+
durationMs={duration}
163+
zoomedWidth={zoomedWidth}
164+
scrollLeft={scrollLeft}
165+
visibleWidth={visibleWidth}
166+
height={timelineStampsHeight}
167+
/>
168+
</div>
169+
<ScrollContainer
170+
innerRef={scrollContainerRef}
171+
css={{
172+
overflowY: "hidden",
173+
width: "100%",
174+
height: `${timelineHeight}px`,
175+
}}
176+
vertical={false}
177+
horizontal={true}
178+
// dom elements with this id in the container will not trigger scrolling when dragged
179+
ignoreElements={".prevent-drag-scroll"}
180+
hideScrollbars={false} // ScrollContainer hides scrollbars per default
181+
onScroll={updateScrollMetrics}
182+
onEndScroll={updateScroll}
183+
>
150184
<div ref={ref} css={timelineStyle} onMouseDown={e => setCurrentlyAtToClick(e)}>
151185
<Scrubber
152186
ref={scrubberRef}
@@ -157,15 +191,15 @@ const Timeline: React.FC<{
157191
setCurrentlyAt={setCurrentlyAt}
158192
setIsPlaying={setIsPlaying}
159193
/>
160-
<div css={{ position: "relative", height: timelineHeight + "px" }}>
194+
<div css={{ position: "relative", height: timelineHeight + "px", top: `${timelineStampsHeight}px` }}>
161195
<Waveforms
162-
timelineHeight={!isChapters ? timelineHeight : (timelineHeight / 4) * 3}
163-
topOffset={!isChapters ? undefined : (timelineHeight / 4) * 1}
196+
timelineHeight={!isChapters ? waveformHeight : (waveformHeight / 4) * 3}
197+
topOffset={!isChapters ? undefined : (waveformHeight / 4) * 1}
164198
/>
165199
{isChapters &&
166200
<SegmentsList
167201
timelineWidth={width}
168-
timelineHeight={(timelineHeight / 4) * 1}
202+
timelineHeight={(waveformHeight / 4) * 1}
169203
styleByActiveSegment={styleByActiveSegment}
170204
tabable={true}
171205
selectSegments={chapterSelectSegments}
@@ -175,7 +209,7 @@ const Timeline: React.FC<{
175209
}
176210
<SegmentsList
177211
timelineWidth={width}
178-
timelineHeight={!isChapters ? timelineHeight : (timelineHeight / 4) * 3}
212+
timelineHeight={!isChapters ? waveformHeight : (waveformHeight / 4) * 3}
179213
styleByActiveSegment={!isChapters ? styleByActiveSegment : false}
180214
tabable={true}
181215
selectSegments={selectSegments}
@@ -184,8 +218,8 @@ const Timeline: React.FC<{
184218
/>
185219
</div>
186220
</div>
187-
</CuttingActionsContextMenu>
188-
</ScrollContainer>
221+
</ScrollContainer>
222+
</CuttingActionsContextMenu>
189223
);
190224
};
191225

@@ -310,12 +344,11 @@ export const Scrubber = React.forwardRef<HTMLDivElement, ScrubberProps>((props,
310344
height: timelineHeight + 20 + "px", // TODO: CHECK IF height: "100%",
311345
width: "1px",
312346
position: "absolute",
313-
zIndex: 2,
347+
zIndex: 20,
314348
display: "flex",
315349
flexDirection: "column",
316350
justifyContent: "space-between",
317351
alignItems: "center",
318-
top: "-20px",
319352
});
320353

321354
const scrubberDragHandleStyle = css({

0 commit comments

Comments
 (0)