From b2c01a141a2669506856066fd6521dacf29223d2 Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Thu, 28 Dec 2023 13:25:45 +0100 Subject: [PATCH] Studio clock (#663) * refactor: trim rundown shows only upcoming * style: small tweaks --------- Co-authored-by: Carlos Valente --- .../assets/fonts/DSEG7ClassicMini-Regular.ttf | Bin 0 -> 23116 bytes .../src/assets/fonts/digital-7.monoitalic.ttf | Bin 22984 -> 0 bytes .../features/viewers/studio/StudioClock.scss | 307 ++++++++++-------- .../features/viewers/studio/StudioClock.tsx | 80 ++--- .../__tests__/studioClock.utils.test.ts | 139 ++------ .../viewers/studio/studioClock.utils.ts | 79 +---- apps/server/src/external/styles/override.css | 1 + 7 files changed, 251 insertions(+), 355 deletions(-) create mode 100644 apps/client/src/assets/fonts/DSEG7ClassicMini-Regular.ttf delete mode 100644 apps/client/src/assets/fonts/digital-7.monoitalic.ttf diff --git a/apps/client/src/assets/fonts/DSEG7ClassicMini-Regular.ttf b/apps/client/src/assets/fonts/DSEG7ClassicMini-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ffe3dd7da912f34bf87999b80a42a8fa3f49265e GIT binary patch literal 23116 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[]); - 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 (
+ {timeFormat == '12' &&
{isAm ? 'am' : 'pm'}
}
{clock}
- {eventNext?.title ?? ''} + {eventNext?.title ?? '---'}
-
- {selectedId !== null && formatDisplay(time.current)} +
+ {isNegative ? '-' : ''} + {formatDisplay(time.current)}
{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
-
-
    - {schedule.map((s) => ( -
  • -
    - {`${s.time} ${s.title}`} +
      + {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 ( +
    • + + + + + {event.title}
    • - ))} -
    -
    + ); + })} +
); diff --git a/apps/client/src/features/viewers/studio/__tests__/studioClock.utils.test.ts b/apps/client/src/features/viewers/studio/__tests__/studioClock.utils.test.ts index d76c41f42..99b6868f4 100644 --- a/apps/client/src/features/viewers/studio/__tests__/studioClock.utils.test.ts +++ b/apps/client/src/features/viewers/studio/__tests__/studioClock.utils.test.ts @@ -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); }); }); diff --git a/apps/client/src/features/viewers/studio/studioClock.utils.ts b/apps/client/src/features/viewers/studio/studioClock.utils.ts index fac8c724a..a3d8068b1 100644 --- a/apps/client/src/features/viewers/studio/studioClock.utils.ts +++ b/apps/client/src/features/viewers/studio/studioClock.utils.ts @@ -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 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, - }; - }); -}; +} diff --git a/apps/server/src/external/styles/override.css b/apps/server/src/external/styles/override.css index cc5a7cf21..61b45e6b5 100644 --- a/apps/server/src/external/styles/override.css +++ b/apps/server/src/external/styles/override.css @@ -23,6 +23,7 @@ --studio-idle: #cfcfcf; --studio-active-label: #101010; --studio-idle-label: #595959; + --studio-overtime: #101010; } .timer {