-
-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
736 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
72 changes: 72 additions & 0 deletions
72
apps/client/src/features/timeline/Timeline/Timeline.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
$timeline-entry-height: 20px; | ||
|
||
.timeline { | ||
flex: 1; | ||
} | ||
|
||
.timelineEvents { | ||
position: relative; | ||
} | ||
|
||
.entryIndicator { | ||
position: absolute; | ||
background-color: var(--color, $ui-white); | ||
height: $timeline-entry-height; | ||
} | ||
|
||
.entryContent { | ||
position: absolute; | ||
margin-top: $timeline-entry-height; | ||
padding-top: var(--top, 0); | ||
border-left: 2px solid var(--color, $ui-white); | ||
|
||
color: $ui-white; | ||
|
||
white-space: nowrap; | ||
|
||
> div { | ||
min-width: 100%; | ||
padding-inline: 0.5em; | ||
width: fit-content; | ||
background-color: $ui-black; | ||
} | ||
|
||
&.lastElement { | ||
border-left: none; | ||
border-right: 2px solid var(--color, $ui-black); | ||
text-align: right; | ||
|
||
transform: translateX(-100%); | ||
} | ||
} | ||
|
||
.start, | ||
.title { | ||
font-weight: 600; | ||
} | ||
|
||
// for elapsed events, we can hide some stuff | ||
[data-status='finished'] { | ||
color: $gray-500; | ||
.status { | ||
display: none | ||
} | ||
} | ||
|
||
[data-status='live'] { | ||
font-weight: 600; | ||
color: $ui-white; | ||
|
||
.status { | ||
color: $active-red; | ||
} | ||
} | ||
|
||
[data-status='future'] { | ||
font-weight: 600; | ||
color: $gray-300; | ||
|
||
.status { | ||
color: $green-500; | ||
} | ||
} |
121 changes: 121 additions & 0 deletions
121
apps/client/src/features/timeline/Timeline/Timeline.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { memo } from 'react'; | ||
import { useViewportSize } from '@mantine/hooks'; | ||
import { isOntimeEvent, MaybeNumber } from 'ontime-types'; | ||
import { dayInMs, getFirstEventNormal, getLastEventNormal, MILLIS_PER_HOUR } from 'ontime-utils'; | ||
|
||
import useRundown from '../../../common/hooks-query/useRundown'; | ||
|
||
import { type ProgressStatus, TimelineEntry } from './TimelineEntry'; | ||
import { TimelineMarkers } from './TimelineMarkers'; | ||
import { ProgressBar } from './TimelineProgressBar'; | ||
import { getElementPosition, getEndHour, getEstimatedWidth, getLaneLevel, getStartHour } from './timelineUtils'; | ||
|
||
import style from './Timeline.module.scss'; | ||
|
||
export default memo(Timeline); | ||
|
||
function useTimeline() { | ||
const { data } = useRundown(); | ||
if (data.revision === -1) { | ||
return null; | ||
} | ||
|
||
const { firstEvent } = getFirstEventNormal(data.rundown, data.order); | ||
const { lastEvent } = getLastEventNormal(data.rundown, data.order); | ||
const firstStart = firstEvent?.timeStart ?? 0; | ||
const lastEnd = lastEvent?.timeEnd ?? 0; | ||
const normalisedLastEnd = lastEnd < firstStart ? lastEnd + dayInMs : lastEnd; | ||
|
||
// timeline is padded to nearest hours (floor and ceil) | ||
const startHour = getStartHour(firstStart) * MILLIS_PER_HOUR; | ||
const endHour = getEndHour(normalisedLastEnd) * MILLIS_PER_HOUR; | ||
|
||
return { | ||
rundown: data.rundown, | ||
order: data.order, | ||
startHour, | ||
endHour, | ||
}; | ||
} | ||
|
||
interface TimelineProps { | ||
selectedEventId: string | null; | ||
} | ||
|
||
function Timeline(props: TimelineProps) { | ||
const { selectedEventId } = props; | ||
const { width: screenWidth } = useViewportSize(); | ||
const timelineData = useTimeline(); | ||
|
||
if (timelineData === null) { | ||
return null; | ||
} | ||
|
||
const { rundown, order, startHour, endHour } = timelineData; | ||
|
||
let hasTimelinePassedMidnight = false; | ||
let previousEventStartTime: MaybeNumber = null; | ||
let eventStatus: ProgressStatus = 'finished'; | ||
// a list of the right most element for each lane | ||
const rightMostElements: Record<number, number> = {}; | ||
|
||
return ( | ||
<div className={style.timeline}> | ||
<TimelineMarkers /> | ||
<div className={style.timelineEvents}> | ||
{order.map((eventId) => { | ||
// for now we dont render delays and blocks | ||
const event = rundown[eventId]; | ||
if (!isOntimeEvent(event)) { | ||
return null; | ||
} | ||
|
||
// keep track of progress of rundown | ||
if (eventStatus === 'live') { | ||
eventStatus = 'future'; | ||
} | ||
if (eventId === selectedEventId) { | ||
eventStatus = 'live'; | ||
} | ||
|
||
// we need to offset the start to account for midnight | ||
if (!hasTimelinePassedMidnight) { | ||
hasTimelinePassedMidnight = previousEventStartTime !== null && event.timeStart < previousEventStartTime; | ||
} | ||
const normalisedStart = hasTimelinePassedMidnight ? event.timeStart + dayInMs : event.timeStart; | ||
previousEventStartTime = normalisedStart; | ||
|
||
const { left: elementLeftPosition, width: elementWidth } = getElementPosition( | ||
startHour, | ||
endHour, | ||
normalisedStart, | ||
event.duration, | ||
screenWidth, | ||
); | ||
const estimatedRightPosition = elementLeftPosition + getEstimatedWidth(event.title); | ||
const laneLevel = getLaneLevel(rightMostElements, elementLeftPosition); | ||
|
||
if (rightMostElements[laneLevel] === undefined || rightMostElements[laneLevel] < estimatedRightPosition) { | ||
rightMostElements[laneLevel] = estimatedRightPosition; | ||
} | ||
|
||
return ( | ||
<TimelineEntry | ||
key={eventId} | ||
colour={event.colour} | ||
duration={event.duration} | ||
isLast={eventId === order[order.length - 1]} | ||
lane={laneLevel} | ||
left={elementLeftPosition} | ||
status={eventStatus} | ||
start={event.timeStart} | ||
title={event.title} | ||
width={elementWidth} | ||
/> | ||
); | ||
})} | ||
</div> | ||
<ProgressBar startHour={startHour} endHour={endHour} /> | ||
</div> | ||
); | ||
} |
78 changes: 78 additions & 0 deletions
78
apps/client/src/features/timeline/Timeline/TimelineEntry.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { useClock } from '../../../common/hooks/useSocket'; | ||
import { cx } from '../../../common/utils/styleUtils'; | ||
import { formatDuration, formatTime } from '../../../common/utils/time'; | ||
|
||
import { getStatusLabel } from './timelineUtils'; | ||
|
||
import style from './Timeline.module.scss'; | ||
|
||
export type ProgressStatus = 'finished' | 'live' | 'future'; | ||
|
||
interface TimelineEntry { | ||
colour: string; | ||
duration: number; | ||
isLast: boolean; | ||
lane: number; | ||
left: number; | ||
status: ProgressStatus; | ||
start: number; | ||
title: string; | ||
width: number; | ||
} | ||
|
||
const laneHeight = 120; | ||
const formatOptions = { | ||
format12: 'hh:mm a', | ||
format24: 'HH:mm', | ||
}; | ||
|
||
export function TimelineEntry(props: TimelineEntry) { | ||
const { colour, duration, isLast, lane, left, status, start, title, width } = props; | ||
|
||
const formattedStartTime = formatTime(start, formatOptions); | ||
const formattedDuration = `Dur ${formatDuration(duration)}`; | ||
|
||
const contentClasses = cx([style.entryContent, isLast && style.lastElement]); | ||
|
||
return ( | ||
<> | ||
<div | ||
className={style.entryIndicator} | ||
style={{ | ||
'--color': colour, | ||
left: `${left}px`, | ||
width: `${width}px`, | ||
}} | ||
/> | ||
<div | ||
className={contentClasses} | ||
data-status={status} | ||
style={{ | ||
'--color': colour, | ||
'--top': `${lane * laneHeight}px`, | ||
zIndex: 5 - lane, | ||
left: `${left}px`, | ||
}} | ||
> | ||
<div className={style.start}>{formattedStartTime}</div> | ||
<div className={style.title}>{title}</div> | ||
<div className={style.duration}>{formattedDuration}</div> | ||
<TimelineEntryStatus status={status} start={start} /> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
interface TimelineEntryStatusProps { | ||
status: ProgressStatus; | ||
start: number; | ||
} | ||
// we isolate this component to avoid re-rendering too many elements | ||
function TimelineEntryStatus(props: TimelineEntryStatusProps) { | ||
const { status, start } = props; | ||
// TODO: account for offset instead of just using the clock | ||
const { clock } = useClock(); | ||
const statusText = getStatusLabel(start - clock, status); | ||
|
||
return <div className={style.status}>{statusText}</div>; | ||
} |
19 changes: 19 additions & 0 deletions
19
apps/client/src/features/timeline/Timeline/TimelineMarkers.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
.markers { | ||
width: 100%; | ||
color: $ui-white; | ||
background-color: $white-10; | ||
display: flex; | ||
height: 1rem; | ||
line-height: 1rem; | ||
font-size: calc(1rem - 2px); | ||
justify-content: space-evenly; | ||
text-align: center; | ||
|
||
& > * { | ||
flex-grow: 1; | ||
} | ||
|
||
:nth-child(odd) { | ||
background-color: $white-20; | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
apps/client/src/features/timeline/Timeline/TimelineMarkers.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import useRundown from '../../../common/hooks-query/useRundown'; | ||
|
||
import { getTimelineSections } from './timelineUtils'; | ||
|
||
import style from './TimelineMarkers.module.scss'; | ||
|
||
export function TimelineMarkers() { | ||
const { data } = useRundown(); | ||
|
||
if (data.revision === -1) { | ||
return null; | ||
} | ||
|
||
const elements = getTimelineSections(data.rundown, data.order); | ||
|
||
return ( | ||
<div className={style.markers}> | ||
{elements.map((tag, index) => { | ||
return <span key={index}>{tag}</span>; | ||
})} | ||
</div> | ||
); | ||
} |
Oops, something went wrong.