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 000000000..ffe3dd7da Binary files /dev/null and b/apps/client/src/assets/fonts/DSEG7ClassicMini-Regular.ttf differ diff --git a/apps/client/src/assets/fonts/digital-7.monoitalic.ttf b/apps/client/src/assets/fonts/digital-7.monoitalic.ttf deleted file mode 100644 index a01db8a5d..000000000 Binary files a/apps/client/src/assets/fonts/digital-7.monoitalic.ttf and /dev/null differ diff --git a/apps/client/src/features/viewers/studio/StudioClock.scss b/apps/client/src/features/viewers/studio/StudioClock.scss index cfb997e49..0f230c2fe 100644 --- a/apps/client/src/features/viewers/studio/StudioClock.scss +++ b/apps/client/src/features/viewers/studio/StudioClock.scss @@ -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; diff --git a/apps/client/src/features/viewers/studio/StudioClock.tsx b/apps/client/src/features/viewers/studio/StudioClock.tsx index 79f92da4c..c97e287c5 100644 --- a/apps/client/src/features/viewers/studio/StudioClock.tsx +++ b/apps/client/src/features/viewers/studio/StudioClock.tsx @@ -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([]); - 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 {