Skip to content

Commit 0c62b53

Browse files
committed
New top bar UI - WIP
1 parent b45e406 commit 0c62b53

21 files changed

+402
-370
lines changed

packages/webui/src/client/ui/ClockView/CameraScreen/Part.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { PieceExtended } from '../../../lib/RundownResolver.js'
77
import { getAllowSpeaking, getAllowVibrating } from '../../../lib/localStorage.js'
88
import { getPartInstanceTimingValue } from '../../../lib/rundownTiming.js'
99
import { AutoNextStatus } from '../../RundownView/RundownTiming/AutoNextStatus.js'
10-
import { CurrentPartOrSegmentRemaining } from '../../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js'
10+
import { CurrentPartOrSegmentRemaining } from '../../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js'
1111
import { PartCountdown } from '../../RundownView/RundownTiming/PartCountdown.js'
1212
import { PartDisplayDuration } from '../../RundownView/RundownTiming/PartDuration.js'
1313
import { TimingDataResolution, TimingTickResolution, useTiming } from '../../RundownView/RundownTiming/withTiming.js'

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub'
4040
import { useSetDocumentClass } from '../util/useSetDocumentClass.js'
4141
import { useRundownAndShowStyleIdsForPlaylist } from '../util/useRundownAndShowStyleIdsForPlaylist.js'
4242
import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil.js'
43-
import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js'
43+
import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js'
4444
import {
4545
OverUnderClockComponent,
4646
PlannedEndComponent,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub'
4747
import { useSetDocumentClass, useSetDocumentDarkTheme } from '../util/useSetDocumentClass.js'
4848
import { useRundownAndShowStyleIdsForPlaylist } from '../util/useRundownAndShowStyleIdsForPlaylist.js'
4949
import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil.js'
50-
import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js'
50+
import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js'
5151

5252
interface SegmentUi extends DBSegment {
5353
items: Array<PartUi>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@import '../../../styles/colorScheme';
2+
3+
.countdown {
4+
display: flex;
5+
align-items: baseline;
6+
justify-content: space-between;
7+
gap: 0.6em;
8+
color: rgba(255, 255, 255, 0.6);
9+
transition: color 0.2s;
10+
11+
&__label {
12+
font-size: 0.7em;
13+
font-weight: 600;
14+
letter-spacing: 0.1em;
15+
text-transform: uppercase;
16+
white-space: nowrap;
17+
}
18+
19+
&__value {
20+
font-size: 1.4em;
21+
font-variant-numeric: tabular-nums;
22+
letter-spacing: 0.05em;
23+
}
24+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react'
2+
import Moment from 'react-moment'
3+
import classNames from 'classnames'
4+
import './Countdown.scss'
5+
6+
interface IProps {
7+
label: string
8+
time?: number
9+
className?: string
10+
children?: React.ReactNode
11+
}
12+
13+
export function Countdown({ label, time, className, children }: IProps): JSX.Element {
14+
return (
15+
<span className={classNames('countdown', className)}>
16+
<span className="countdown__label">{label}</span>
17+
<span className="countdown__value">
18+
{time !== undefined ? <Moment interval={0} format="HH:mm:ss" date={time} /> : children}
19+
</span>
20+
</span>
21+
)
22+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import React, { useEffect, useRef } from 'react'
2+
import ClassNames from 'classnames'
3+
import { TimingDataResolution, TimingTickResolution, useTiming } from '../RundownTiming/withTiming.js'
4+
import { RundownUtils } from '../../../lib/rundown.js'
5+
import { SpeechSynthesiser } from '../../../lib/speechSynthesis.js'
6+
import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids'
7+
8+
const SPEAK_ADVANCE = 500
9+
10+
interface IPartRemainingProps {
11+
currentPartInstanceId: PartInstanceId | null
12+
hideOnZero?: boolean
13+
className?: string
14+
heavyClassName?: string
15+
speaking?: boolean
16+
vibrating?: boolean
17+
/** Use the segment budget instead of the part duration if available */
18+
preferSegmentTime?: boolean
19+
}
20+
21+
// global variable for remembering last uttered displayTime
22+
let prevDisplayTime: number | undefined = undefined
23+
24+
function speak(displayTime: number) {
25+
let text = '' // Say nothing
26+
27+
switch (displayTime) {
28+
case -1:
29+
text = 'One'
30+
break
31+
case -2:
32+
text = 'Two'
33+
break
34+
case -3:
35+
text = 'Three'
36+
break
37+
case -4:
38+
text = 'Four'
39+
break
40+
case -5:
41+
text = 'Five'
42+
break
43+
case -6:
44+
text = 'Six'
45+
break
46+
case -7:
47+
text = 'Seven'
48+
break
49+
case -8:
50+
text = 'Eight'
51+
break
52+
case -9:
53+
text = 'Nine'
54+
break
55+
case -10:
56+
text = 'Ten'
57+
break
58+
}
59+
60+
if (text) {
61+
SpeechSynthesiser.speak(text, 'countdown')
62+
}
63+
}
64+
65+
function vibrate(displayTime: number) {
66+
if ('vibrate' in navigator) {
67+
switch (displayTime) {
68+
case 0:
69+
navigator.vibrate([500])
70+
break
71+
case -1:
72+
case -2:
73+
case -3:
74+
navigator.vibrate([250])
75+
break
76+
}
77+
}
78+
}
79+
80+
export const CurrentPartOrSegmentRemaining: React.FC<IPartRemainingProps> = (props) => {
81+
const timingDurations = useTiming(TimingTickResolution.Synced, TimingDataResolution.Synced)
82+
const prevPartInstanceId = useRef<PartInstanceId | null>(null)
83+
84+
useEffect(() => {
85+
if (props.currentPartInstanceId !== prevPartInstanceId.current) {
86+
prevDisplayTime = undefined
87+
prevPartInstanceId.current = props.currentPartInstanceId
88+
}
89+
90+
if (!timingDurations?.currentTime) return
91+
if (timingDurations.currentPartInstanceId !== props.currentPartInstanceId) return
92+
93+
let displayTime = (timingDurations.remainingTimeOnCurrentPart || 0) * -1
94+
95+
if (displayTime !== 0) {
96+
displayTime += SPEAK_ADVANCE
97+
displayTime = Math.floor(displayTime / 1000)
98+
}
99+
100+
if (prevDisplayTime !== displayTime) {
101+
if (props.speaking) {
102+
speak(displayTime)
103+
}
104+
105+
if (props.vibrating) {
106+
vibrate(displayTime)
107+
}
108+
109+
prevDisplayTime = displayTime
110+
}
111+
}, [
112+
props.currentPartInstanceId,
113+
timingDurations?.currentTime,
114+
timingDurations?.currentPartInstanceId,
115+
timingDurations?.remainingTimeOnCurrentPart,
116+
props.speaking,
117+
props.vibrating,
118+
])
119+
120+
if (!timingDurations?.currentTime) return null
121+
if (timingDurations.currentPartInstanceId !== props.currentPartInstanceId) return null
122+
123+
let displayTimecode = timingDurations.remainingTimeOnCurrentPart
124+
if (props.preferSegmentTime) {
125+
displayTimecode = timingDurations.remainingBudgetOnCurrentSegment ?? displayTimecode
126+
}
127+
128+
if (displayTimecode === undefined) return null
129+
displayTimecode *= -1
130+
131+
return (
132+
<span
133+
className={ClassNames(props.className, Math.floor(displayTimecode / 1000) > 0 ? props.heavyClassName : undefined)}
134+
role="timer"
135+
>
136+
{RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)}
137+
</span>
138+
)
139+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids'
2+
import { FreezeFrameIcon } from '../../../lib/ui/icons/freezeFrame'
3+
import { useTiming, TimingTickResolution, TimingDataResolution } from '../RundownTiming/withTiming'
4+
import { useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData'
5+
import { PartInstances, PieceInstances } from '../../../collections'
6+
import { VTContent } from '@sofie-automation/blueprints-integration'
7+
8+
export function HeaderFreezeFrameIcon({ partInstanceId }: { partInstanceId: PartInstanceId }) {
9+
const timingDurations = useTiming(TimingTickResolution.Synced, TimingDataResolution.Synced)
10+
11+
const freezeFrameIcon = useTracker(
12+
() => {
13+
const partInstance = PartInstances.findOne(partInstanceId)
14+
if (!partInstance) return null
15+
16+
// We use the exact display duration from the timing context just like VTSourceRenderer does.
17+
// Fallback to static displayDuration or expectedDuration if timing context is unavailable.
18+
const partDisplayDuration =
19+
(timingDurations.partDisplayDurations && timingDurations.partDisplayDurations[partInstanceId as any]) ??
20+
partInstance.part.displayDuration ??
21+
partInstance.part.expectedDuration ??
22+
0
23+
24+
const partDuration = timingDurations.partDurations
25+
? timingDurations.partDurations[partInstanceId as any]
26+
: partDisplayDuration
27+
28+
const pieceInstances = PieceInstances.find({ partInstanceId }).fetch()
29+
30+
for (const pieceInstance of pieceInstances) {
31+
const piece = pieceInstance.piece
32+
if (piece.virtual) continue
33+
34+
const content = piece.content as VTContent | undefined
35+
if (!content || content.loop || content.sourceDuration === undefined) {
36+
continue
37+
}
38+
39+
const seek = content.seek || 0
40+
const renderedInPoint = typeof piece.enable.start === 'number' ? piece.enable.start : 0
41+
const pieceDuration = content.sourceDuration - seek
42+
43+
const isAutoNext = partInstance.part.autoNext
44+
45+
if (
46+
(isAutoNext && renderedInPoint + pieceDuration < partDuration) ||
47+
(!isAutoNext && Math.abs(renderedInPoint + pieceDuration - partDisplayDuration) > 500)
48+
) {
49+
return <FreezeFrameIcon className="freeze-frame-icon" />
50+
}
51+
}
52+
return null
53+
},
54+
[partInstanceId, timingDurations.partDisplayDurations, timingDurations.partDurations],
55+
null
56+
)
57+
58+
return freezeFrameIcon
59+
}

0 commit comments

Comments
 (0)