mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 21:24:11 +00:00
V3 (#657)
* refactor: cleanup routes * style: smaller base font * chore: upgrade dependencies * chore: lock node version to electron * refactor: pass HTTP to integration controller (#652) * refactor: deprecate onair control * refactor: remove playback router * Several project files user folder (#617) * chore: automated screenshots (#667) * feat: app settings (#658) * refactor: remove deprecated event data (#674) * Studio clock (#663) --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Feat: reorder events with alt+ctrl + arrow up/down (#645) * Warning and danger per event (#677) --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * refactor: stabilise actionHandler (#683) Co-authored-by: Fabian Posenau <fabian@fphome.de> * improvement: hide seconds (#675) * wip: overview (#688) * fix: focus cursor (#695) * refactor: update lower third (#665) * Refactor/time formatting (#696) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * feat: multiple selection (#703) --------- Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com> Co-authored-by: Alex <ac@omnivox.dk> * fix: test - go to `Edit mode` befor tying to click `Event options` button (#708) * refactor: runtime service (#715) * fix: issue with loosing cursor position on message (#719) * remove info panel (#721) * Event editor continue (#722) * update API - part (#709) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * refactor: update timers (#729) * feat: many timers (#706) --------- Co-authored-by: arc-alex <ac@omnivox.dk> * refactor: excel cleanup (#734) * refactor: allow import of blocks and skip import (#735) * Project manager (#697) * refactor: UI for linking events (#763) * upgraded pipeline actions (#777) * Over under (#771) * custom fields (#744) --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Sheets settings (#774) --------- Co-authored-by: arc-alex <ac@omnivox.dk> * style: tweaks to lower thirds (#785) * refactor: delays account for gaps (#784) * refactor: partial state updates (#780) * feat: generate crash report (#787) * Sheet use limited input device auth flow (#782) --------- Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Custom fields views (#789) * refactor: deprecate presenter and subtitle (#795) * refactor: organise API around resources (#798) --------- Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com> * Time to end (#804) * Skip fixes (#805) * fix: onair derives from playback * Param nav (#822) --------- Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk> * refactor: download files from interface (#831) * Quick options (#814) * End pause (#832) * chore: bump node version in docker (#834) * refactor: follow in run mode (#840) * fix: uncaught error in http integration (#837) * Apply project (#843) Co-authored-by: Matteo Gheza <matteo.gheza07@gmail.com> Co-authored-by: Ary <arylmoraesn@gmail.com> Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk> Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Alex Rohleder <alexrohleder96@gmail.com> Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com> Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com> Co-authored-by: Fabian Posenau <fabianpos99+github@gmail.com>
This commit is contained in:
@@ -1,183 +1,208 @@
|
||||
@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: #300000;
|
||||
$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;
|
||||
grid-area: clock;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
justify-items: center;
|
||||
padding-top: 3em;
|
||||
gap: 0.5em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
margin: 4vh auto;
|
||||
font-family: digital-clock, monospace;
|
||||
text-transform: uppercase;
|
||||
font-family: seven-seg;
|
||||
}
|
||||
|
||||
.clock-indicators {
|
||||
.clock-indicators {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.min,
|
||||
.hours {
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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});
|
||||
&--active {
|
||||
background: var(--studio-active, $red-active);
|
||||
}
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
.min {
|
||||
min-height: $size-min;
|
||||
width: $size-min;
|
||||
top: calc(50% - #{$half_min});
|
||||
left: calc(50% - #{$half_min});
|
||||
}
|
||||
|
||||
|
||||
.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: var(--studio-active, $red-active);
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.next-countdown::before {
|
||||
content: '-';
|
||||
.hours {
|
||||
min-height: $size-hours;
|
||||
width: $size-hours;
|
||||
top: calc(50% - #{$half_hours});
|
||||
left: calc(50% - #{$half_hours});
|
||||
}
|
||||
}
|
||||
|
||||
/* ============= SCHEDULE STUFF =================*/
|
||||
.studio-timer {
|
||||
font-size: calc(var(--clock-size) / 5);
|
||||
line-height: 1em;
|
||||
|
||||
.schedule-container {
|
||||
grid-area: schd;
|
||||
margin: 4vh 0;
|
||||
font-family: digital-clock, monospace;
|
||||
text-transform: uppercase;
|
||||
color: var(--studio-active, $red-active);
|
||||
|
||||
.onAir {
|
||||
padding-bottom: 2vh;
|
||||
font-size: 15vh;
|
||||
line-height: 0.9em;
|
||||
color: var(--studio-active, $red-active);
|
||||
|
||||
&--idle {
|
||||
color: var(--studio-idle, $red-idle);
|
||||
}
|
||||
&--with-seconds {
|
||||
font-size: calc(var(--clock-size) / 6.5);
|
||||
}
|
||||
|
||||
.schedule {
|
||||
ul {
|
||||
color: var(--studio-idle-label, $cyan-idle);
|
||||
font-size: 3.75vh;
|
||||
line-height: 1em;
|
||||
list-style: none;
|
||||
}
|
||||
&::before {
|
||||
content: '88:88';
|
||||
mix-blend-mode: hard-light;
|
||||
opacity: 0.1;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 1.5vh;
|
||||
padding-left: 0.25em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clock__ampm {
|
||||
font-family: monospace;
|
||||
font-size: calc(var(--clock-size) / 20);
|
||||
position: relative;
|
||||
top: -2em;
|
||||
margin-bottom: -2em;
|
||||
color: var(--studio-active, $red-active);
|
||||
}
|
||||
|
||||
.now {
|
||||
color: var(--studio-active-label, $cyan-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;
|
||||
}
|
||||
|
||||
.next {
|
||||
color: var(--studio-active, $red-active);
|
||||
}
|
||||
.next-countdown {
|
||||
color: var(--studio-active, $red-active);
|
||||
font-size: calc(var(--clock-size) / 10);
|
||||
line-height: 1em;
|
||||
|
||||
.user-colour {
|
||||
width: 0.35em;
|
||||
height: 0.35em;
|
||||
aspect-ratio: 1;
|
||||
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) {
|
||||
.studio-clock {
|
||||
display: grid;
|
||||
grid-template-areas: "clck";
|
||||
grid-template-areas: 'clock';
|
||||
grid-template-columns: 100%;
|
||||
place-content: center;
|
||||
|
||||
.schedule-container {
|
||||
display: none;
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import type { OntimeEvent, OntimeRundown, Settings, ViewSettings } from 'ontime-types';
|
||||
import { SupportedEvent } from 'ontime-types';
|
||||
import { formatDisplay } from 'ontime-utils';
|
||||
import { isOntimeEvent, Playback } from 'ontime-types';
|
||||
import { millisToString, removeSeconds } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { overrideStylesURL } from '../../../common/api/constants';
|
||||
import { getStudioClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
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 { useWindowTitle } from '../../../common/hooks/useWindowTitle';
|
||||
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
||||
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import { isStringBoolean } from '../common/viewUtils';
|
||||
|
||||
import { type ScheduleEvent, formatEventList, trimRundown } from './studioClock.utils';
|
||||
import { secondsInMillis, trimRundown } from './studioClock.utils';
|
||||
|
||||
import './StudioClock.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: false,
|
||||
format: 'hh:mm',
|
||||
};
|
||||
|
||||
interface StudioClockProps {
|
||||
isMirrored: boolean;
|
||||
eventNext: OntimeEvent | null;
|
||||
time: TimeManagerType;
|
||||
time: ViewExtendedTimer;
|
||||
backstageEvents: OntimeRundown;
|
||||
selectedId: string | null;
|
||||
nextId: string | null;
|
||||
@@ -38,61 +33,65 @@ interface StudioClockProps {
|
||||
export default function StudioClock(props: StudioClockProps) {
|
||||
const { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, viewSettings, settings } = props;
|
||||
|
||||
// TODO: can we prevent the Flash of Unstyled Content on the 7segment fonts?
|
||||
// deferring rendering seems to affect styling (font and useFitText)
|
||||
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');
|
||||
formatOptions.showSeconds = Boolean(showSeconds);
|
||||
formatOptions.format = `hh:mm${formatOptions.showSeconds ? ':ss' : ''}`;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Studio Clock';
|
||||
}, []);
|
||||
useWindowTitle('Studio Clock');
|
||||
|
||||
// Prepare event list
|
||||
useEffect(() => {
|
||||
if (!backstageEvents) {
|
||||
return;
|
||||
}
|
||||
let clock = formatTime(time.clock);
|
||||
let hasAmPm = '';
|
||||
if (clock.includes('AM')) {
|
||||
clock = clock.replace('PM', '');
|
||||
hasAmPm = 'AM';
|
||||
} else if (clock.includes('PM')) {
|
||||
clock = clock.replace('PM', '');
|
||||
hasAmPm = 'PM';
|
||||
}
|
||||
|
||||
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 defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||
const studioClockOptions = getStudioClockOptions(defaultFormat);
|
||||
|
||||
const delayed = backstageEvents.filter((event) => isOntimeEvent(event)) as OntimeEvent[];
|
||||
const trimmedRundown = trimRundown(delayed, selectedId, MAX_TITLES);
|
||||
let timer = millisToString(time.current, { fallback: '---' });
|
||||
const hideSeconds = isStringBoolean(searchParams.get('hideTimerSeconds'));
|
||||
if (time.current != null && hideSeconds) {
|
||||
timer = removeSeconds(timer);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={studioClockOptions} />
|
||||
<div className='clock-container'>
|
||||
<div className={`studio-timer ${showSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
||||
{hasAmPm && <div className='clock__ampm'>{hasAmPm}</div>}
|
||||
<div className={`studio-timer ${!hideSeconds ? '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' : ''}
|
||||
`}
|
||||
>
|
||||
{timer}
|
||||
</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), { format12: 'h:mm a', format24: 'HH:mm' });
|
||||
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 { secondsInMillis, 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,33 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('secondsInMillis()', () => {
|
||||
it('return 0 if value is null', () => {
|
||||
expect(secondsInMillis(null)).toBe(0);
|
||||
});
|
||||
it('returns the seconds value of a millis date', () => {
|
||||
const date = 1686255053619; // Thu Jun 08 2023 20:10:53
|
||||
const seconds = secondsInMillis(date);
|
||||
expect(seconds).toBe(53);
|
||||
});
|
||||
});
|
||||
@@ -1,76 +1,34 @@
|
||||
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;
|
||||
};
|
||||
import { MaybeNumber, OntimeEvent } from 'ontime-types';
|
||||
import { MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* @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}]
|
||||
* @description Returns amount of seconds in a date given in milliseconds. For studio clock second indicator
|
||||
* @param {MaybeNumber} millis time to format
|
||||
* @returns amount of elapsed seconds
|
||||
*/
|
||||
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,
|
||||
};
|
||||
});
|
||||
};
|
||||
export function secondsInMillis(millis: MaybeNumber): number {
|
||||
if (!millis) {
|
||||
return 0;
|
||||
}
|
||||
return Math.floor((millis % MILLIS_PER_MINUTE) / MILLIS_PER_SECOND);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user