mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-05 06:19:11 +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 *;
|
@use '../../../theme/viewerDefs' as *;
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "digital-clock";
|
font-family: 'seven-seg';
|
||||||
src: local('digital-7'), url('./../../../assets/fonts/digital-7.monoitalic.ttf') format('truetype') ;
|
src:
|
||||||
|
local('DSEG7'),
|
||||||
|
url('./../../../assets/fonts/DSEG7ClassicMini-Regular.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =============== CLOCK STUFF ==================*/
|
/* =============== CLOCK STUFF ==================*/
|
||||||
|
|
||||||
$clock-size: 90vh;
|
$size-hours: clamp(12px, 2.5vmin, 22px);
|
||||||
$size-hours: min(3vh, 20px);
|
$half-hours: calc($size-hours / 2);
|
||||||
$half-hours: min(1.5vh, 10px);
|
$size-min: clamp(10px, 2vmin, 18px);
|
||||||
$size-min: min(2.5vh, 18px);
|
$half-min: calc($size-min / 2);
|
||||||
$half-min: min(1.25vh, 9px);
|
|
||||||
$red-active: #c53030;
|
$red-active: $red-700;
|
||||||
$red-idle: #000000;
|
$red-idle: $red-1350;
|
||||||
|
|
||||||
$cyan-active: #0ff;
|
$cyan-active: #0ff;
|
||||||
$cyan-idle: #0aa;
|
$cyan-idle: #0aa;
|
||||||
|
|
||||||
|
$orange-active: #f60;
|
||||||
|
|
||||||
.studio-clock {
|
.studio-clock {
|
||||||
margin: 0;
|
--clock-size: min(90vh, 90vw);
|
||||||
box-sizing: border-box; /* reset */
|
--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;
|
overflow: hidden;
|
||||||
width: 100%; /* restrict the page width to viewport */
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
padding: 1vw;
|
padding: 1vw;
|
||||||
|
|
||||||
background: var(--background-color-override, #000);
|
background: var(--background-color-override, #000);
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 95vh 1fr;
|
// we use vh since we are dealing with a square and the smaller dimension is the height
|
||||||
gap: 2vw;
|
grid-template-columns: calc(var(--clock-size) + 5vw) 1fr;
|
||||||
grid-template-areas: "clck schd";
|
gap: 2.5vw;
|
||||||
|
grid-template-areas: 'clock schedule';
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.clock-container {
|
.clock-container {
|
||||||
display: flex;
|
grid-area: clock;
|
||||||
flex-direction: column;
|
display: grid;
|
||||||
align-items: center;
|
place-content: center;
|
||||||
grid-area: clck;
|
justify-items: center;
|
||||||
width: $clock-size;
|
padding-top: 3em;
|
||||||
height: $clock-size;
|
gap: 0.5em;
|
||||||
aspect-ratio: 1;
|
width: 100%;
|
||||||
text-align: center;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 4vh auto;
|
font-family: seven-seg;
|
||||||
font-family: digital-clock, monospace;
|
}
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
.clock-indicators {
|
.clock-indicators {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.min,
|
.min,
|
||||||
.hours {
|
.hours {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--studio-idle, $red-idle);
|
background: var(--studio-idle, $red-idle);
|
||||||
|
|
||||||
&--active {
|
&--active {
|
||||||
background: var(--studio-active, $red-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: '-';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============= SCHEDULE STUFF =================*/
|
.min {
|
||||||
|
min-height: $size-min;
|
||||||
|
width: $size-min;
|
||||||
|
top: calc(50% - #{$half_min});
|
||||||
|
left: calc(50% - #{$half_min});
|
||||||
|
}
|
||||||
|
|
||||||
.schedule-container {
|
.hours {
|
||||||
grid-area: schd;
|
min-height: $size-hours;
|
||||||
margin: 4vh 0;
|
width: $size-hours;
|
||||||
font-family: digital-clock, monospace;
|
top: calc(50% - #{$half_hours});
|
||||||
text-transform: uppercase;
|
left: calc(50% - #{$half_hours});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.onAir {
|
.studio-timer {
|
||||||
padding-bottom: 2vh;
|
font-size: calc(var(--clock-size) / 5);
|
||||||
font-size: 15vh;
|
line-height: 1em;
|
||||||
line-height: 0.9em;
|
|
||||||
color: var(--studio-active, $red-active);
|
|
||||||
|
|
||||||
&--idle {
|
color: var(--studio-active, $red-active);
|
||||||
color: var(--studio-idle, $red-active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule {
|
&--with-seconds {
|
||||||
ul {
|
font-size: calc(var(--clock-size) / 6.5);
|
||||||
color: var(--studio-idle-label, $cyan-idle);
|
}
|
||||||
font-size: 3.75vh;
|
|
||||||
line-height: 1em;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
&::before {
|
||||||
margin-bottom: 1.5vh;
|
content: '88:88';
|
||||||
padding-left: 0.25em;
|
mix-blend-mode: hard-light;
|
||||||
display: flex;
|
opacity: 0.1;
|
||||||
align-items: center;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.now {
|
.ampmIndicator {
|
||||||
color: var(--studio-active-label, $cyan-active);
|
font-family: monospace;
|
||||||
}
|
font-size: calc(var(--clock-size) / 20);
|
||||||
|
position: relative;
|
||||||
|
top: -2em;
|
||||||
|
margin-bottom: -2em;
|
||||||
|
color: var(--studio-active, $red-active);
|
||||||
|
}
|
||||||
|
|
||||||
.next {
|
.next-title {
|
||||||
color: var(--studio-active, $red-active);
|
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 {
|
.next-countdown {
|
||||||
width: 0.35em;
|
color: var(--studio-active, $red-active);
|
||||||
height: 0.35em;
|
font-size: calc(var(--clock-size) / 10);
|
||||||
aspect-ratio: 1;
|
line-height: 1em;
|
||||||
background-color: var(--studio-idle, $red-idle);
|
|
||||||
margin-right: 0.35em;
|
&--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) {
|
@media only screen and (max-width: 1200px) {
|
||||||
.studio-clock {
|
.studio-clock {
|
||||||
display: grid;
|
grid-template-areas: 'clock';
|
||||||
grid-template-areas: "clck";
|
|
||||||
grid-template-columns: 100%;
|
grid-template-columns: 100%;
|
||||||
place-content: center;
|
|
||||||
|
|
||||||
.schedule-container {
|
.schedule-container {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import type { OntimeEvent, OntimeRundown, Settings, ViewSettings } from 'ontime-types';
|
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 { formatDisplay } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
@@ -12,9 +12,11 @@ import useFitText from '../../../common/hooks/useFitText';
|
|||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { secondsInMillis } from '../../../common/utils/dateConfig';
|
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';
|
import './StudioClock.scss';
|
||||||
|
|
||||||
@@ -42,14 +44,15 @@ export default function StudioClock(props: StudioClockProps) {
|
|||||||
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
|
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
|
||||||
|
|
||||||
const [schedule, setSchedule] = useState<ScheduleEvent[]>([]);
|
|
||||||
|
|
||||||
const activeIndicators = [...Array(12).keys()];
|
const activeIndicators = [...Array(12).keys()];
|
||||||
const secondsIndicators = [...Array(60).keys()];
|
const secondsIndicators = [...Array(60).keys()];
|
||||||
const MAX_TITLES = 12;
|
|
||||||
|
// TODO: fit titles on screen
|
||||||
|
const MAX_TITLES = 11;
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const showSeconds = searchParams.get('seconds');
|
const showSeconds = searchParams.get('seconds');
|
||||||
|
const timeFormat = resolveTimeFormat();
|
||||||
formatOptions.showSeconds = Boolean(showSeconds);
|
formatOptions.showSeconds = Boolean(showSeconds);
|
||||||
formatOptions.format = `hh:mm${formatOptions.showSeconds ? ':ss' : ''}`;
|
formatOptions.format = `hh:mm${formatOptions.showSeconds ? ':ss' : ''}`;
|
||||||
|
|
||||||
@@ -57,42 +60,38 @@ export default function StudioClock(props: StudioClockProps) {
|
|||||||
document.title = 'ontime - Studio Clock';
|
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 clock = formatTime(time.clock, formatOptions);
|
||||||
const secondsNow = secondsInMillis(time.clock);
|
const secondsNow = secondsInMillis(time.clock);
|
||||||
const isNegative = (time.current ?? 0) < 0;
|
const isNegative = (time.current ?? 0) < 0;
|
||||||
|
const isPaused = time.playback === Playback.Pause;
|
||||||
|
|
||||||
const studioClockOptions = getStudioClockOptions(settings?.timeFormat ?? '24');
|
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 (
|
return (
|
||||||
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={studioClockOptions} />
|
<ViewParamsEditor paramFields={studioClockOptions} />
|
||||||
<div className='clock-container'>
|
<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 className={`studio-timer ${showSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
||||||
<div
|
<div
|
||||||
ref={titleRef}
|
ref={titleRef}
|
||||||
className='next-title'
|
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>
|
||||||
<div className={isNegative ? 'next-countdown' : 'next-countdown next-countdown--overtime'}>
|
<div
|
||||||
{selectedId !== null && formatDisplay(time.current)}
|
className={`
|
||||||
|
next-countdown ${isNegative ? ' next-countdown--overtime' : ''} ${isPaused ? ' next-countdown--paused' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{isNegative ? '-' : ''}
|
||||||
|
{formatDisplay(time.current)}
|
||||||
</div>
|
</div>
|
||||||
<div className='clock-indicators'>
|
<div className='clock-indicators'>
|
||||||
{activeIndicators.map((i) => (
|
{activeIndicators.map((i) => (
|
||||||
@@ -100,7 +99,7 @@ export default function StudioClock(props: StudioClockProps) {
|
|||||||
key={i}
|
key={i}
|
||||||
className='hours hours--active'
|
className='hours hours--active'
|
||||||
style={{
|
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}
|
key={i}
|
||||||
className={i <= secondsNow ? 'min min--active' : 'min'}
|
className={i <= secondsNow ? 'min min--active' : 'min'}
|
||||||
style={{
|
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
|
ON AIR
|
||||||
</div>
|
</div>
|
||||||
<div className='schedule'>
|
<ul className='schedule'>
|
||||||
<ul>
|
{trimmedRundown.map((event) => {
|
||||||
{schedule.map((s) => (
|
const start = formatTime(event.timeStart + (event?.delay ?? 0));
|
||||||
<li key={s.id} className={s.isNow ? 'now' : s.isNext ? 'next' : ''}>
|
const isSelected = event.id === selectedId;
|
||||||
<div className='user-colour' style={{ backgroundColor: `${s.colour !== '' ? s.colour : ''}` }} />
|
const isNext = event.id === nextId;
|
||||||
{`${s.time} ${s.title}`}
|
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>
|
</li>
|
||||||
))}
|
);
|
||||||
</ul>
|
})}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</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', () => {
|
describe('test trimEventlist function', () => {
|
||||||
const limit = 8;
|
const limit = 8;
|
||||||
@@ -40,14 +40,14 @@ describe('test trimEventlist function', () => {
|
|||||||
it('when we use the third item', () => {
|
it('when we use the third item', () => {
|
||||||
const selectedId = '3';
|
const selectedId = '3';
|
||||||
const expected = [
|
const expected = [
|
||||||
{ id: '1' },
|
|
||||||
{ id: '2' },
|
|
||||||
{ id: '3' },
|
{ id: '3' },
|
||||||
{ id: '4' },
|
{ id: '4' },
|
||||||
{ id: '5' },
|
{ id: '5' },
|
||||||
{ id: '6' },
|
{ id: '6' },
|
||||||
{ id: '7' },
|
{ id: '7' },
|
||||||
{ id: '8' },
|
{ id: '8' },
|
||||||
|
{ id: '9' },
|
||||||
|
{ id: '10' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||||
@@ -58,14 +58,14 @@ describe('test trimEventlist function', () => {
|
|||||||
it('when we use the fourth item', () => {
|
it('when we use the fourth item', () => {
|
||||||
const selectedId = '4';
|
const selectedId = '4';
|
||||||
const expected = [
|
const expected = [
|
||||||
{ id: '2' },
|
|
||||||
{ id: '3' },
|
|
||||||
{ id: '4' },
|
{ id: '4' },
|
||||||
{ id: '5' },
|
{ id: '5' },
|
||||||
{ id: '6' },
|
{ id: '6' },
|
||||||
{ id: '7' },
|
{ id: '7' },
|
||||||
{ id: '8' },
|
{ id: '8' },
|
||||||
{ id: '9' },
|
{ id: '9' },
|
||||||
|
{ id: '10' },
|
||||||
|
{ id: '11' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||||
@@ -73,6 +73,15 @@ describe('test trimEventlist function', () => {
|
|||||||
expect(l).toStrictEqual(expected);
|
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', () => {
|
it('if selected is not found', () => {
|
||||||
const selectedId = '15';
|
const selectedId = '15';
|
||||||
const expected = [
|
const expected = [
|
||||||
@@ -90,114 +99,22 @@ describe('test trimEventlist function', () => {
|
|||||||
expect(l.length).toBe(limit);
|
expect(l.length).toBe(limit);
|
||||||
expect(l).toStrictEqual(expected);
|
expect(l).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('test formatEvents function', () => {
|
it('if there is nothing selected', () => {
|
||||||
const testEvent = [
|
const selectedId = null;
|
||||||
{
|
|
||||||
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';
|
|
||||||
const expected = [
|
const expected = [
|
||||||
{
|
{ id: '1' },
|
||||||
id: '5946',
|
{ id: '2' },
|
||||||
time: '08:00 - 08:30',
|
{ id: '3' },
|
||||||
title: 'Welcome to Ontime',
|
{ id: '4' },
|
||||||
isNow: false,
|
{ id: '5' },
|
||||||
isNext: false,
|
{ id: '6' },
|
||||||
colour: '',
|
{ id: '7' },
|
||||||
},
|
{ id: '8' },
|
||||||
{
|
|
||||||
id: '8ee5',
|
|
||||||
time: '09:40 - 09:50',
|
|
||||||
title: 'Unless recalled by the OSC address',
|
|
||||||
isNow: false,
|
|
||||||
isNext: false,
|
|
||||||
colour: '',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const parsed = formatEventList(testEvent as OntimeEvent[], selectedId, nextId, { showEnd: true });
|
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||||
expect(parsed).toStrictEqual(expected);
|
expect(l.length).toBe(limit);
|
||||||
});
|
expect(l).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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,76 +1,21 @@
|
|||||||
import { OntimeEvent } from 'ontime-types';
|
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
|
* @description Returns trimmed event list array
|
||||||
* @param {Object[]} rundown - given rundown
|
* @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
|
* @param {number} limit - max number of events to return
|
||||||
* @returns {Object[]} Event list with maximum <limit> objects
|
* @returns {Object[]} Event list with maximum <limit> objects
|
||||||
*/
|
*/
|
||||||
export const trimRundown = (rundown: OntimeEvent[], selectedId: string, limit: number): OntimeEvent[] => {
|
export function trimRundown(rundown: OntimeEvent[], selectedId: string | null, limit: number): OntimeEvent[] {
|
||||||
if (rundown == null) return [];
|
if (rundown.length < 1) return [];
|
||||||
|
const startIndex = selectedId
|
||||||
const BEFORE = 2;
|
? Math.max(
|
||||||
const trimmedRundown = [...rundown];
|
rundown.findIndex((event) => event.id === selectedId),
|
||||||
|
0,
|
||||||
// limit events length if necessary
|
)
|
||||||
if (limit != null) {
|
: 0;
|
||||||
while (trimmedRundown.length > limit) {
|
const endIndex = Math.min(startIndex + limit, rundown.length);
|
||||||
const idx = trimmedRundown.findIndex((e) => e.id === selectedId);
|
const trimmedRundown = rundown.slice(startIndex, endIndex);
|
||||||
if (idx <= BEFORE) {
|
|
||||||
trimmedRundown.pop();
|
|
||||||
} else {
|
|
||||||
trimmedRundown.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return trimmedRundown;
|
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-idle: #cfcfcf;
|
||||||
--studio-active-label: #101010;
|
--studio-active-label: #101010;
|
||||||
--studio-idle-label: #595959;
|
--studio-idle-label: #595959;
|
||||||
|
--studio-overtime: #101010;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timer {
|
.timer {
|
||||||
|
|||||||
Reference in New Issue
Block a user