Skip to content

Commit 267ec6f

Browse files
committed
SOFIE-261 | (WIP) add estimates over/under to t-timers UI in director
screen
1 parent b7dd288 commit 267ec6f

File tree

9 files changed

+275
-25
lines changed

9 files changed

+275
-25
lines changed

packages/corelib/src/dataModel/RundownPlaylist.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,23 @@ export interface RundownTTimer {
165165
*/
166166
state: TimerState | null
167167

168+
/** The estimated time when we expect to reach the anchor part, for calculating over/under diff.
169+
*
170+
* Based on scheduled durations of remaining parts and segments up to the anchor.
171+
* Running means we are progressing towards the anchor (estimate moves with real time).
172+
* Paused means we are pushing (e.g. overrunning the current segment, so the anchor is being delayed).
173+
*
174+
* Calculated automatically when anchorPartId is set, or can be set manually by a blueprint.
175+
*/
176+
estimateState?: TimerState
177+
178+
/** The target Part that this timer is counting towards (the "timing anchor").
179+
*
180+
* When set, the server calculates estimateState based on when we expect to reach this part.
181+
* If not set, estimateState is not calculated automatically but can still be set manually by a blueprint.
182+
*/
183+
anchorPartId?: PartId
184+
168185
/*
169186
* Future ideas:
170187
* allowUiControl: boolean
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
2+
3+
/**
4+
* Calculate the display diff for a T-Timer.
5+
* For countdown/timeOfDay: positive = time remaining, negative = overrun.
6+
* For freeRun: positive = elapsed time.
7+
*/
8+
export function calculateTTimerDiff(timer: RundownTTimer, now: number): number {
9+
if (!timer.state) {
10+
return 0
11+
}
12+
13+
// Get current time: either frozen duration or calculated from zeroTime
14+
const currentTime = timer.state.paused ? timer.state.duration : timer.state.zeroTime - now
15+
16+
// Free run counts up, so negate to get positive elapsed time
17+
if (timer.mode?.type === 'freeRun') {
18+
return -currentTime
19+
}
20+
21+
// Apply stopAtZero if configured
22+
if (timer.mode?.stopAtZero && currentTime < 0) {
23+
return 0
24+
}
25+
26+
return currentTime
27+
}
28+
29+
/**
30+
* Calculate the over/under difference between the timer's current value
31+
* and its estimate.
32+
*
33+
* Positive = over (behind schedule, will reach anchor after timer hits zero)
34+
* Negative = under (ahead of schedule, will reach anchor before timer hits zero)
35+
*
36+
* Returns undefined if no estimate is available.
37+
*/
38+
export function calculateTTimerOverUnder(timer: RundownTTimer, now: number): number | undefined {
39+
if (!timer.state || !timer.estimateState) {
40+
return undefined
41+
}
42+
43+
const duration = timer.state.paused ? timer.state.duration : timer.state.zeroTime - now
44+
const estimateDuration = timer.estimateState.paused
45+
? timer.estimateState.duration
46+
: timer.estimateState.zeroTime - now
47+
48+
return duration - estimateDuration
49+
}
50+
51+
// TODO: remove this mock
52+
let mockTimer: RundownTTimer | undefined
53+
54+
export function getDefaultTTimer(tTimers: [RundownTTimer, RundownTTimer, RundownTTimer]): RundownTTimer | undefined {
55+
const active = tTimers.find((t) => t.mode)
56+
if (active) return active
57+
58+
if (!mockTimer) {
59+
const now = Date.now()
60+
mockTimer = {
61+
index: 0,
62+
label: 'MOCK TIMER',
63+
mode: {
64+
type: 'countdown',
65+
},
66+
state: {
67+
zeroTime: now + 30 * 60 * 1000,
68+
duration: 0,
69+
paused: false,
70+
},
71+
estimateState: {
72+
zeroTime: now + 25 * 60 * 1000, // Estimate was 25 mins -> we are 5 mins over
73+
duration: 0,
74+
paused: false,
75+
},
76+
} as any
77+
}
78+
79+
return mockTimer
80+
}

packages/webui/src/client/styles/countdown/director.scss

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,5 +428,51 @@ $hold-status-color: $liveline-timecode-color;
428428
.clocks-counter-heavy {
429429
font-weight: 600;
430430
}
431+
432+
.director-screen__body__t-timer {
433+
position: absolute;
434+
bottom: 1vh;
435+
right: 1vw;
436+
text-align: right;
437+
font-size: 5vh;
438+
z-index: 10;
439+
line-height: 1;
440+
441+
.t-timer-display {
442+
display: flex;
443+
align-items: baseline;
444+
justify-content: flex-end;
445+
font-weight: 500;
446+
background: rgba(0, 0, 0, 0.5);
447+
padding: 0.2em 0.4em;
448+
border-radius: 0.5em;
449+
450+
&__label {
451+
color: $general-next-color;
452+
margin-right: 0.5em;
453+
font-size: 0.6em;
454+
text-transform: uppercase;
455+
}
456+
457+
&__value {
458+
color: #fff;
459+
font-variant-numeric: tabular-nums;
460+
}
461+
462+
&__over-under {
463+
margin-left: 0.5em;
464+
font-size: 0.8em;
465+
font-variant-numeric: tabular-nums;
466+
467+
&--over {
468+
color: $general-late-color;
469+
}
470+
471+
&--under {
472+
color: #0f0;
473+
}
474+
}
475+
}
476+
}
431477
}
432478
}

packages/webui/src/client/styles/countdown/presenter.scss

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ $hold-status-color: $liveline-timecode-color;
163163

164164
.presenter-screen__rundown-status-bar {
165165
display: grid;
166-
grid-template-columns: auto fit-content(5em);
166+
grid-template-columns: auto fit-content(20em) fit-content(5em);
167167
grid-template-rows: fit-content(1em);
168168
font-size: 6em;
169169
color: #888;
@@ -176,6 +176,45 @@ $hold-status-color: $liveline-timecode-color;
176176
line-height: 1.44em;
177177
}
178178

179+
.presenter-screen__rundown-status-bar__t-timer {
180+
margin-right: 1em;
181+
font-size: 0.8em;
182+
align-self: center;
183+
justify-self: end;
184+
185+
.t-timer-display {
186+
display: flex;
187+
align-items: baseline;
188+
justify-content: flex-end;
189+
190+
&__label {
191+
color: $general-next-color;
192+
margin-right: 0.5em;
193+
font-size: 0.6em;
194+
text-transform: uppercase;
195+
}
196+
197+
&__value {
198+
color: #fff;
199+
font-variant-numeric: tabular-nums;
200+
}
201+
202+
&__over-under {
203+
margin-left: 0.5em;
204+
font-size: 0.8em;
205+
font-variant-numeric: tabular-nums;
206+
207+
&--over {
208+
color: $general-late-color;
209+
}
210+
211+
&--under {
212+
color: #0f0;
213+
}
214+
}
215+
}
216+
}
217+
179218
.presenter-screen__rundown-status-bar__countdown {
180219
white-space: nowrap;
181220

packages/webui/src/client/styles/rundownView.scss

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3644,5 +3644,21 @@ svg.icon {
36443644
margin: 0 0.05em;
36453645
color: #888;
36463646
}
3647+
3648+
.timing__header_t-timers__timer__over-under {
3649+
font-size: 0.75em;
3650+
font-weight: 400;
3651+
font-variant-numeric: tabular-nums;
3652+
margin-left: 0.5em;
3653+
white-space: nowrap;
3654+
3655+
&.timing__header_t-timers__timer__over-under--over {
3656+
color: $general-late-color;
3657+
}
3658+
3659+
&.timing__header_t-timers__timer__over-under--under {
3660+
color: #0f0;
3661+
}
3662+
}
36473663
}
36483664
}

packages/webui/src/client/ui/ClockView/DirectorScreen.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import { AdjustLabelFit } from '../util/AdjustLabelFit.js'
5151
import { AutoNextStatus } from '../RundownView/RundownTiming/AutoNextStatus.js'
5252
import { useTranslation } from 'react-i18next'
5353
import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase'
54+
import { TTimerDisplay } from './TTimerDisplay.js'
55+
import { getDefaultTTimer } from '../../lib/tTimerUtils.js'
5456
import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance.js'
5557

5658
interface SegmentUi extends DBSegment {
@@ -550,6 +552,8 @@ function DirectorScreenRender({
550552
}
551553
}
552554

555+
const activeTTimer = getDefaultTTimer(playlist.tTimers)
556+
553557
return (
554558
<div className="director-screen">
555559
<div className="director-screen__top">
@@ -754,6 +758,11 @@ function DirectorScreenRender({
754758
</>
755759
) : null}
756760
</div>
761+
{!!activeTTimer && (
762+
<div className="director-screen__body__t-timer">
763+
<TTimerDisplay timer={activeTTimer} />
764+
</div>
765+
)}
757766
</div>
758767
</div>
759768
)

packages/webui/src/client/ui/ClockView/PresenterScreen.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import { useSetDocumentClass, useSetDocumentDarkTheme } from '../util/useSetDocu
4848
import { useRundownAndShowStyleIdsForPlaylist } from '../util/useRundownAndShowStyleIdsForPlaylist.js'
4949
import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil.js'
5050
import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js'
51+
import { TTimerDisplay } from './TTimerDisplay.js'
52+
import { getDefaultTTimer } from '../../lib/tTimerUtils.js'
5153

5254
interface SegmentUi extends DBSegment {
5355
items: Array<PartUi>
@@ -482,6 +484,7 @@ function PresenterScreenContentDefaultLayout({
482484

483485
const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing)
484486
const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) ?? 0
487+
const activeTTimer = getDefaultTTimer(playlist.tTimers)
485488

486489
return (
487490
<div className="presenter-screen">
@@ -587,6 +590,9 @@ function PresenterScreenContentDefaultLayout({
587590
<div className="presenter-screen__rundown-status-bar__rundown-name">
588591
{playlist ? playlist.name : 'UNKNOWN'}
589592
</div>
593+
<div className="presenter-screen__rundown-status-bar__t-timer">
594+
{!!activeTTimer && <TTimerDisplay timer={activeTTimer} />}
595+
</div>
590596
<div
591597
className={ClassNames('presenter-screen__rundown-status-bar__countdown', {
592598
over: Math.floor(overUnderClock / 1000) >= 0,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
2+
import { RundownUtils } from '../../lib/rundown'
3+
import { calculateTTimerDiff, calculateTTimerOverUnder } from '../../lib/tTimerUtils'
4+
import { useTiming } from '../RundownView/RundownTiming/withTiming'
5+
import classNames from 'classnames'
6+
7+
interface TTimerDisplayProps {
8+
timer: RundownTTimer
9+
}
10+
11+
export function TTimerDisplay({ timer }: Readonly<TTimerDisplayProps>): JSX.Element | null {
12+
useTiming()
13+
14+
if (!timer.mode) return null
15+
16+
const now = Date.now()
17+
18+
const diff = calculateTTimerDiff(timer, now)
19+
const overUnder = calculateTTimerOverUnder(timer, now)
20+
21+
const timerStr = RundownUtils.formatDiffToTimecode(Math.abs(diff), false, true, true, false, true)
22+
const timerSign = diff >= 0 ? '' : '-'
23+
24+
return (
25+
<div className="t-timer-display">
26+
<span className="t-timer-display__label">{timer.label}</span>
27+
<span className="t-timer-display__value">
28+
{timerSign}
29+
{timerStr}
30+
</span>
31+
{overUnder !== undefined && (
32+
<span
33+
className={classNames('t-timer-display__over-under', {
34+
't-timer-display__over-under--over': overUnder > 0,
35+
't-timer-display__over-under--under': overUnder < 0,
36+
})}
37+
>
38+
{overUnder >= 0 ? '+' : '\u2013'}
39+
{RundownUtils.formatDiffToTimecode(Math.abs(overUnder), false, true, true, false, true)}
40+
</span>
41+
)}
42+
</div>
43+
)
44+
}

0 commit comments

Comments
 (0)