Skip to content

Commit b71bec5

Browse files
committed
chore: add timing to l3d preview
1 parent e0d352a commit b71bec5

File tree

13 files changed

+188
-41
lines changed

13 files changed

+188
-41
lines changed

packages/blueprints-integration/src/previews.ts

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface TablePreview extends PreviewBase {
3333
type: PreviewType.Table
3434

3535
entries: { key: string; value: string }[]
36+
displayTiming: boolean
3637
}
3738
export interface ScriptPreview extends PreviewBase {
3839
type: PreviewType.Script

packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss

+17
Original file line numberDiff line numberDiff line change
@@ -277,4 +277,21 @@
277277
padding: 5px;
278278
font-weight: 700;
279279
}
280+
281+
.preview-popUp__timing {
282+
padding: 5px;
283+
284+
overflow: none;
285+
white-space: nowrap;
286+
text-overflow: ellipsis;
287+
288+
font-feature-settings: 'liga' off;
289+
font-style: normal;
290+
font-weight: 500;
291+
line-height: 100%; /* 15px */
292+
293+
.label {
294+
font-weight: 300;
295+
}
296+
}
280297
}

packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContent.tsx

+47-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import React from 'react'
22
import { PreviewContent } from './PreviewPopUpContext'
33
import { WarningIconSmall } from '../../lib/ui/icons/notifications'
44
import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage'
5-
import { useTranslation } from 'react-i18next'
5+
import { TFunction, useTranslation } from 'react-i18next'
66
import { VTPreviewElement } from './Previews/VTPreview'
77
import { IFramePreview } from './Previews/IFramePreview'
88
import { BoxLayoutPreview } from './Previews/BoxLayoutPreview'
99
import { ScriptPreview } from './Previews/ScriptPreview'
10+
import { RundownUtils } from '../../lib/rundown'
11+
import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance'
12+
import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep'
13+
import { PieceLifespan } from '@sofie-automation/blueprints-integration'
1014

1115
interface PreviewPopUpContentProps {
1216
content: PreviewContent
@@ -72,7 +76,49 @@ export function PreviewPopUpContent({ content, time }: PreviewPopUpContentProps)
7276
{content.total && '/' + content.total}
7377
</div>
7478
)
79+
case 'timing':
80+
return (
81+
<div className="preview-popUp__timing">
82+
<span className="label">IN: </span> {RundownUtils.formatTimeToShortTime(content.timeAsRendered?.in || 0)}
83+
<span className="label"> DURATION: </span>
84+
{getDurationText(t, content.lifespan, content.timeAsRendered, content.enable)}
85+
</div>
86+
)
7587
default:
7688
return <></>
7789
}
7890
}
91+
92+
function getDurationText(
93+
t: TFunction,
94+
lifespan: PieceLifespan,
95+
timeAsRendered?: { in?: number; dur?: number },
96+
enable?: ReadonlyObjectDeep<PieceInstancePiece>['enable']
97+
): string {
98+
if (!timeAsRendered?.dur && !enable?.duration) {
99+
return getLifeSpanText(t, lifespan)
100+
} else {
101+
return RundownUtils.formatTimeToShortTime(
102+
timeAsRendered?.dur ?? (typeof enable?.duration === 'number' ? enable?.duration : 0)
103+
)
104+
}
105+
}
106+
107+
function getLifeSpanText(t: TFunction, lifespan: PieceLifespan): string {
108+
switch (lifespan) {
109+
case PieceLifespan.WithinPart:
110+
return t('Until next take')
111+
case PieceLifespan.OutOnSegmentChange:
112+
return t('Until next segment')
113+
case PieceLifespan.OutOnSegmentEnd:
114+
return t('Until end of segment')
115+
case PieceLifespan.OutOnRundownChange:
116+
return t('Until next rundown')
117+
case PieceLifespan.OutOnRundownEnd:
118+
return t('Until end of rundown')
119+
case PieceLifespan.OutOnShowStyleEnd:
120+
return t('Until end of showstyle')
121+
default:
122+
return ''
123+
}
124+
}

packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx

+30-4
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ import { PreviewPopUpContent } from './PreviewPopUpContent'
55
import {
66
JSONBlobParse,
77
NoraPayload,
8+
PieceLifespan,
89
PreviewType,
910
ScriptContent,
1011
SourceLayerType,
1112
SplitsContent,
1213
SplitsContentBoxContent,
1314
SplitsContentBoxProperties,
15+
TransitionContent,
1416
VTContent,
1517
} from '@sofie-automation/blueprints-integration'
16-
import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep'
18+
import { ReadonlyDeep, ReadonlyObjectDeep } from 'type-fest/source/readonly-deep'
1719
import { PieceContentStatusObj } from '@sofie-automation/meteor-lib/dist/api/pieceContentStatus'
1820
import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage'
1921
import _ from 'underscore'
@@ -28,7 +30,8 @@ type VirtualElement = {
2830
export function convertSourceLayerItemToPreview(
2931
sourceLayerType: SourceLayerType | undefined,
3032
item: ReadonlyObjectDeep<PieceInstancePiece> | IAdLibListItem,
31-
contentStatus?: ReadonlyObjectDeep<PieceContentStatusObj>
33+
contentStatus?: ReadonlyObjectDeep<PieceContentStatusObj>,
34+
timeAsRendered?: { in?: number | null; dur?: number | null }
3235
): PreviewContent[] {
3336
// first try to read the popup preview
3437
if (item.content.popUpPreview) {
@@ -79,6 +82,14 @@ export function convertSourceLayerItemToPreview(
7982
type: 'data',
8083
content: [...popupPreview.preview.entries],
8184
})
85+
if (popupPreview.preview.displayTiming) {
86+
contents.push({
87+
type: 'timing',
88+
timeAsRendered,
89+
enable: 'enable' in item ? item.enable : undefined,
90+
lifespan: item.lifespan,
91+
})
92+
}
8293
break
8394
case PreviewType.VT:
8495
if (contentStatus?.previewUrl) {
@@ -194,7 +205,13 @@ export function convertSourceLayerItemToPreview(
194205
type: 'title',
195206
content: item.name,
196207
},
197-
// todo - item inpoint and duration
208+
// note - this may have contained some NORA data before but idk the details on how to add that back
209+
{
210+
type: 'timing',
211+
timeAsRendered,
212+
enable: 'enable' in item ? item.enable : undefined,
213+
lifespan: item.lifespan,
214+
},
198215
]
199216
} else if (sourceLayerType === SourceLayerType.SCRIPT) {
200217
const content = item.content as ScriptContent
@@ -210,6 +227,9 @@ export function convertSourceLayerItemToPreview(
210227
} else if (sourceLayerType === SourceLayerType.SPLITS) {
211228
const content = item.content as SplitsContent
212229
return [{ type: 'boxLayout', boxSourceConfiguration: content.boxSourceConfiguration }]
230+
} else if (sourceLayerType === SourceLayerType.TRANSITION) {
231+
const content = item.content as TransitionContent
232+
if (content.preview) return [{ type: 'image', src: '/api/private/blueprints/assets/' + content.preview }]
213233
}
214234

215235
return []
@@ -252,7 +272,7 @@ export type PreviewContent =
252272
}
253273
| {
254274
type: 'boxLayout'
255-
boxSourceConfiguration: (SplitsContentBoxContent & SplitsContentBoxProperties)[]
275+
boxSourceConfiguration: ReadonlyDeep<(SplitsContentBoxContent & SplitsContentBoxProperties)[]>
256276
showLabels?: boolean
257277
backgroundArt?: string
258278
}
@@ -265,6 +285,12 @@ export type PreviewContent =
265285
current: number
266286
total?: number
267287
}
288+
| {
289+
type: 'timing'
290+
timeAsRendered?: { in?: number | null; dur?: number | null }
291+
enable?: ReadonlyObjectDeep<PieceInstancePiece>['enable']
292+
lifespan: PieceLifespan
293+
}
268294

269295
export interface IPreviewPopUpSession {
270296
/**

packages/webui/src/client/ui/PreviewPopUp/Previews/VTPreview.tsx

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { useEffect, useRef } from 'react'
1+
import { useContext, useEffect, useRef } from 'react'
22
import { StyledTimecode } from '../../../lib/StyledTimecode'
33
import classNames from 'classnames'
4+
import StudioContext from '../../RundownView/StudioContext'
45

56
interface VTPreviewProps {
67
content: {
@@ -12,6 +13,7 @@ interface VTPreviewProps {
1213
}
1314
time: number | null
1415
}
16+
1517
function setVideoElementPosition(
1618
vEl: HTMLVideoElement,
1719
timePosition: number,
@@ -28,8 +30,10 @@ function setVideoElementPosition(
2830
}
2931
vEl.currentTime = targetTime / 1000
3032
}
33+
3134
export function VTPreviewElement({ content, time }: VTPreviewProps): React.ReactElement {
3235
const videoElement = useRef<HTMLVideoElement>(null)
36+
const studioContext = useContext(StudioContext)
3337

3438
useEffect(() => {
3539
if (!videoElement.current) return
@@ -45,16 +49,17 @@ export function VTPreviewElement({ content, time }: VTPreviewProps): React.React
4549

4650
const itemDuration = content.itemDuration ?? 0
4751
const offsetTimePosition = (time ?? 0) + (content.seek ?? 0)
48-
const showFrameMarker = offsetTimePosition === 0 || (itemDuration > 0 && offsetTimePosition >= itemDuration)
52+
// note - we should probably be following the content's framerate for this
53+
const showFrameMarker = offsetTimePosition <= 20 || (itemDuration > 0 && offsetTimePosition >= itemDuration - 20)
4954

50-
// todo - add studio settings. I _really_ don't look forward to prop drilling these....
5155
return (
5256
<div className="preview-popUp__video">
5357
{showFrameMarker && (
5458
<div
5559
className={classNames('preview-popUp__video-frame-marker', {
56-
'preview-popUp__video-frame-marker--first-frame': offsetTimePosition === 0,
57-
'preview-popUp__video-frame-marker--last-frame': itemDuration > 0 && offsetTimePosition >= itemDuration,
60+
'preview-popUp__video-frame-marker--first-frame': offsetTimePosition <= 20,
61+
'preview-popUp__video-frame-marker--last-frame':
62+
itemDuration > 0 && offsetTimePosition >= itemDuration - 20,
5863
})}
5964
>
6065
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -64,7 +69,7 @@ export function VTPreviewElement({ content, time }: VTPreviewProps): React.React
6469
)}
6570
<video src={content.src} ref={videoElement} crossOrigin="anonymous" playsInline={true} muted={true} />
6671
<div className="time">
67-
<StyledTimecode studioSettings={undefined} time={Math.round(time ?? 0)} />
72+
<StyledTimecode studioSettings={studioContext?.settings} time={Math.round(time ?? 0)} />
6873
</div>
6974
</div>
7075
)

packages/webui/src/client/ui/RundownView.tsx

+20-18
Original file line numberDiff line numberDiff line change
@@ -3243,24 +3243,26 @@ const RundownViewContent = translateWithTracker<IPropsWithReady, IState, ITracke
32433243
renderDetachedShelf() {
32443244
return (
32453245
<RundownTimingProvider playlist={this.props.playlist} defaultDuration={Settings.defaultDisplayDuration}>
3246-
<ErrorBoundary>
3247-
<Shelf
3248-
buckets={this.props.buckets}
3249-
isExpanded={this.state.isInspectorShelfExpanded}
3250-
onChangeExpanded={this.onShelfChangeExpanded}
3251-
hotkeys={this.defaultHotkeys(this.props.t)}
3252-
playlist={this.props.playlist}
3253-
showStyleBase={this.props.showStyleBase}
3254-
showStyleVariant={this.props.showStyleVariant}
3255-
studioMode={this.props.userPermissions.studio}
3256-
onChangeBottomMargin={this.onChangeBottomMargin}
3257-
rundownLayout={this.state.shelfLayout}
3258-
studio={this.props.studio}
3259-
fullViewport={true}
3260-
shelfDisplayOptions={this.props.shelfDisplayOptions}
3261-
bucketDisplayFilter={this.props.bucketDisplayFilter}
3262-
/>
3263-
</ErrorBoundary>
3246+
<PreviewPopUpContextProvider>
3247+
<ErrorBoundary>
3248+
<Shelf
3249+
buckets={this.props.buckets}
3250+
isExpanded={this.state.isInspectorShelfExpanded}
3251+
onChangeExpanded={this.onShelfChangeExpanded}
3252+
hotkeys={this.defaultHotkeys(this.props.t)}
3253+
playlist={this.props.playlist}
3254+
showStyleBase={this.props.showStyleBase}
3255+
showStyleVariant={this.props.showStyleVariant}
3256+
studioMode={this.props.userPermissions.studio}
3257+
onChangeBottomMargin={this.onChangeBottomMargin}
3258+
rundownLayout={this.state.shelfLayout}
3259+
studio={this.props.studio}
3260+
fullViewport={true}
3261+
shelfDisplayOptions={this.props.shelfDisplayOptions}
3262+
bucketDisplayFilter={this.props.bucketDisplayFilter}
3263+
/>
3264+
</ErrorBoundary>
3265+
</PreviewPopUpContextProvider>
32643266
<ErrorBoundary>{this.renderSorensenContext()}</ErrorBoundary>
32653267
</RundownTimingProvider>
32663268
)

packages/webui/src/client/ui/SegmentList/LinePartPieceIndicator/LinePartScriptPiece.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export function LinePartScriptPiece({ pieces }: IProps): JSX.Element {
3030
const contentStatus = thisPieces[0] && useContentStatusForPieceInstance(thisPieces[0].instance)
3131
const previewContents =
3232
thisPieces[0] &&
33-
convertSourceLayerItemToPreview(thisPieces[0].sourceLayer?.type, thisPieces[0].instance.piece, contentStatus)
33+
convertSourceLayerItemToPreview(thisPieces[0].sourceLayer?.type, thisPieces[0].instance.piece, contentStatus, {
34+
in: thisPieces[0].renderedInPoint,
35+
dur: thisPieces[0].renderedDuration,
36+
})
3437

3538
function onMouseEnter(e: React.PointerEvent<HTMLDivElement>) {
3639
// setHover(true)

packages/webui/src/client/ui/SegmentList/LinePartSecondaryPiece/LinePartSecondaryPiece.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ export const LinePartSecondaryPiece: React.FC<IProps> = React.memo(function Line
4848

4949
const previewContext = useContext(PreviewPopUpContext)
5050
const previewSession = useRef<IPreviewPopUpSession | null>(null)
51-
const previewContents = convertSourceLayerItemToPreview(piece.sourceLayer?.type, piece.instance.piece, contentStatus)
51+
const previewContents = convertSourceLayerItemToPreview(
52+
piece.sourceLayer?.type,
53+
piece.instance.piece,
54+
contentStatus,
55+
{ in: piece.renderedInPoint, dur: piece.renderedDuration }
56+
)
5257

5358
const onPointerEnter = (e: React.PointerEvent<HTMLDivElement>) => {
5459
if (e.pointerType !== 'mouse') {

packages/webui/src/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnailInner.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function StoryboardPartThumbnailInner({
5656
if (e.pointerType !== 'mouse') {
5757
return
5858
}
59-
// setHover(true)
59+
setHover(true)
6060

6161
const newOffset = thumbnailEl.current && getElementDocumentOffset(thumbnailEl.current)
6262
if (newOffset !== null) {

packages/webui/src/client/ui/SegmentTimeline/Parts/InvalidPartCover.tsx

+20-4
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
1-
import React from 'react'
1+
import React, { useContext, useRef } from 'react'
22
import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
3+
import { IPreviewPopUpSession, PreviewPopUpContext } from '../../PreviewPopUp/PreviewPopUpContext'
34

45
interface IProps {
56
className?: string
67
part: DBPart
78
align?: 'start' | 'center' | 'end'
89
}
910

10-
export function InvalidPartCover({ className }: Readonly<IProps>): JSX.Element {
11+
export function InvalidPartCover({ className, part }: Readonly<IProps>): JSX.Element {
1112
const element = React.createRef<HTMLDivElement>()
1213

13-
function onMouseEnter() {
14+
const previewContext = useContext(PreviewPopUpContext)
15+
const previewSession = useRef<IPreviewPopUpSession | null>(null)
16+
17+
function onMouseEnter(e: React.MouseEvent<HTMLDivElement>) {
1418
if (!element.current) {
1519
return
1620
}
21+
22+
if (part.invalidReason?.message && !previewSession.current) {
23+
previewSession.current = previewContext.requestPreview(e.target as HTMLDivElement, [
24+
{
25+
type: 'warning',
26+
content: part.invalidReason?.message,
27+
},
28+
])
29+
}
1730
}
1831

1932
function onMouseLeave() {
20-
//
33+
if (previewSession.current) {
34+
previewSession.current.close()
35+
previewSession.current = null
36+
}
2137
}
2238

2339
return (

packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,10 @@ export const SourceLayerItem = (props: Readonly<ISourceLayerItemProps>): JSX.Ele
224224
previewSession.current.close()
225225
previewSession.current = null
226226
} else {
227-
const previewContents = convertSourceLayerItemToPreview(layer.type, piece.instance.piece, contentStatus)
227+
const previewContents = convertSourceLayerItemToPreview(layer.type, piece.instance.piece, contentStatus, {
228+
in: props.piece.renderedInPoint,
229+
dur: props.piece.renderedDuration,
230+
})
228231

229232
if (previewContents.length) {
230233
previewSession.current = previewContext.requestPreview(e.target as any, previewContents, {

0 commit comments

Comments
 (0)