mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 18:03:47 +00:00
refactor: compound timeline
This commit is contained in:
committed by
Carlos Valente
parent
f11218a757
commit
bad9dab0a0
@@ -1,39 +1,40 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
$timeline-entry-height: 20px;
|
||||
$lane-height: 120px;
|
||||
$timeline-height: 1rem;
|
||||
$view-background: $ui-black;
|
||||
|
||||
.timeline {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
color: $ui-white;
|
||||
background-color: $ui-black;
|
||||
}
|
||||
|
||||
.timelineEvents {
|
||||
background-color: $view-background;
|
||||
box-sizing: content-box;
|
||||
// create progress background
|
||||
box-shadow: inset 0 1rem 0 0 $gray-1100;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
border-left: 1px solid $ui-black;
|
||||
border-left: 1px solid $view-background;
|
||||
// avoiding content being larger than the view
|
||||
height: calc(100% - 3rem);
|
||||
}
|
||||
|
||||
// decorate timeline element
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
box-sizing: content-box;
|
||||
top: -$timeline-height;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: $timeline-height;
|
||||
background-color: $white-40;
|
||||
// generate combined timeline
|
||||
.timelineBlock {
|
||||
height: $timeline-height;
|
||||
background: $white-60;
|
||||
width: 100%;
|
||||
|
||||
&[data-status='done'] {
|
||||
background: $active-red;
|
||||
}
|
||||
|
||||
&[data-status='live'] {
|
||||
background: linear-gradient(to right, $active-red var(--progress, 0%), $white-60 var(--progress, 0%));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +67,8 @@ $timeline-height: 1rem;
|
||||
line-height: 1rem;
|
||||
|
||||
background-color: var(--lighter, $viewer-card-bg-color);
|
||||
border-bottom: 2px solid $ui-black;
|
||||
border-bottom: 2px solid $view-background;
|
||||
border-top: 2px solid $view-background;
|
||||
box-shadow: 0 0.25rem 0 0 var(--color, $gray-300);
|
||||
|
||||
&[data-status='done'] {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { isOntimeEvent, isPlayableEvent, MaybeNumber, OntimeRundown } from 'onti
|
||||
import { checkIsNextDay, dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
||||
|
||||
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
||||
import TimelineProgressBar from './timeline-progress-bar/TimelineProgressBar';
|
||||
import { getElementPosition, getEndHour, getStartHour } from './timeline.utils';
|
||||
import { ProgressStatus, TimelineEntry } from './TimelineEntry';
|
||||
|
||||
@@ -39,57 +38,54 @@ function Timeline(props: TimelineProps) {
|
||||
return (
|
||||
<div className={style.timeline}>
|
||||
<TimelineMarkers startHour={startHour} endHour={endHour} />
|
||||
<TimelineProgressBar startHour={startHour} endHour={endHour} />
|
||||
<div className={style.timelineEvents}>
|
||||
{rundown.map((event) => {
|
||||
// for now we dont render delays and blocks
|
||||
if (!isOntimeEvent(event) || !isPlayableEvent(event)) {
|
||||
return null;
|
||||
}
|
||||
{rundown.map((event) => {
|
||||
// for now we dont render delays and blocks
|
||||
if (!isOntimeEvent(event) || !isPlayableEvent(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// keep track of progress of rundown
|
||||
if (eventStatus === 'live') {
|
||||
eventStatus = 'future';
|
||||
}
|
||||
if (event.id === selectedEventId) {
|
||||
eventStatus = 'live';
|
||||
}
|
||||
// keep track of progress of rundown
|
||||
if (eventStatus === 'live') {
|
||||
eventStatus = 'future';
|
||||
}
|
||||
if (event.id === selectedEventId) {
|
||||
eventStatus = 'live';
|
||||
}
|
||||
|
||||
// we only need to check for next day if we have a previous event
|
||||
if (
|
||||
previousEventStartTime !== null &&
|
||||
checkIsNextDay(previousEventStartTime, event.timeStart, event.duration)
|
||||
) {
|
||||
elapsedDays++;
|
||||
}
|
||||
const normalisedStart = event.timeStart + elapsedDays * dayInMs;
|
||||
// we only need to check for next day if we have a previous event
|
||||
if (
|
||||
previousEventStartTime !== null &&
|
||||
checkIsNextDay(previousEventStartTime, event.timeStart, event.duration)
|
||||
) {
|
||||
elapsedDays++;
|
||||
}
|
||||
const normalisedStart = event.timeStart + elapsedDays * dayInMs;
|
||||
|
||||
const { left: elementLeftPosition, width: elementWidth } = getElementPosition(
|
||||
startHour * MILLIS_PER_HOUR,
|
||||
endHour * MILLIS_PER_HOUR,
|
||||
normalisedStart + (event.delay ?? 0),
|
||||
event.duration,
|
||||
screenWidth,
|
||||
);
|
||||
const { left: elementLeftPosition, width: elementWidth } = getElementPosition(
|
||||
startHour * MILLIS_PER_HOUR,
|
||||
endHour * MILLIS_PER_HOUR,
|
||||
normalisedStart + (event.delay ?? 0),
|
||||
event.duration,
|
||||
screenWidth,
|
||||
);
|
||||
|
||||
// prepare values for next iteration
|
||||
previousEventStartTime = normalisedStart;
|
||||
// prepare values for next iteration
|
||||
previousEventStartTime = normalisedStart;
|
||||
|
||||
return (
|
||||
<TimelineEntry
|
||||
key={event.id}
|
||||
colour={event.colour}
|
||||
delay={event.delay ?? 0}
|
||||
duration={event.duration}
|
||||
left={elementLeftPosition}
|
||||
status={eventStatus}
|
||||
start={normalisedStart} // dataset solves issues related to crossing midnight
|
||||
title={event.title}
|
||||
width={elementWidth}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
return (
|
||||
<TimelineEntry
|
||||
key={event.id}
|
||||
colour={event.colour}
|
||||
delay={event.delay ?? 0}
|
||||
duration={event.duration}
|
||||
left={elementLeftPosition}
|
||||
status={eventStatus}
|
||||
start={normalisedStart} // dataset solves issues related to crossing midnight
|
||||
title={event.title}
|
||||
width={elementWidth}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTimelineStatus } from '../../../common/hooks/useSocket';
|
||||
import { useTimelineStatus, useTimer } from '../../../common/hooks/useSocket';
|
||||
import { getProgress } from '../../../common/utils/getProgress';
|
||||
import { alpha, cx } from '../../../common/utils/styleUtils';
|
||||
import { formatDuration, formatTime } from '../../../common/utils/time';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
@@ -48,6 +49,7 @@ export function TimelineEntry(props: TimelineEntryProps) {
|
||||
width: `${width}px`,
|
||||
}}
|
||||
>
|
||||
{status === 'live' ? <ActiveBlock /> : <div data-status={status} className={style.timelineBlock} />}
|
||||
<div
|
||||
className={contentClasses}
|
||||
data-status={status}
|
||||
@@ -77,7 +79,7 @@ interface TimelineEntryStatusProps {
|
||||
status: ProgressStatus;
|
||||
}
|
||||
|
||||
// we isolate this component to avoid isolate re-renders provoked by the clock changes
|
||||
// extract component to isolate re-renders provoked by the clock changes
|
||||
function TimelineEntryStatus(props: TimelineEntryStatusProps) {
|
||||
const { delay, start, status } = props;
|
||||
const { clock, offset } = useTimelineStatus();
|
||||
@@ -93,3 +95,10 @@ function TimelineEntryStatus(props: TimelineEntryStatusProps) {
|
||||
|
||||
return <div className={style.status}>{statusText}</div>;
|
||||
}
|
||||
|
||||
/** Generates a block level progress bar */
|
||||
function ActiveBlock() {
|
||||
const { current, duration } = useTimer();
|
||||
const progress = getProgress(current, duration);
|
||||
return <div data-status='live' className={style.timelineBlock} style={{ '--progress': `${progress}%` }} />;
|
||||
}
|
||||
|
||||
@@ -56,12 +56,7 @@ describe('getCSSPosition()', () => {
|
||||
describe('makeTimelineSections', () => {
|
||||
it('creates an array between the hours given, end excluded', () => {
|
||||
const result = makeTimelineSections(11, 17);
|
||||
expect(result).toEqual(['11:00', '12:00', '13:00', '14:00', '15:00', '16:00']);
|
||||
});
|
||||
|
||||
it('wraps around midnight', () => {
|
||||
const result = makeTimelineSections(22, 26);
|
||||
expect(result).toEqual(['22:00', '23:00', '00:00', '01:00']);
|
||||
expect(result).toEqual([11, 12, 13, 14, 15, 16]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+8
-7
@@ -1,16 +1,17 @@
|
||||
.markers {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
color: $ui-white;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
height: 1rem;
|
||||
line-height: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: calc(1rem - 2px);
|
||||
justify-content: space-evenly;
|
||||
|
||||
& > span {
|
||||
flex-grow: 1;
|
||||
border-left: 1px solid $white-7;
|
||||
height: 100vh;
|
||||
|
||||
&:not(:first-child) {
|
||||
border-left: 1px solid $white-7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ interface TimelineMarkersProps {
|
||||
endHour: number;
|
||||
}
|
||||
|
||||
/** Creates a line for every hour in the timeline */
|
||||
export default function TimelineMarkers(props: TimelineMarkersProps) {
|
||||
const { startHour, endHour } = props;
|
||||
|
||||
@@ -14,9 +15,9 @@ export default function TimelineMarkers(props: TimelineMarkersProps) {
|
||||
|
||||
return (
|
||||
<div className={style.markers}>
|
||||
{elements.map((tag, index) => {
|
||||
return <span key={`${index}-${tag}`}>{tag}</span>;
|
||||
})}
|
||||
{elements.map((tag) => (
|
||||
<span key={tag} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
.progressBar {
|
||||
width: 100%;
|
||||
height: 1rem;
|
||||
position: relative;
|
||||
|
||||
background-color: $gray-1000;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
|
||||
background-color: $active-red;
|
||||
transition-duration: 0.3s;
|
||||
transition-property: width;
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||
|
||||
import { useClock } from '../../../../common/hooks/useSocket';
|
||||
import { getRelativePositionX } from '../timeline.utils';
|
||||
|
||||
import style from './TimelineProgressBar.module.scss';
|
||||
|
||||
interface ProgressBarProps {
|
||||
startHour: number;
|
||||
endHour: number;
|
||||
}
|
||||
|
||||
export default function ProgressBar(props: ProgressBarProps) {
|
||||
const { startHour, endHour } = props;
|
||||
// TODO: how to account for days?
|
||||
const { clock } = useClock();
|
||||
|
||||
const width = getRelativePositionX(startHour * MILLIS_PER_HOUR, endHour * MILLIS_PER_HOUR, clock);
|
||||
|
||||
return (
|
||||
<div className={style.progressBar}>
|
||||
<div className={style.progress} style={{ width: `${width}%` }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
getTimeFromPrevious,
|
||||
isNewLatest,
|
||||
MILLIS_PER_HOUR,
|
||||
millisToString,
|
||||
removeSeconds,
|
||||
} from 'ontime-utils';
|
||||
|
||||
import { clamp } from '../../../common/utils/math';
|
||||
@@ -71,7 +69,7 @@ export function getEndHour(endTime: number): number {
|
||||
export function makeTimelineSections(firstHour: number, lastHour: number) {
|
||||
const timelineSections = [];
|
||||
for (let i = firstHour; i < lastHour; i++) {
|
||||
timelineSections.push(removeSeconds(millisToString((i % 24) * MILLIS_PER_HOUR)));
|
||||
timelineSections.push(i);
|
||||
}
|
||||
return timelineSections;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user