mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 02:13:48 +00:00
Studio clock (#663)
* refactor: trim rundown shows only upcoming * style: small tweaks --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
committed by
GitHub
parent
dc4ba4012c
commit
b2c01a141a
Binary file not shown.
Binary file not shown.
@@ -1,182 +1,209 @@
|
||||
@use '../../../theme/ontimeColours' as *;
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
@font-face {
|
||||
font-family: "digital-clock";
|
||||
src: local('digital-7'), url('./../../../assets/fonts/digital-7.monoitalic.ttf') format('truetype') ;
|
||||
font-family: 'seven-seg';
|
||||
src:
|
||||
local('DSEG7'),
|
||||
url('./../../../assets/fonts/DSEG7ClassicMini-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* =============== CLOCK STUFF ==================*/
|
||||
|
||||
$clock-size: 90vh;
|
||||
$size-hours: min(3vh, 20px);
|
||||
$half-hours: min(1.5vh, 10px);
|
||||
$size-min: min(2.5vh, 18px);
|
||||
$half-min: min(1.25vh, 9px);
|
||||
$red-active: #c53030;
|
||||
$red-idle: #000000;
|
||||
$size-hours: clamp(12px, 2.5vmin, 22px);
|
||||
$half-hours: calc($size-hours / 2);
|
||||
$size-min: clamp(10px, 2vmin, 18px);
|
||||
$half-min: calc($size-min / 2);
|
||||
|
||||
$red-active: $red-700;
|
||||
$red-idle: $red-1350;
|
||||
|
||||
$cyan-active: #0ff;
|
||||
$cyan-idle: #0aa;
|
||||
|
||||
$orange-active: #f60;
|
||||
|
||||
.studio-clock {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
--clock-size: min(90vh, 90vw);
|
||||
--size-hours-gap: #{calc($size-hours * 1.5)};
|
||||
--half-size: calc(var(--clock-size) / 2);
|
||||
--smaller-half-size: calc(var(--half-size) - var(--size-hours-gap));
|
||||
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
padding: 1vw;
|
||||
|
||||
background: var(--background-color-override, #000);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 95vh 1fr;
|
||||
gap: 2vw;
|
||||
grid-template-areas: "clck schd";
|
||||
// we use vh since we are dealing with a square and the smaller dimension is the height
|
||||
grid-template-columns: calc(var(--clock-size) + 5vw) 1fr;
|
||||
gap: 2.5vw;
|
||||
grid-template-areas: 'clock schedule';
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.clock-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
grid-area: clck;
|
||||
width: $clock-size;
|
||||
height: $clock-size;
|
||||
aspect-ratio: 1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
margin: 4vh auto;
|
||||
font-family: digital-clock, monospace;
|
||||
text-transform: uppercase;
|
||||
.clock-container {
|
||||
grid-area: clock;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
justify-items: center;
|
||||
padding-top: 3em;
|
||||
gap: 0.5em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
font-family: seven-seg;
|
||||
}
|
||||
|
||||
.clock-indicators {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.clock-indicators {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.min,
|
||||
.hours {
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
background: var(--studio-idle, $red-idle);
|
||||
.min,
|
||||
.hours {
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
background: var(--studio-idle, $red-idle);
|
||||
|
||||
&--active {
|
||||
background: var(--studio-active, $red-active);
|
||||
}
|
||||
}
|
||||
|
||||
.min {
|
||||
min-height: $size-min;
|
||||
width: $size-min;
|
||||
top: calc(50% - #{$half_min});
|
||||
left: calc(50% - #{$half_min});
|
||||
}
|
||||
|
||||
.hours {
|
||||
min-height: $size-hours;
|
||||
width: $size-hours;
|
||||
top: calc(50% - #{$half_hours});
|
||||
left: calc(50% - #{$half_hours});
|
||||
}
|
||||
}
|
||||
|
||||
.studio-timer {
|
||||
color: var(--studio-active, $red-active);
|
||||
font-size: calc(#{$clock-size} / 3);
|
||||
margin-top: calc(50% - calc(#{$clock-size} / 7));
|
||||
line-height: 0.8em;
|
||||
|
||||
&--with-seconds {
|
||||
font-size: calc(#{$clock-size} / 4.6);
|
||||
margin-top: calc(50% - calc(#{$clock-size} / 11));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.next-title:after,
|
||||
.next-title:after,
|
||||
.next-countdown--overtime:after {
|
||||
content: '\200b';
|
||||
}
|
||||
|
||||
.next-title {
|
||||
color: var(--studio-idle-label, $cyan-idle);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.next-countdown {
|
||||
color: var(--studio-active-label, $cyan-active);
|
||||
|
||||
font-size: 10vh;
|
||||
line-height: 1em;
|
||||
|
||||
&--overtime {
|
||||
color: darken($red-active, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.next-countdown::before {
|
||||
content: '-';
|
||||
&--active {
|
||||
background: var(--studio-active, $red-active);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============= SCHEDULE STUFF =================*/
|
||||
.min {
|
||||
min-height: $size-min;
|
||||
width: $size-min;
|
||||
top: calc(50% - #{$half_min});
|
||||
left: calc(50% - #{$half_min});
|
||||
}
|
||||
|
||||
.schedule-container {
|
||||
grid-area: schd;
|
||||
margin: 4vh 0;
|
||||
font-family: digital-clock, monospace;
|
||||
text-transform: uppercase;
|
||||
.hours {
|
||||
min-height: $size-hours;
|
||||
width: $size-hours;
|
||||
top: calc(50% - #{$half_hours});
|
||||
left: calc(50% - #{$half_hours});
|
||||
}
|
||||
}
|
||||
|
||||
.onAir {
|
||||
padding-bottom: 2vh;
|
||||
font-size: 15vh;
|
||||
line-height: 0.9em;
|
||||
color: var(--studio-active, $red-active);
|
||||
.studio-timer {
|
||||
font-size: calc(var(--clock-size) / 5);
|
||||
line-height: 1em;
|
||||
|
||||
&--idle {
|
||||
color: var(--studio-idle, $red-active);
|
||||
}
|
||||
}
|
||||
color: var(--studio-active, $red-active);
|
||||
|
||||
.schedule {
|
||||
ul {
|
||||
color: var(--studio-idle-label, $cyan-idle);
|
||||
font-size: 3.75vh;
|
||||
line-height: 1em;
|
||||
list-style: none;
|
||||
}
|
||||
&--with-seconds {
|
||||
font-size: calc(var(--clock-size) / 6.5);
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 1.5vh;
|
||||
padding-left: 0.25em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
&::before {
|
||||
content: '88:88';
|
||||
mix-blend-mode: hard-light;
|
||||
opacity: 0.1;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.now {
|
||||
color: var(--studio-active-label, $cyan-active);
|
||||
}
|
||||
.ampmIndicator {
|
||||
font-family: monospace;
|
||||
font-size: calc(var(--clock-size) / 20);
|
||||
position: relative;
|
||||
top: -2em;
|
||||
margin-bottom: -2em;
|
||||
color: var(--studio-active, $red-active);
|
||||
}
|
||||
|
||||
.next {
|
||||
color: var(--studio-active, $red-active);
|
||||
}
|
||||
.next-title {
|
||||
font-family: monospace;
|
||||
font-weight: 400;
|
||||
color: var(--studio-active-label, $cyan-active);
|
||||
font-size: calc(var(--clock-size) / 10);
|
||||
text-align: center;
|
||||
overflow: clip hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-colour {
|
||||
width: 0.35em;
|
||||
height: 0.35em;
|
||||
aspect-ratio: 1;
|
||||
background-color: var(--studio-idle, $red-idle);
|
||||
margin-right: 0.35em;
|
||||
}
|
||||
}
|
||||
.next-countdown {
|
||||
color: var(--studio-active, $red-active);
|
||||
font-size: calc(var(--clock-size) / 10);
|
||||
line-height: 1em;
|
||||
|
||||
&--overtime {
|
||||
color: var(--studio-active, $red-active);
|
||||
}
|
||||
&--paused {
|
||||
color: var(--studio-overtime, $orange-active);
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-container {
|
||||
grid-area: schedule;
|
||||
font-family: monospace;
|
||||
margin-right: 2vw;
|
||||
}
|
||||
|
||||
.onAir {
|
||||
font-size: 13vh;
|
||||
padding-top: 6vh;
|
||||
padding-bottom: 0.25em;
|
||||
line-height: 0.8em;
|
||||
color: var(--studio-active, $red-active);
|
||||
|
||||
&--idle {
|
||||
color: var(--studio-idle, $red-idle);
|
||||
}
|
||||
}
|
||||
|
||||
.schedule {
|
||||
color: var(--studio-idle-label, $red-idle);
|
||||
font-size: 3.5vh;
|
||||
line-height: 1.25em;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.schedule__item {
|
||||
margin-bottom: 1.5vh;
|
||||
padding-left: 0.25em;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 0.5em;
|
||||
white-space: nowrap;
|
||||
|
||||
&--now {
|
||||
color: var(--studio-active-label, $red-active);
|
||||
}
|
||||
|
||||
&--next {
|
||||
color: var(--studio-active, $cyan-active);
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
&--future {
|
||||
color: var(--studio-idle-label, $cyan-idle);
|
||||
}
|
||||
}
|
||||
|
||||
.event {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
|
||||
&__colour {
|
||||
width: 0.35em;
|
||||
height: 0.35em;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 0.35em;
|
||||
background-color: var(--studio-idle, $red-idle);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.studio-clock {
|
||||
display: grid;
|
||||
grid-template-areas: "clck";
|
||||
grid-template-areas: 'clock';
|
||||
grid-template-columns: 100%;
|
||||
place-content: center;
|
||||
|
||||
.schedule-container {
|
||||
display: none;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import type { OntimeEvent, OntimeRundown, Settings, ViewSettings } from 'ontime-types';
|
||||
import { SupportedEvent } from 'ontime-types';
|
||||
import { isOntimeEvent, Playback } from 'ontime-types';
|
||||
import { formatDisplay } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
@@ -12,9 +12,11 @@ import useFitText from '../../../common/hooks/useFitText';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { secondsInMillis } from '../../../common/utils/dateConfig';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { formatTime, resolveTimeFormat } from '../../../common/utils/time';
|
||||
import { mth } from '../../../common/utils/timeConstants';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
|
||||
import { type ScheduleEvent, formatEventList, trimRundown } from './studioClock.utils';
|
||||
import { trimRundown } from './studioClock.utils';
|
||||
|
||||
import './StudioClock.scss';
|
||||
|
||||
@@ -42,14 +44,15 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
|
||||
|
||||
const [schedule, setSchedule] = useState<ScheduleEvent[]>([]);
|
||||
|
||||
const activeIndicators = [...Array(12).keys()];
|
||||
const secondsIndicators = [...Array(60).keys()];
|
||||
const MAX_TITLES = 12;
|
||||
|
||||
// TODO: fit titles on screen
|
||||
const MAX_TITLES = 11;
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const showSeconds = searchParams.get('seconds');
|
||||
const timeFormat = resolveTimeFormat();
|
||||
formatOptions.showSeconds = Boolean(showSeconds);
|
||||
formatOptions.format = `hh:mm${formatOptions.showSeconds ? ':ss' : ''}`;
|
||||
|
||||
@@ -57,42 +60,38 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
document.title = 'ontime - Studio Clock';
|
||||
}, []);
|
||||
|
||||
// Prepare event list
|
||||
useEffect(() => {
|
||||
if (!backstageEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delayed = backstageEvents.filter((event) => event.type === SupportedEvent.Event);
|
||||
const trimmed = trimRundown(delayed as OntimeEvent[], selectedId || '', MAX_TITLES);
|
||||
|
||||
const formatted = formatEventList(trimmed, selectedId || '', nextId || '', {
|
||||
showEnd: false,
|
||||
});
|
||||
setSchedule(formatted);
|
||||
}, [backstageEvents, nextId, selectedId]);
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const secondsNow = secondsInMillis(time.clock);
|
||||
const isNegative = (time.current ?? 0) < 0;
|
||||
const isPaused = time.playback === Playback.Pause;
|
||||
|
||||
const studioClockOptions = getStudioClockOptions(settings?.timeFormat ?? '24');
|
||||
|
||||
const delayed = backstageEvents.filter((event) => isOntimeEvent(event)) as OntimeEvent[];
|
||||
const trimmedRundown = trimRundown(delayed, selectedId, MAX_TITLES);
|
||||
const isAm = time.clock / (mth * 12) > 12;
|
||||
|
||||
return (
|
||||
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={studioClockOptions} />
|
||||
<div className='clock-container'>
|
||||
{timeFormat == '12' && <div className='ampmIndicator'>{isAm ? 'am' : 'pm'}</div>}
|
||||
<div className={`studio-timer ${showSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
||||
<div
|
||||
ref={titleRef}
|
||||
className='next-title'
|
||||
style={{ fontSize: titleFontSize, height: '10vh', width: '100%', maxWidth: '75%' }}
|
||||
style={{ fontSize: titleFontSize, height: '12.5vh', width: '100%', maxWidth: '80%' }}
|
||||
>
|
||||
{eventNext?.title ?? ''}
|
||||
{eventNext?.title ?? '---'}
|
||||
</div>
|
||||
<div className={isNegative ? 'next-countdown' : 'next-countdown next-countdown--overtime'}>
|
||||
{selectedId !== null && formatDisplay(time.current)}
|
||||
<div
|
||||
className={`
|
||||
next-countdown ${isNegative ? ' next-countdown--overtime' : ''} ${isPaused ? ' next-countdown--paused' : ''}
|
||||
`}
|
||||
>
|
||||
{isNegative ? '-' : ''}
|
||||
{formatDisplay(time.current)}
|
||||
</div>
|
||||
<div className='clock-indicators'>
|
||||
{activeIndicators.map((i) => (
|
||||
@@ -100,7 +99,7 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
key={i}
|
||||
className='hours hours--active'
|
||||
style={{
|
||||
transform: `rotate(${(360 / 12) * i - 90}deg) translateX(40vh)`,
|
||||
transform: `rotate(${(360 / 12) * i - 90}deg) translateX(var(--smaller-half-size))`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -109,7 +108,7 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
key={i}
|
||||
className={i <= secondsNow ? 'min min--active' : 'min'}
|
||||
style={{
|
||||
transform: `rotate(${(360 / 60) * i - 90}deg) translateX(43vh)`,
|
||||
transform: `rotate(${(360 / 60) * i - 90}deg) translateX(var(--half-size))`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -122,16 +121,23 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
>
|
||||
ON AIR
|
||||
</div>
|
||||
<div className='schedule'>
|
||||
<ul>
|
||||
{schedule.map((s) => (
|
||||
<li key={s.id} className={s.isNow ? 'now' : s.isNext ? 'next' : ''}>
|
||||
<div className='user-colour' style={{ backgroundColor: `${s.colour !== '' ? s.colour : ''}` }} />
|
||||
{`${s.time} ${s.title}`}
|
||||
<ul className='schedule'>
|
||||
{trimmedRundown.map((event) => {
|
||||
const start = formatTime(event.timeStart + (event?.delay ?? 0));
|
||||
const isSelected = event.id === selectedId;
|
||||
const isNext = event.id === nextId;
|
||||
const classes = `schedule__item schedule__item${isSelected ? '--now' : isNext ? '--next' : '--future'}`;
|
||||
return (
|
||||
<li key={event.id} className={classes}>
|
||||
<span className='event'>
|
||||
<span className='event__colour' style={{ backgroundColor: `${event.colour}` }} />
|
||||
<SuperscriptTime time={start} />
|
||||
</span>
|
||||
<span>{event.title}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { formatEventList, trimRundown } from '../studioClock.utils';
|
||||
import { trimRundown } from '../studioClock.utils';
|
||||
|
||||
describe('test trimEventlist function', () => {
|
||||
const limit = 8;
|
||||
@@ -40,14 +40,14 @@ describe('test trimEventlist function', () => {
|
||||
it('when we use the third item', () => {
|
||||
const selectedId = '3';
|
||||
const expected = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' },
|
||||
{ id: '10' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||
@@ -58,14 +58,14 @@ describe('test trimEventlist function', () => {
|
||||
it('when we use the fourth item', () => {
|
||||
const selectedId = '4';
|
||||
const expected = [
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' },
|
||||
{ id: '10' },
|
||||
{ id: '11' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||
@@ -73,6 +73,15 @@ describe('test trimEventlist function', () => {
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('result array is smaller than limit', () => {
|
||||
const selectedId = '8';
|
||||
const expected = [{ id: '8' }, { id: '9' }, { id: '10' }, { id: '11' }, { id: '12' }];
|
||||
|
||||
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||
expect(l.length).toBe(expected.length);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if selected is not found', () => {
|
||||
const selectedId = '15';
|
||||
const expected = [
|
||||
@@ -90,114 +99,22 @@ describe('test trimEventlist function', () => {
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test formatEvents function', () => {
|
||||
const testEvent = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
subtitle: 'Subtitles are useful',
|
||||
presenter: 'cpvalente',
|
||||
note: 'Maybe a running note for the operator?',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
isPublic: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: 'In green, below',
|
||||
timeStart: 34800000,
|
||||
timeEnd: 35400000,
|
||||
isPublic: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
|
||||
it('it parses correctly', () => {
|
||||
const selectedId = 'otherEvent';
|
||||
const nextId = 'notHere';
|
||||
it('if there is nothing selected', () => {
|
||||
const selectedId = null;
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent as OntimeEvent[], selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('it handles selected correctly', () => {
|
||||
const selectedId = '5946';
|
||||
const nextId = '8ee5';
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: true,
|
||||
colour: '',
|
||||
},
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent as OntimeEvent[], selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('it handles next correctly', () => {
|
||||
const selectedId = '8ee5';
|
||||
const nextId = 'notHere';
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent as OntimeEvent[], selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,76 +1,21 @@
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
export type ScheduleEvent = {
|
||||
id: string;
|
||||
time: string;
|
||||
title: string;
|
||||
isNow: boolean;
|
||||
isNext: boolean;
|
||||
colour: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Returns trimmed event list array
|
||||
* @param {Object[]} rundown - given rundown
|
||||
* @param {string} selectedId - id of currently selected event
|
||||
* @param {string | null} selectedId - id of currently selected event
|
||||
* @param {number} limit - max number of events to return
|
||||
* @returns {Object[]} Event list with maximum <limit> objects
|
||||
*/
|
||||
export const trimRundown = (rundown: OntimeEvent[], selectedId: string, limit: number): OntimeEvent[] => {
|
||||
if (rundown == null) return [];
|
||||
|
||||
const BEFORE = 2;
|
||||
const trimmedRundown = [...rundown];
|
||||
|
||||
// limit events length if necessary
|
||||
if (limit != null) {
|
||||
while (trimmedRundown.length > limit) {
|
||||
const idx = trimmedRundown.findIndex((e) => e.id === selectedId);
|
||||
if (idx <= BEFORE) {
|
||||
trimmedRundown.pop();
|
||||
} else {
|
||||
trimmedRundown.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
export function trimRundown(rundown: OntimeEvent[], selectedId: string | null, limit: number): OntimeEvent[] {
|
||||
if (rundown.length < 1) return [];
|
||||
const startIndex = selectedId
|
||||
? Math.max(
|
||||
rundown.findIndex((event) => event.id === selectedId),
|
||||
0,
|
||||
)
|
||||
: 0;
|
||||
const endIndex = Math.min(startIndex + limit, rundown.length);
|
||||
const trimmedRundown = rundown.slice(startIndex, endIndex);
|
||||
return trimmedRundown;
|
||||
};
|
||||
|
||||
type FormatEventListOptionsProp = {
|
||||
showEnd?: boolean;
|
||||
};
|
||||
/**
|
||||
* @description Returns list of events formatted to be displayed
|
||||
* @param {Object[]} rundown - given rundown
|
||||
* @param {string} selectedId - id of currently selected event
|
||||
* @param {string} nextId - id of next event
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.showEnd] - whether to show the end time
|
||||
* @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}]
|
||||
*/
|
||||
export const formatEventList = (
|
||||
rundown: OntimeEvent[],
|
||||
selectedId: string,
|
||||
nextId: string,
|
||||
options: FormatEventListOptionsProp,
|
||||
): ScheduleEvent[] => {
|
||||
if (rundown == null) return [];
|
||||
const { showEnd = false } = options;
|
||||
|
||||
// format list
|
||||
return rundown.map((event) => {
|
||||
const start = formatTime(event.timeStart + (event.delay || 0));
|
||||
const end = formatTime(event.timeEnd + (event.delay || 0));
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
time: showEnd ? `${start} - ${end}` : start,
|
||||
title: event.title,
|
||||
isNow: event.id === selectedId,
|
||||
isNext: event.id === nextId,
|
||||
colour: event.colour,
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
--studio-idle: #cfcfcf;
|
||||
--studio-active-label: #101010;
|
||||
--studio-idle-label: #595959;
|
||||
--studio-overtime: #101010;
|
||||
}
|
||||
|
||||
.timer {
|
||||
|
||||
Reference in New Issue
Block a user