From f9427d075be7a8f56257366f548b8d2546213619 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Thu, 3 Jun 2021 17:26:51 +0100
Subject: [PATCH 001/112] feat: Playlist/Rundown expectedEnd display in lobby /
header
---
meteor/client/styles/rundownList.scss | 10 ++++++----
meteor/client/ui/ClockView/PresenterScreen.tsx | 1 +
meteor/client/ui/RundownList.tsx | 3 +++
.../client/ui/RundownList/RundownListItemView.tsx | 11 +++++++++++
meteor/client/ui/RundownList/RundownPlaylistUi.tsx | 11 +++++++++++
meteor/client/ui/RundownView.tsx | 13 +++++++++++--
.../ui/StudioScreenSaver/StudioScreenSaver.tsx | 1 +
meteor/lib/collections/RundownPlaylists.ts | 4 ++++
meteor/lib/collections/Rundowns.ts | 1 +
meteor/server/api/rundownPlaylist.ts | 3 +++
.../server/migration/deprecatedDataTypes/1_0_1.ts | 2 ++
packages/blueprints-integration/src/rundown.ts | 4 ++++
12 files changed, 58 insertions(+), 6 deletions(-)
diff --git a/meteor/client/styles/rundownList.scss b/meteor/client/styles/rundownList.scss
index 9f4fbe7e97..89fa18937d 100644
--- a/meteor/client/styles/rundownList.scss
+++ b/meteor/client/styles/rundownList.scss
@@ -10,7 +10,7 @@
* Note that the standard size is used for several columns, which is why these
* percentages do not add up to a perfect 100% (ie. 1.0)
*/
- --nameColumnSize: 0.4;
+ --nameColumnSize: 0.3;
--showStyleColumnSize: 0.17;
--standardColumnSize: 0.11;
--layoutSelectionColumnSize: 0.075;
@@ -23,6 +23,7 @@
--showStyleColumnWidth: calc(var(--componentWidth) * var(--showStyleColumnSize));
--airTimeColumnWidth: calc(var(--componentWidth) * var(--standardColumnSize));
--durationColumnWidth: calc(var(--componentWidth) * var(--standardColumnSize));
+ --expectedEndColumnWidth: calc(var(--componentWidth) * var(--standardColumnSize));
--lastModifiedColumnWidth: calc(var(--componentWidth) * var(--standardColumnSize));
--layoutSelectionColumnWidth: calc(var(--componentWidth) * var(--layoutSelectionColumnSize));
--actionsColumnWidth: calc(var(--componentWidth) * var(--actionsColumnSize));
@@ -36,7 +37,7 @@
display: grid;
grid-template-columns:
var(--nameColumnWidth) var(--showStyleColumnWidth) var(--airTimeColumnWidth)
- var(--durationColumnWidth) var(--lastModifiedColumnWidth) var(--layoutSelectionColumnWidth);
+ var(--durationColumnWidth) var(--expectedEndColumnWidth) var(--lastModifiedColumnWidth) var(--layoutSelectionColumnWidth);
background-color: #898989;
color: #fff;
@@ -173,7 +174,7 @@
// for the header, the first column spans the first four item columns
grid-template-columns:
calc(var(--nameColumnWidth) + var(--showStyleColumnWidth)) var(--airTimeColumnWidth)
- var(--durationColumnWidth) var(--lastModifiedColumnWidth);
+ var(--durationColumnWidth) var(--expectedEndColumnWidth) var(--lastModifiedColumnWidth);
> span {
align-self: center;
@@ -246,6 +247,7 @@
--showStyleColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--showStyleColumnSize));
--airTimeColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--standardColumnSize));
--durationColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--standardColumnSize));
+ --expectedEndColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--standardColumnSize));
--lastModifiedColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--standardColumnSize));
--actionsColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--actionsColumnSize));
@@ -304,7 +306,7 @@
display: grid;
grid-template-columns:
var(--nameColumnWidth) var(--showStyleColumnWidth) var(--airTimeColumnWidth)
- var(--durationColumnWidth) var(--lastModifiedColumnWidth) var(--layoutSelectionColumnWidth);
+ var(--durationColumnWidth) var(--expectedEndColumnWidth) var(--lastModifiedColumnWidth) var(--layoutSelectionColumnWidth);
.rundown-list-item__actions {
padding: 0 0 3px; // cater for icons being larger than the font
diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx
index d401c4b376..cea32acc2f 100644
--- a/meteor/client/ui/ClockView/PresenterScreen.tsx
+++ b/meteor/client/ui/ClockView/PresenterScreen.tsx
@@ -74,6 +74,7 @@ function getShowStyleBaseIdSegmentPartUi(
name: 1,
expectedStart: 1,
expectedDuration: 1,
+ expectedEnd: 1,
},
})
showStyleBaseId = currentRundown?.showStyleBaseId
diff --git a/meteor/client/ui/RundownList.tsx b/meteor/client/ui/RundownList.tsx
index fff39a694b..ab4d11e095 100644
--- a/meteor/client/ui/RundownList.tsx
+++ b/meteor/client/ui/RundownList.tsx
@@ -303,6 +303,9 @@ export const RundownList = translateWithTracker((): IRundownsListProps => {
{t('Show Style')}
{t('On Air Start Time')}
{t('Duration')}
+ {this.props.rundownPlaylists.some(
+ (p) => !!p.expectedEnd || p.rundowns.some((r) => r.expectedEnd)
+ ) && {t('Expected End Time')} }
{t('Last Updated')}
{this.props.rundownLayouts.some((l) => l.exposeAsShelf || l.exposeAsStandalone) && (
{t('Shelf Layout')}
diff --git a/meteor/client/ui/RundownList/RundownListItemView.tsx b/meteor/client/ui/RundownList/RundownListItemView.tsx
index 88fb141dba..0694343135 100644
--- a/meteor/client/ui/RundownList/RundownListItemView.tsx
+++ b/meteor/client/ui/RundownList/RundownListItemView.tsx
@@ -109,6 +109,8 @@ export default withTranslation()(function RundownListItemView(props: Translated<
{rundown.expectedStart ? (
+ ) : rundown.expectedEnd && rundown.expectedDuration ? (
+
) : (
{t('Not set')}
)}
@@ -138,6 +140,15 @@ export default withTranslation()(function RundownListItemView(props: Translated<
{t('Not set')}
)}
+
+ {rundown.expectedEnd ? (
+
+ ) : rundown.expectedStart && rundown.expectedDuration ? (
+
+ ) : (
+ {t('Not set')}
+ )}
+
diff --git a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
index 85ddb98d60..40155473fd 100644
--- a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
+++ b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
@@ -341,6 +341,8 @@ export const RundownPlaylistUi = DropTarget(
{playlist.expectedStart ? (
+ ) : playlist.expectedEnd && playlist.expectedDuration ? (
+
) : (
{t('Not set')}
)}
@@ -356,6 +358,15 @@ export const RundownPlaylistUi = DropTarget(
{t('Not set')}
)}
+
+ {playlist.expectedEnd ? (
+
+ ) : playlist.expectedStart && playlist.expectedDuration ? (
+
+ ) : (
+ {t('Not set')}
+ )}
+
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 66eb8b8e22..3f7ba38268 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -340,7 +340,7 @@ const TimingDisplay = withTranslation()(
) : null}
)}
- {rundownPlaylist.expectedDuration ? (
+ {rundownPlaylist.expectedEnd || rundownPlaylist.expectedDuration ? (
{!rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
@@ -351,6 +351,11 @@ const TimingDisplay = withTranslation()(
date={rundownPlaylist.expectedStart + rundownPlaylist.expectedDuration}
/>
+ ) : !rundownPlaylist.loop && rundownPlaylist.expectedEnd ? (
+
+ {t('Planned End')}
+
+
) : null}
{!rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
@@ -389,13 +394,17 @@ const TimingDisplay = withTranslation()(
) : (
+ {console.log(this.props.rundownPlaylist.expectedEnd)}
{!rundownPlaylist.loop && this.props.timingDurations ? (
{t('Expected End')}
) : null}
diff --git a/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx b/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx
index 027cfe1aa4..2090bdeeed 100644
--- a/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx
+++ b/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx
@@ -58,6 +58,7 @@ export const findNextPlaylist = (props: IProps) => {
name: 1,
expectedStart: 1,
expectedDuration: 1,
+ expectedEnd: 1,
studioId: 1,
},
}
diff --git a/meteor/lib/collections/RundownPlaylists.ts b/meteor/lib/collections/RundownPlaylists.ts
index d48d987b72..985b1a8b03 100644
--- a/meteor/lib/collections/RundownPlaylists.ts
+++ b/meteor/lib/collections/RundownPlaylists.ts
@@ -65,6 +65,8 @@ export interface DBRundownPlaylist {
expectedStart?: Time
/** How long the playlist is expected to take ON AIR */
expectedDuration?: number
+ /** When the playlist is expected to end */
+ expectedEnd?: Time
/** Is the playlist in rehearsal mode (can be used, when active: true) */
rehearsal?: boolean
/** Playout hold state */
@@ -120,6 +122,7 @@ export class RundownPlaylist implements DBRundownPlaylist {
public rundownsStartedPlayback?: Record
public expectedStart?: Time
public expectedDuration?: number
+ public expectedEnd?: Time
public rehearsal?: boolean
public holdState?: RundownHoldState
public activationId?: RundownPlaylistActivationId
@@ -210,6 +213,7 @@ export class RundownPlaylist implements DBRundownPlaylist {
playlistId: 1,
expectedStart: 1,
expectedDuration: 1,
+ expectedEnd: 1,
showStyleBaseId: 1,
},
})
diff --git a/meteor/lib/collections/Rundowns.ts b/meteor/lib/collections/Rundowns.ts
index c8013fe2f5..df1335204d 100644
--- a/meteor/lib/collections/Rundowns.ts
+++ b/meteor/lib/collections/Rundowns.ts
@@ -86,6 +86,7 @@ export class Rundown implements DBRundown {
public description?: string
public expectedStart?: Time
public expectedDuration?: number
+ public expectedEnd?: Time
public metaData?: unknown
// From IBlueprintRundownDB:
public _id: RundownId
diff --git a/meteor/server/api/rundownPlaylist.ts b/meteor/server/api/rundownPlaylist.ts
index 0c645337ce..21c414645e 100644
--- a/meteor/server/api/rundownPlaylist.ts
+++ b/meteor/server/api/rundownPlaylist.ts
@@ -171,6 +171,7 @@ export function produceRundownPlaylistInfoFromRundown(
name: playlistInfo.playlist.name,
expectedStart: playlistInfo.playlist.expectedStart,
expectedDuration: playlistInfo.playlist.expectedDuration,
+ expectedEnd: playlistInfo.playlist.expectedEnd,
loop: playlistInfo.playlist.loop,
@@ -212,6 +213,7 @@ function defaultPlaylistForRundown(
name: rundown.name,
expectedStart: rundown.expectedStart,
expectedDuration: rundown.expectedDuration,
+ expectedEnd: rundown.expectedEnd,
modified: getCurrentTime(),
}
@@ -485,6 +487,7 @@ function sortDefaultRundownInPlaylistOrder(rundowns: ReadonlyDeep, ReadonlyDeep>(rundowns, {
sort: {
expectedStart: 1,
+ expectedEnd: 1,
name: 1,
_id: 1,
},
diff --git a/meteor/server/migration/deprecatedDataTypes/1_0_1.ts b/meteor/server/migration/deprecatedDataTypes/1_0_1.ts
index 7c7174f357..8c386680ad 100644
--- a/meteor/server/migration/deprecatedDataTypes/1_0_1.ts
+++ b/meteor/server/migration/deprecatedDataTypes/1_0_1.ts
@@ -14,6 +14,7 @@ export interface Rundown {
name: string
expectedStart?: Time
expectedDuration?: number
+ expectedEnd?: Time
metaData?: {
[key: string]: any
}
@@ -61,6 +62,7 @@ export function makePlaylistFromRundown_1_0_0(
nextPartInstanceId: null,
expectedDuration: rundown.expectedDuration,
expectedStart: rundown.expectedStart,
+ expectedEnd: rundown.expectedEnd,
holdState: rundown.holdState,
name: rundown.name,
nextPartManual: rundown.nextPartManual,
diff --git a/packages/blueprints-integration/src/rundown.ts b/packages/blueprints-integration/src/rundown.ts
index a6322d3c11..845e6ea758 100644
--- a/packages/blueprints-integration/src/rundown.ts
+++ b/packages/blueprints-integration/src/rundown.ts
@@ -13,6 +13,8 @@ export interface IBlueprintRundownPlaylistInfo {
expectedStart?: Time
/** Expected duration of the rundown playlist */
expectedDuration?: number
+ /** Expected end time of the rundown playlist */
+ expectedEnd?: Time
/** Should the rundown playlist use out-of-order timing mode (unplayed content will be played eventually) as opposed to normal timing mode (unplayed content behind the OnAir line has been skipped) */
outOfOrderTiming?: boolean
/** Should the rundown playlist loop at the end */
@@ -32,6 +34,8 @@ export interface IBlueprintRundown {
expectedStart?: Time
/** Expected duration of the rundown */
expectedDuration?: number
+ /** Expected end time of the rundown */
+ expectedEnd?: Time
/** Arbitrary data storage for plugins */
metaData?: TMetadata
From 55cec52eb8e2bb6a09b4f9d4b1f988d4df32d8e3 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Fri, 4 Jun 2021 13:33:04 +0100
Subject: [PATCH 002/112] feat: End time diff
---
meteor/client/ui/RundownView.tsx | 72 ++++++++++++++++++++++----------
1 file changed, 49 insertions(+), 23 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 3f7ba38268..bd5148acf1 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -270,6 +270,20 @@ const TimingDisplay = withTranslation()(
)
}
+
+ private getEndTimeDiff(expectedDuration: number) {
+ let diff = 0
+ if (this.props.rundownPlaylist.expectedEnd) {
+ let nowDiff = getCurrentTime() - this.props.rundownPlaylist.expectedEnd
+ let durationDiff = expectedDuration - (this.props.timingDurations.asPlayedRundownDuration ?? 0)
+ diff = nowDiff + durationDiff
+ } else {
+ diff = (this.props.timingDurations.asPlayedRundownDuration || 0) - expectedDuration
+ }
+
+ return diff
+ }
+
render() {
const { t, rundownPlaylist } = this.props
@@ -342,7 +356,12 @@ const TimingDisplay = withTranslation()(
)}
{rundownPlaylist.expectedEnd || rundownPlaylist.expectedDuration ? (
- {!rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
+ {!rundownPlaylist.loop && rundownPlaylist.expectedEnd ? (
+
+ {t('Planned End')}
+
+
+ ) : !rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
{t('Planned End')}
- ) : !rundownPlaylist.loop && rundownPlaylist.expectedEnd ? (
-
- {t('Planned End')}
-
-
) : null}
- {!rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
-
- {RundownUtils.formatDiffToTimecode(
- getCurrentTime() - (rundownPlaylist.expectedStart + rundownPlaylist.expectedDuration),
- true,
- true,
- true
- )}
-
- ) : null}
- {rundownPlaylist.expectedDuration && this.props.rundownCount < 2 ? ( // TEMPORARY: disable the diff counter for playlists longer than one rundown -- Jan Starzak, 2021-05-06
+ {!rundownPlaylist.loop &&
+ (rundownPlaylist.expectedEnd ? (
+
+ {RundownUtils.formatDiffToTimecode(
+ getCurrentTime() - rundownPlaylist.expectedEnd,
+ true,
+ true,
+ true
+ )}
+
+ ) : rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
+
+ {RundownUtils.formatDiffToTimecode(
+ getCurrentTime() - (rundownPlaylist.expectedStart + rundownPlaylist.expectedDuration),
+ true,
+ true,
+ true
+ )}
+
+ ) : null)}
+ {rundownPlaylist.expectedDuration ? (
{t('Diff')}
{RundownUtils.formatDiffToTimecode(
- (this.props.timingDurations.asPlayedRundownDuration || 0) - rundownPlaylist.expectedDuration,
+ this.getEndTimeDiff(rundownPlaylist.expectedDuration),
true,
false,
true,
@@ -394,20 +418,22 @@ const TimingDisplay = withTranslation()(
) : (
- {console.log(this.props.rundownPlaylist.expectedEnd)}
{!rundownPlaylist.loop && this.props.timingDurations ? (
{t('Expected End')}
) : null}
+ {!rundownPlaylist.loop && this.props.rundownPlaylist.expectedEnd ? (
+
+ {t('Planned End')}
+
+
+ ) : null}
{this.props.timingDurations && this.props.rundownCount < 2 ? ( // TEMPORARY: disable the diff counter for playlists longer than one rundown -- Jan Starzak, 2021-05-06
Date: Fri, 4 Jun 2021 14:33:21 +0100
Subject: [PATCH 003/112] fix: End time diff
---
meteor/client/ui/RundownView.tsx | 19 ++++---------------
1 file changed, 4 insertions(+), 15 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index bd5148acf1..897b17abad 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -271,19 +271,6 @@ const TimingDisplay = withTranslation()(
)
}
- private getEndTimeDiff(expectedDuration: number) {
- let diff = 0
- if (this.props.rundownPlaylist.expectedEnd) {
- let nowDiff = getCurrentTime() - this.props.rundownPlaylist.expectedEnd
- let durationDiff = expectedDuration - (this.props.timingDurations.asPlayedRundownDuration ?? 0)
- diff = nowDiff + durationDiff
- } else {
- diff = (this.props.timingDurations.asPlayedRundownDuration || 0) - expectedDuration
- }
-
- return diff
- }
-
render() {
const { t, rundownPlaylist } = this.props
@@ -375,7 +362,9 @@ const TimingDisplay = withTranslation()(
(rundownPlaylist.expectedEnd ? (
{RundownUtils.formatDiffToTimecode(
- getCurrentTime() - rundownPlaylist.expectedEnd,
+ getCurrentTime() -
+ rundownPlaylist.expectedEnd +
+ (this.props.timingDurations.asPlayedRundownDuration ?? 0),
true,
true,
true
@@ -404,7 +393,7 @@ const TimingDisplay = withTranslation()(
>
{t('Diff')}
{RundownUtils.formatDiffToTimecode(
- this.getEndTimeDiff(rundownPlaylist.expectedDuration),
+ (this.props.timingDurations.asPlayedRundownDuration || 0) - rundownPlaylist.expectedDuration,
true,
false,
true,
From a6c0bc2543fe7c4664728f4d39b994485ebe804a Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 9 Jun 2021 15:54:35 +0100
Subject: [PATCH 004/112] wip: Move rundown timing to lib and add tests
---
.../RundownTiming/RundownTiming.ts | 35 --
.../RundownTiming/RundownTimingProvider.tsx | 342 +------------
.../SegmentTimelineContainer.tsx | 11 +-
.../rundown/__tests__/rundownTiming.test.ts | 377 ++++++++++++++
meteor/lib/rundown/rundownTiming.ts | 458 ++++++++++++++++++
5 files changed, 849 insertions(+), 374 deletions(-)
create mode 100644 meteor/lib/rundown/__tests__/rundownTiming.test.ts
create mode 100644 meteor/lib/rundown/rundownTiming.ts
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts b/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
index 98fe3c934d..16524cf1e0 100644
--- a/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
@@ -87,38 +87,3 @@ export namespace RundownTiming {
timingDurations: RundownTimingContext
}
}
-
-/**
- * Computes the actual (as-played fallbacking to expected) duration of a segment, consisting of given parts
- * @export
- * @param {RundownTiming.RundownTimingContext} timingDurations The timing durations calculated for the Rundown
- * @param {Array} partIds The IDs of parts that are members of the segment
- * @return number
- */
-export function computeSegmentDuration(
- timingDurations: RundownTiming.RundownTimingContext,
- partIds: PartId[],
- display?: boolean
-): number {
- const partDurations = timingDurations.partDurations
-
- if (partDurations === undefined) return 0
-
- return partIds.reduce((memo, partId) => {
- const pId = unprotectString(partId)
- const partDuration =
- (partDurations ? (partDurations[pId] !== undefined ? partDurations[pId] : 0) : 0) ||
- (display ? Settings.defaultDisplayDuration : 0)
- return memo + partDuration
- }, 0)
-}
-
-export function computeSegmentDisplayDuration(
- timingDurations: RundownTiming.RundownTimingContext,
- parts: PartUi[]
-): number {
- return parts.reduce(
- (memo, part) => memo + SegmentTimelinePartClass.getPartDisplayDuration(part, timingDurations),
- 0
- )
-}
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
index 248c5dd3fc..fcfad1a16b 100644
--- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
@@ -4,19 +4,12 @@ import * as PropTypes from 'prop-types'
import * as _ from 'underscore'
import { withTracker } from '../../../lib/ReactMeteorData/react-meteor-data'
import { Part, PartId } from '../../../../lib/collections/Parts'
-import { getCurrentTime, literal, protectString, unprotectString } from '../../../../lib/lib'
+import { getCurrentTime } from '../../../../lib/lib'
import { MeteorReactComponent } from '../../../lib/MeteorReactComponent'
import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists'
-import {
- PartInstance,
- wrapPartToTemporaryInstance,
- findPartInstanceInMapOrWrapToTemporary,
-} from '../../../../lib/collections/PartInstances'
-import { Settings } from '../../../../lib/Settings'
+import { PartInstance } from '../../../../lib/collections/PartInstances'
import { RundownTiming, TimeEventArgs } from './RundownTiming'
-
-// Minimum duration that a part can be assigned. Used by gap parts to allow them to "compress" to indicate time running out.
-const MINIMAL_NONZERO_DURATION = 1
+import { RundownTimingCalculator } from '../../../../lib/rundown/rundownTiming'
const TIMING_DEFAULT_REFRESH_INTERVAL = 1000 / 60 // the interval for high-resolution events (timeupdateHR)
const LOW_RESOLUTION_TIMING_DECIMATOR = 15 // the low-resolution events will be called every
@@ -125,18 +118,7 @@ export const RundownTimingProvider = withTracker<
refreshTimerInterval: number
refreshDecimator: number
- private temporaryPartInstances: Map = new Map()
-
- private linearParts: Array<[PartId, number | null]> = []
- // look at the comments on RundownTimingContext to understand what these do
- private partDurations: Record = {}
- private partExpectedDurations: Record = {}
- private partPlayed: Record = {}
- private partStartsAt: Record = {}
- private partDisplayStartsAt: Record = {}
- private partDisplayDurations: Record = {}
- private partDisplayDurationsNoPlayback: Record = {}
- private displayDurationGroups: Record = {}
+ private timingCalculator: RundownTimingCalculator = new RundownTimingCalculator()
constructor(props: IRundownTimingProviderProps & IRundownTimingProviderTrackedProps) {
super(props)
@@ -180,7 +162,7 @@ export const RundownTimingProvider = withTracker<
}
if (prevProps.parts !== this.props.parts) {
// empty the temporary Part Instances cache
- this.temporaryPartInstances.clear()
+ this.timingCalculator.clearTempPartInstances()
this.onRefreshTimer()
}
}
@@ -211,318 +193,18 @@ export const RundownTimingProvider = withTracker<
window.dispatchEvent(event)
}
- private getPartInstanceOrGetCachedTemp(partInstancesMap: Map, part: Part): PartInstance {
- const origPartId = part._id
- const partInstance = partInstancesMap.get(origPartId)
- if (partInstance !== undefined) {
- return partInstance
- } else {
- let tempPartInstance = this.temporaryPartInstances.get(origPartId)
- if (tempPartInstance !== undefined) {
- return tempPartInstance
- } else {
- tempPartInstance = wrapPartToTemporaryInstance(protectString(''), part)
- this.temporaryPartInstances.set(origPartId, tempPartInstance)
- return tempPartInstance
- }
- }
- }
-
updateDurations(now: number, isLowResolution: boolean) {
- let totalRundownDuration = 0
- let remainingRundownDuration = 0
- let asPlayedRundownDuration = 0
- let asDisplayedRundownDuration = 0
- let waitAccumulator = 0
- let currentRemaining = 0
- let startsAtAccumulator = 0
- let displayStartsAtAccumulator = 0
-
- Object.keys(this.displayDurationGroups).forEach((key) => delete this.displayDurationGroups[key])
- this.linearParts.length = 0
-
const { playlist, parts, partInstancesMap } = this.props
-
- let nextAIndex = -1
- let currentAIndex = -1
-
- if (playlist && parts) {
- parts.forEach((origPart, itIndex) => {
- const partInstance = this.getPartInstanceOrGetCachedTemp(partInstancesMap, origPart)
-
- // add piece to accumulator
- const aIndex = this.linearParts.push([partInstance.part._id, waitAccumulator]) - 1
-
- // if this is next segementLine, clear previous countdowns and clear accumulator
- if (playlist.nextPartInstanceId === partInstance._id) {
- nextAIndex = aIndex
- } else if (playlist.currentPartInstanceId === partInstance._id) {
- currentAIndex = aIndex
- }
-
- const partCounts =
- playlist.outOfOrderTiming ||
- !playlist.activationId ||
- (itIndex >= currentAIndex && currentAIndex >= 0) ||
- (itIndex >= nextAIndex && nextAIndex >= 0 && currentAIndex === -1)
-
- const partIsUntimed = partInstance.part.untimed || false
-
- // expected is just a sum of expectedDurations
- totalRundownDuration += partInstance.part.expectedDuration || 0
-
- const lastStartedPlayback = partInstance.timings?.startedPlayback
- const playOffset = partInstance.timings?.playOffset || 0
-
- let partDuration = 0
- let partExpectedDuration = 0
- let partDisplayDuration = 0
- let partDisplayDurationNoPlayback = 0
-
- let displayDurationFromGroup = 0
-
- partExpectedDuration = partInstance.part.expectedDuration || partInstance.timings?.duration || 0
-
- // Display Duration groups are groups of two or more Parts, where some of them have an
- // expectedDuration and some have 0.
- // Then, some of them will have a displayDuration. The expectedDurations are pooled together, the parts with
- // display durations will take up that much time in the Rundown. The left-over time from the display duration group
- // will be used by Parts without expectedDurations.
- let memberOfDisplayDurationGroup = false
- // using a separate displayDurationGroup processing flag simplifies implementation
- if (
- partInstance.part.displayDurationGroup &&
- // either this is not the first element of the displayDurationGroup
- (this.displayDurationGroups[partInstance.part.displayDurationGroup] !== undefined ||
- // or there is a following member of this displayDurationGroup
- (parts[itIndex + 1] &&
- parts[itIndex + 1].displayDurationGroup === partInstance.part.displayDurationGroup)) &&
- !partInstance.part.floated &&
- !partIsUntimed
- ) {
- this.displayDurationGroups[partInstance.part.displayDurationGroup] =
- (this.displayDurationGroups[partInstance.part.displayDurationGroup] || 0) +
- (partInstance.part.expectedDuration || 0)
- displayDurationFromGroup =
- partInstance.part.displayDuration ||
- Math.max(
- 0,
- this.displayDurationGroups[partInstance.part.displayDurationGroup],
- partInstance.part.gap
- ? MINIMAL_NONZERO_DURATION
- : this.props.defaultDuration || Settings.defaultDisplayDuration
- )
- partExpectedDuration =
- partExpectedDuration || this.displayDurationGroups[partInstance.part.displayDurationGroup] || 0
- memberOfDisplayDurationGroup = true
- }
-
- // This is where we actually calculate all the various variants of duration of a part
- if (lastStartedPlayback && !partInstance.timings?.duration) {
- // if duration isn't available, check if `takeOut` has already been set and use the difference
- // between startedPlayback and takeOut as a temporary duration
- const duration =
- partInstance.timings?.duration ||
- (partInstance.timings?.takeOut ? lastStartedPlayback - partInstance.timings?.takeOut : undefined)
- currentRemaining = Math.max(
- 0,
- (duration ||
- (memberOfDisplayDurationGroup ? displayDurationFromGroup : partInstance.part.expectedDuration) ||
- 0) -
- (now - lastStartedPlayback)
- )
- partDuration =
- Math.max(duration || partInstance.part.expectedDuration || 0, now - lastStartedPlayback) - playOffset
- // because displayDurationGroups have no actual timing on them, we need to have a copy of the
- // partDisplayDuration, but calculated as if it's not playing, so that the countdown can be
- // calculated
- partDisplayDurationNoPlayback =
- duration ||
- (memberOfDisplayDurationGroup ? displayDurationFromGroup : partInstance.part.expectedDuration) ||
- this.props.defaultDuration ||
- Settings.defaultDisplayDuration
- partDisplayDuration = Math.max(partDisplayDurationNoPlayback, now - lastStartedPlayback)
- this.partPlayed[unprotectString(partInstance.part._id)] = now - lastStartedPlayback
- } else {
- partDuration = (partInstance.timings?.duration || partInstance.part.expectedDuration || 0) - playOffset
- partDisplayDurationNoPlayback = Math.max(
- 0,
- (partInstance.timings?.duration && partInstance.timings?.duration + playOffset) ||
- displayDurationFromGroup ||
- partInstance.part.expectedDuration ||
- this.props.defaultDuration ||
- Settings.defaultDisplayDuration
- )
- partDisplayDuration = partDisplayDurationNoPlayback
- this.partPlayed[unprotectString(partInstance.part._id)] = (partInstance.timings?.duration || 0) - playOffset
- }
-
- // asPlayed is the actual duration so far and expected durations in unplayed lines.
- // If item is onAir right now, it's duration is counted as expected duration or current
- // playback duration whichever is larger.
- // Parts that are Untimed are ignored always.
- // Parts that don't count are ignored, unless they are being played or have been played.
- if (!partIsUntimed) {
- if (lastStartedPlayback && !partInstance.timings?.duration) {
- asPlayedRundownDuration += Math.max(partExpectedDuration, now - lastStartedPlayback)
- } else if (partInstance.timings?.duration) {
- asPlayedRundownDuration += partInstance.timings.duration
- } else if (partCounts) {
- asPlayedRundownDuration += partInstance.part.expectedDuration || 0
- }
- }
-
- // asDisplayed is the actual duration so far and expected durations in unplayed lines
- // If item is onAir right now, it's duration is counted as expected duration or current
- // playback duration whichever is larger.
- // All parts are counted.
- if (lastStartedPlayback && !partInstance.timings?.duration) {
- asDisplayedRundownDuration += Math.max(
- memberOfDisplayDurationGroup
- ? Math.max(partExpectedDuration, partInstance.part.expectedDuration || 0)
- : partInstance.part.expectedDuration || 0,
- now - lastStartedPlayback
- )
- } else {
- asDisplayedRundownDuration += partInstance.timings?.duration || partInstance.part.expectedDuration || 0
- }
-
- // the part is the current part but has not yet started playback
- if (playlist.currentPartInstanceId === partInstance._id && !lastStartedPlayback) {
- currentRemaining = partDisplayDuration
- }
-
- // Handle invalid parts by overriding the values to preset values for Invalid parts
- if (partInstance.part.invalid && !partInstance.part.gap) {
- partDisplayDuration = this.props.defaultDuration || Settings.defaultDisplayDuration
- this.partPlayed[unprotectString(partInstance.part._id)] = 0
- }
-
- if (
- memberOfDisplayDurationGroup &&
- partInstance.part.displayDurationGroup &&
- !partInstance.part.floated &&
- !partInstance.part.invalid &&
- !partIsUntimed &&
- (partInstance.timings?.duration || partInstance.timings?.takeOut || partCounts)
- ) {
- this.displayDurationGroups[partInstance.part.displayDurationGroup] =
- this.displayDurationGroups[partInstance.part.displayDurationGroup] - partDisplayDuration
- }
- const partInstancePartId = unprotectString(partInstance.part._id)
- this.partExpectedDurations[partInstancePartId] = partExpectedDuration
- this.partStartsAt[partInstancePartId] = startsAtAccumulator
- this.partDisplayStartsAt[partInstancePartId] = displayStartsAtAccumulator
- this.partDurations[partInstancePartId] = partDuration
- this.partDisplayDurations[partInstancePartId] = partDisplayDuration
- this.partDisplayDurationsNoPlayback[partInstancePartId] = partDisplayDurationNoPlayback
- startsAtAccumulator += this.partDurations[partInstancePartId]
- displayStartsAtAccumulator += this.partDisplayDurations[partInstancePartId] // || this.props.defaultDuration || 3000
- // waitAccumulator is used to calculate the countdowns for Parts relative to the current Part
- // always add the full duration, in case by some manual intervention this segment should play twice
- if (memberOfDisplayDurationGroup) {
- waitAccumulator +=
- partInstance.timings?.duration || partDisplayDuration || partInstance.part.expectedDuration || 0
- } else {
- waitAccumulator += partInstance.timings?.duration || partInstance.part.expectedDuration || 0
- }
-
- // remaining is the sum of unplayed lines + whatever is left of the current segment
- // if outOfOrderTiming is true, count parts before current part towards remaining rundown duration
- // if false (default), past unplayed parts will not count towards remaining time
- if (!lastStartedPlayback && !partInstance.part.floated && partCounts && !partIsUntimed) {
- remainingRundownDuration += partExpectedDuration || 0
- // item is onAir right now, and it's is currently shorter than expectedDuration
- } else if (
- lastStartedPlayback &&
- !partInstance.timings?.duration &&
- playlist.currentPartInstanceId === partInstance._id &&
- lastStartedPlayback + partExpectedDuration > now &&
- !partIsUntimed
- ) {
- remainingRundownDuration += partExpectedDuration - (now - lastStartedPlayback)
- }
- })
-
- // This is where the waitAccumulator-generated data in the linearSegLines is used to calculate the countdowns.
- let localAccum = 0
- for (let i = 0; i < this.linearParts.length; i++) {
- if (i < nextAIndex) {
- // this is a line before next line
- localAccum = this.linearParts[i][1] || 0
- // only null the values if not looping, if looping, these will be offset by the countdown for the last part
- if (!playlist.loop) {
- this.linearParts[i][1] = null // we use null to express 'will not probably be played out, if played in order'
- }
- } else if (i === nextAIndex) {
- // this is a calculation for the next line, which is basically how much there is left of the current line
- localAccum = this.linearParts[i][1] || 0 // if there is no current line, rebase following lines to the next line
- this.linearParts[i][1] = currentRemaining
- } else {
- // these are lines after next line
- // we take whatever value this line has, subtract the value as set on the Next Part
- // (note that the Next Part value will be using currentRemaining as the countdown)
- // and add the currentRemaining countdown, since we are currentRemaining + diff between next and
- // this away from this line.
- this.linearParts[i][1] = (this.linearParts[i][1] || 0) - localAccum + currentRemaining
- }
- }
- // contiunation of linearParts calculations for looping playlists
- if (playlist.loop) {
- for (let i = 0; i < nextAIndex; i++) {
- // offset the parts before the on air line by the countdown for the end of the rundown
- this.linearParts[i][1] = (this.linearParts[i][1] || 0) + waitAccumulator - localAccum + currentRemaining
- }
- }
-
- // if (this.refreshDecimator % LOW_RESOLUTION_TIMING_DECIMATOR === 0) {
- // const c = document.getElementById('debug-console')
- // if (c) c.innerHTML = debugConsole.replace(/\n/g, ' ')
- // }
- }
-
- let remainingTimeOnCurrentPart: number | undefined = undefined
- let currentPartWillAutoNext = false
- if (currentAIndex >= 0) {
- const currentLivePart = parts[currentAIndex]
- const currentLivePartInstance = findPartInstanceInMapOrWrapToTemporary(partInstancesMap, currentLivePart)
-
- const lastStartedPlayback = currentLivePartInstance.timings?.startedPlayback
-
- let onAirPartDuration = currentLivePartInstance.timings?.duration || currentLivePart.expectedDuration || 0
- if (currentLivePart.displayDurationGroup) {
- onAirPartDuration = this.partExpectedDurations[unprotectString(currentLivePart._id)] || onAirPartDuration
- }
-
- remainingTimeOnCurrentPart = lastStartedPlayback
- ? now - (lastStartedPlayback + onAirPartDuration)
- : onAirPartDuration * -1
-
- currentPartWillAutoNext = !!(
- currentLivePart.autoNext &&
- (currentLivePart.expectedDuration !== undefined ? currentLivePart.expectedDuration !== 0 : false)
- )
- }
-
this.durations = Object.assign(
this.durations,
- literal({
- totalRundownDuration,
- remainingRundownDuration,
- asDisplayedRundownDuration,
- asPlayedRundownDuration,
- partCountdown: _.object(this.linearParts),
- partDurations: this.partDurations,
- partPlayed: this.partPlayed,
- partStartsAt: this.partStartsAt,
- partDisplayStartsAt: this.partDisplayStartsAt,
- partExpectedDurations: this.partExpectedDurations,
- partDisplayDurations: this.partDisplayDurations,
- currentTime: now,
- remainingTimeOnCurrentPart,
- currentPartWillAutoNext,
+ this.timingCalculator.updateDurations(
+ now,
isLowResolution,
- })
+ playlist,
+ parts,
+ partInstancesMap,
+ this.props.defaultDuration
+ )
)
}
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
index 023aa3baad..de4ace347a 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
@@ -7,12 +7,7 @@ import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/reac
import { Segments, SegmentId } from '../../../lib/collections/Segments'
import { Studio } from '../../../lib/collections/Studios'
import { SegmentTimeline, SegmentTimelineClass } from './SegmentTimeline'
-import {
- RundownTiming,
- computeSegmentDuration,
- TimingEvent,
- computeSegmentDisplayDuration,
-} from '../RundownView/RundownTiming/RundownTiming'
+import { RundownTiming, TimingEvent } from '../RundownView/RundownTiming/RundownTiming'
import { UIStateStorage } from '../../lib/UIStateStorage'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import {
@@ -48,9 +43,7 @@ import RundownViewEventBus, {
import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper'
import { checkPieceContentStatus, getNoteTypeForPieceStatus, ScanInfoForPackages } from '../../../lib/mediaObjects'
import { getBasicNotesForSegment } from '../../../lib/rundownNotifications'
-import { SegmentTimelinePartClass } from './SegmentTimelinePart'
-import { RundownAPI } from '../../../lib/api/rundown'
-import { Piece, Pieces } from '../../../lib/collections/Pieces'
+import { computeSegmentDuration } from '../../../lib/rundown/rundownTiming'
export const SIMULATED_PLAYBACK_SOFT_MARGIN = 0
export const SIMULATED_PLAYBACK_HARD_MARGIN = 2500
diff --git a/meteor/lib/rundown/__tests__/rundownTiming.test.ts b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
new file mode 100644
index 0000000000..0869c4efbb
--- /dev/null
+++ b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
@@ -0,0 +1,377 @@
+import { PartInstance } from '../../collections/PartInstances'
+import { DBPart, Part, PartId } from '../../collections/Parts'
+import { DBRundownPlaylist, RundownPlaylist } from '../../collections/RundownPlaylists'
+import { literal, protectString } from '../../lib'
+import { RundownTiming, RundownTimingCalculator } from '../rundownTiming'
+
+const DEFAULT_DURATION = 4000
+
+function makeMockPlaylist(): RundownPlaylist {
+ return new RundownPlaylist(
+ literal({
+ _id: protectString('mock-playlist'),
+ externalId: 'mock-playlist',
+ organizationId: protectString('test'),
+ studioId: protectString('studio0'),
+ name: 'Mock Playlist',
+ created: 0,
+ modified: 0,
+ currentPartInstanceId: null,
+ nextPartInstanceId: null,
+ previousPartInstanceId: null,
+ })
+ )
+}
+
+function makeMockPart(
+ id: string,
+ rank: number,
+ rundownId: string,
+ segmentId: string,
+ durations: { expectedDuration?: number; displayDuration?: number; displayDurationGroup?: string }
+): Part {
+ return new Part(
+ literal({
+ _id: protectString(id),
+ externalId: id,
+ title: '',
+ segmentId: protectString(segmentId),
+ _rank: rank,
+ rundownId: protectString(rundownId),
+ ...durations,
+ })
+ )
+}
+
+describe('rundown Timing Calculator', () => {
+ it('Provides output for empty playlist', () => {
+ const timing = new RundownTimingCalculator()
+ const playlist: RundownPlaylist = makeMockPlaylist()
+ const parts: Part[] = []
+ const partInstancesMap: Map = new Map()
+ const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ expect(result).toEqual(
+ literal({
+ isLowResolution: false,
+ asDisplayedRundownDuration: 0,
+ asPlayedRundownDuration: 0,
+ currentPartWillAutoNext: false,
+ currentTime: 0,
+ partCountdown: {},
+ partDisplayDurations: {},
+ partDisplayStartsAt: {},
+ partDurations: {},
+ partExpectedDurations: {},
+ partPlayed: {},
+ partStartsAt: {},
+ remainingRundownDuration: 0,
+ totalRundownDuration: 0,
+ })
+ )
+ })
+
+ it('Calculates time for unplayed playlist with start time and duration', () => {
+ const timing = new RundownTimingCalculator()
+ const playlist: RundownPlaylist = makeMockPlaylist()
+ playlist.expectedStart = 0
+ playlist.expectedDuration = 4000
+ const rundownId = 'rundown1'
+ const segmentId1 = 'segment1'
+ const segmentId2 = 'segment2'
+ const parts: Part[] = []
+ parts.push(makeMockPart('part1', 0, rundownId, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part4', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
+ const partInstancesMap: Map = new Map()
+ const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ expect(result).toEqual(
+ literal({
+ isLowResolution: false,
+ asDisplayedRundownDuration: 4000,
+ asPlayedRundownDuration: 4000,
+ currentPartWillAutoNext: false,
+ currentTime: 0,
+ partCountdown: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDisplayDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partDisplayStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partExpectedDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partPlayed: {
+ part1: 0,
+ part2: 0,
+ part3: 0,
+ part4: 0,
+ },
+ partStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ remainingRundownDuration: 4000,
+ totalRundownDuration: 4000,
+ })
+ )
+ })
+
+ it('Calculates time for unplayed playlist with end time and duration', () => {
+ const timing = new RundownTimingCalculator()
+ const playlist: RundownPlaylist = makeMockPlaylist()
+ playlist.expectedDuration = 4000
+ playlist.expectedEnd = 4000
+ const rundownId = 'rundown1'
+ const segmentId1 = 'segment1'
+ const segmentId2 = 'segment2'
+ const parts: Part[] = []
+ parts.push(makeMockPart('part1', 0, rundownId, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part4', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
+ const partInstancesMap: Map = new Map()
+ const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ expect(result).toEqual(
+ literal({
+ isLowResolution: false,
+ asDisplayedRundownDuration: 4000,
+ asPlayedRundownDuration: 4000,
+ currentPartWillAutoNext: false,
+ currentTime: 0,
+ partCountdown: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDisplayDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partDisplayStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partExpectedDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partPlayed: {
+ part1: 0,
+ part2: 0,
+ part3: 0,
+ part4: 0,
+ },
+ partStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ remainingRundownDuration: 4000,
+ totalRundownDuration: 4000,
+ })
+ )
+ })
+
+ it('Produces timing per rundown with start time and duration', () => {
+ const timing = new RundownTimingCalculator()
+ const playlist: RundownPlaylist = makeMockPlaylist()
+ playlist.expectedStart = 0
+ playlist.expectedDuration = 4000
+ const rundownId1 = 'rundown1'
+ const rundownId2 = 'rundown2'
+ const segmentId1 = 'segment1'
+ const segmentId2 = 'segment2'
+ const parts: Part[] = []
+ parts.push(makeMockPart('part1', 0, rundownId1, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part2', 0, rundownId1, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part3', 0, rundownId2, segmentId2, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part4', 0, rundownId2, segmentId2, { expectedDuration: 1000 }))
+ const partInstancesMap: Map = new Map()
+ const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ expect(result).toEqual(
+ literal({
+ isLowResolution: false,
+ asDisplayedRundownDuration: 4000,
+ asPlayedRundownDuration: 4000,
+ currentPartWillAutoNext: false,
+ currentTime: 0,
+ partCountdown: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDisplayDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partDisplayStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partExpectedDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partPlayed: {
+ part1: 0,
+ part2: 0,
+ part3: 0,
+ part4: 0,
+ },
+ partStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ remainingRundownDuration: 4000,
+ totalRundownDuration: 4000,
+ })
+ )
+ })
+
+ it('Handles display duration groups', () => {
+ const timing = new RundownTimingCalculator()
+ const playlist: RundownPlaylist = makeMockPlaylist()
+ playlist.expectedStart = 0
+ playlist.expectedDuration = 4000
+ const rundownId1 = 'rundown1'
+ const segmentId1 = 'segment1'
+ const segmentId2 = 'segment2'
+ const parts: Part[] = []
+ parts.push(
+ makeMockPart('part1', 0, rundownId1, segmentId1, {
+ expectedDuration: 1000,
+ displayDuration: 2000,
+ displayDurationGroup: 'test',
+ })
+ )
+ parts.push(
+ makeMockPart('part2', 0, rundownId1, segmentId1, {
+ expectedDuration: 1000,
+ displayDuration: 3000,
+ displayDurationGroup: 'test',
+ })
+ )
+ parts.push(
+ makeMockPart('part3', 0, rundownId1, segmentId2, {
+ expectedDuration: 1000,
+ displayDuration: 4000,
+ displayDurationGroup: 'test',
+ })
+ )
+ parts.push(
+ makeMockPart('part4', 0, rundownId1, segmentId2, {
+ expectedDuration: 1000,
+ displayDuration: 5000,
+ displayDurationGroup: 'test',
+ })
+ )
+ const partInstancesMap: Map = new Map()
+ const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ expect(result).toEqual(
+ literal({
+ isLowResolution: false,
+ asDisplayedRundownDuration: 4000,
+ asPlayedRundownDuration: 4000,
+ currentPartWillAutoNext: false,
+ currentTime: 0,
+ partCountdown: {
+ part1: 0,
+ part2: 2000,
+ part3: 5000,
+ part4: 9000,
+ },
+ partDisplayDurations: {
+ part1: 2000,
+ part2: 3000,
+ part3: 4000,
+ part4: 5000,
+ },
+ partDisplayStartsAt: {
+ part1: 0,
+ part2: 2000,
+ part3: 5000,
+ part4: 9000,
+ },
+ partDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partExpectedDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partPlayed: {
+ part1: 0,
+ part2: 0,
+ part3: 0,
+ part4: 0,
+ },
+ partStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ remainingRundownDuration: 4000,
+ totalRundownDuration: 4000,
+ })
+ )
+ })
+})
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
new file mode 100644
index 0000000000..1bec59d69e
--- /dev/null
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -0,0 +1,458 @@
+import _ from 'underscore'
+import {
+ findPartInstanceInMapOrWrapToTemporary,
+ PartInstance,
+ wrapPartToTemporaryInstance,
+} from '../collections/PartInstances'
+import { Part, PartId } from '../collections/Parts'
+import { RundownPlaylist } from '../collections/RundownPlaylists'
+import { unprotectString, literal, protectString } from '../lib'
+import { Settings } from '../Settings'
+
+// Minimum duration that a part can be assigned. Used by gap parts to allow them to "compress" to indicate time running out.
+const MINIMAL_NONZERO_DURATION = 1
+
+export class RundownTimingCalculator {
+ private temporaryPartInstances: Map = new Map()
+
+ private linearParts: Array<[PartId, number | null]> = []
+ // look at the comments on RundownTimingContext to understand what these do
+ private partDurations: Record = {}
+ private partExpectedDurations: Record = {}
+ private partPlayed: Record = {}
+ private partStartsAt: Record = {}
+ private partDisplayStartsAt: Record = {}
+ private partDisplayDurations: Record = {}
+ private partDisplayDurationsNoPlayback: Record = {}
+ private displayDurationGroups: Record = {}
+
+ updateDurations(
+ now: number,
+ isLowResolution: boolean,
+ playlist: RundownPlaylist | undefined,
+ parts: Part[],
+ partInstancesMap: Map,
+ /** Fallback duration for Parts that have no as-played duration of their own. */
+ defaultDuration?: number
+ ) {
+ let totalRundownDuration = 0
+ let remainingRundownDuration = 0
+ let asPlayedRundownDuration = 0
+ let asDisplayedRundownDuration = 0
+ let waitAccumulator = 0
+ let currentRemaining = 0
+ let startsAtAccumulator = 0
+ let displayStartsAtAccumulator = 0
+
+ Object.keys(this.displayDurationGroups).forEach((key) => delete this.displayDurationGroups[key])
+ this.linearParts.length = 0
+
+ let nextAIndex = -1
+ let currentAIndex = -1
+
+ if (playlist && parts) {
+ parts.forEach((origPart, itIndex) => {
+ const partInstance = this.getPartInstanceOrGetCachedTemp(partInstancesMap, origPart)
+
+ // add piece to accumulator
+ const aIndex = this.linearParts.push([partInstance.part._id, waitAccumulator]) - 1
+
+ // if this is next segementLine, clear previous countdowns and clear accumulator
+ if (playlist.nextPartInstanceId === partInstance._id) {
+ nextAIndex = aIndex
+ } else if (playlist.currentPartInstanceId === partInstance._id) {
+ currentAIndex = aIndex
+ }
+
+ const partCounts =
+ playlist.outOfOrderTiming ||
+ !playlist.activationId ||
+ (itIndex >= currentAIndex && currentAIndex >= 0) ||
+ (itIndex >= nextAIndex && nextAIndex >= 0 && currentAIndex === -1)
+
+ const partIsUntimed = partInstance.part.untimed || false
+
+ // expected is just a sum of expectedDurations
+ totalRundownDuration += partInstance.part.expectedDuration || 0
+
+ const lastStartedPlayback = partInstance.timings?.startedPlayback
+ const playOffset = partInstance.timings?.playOffset || 0
+
+ let partDuration = 0
+ let partExpectedDuration = 0
+ let partDisplayDuration = 0
+ let partDisplayDurationNoPlayback = 0
+
+ let displayDurationFromGroup = 0
+
+ partExpectedDuration = partInstance.part.expectedDuration || partInstance.timings?.duration || 0
+
+ // Display Duration groups are groups of two or more Parts, where some of them have an
+ // expectedDuration and some have 0.
+ // Then, some of them will have a displayDuration. The expectedDurations are pooled together, the parts with
+ // display durations will take up that much time in the Rundown. The left-over time from the display duration group
+ // will be used by Parts without expectedDurations.
+ let memberOfDisplayDurationGroup = false
+ // using a separate displayDurationGroup processing flag simplifies implementation
+ if (
+ partInstance.part.displayDurationGroup &&
+ // either this is not the first element of the displayDurationGroup
+ (this.displayDurationGroups[partInstance.part.displayDurationGroup] !== undefined ||
+ // or there is a following member of this displayDurationGroup
+ (parts[itIndex + 1] &&
+ parts[itIndex + 1].displayDurationGroup === partInstance.part.displayDurationGroup)) &&
+ !partInstance.part.floated &&
+ !partIsUntimed
+ ) {
+ this.displayDurationGroups[partInstance.part.displayDurationGroup] =
+ (this.displayDurationGroups[partInstance.part.displayDurationGroup] || 0) +
+ (partInstance.part.expectedDuration || 0)
+ displayDurationFromGroup =
+ partInstance.part.displayDuration ||
+ Math.max(
+ 0,
+ this.displayDurationGroups[partInstance.part.displayDurationGroup],
+ partInstance.part.gap
+ ? MINIMAL_NONZERO_DURATION
+ : defaultDuration || Settings.defaultDisplayDuration
+ )
+ partExpectedDuration =
+ partExpectedDuration || this.displayDurationGroups[partInstance.part.displayDurationGroup] || 0
+ memberOfDisplayDurationGroup = true
+ }
+
+ // This is where we actually calculate all the various variants of duration of a part
+ if (lastStartedPlayback && !partInstance.timings?.duration) {
+ // if duration isn't available, check if `takeOut` has already been set and use the difference
+ // between startedPlayback and takeOut as a temporary duration
+ const duration =
+ partInstance.timings?.duration ||
+ (partInstance.timings?.takeOut
+ ? lastStartedPlayback - partInstance.timings?.takeOut
+ : undefined)
+ currentRemaining = Math.max(
+ 0,
+ (duration ||
+ (memberOfDisplayDurationGroup
+ ? displayDurationFromGroup
+ : partInstance.part.expectedDuration) ||
+ 0) -
+ (now - lastStartedPlayback)
+ )
+ partDuration =
+ Math.max(duration || partInstance.part.expectedDuration || 0, now - lastStartedPlayback) -
+ playOffset
+ // because displayDurationGroups have no actual timing on them, we need to have a copy of the
+ // partDisplayDuration, but calculated as if it's not playing, so that the countdown can be
+ // calculated
+ partDisplayDurationNoPlayback =
+ duration ||
+ (memberOfDisplayDurationGroup
+ ? displayDurationFromGroup
+ : partInstance.part.expectedDuration) ||
+ defaultDuration ||
+ Settings.defaultDisplayDuration
+ partDisplayDuration = Math.max(partDisplayDurationNoPlayback, now - lastStartedPlayback)
+ this.partPlayed[unprotectString(partInstance.part._id)] = now - lastStartedPlayback
+ } else {
+ partDuration =
+ (partInstance.timings?.duration || partInstance.part.expectedDuration || 0) - playOffset
+ partDisplayDurationNoPlayback = Math.max(
+ 0,
+ (partInstance.timings?.duration && partInstance.timings?.duration + playOffset) ||
+ displayDurationFromGroup ||
+ partInstance.part.expectedDuration ||
+ defaultDuration ||
+ Settings.defaultDisplayDuration
+ )
+ partDisplayDuration = partDisplayDurationNoPlayback
+ this.partPlayed[unprotectString(partInstance.part._id)] =
+ (partInstance.timings?.duration || 0) - playOffset
+ }
+
+ // asPlayed is the actual duration so far and expected durations in unplayed lines.
+ // If item is onAir right now, it's duration is counted as expected duration or current
+ // playback duration whichever is larger.
+ // Parts that are Untimed are ignored always.
+ // Parts that don't count are ignored, unless they are being played or have been played.
+ if (!partIsUntimed) {
+ if (lastStartedPlayback && !partInstance.timings?.duration) {
+ asPlayedRundownDuration += Math.max(partExpectedDuration, now - lastStartedPlayback)
+ } else if (partInstance.timings?.duration) {
+ asPlayedRundownDuration += partInstance.timings.duration
+ } else if (partCounts) {
+ asPlayedRundownDuration += partInstance.part.expectedDuration || 0
+ }
+ }
+
+ // asDisplayed is the actual duration so far and expected durations in unplayed lines
+ // If item is onAir right now, it's duration is counted as expected duration or current
+ // playback duration whichever is larger.
+ // All parts are counted.
+ if (lastStartedPlayback && !partInstance.timings?.duration) {
+ asDisplayedRundownDuration += Math.max(
+ memberOfDisplayDurationGroup
+ ? Math.max(partExpectedDuration, partInstance.part.expectedDuration || 0)
+ : partInstance.part.expectedDuration || 0,
+ now - lastStartedPlayback
+ )
+ } else {
+ asDisplayedRundownDuration +=
+ partInstance.timings?.duration || partInstance.part.expectedDuration || 0
+ }
+
+ // the part is the current part but has not yet started playback
+ if (playlist.currentPartInstanceId === partInstance._id && !lastStartedPlayback) {
+ currentRemaining = partDisplayDuration
+ }
+
+ // Handle invalid parts by overriding the values to preset values for Invalid parts
+ if (partInstance.part.invalid && !partInstance.part.gap) {
+ partDisplayDuration = defaultDuration || Settings.defaultDisplayDuration
+ this.partPlayed[unprotectString(partInstance.part._id)] = 0
+ }
+
+ if (
+ memberOfDisplayDurationGroup &&
+ partInstance.part.displayDurationGroup &&
+ !partInstance.part.floated &&
+ !partInstance.part.invalid &&
+ !partIsUntimed &&
+ (partInstance.timings?.duration || partInstance.timings?.takeOut || partCounts)
+ ) {
+ this.displayDurationGroups[partInstance.part.displayDurationGroup] =
+ this.displayDurationGroups[partInstance.part.displayDurationGroup] - partDisplayDuration
+ }
+ const partInstancePartId = unprotectString(partInstance.part._id)
+ this.partExpectedDurations[partInstancePartId] = partExpectedDuration
+ this.partStartsAt[partInstancePartId] = startsAtAccumulator
+ this.partDisplayStartsAt[partInstancePartId] = displayStartsAtAccumulator
+ this.partDurations[partInstancePartId] = partDuration
+ this.partDisplayDurations[partInstancePartId] = partDisplayDuration
+ this.partDisplayDurationsNoPlayback[partInstancePartId] = partDisplayDurationNoPlayback
+ startsAtAccumulator += this.partDurations[partInstancePartId]
+ displayStartsAtAccumulator += this.partDisplayDurations[partInstancePartId] // || this.props.defaultDuration || 3000
+ // waitAccumulator is used to calculate the countdowns for Parts relative to the current Part
+ // always add the full duration, in case by some manual intervention this segment should play twice
+ if (memberOfDisplayDurationGroup) {
+ waitAccumulator +=
+ partInstance.timings?.duration || partDisplayDuration || partInstance.part.expectedDuration || 0
+ } else {
+ waitAccumulator += partInstance.timings?.duration || partInstance.part.expectedDuration || 0
+ }
+
+ // remaining is the sum of unplayed lines + whatever is left of the current segment
+ // if outOfOrderTiming is true, count parts before current part towards remaining rundown duration
+ // if false (default), past unplayed parts will not count towards remaining time
+ if (!lastStartedPlayback && !partInstance.part.floated && partCounts && !partIsUntimed) {
+ remainingRundownDuration += partExpectedDuration || 0
+ // item is onAir right now, and it's is currently shorter than expectedDuration
+ } else if (
+ lastStartedPlayback &&
+ !partInstance.timings?.duration &&
+ playlist.currentPartInstanceId === partInstance._id &&
+ lastStartedPlayback + partExpectedDuration > now &&
+ !partIsUntimed
+ ) {
+ remainingRundownDuration += partExpectedDuration - (now - lastStartedPlayback)
+ }
+ })
+
+ // This is where the waitAccumulator-generated data in the linearSegLines is used to calculate the countdowns.
+ let localAccum = 0
+ for (let i = 0; i < this.linearParts.length; i++) {
+ if (i < nextAIndex) {
+ // this is a line before next line
+ localAccum = this.linearParts[i][1] || 0
+ // only null the values if not looping, if looping, these will be offset by the countdown for the last part
+ if (!playlist.loop) {
+ this.linearParts[i][1] = null // we use null to express 'will not probably be played out, if played in order'
+ }
+ } else if (i === nextAIndex) {
+ // this is a calculation for the next line, which is basically how much there is left of the current line
+ localAccum = this.linearParts[i][1] || 0 // if there is no current line, rebase following lines to the next line
+ this.linearParts[i][1] = currentRemaining
+ } else {
+ // these are lines after next line
+ // we take whatever value this line has, subtract the value as set on the Next Part
+ // (note that the Next Part value will be using currentRemaining as the countdown)
+ // and add the currentRemaining countdown, since we are currentRemaining + diff between next and
+ // this away from this line.
+ this.linearParts[i][1] = (this.linearParts[i][1] || 0) - localAccum + currentRemaining
+ }
+ }
+ // contiunation of linearParts calculations for looping playlists
+ if (playlist.loop) {
+ for (let i = 0; i < nextAIndex; i++) {
+ // offset the parts before the on air line by the countdown for the end of the rundown
+ this.linearParts[i][1] =
+ (this.linearParts[i][1] || 0) + waitAccumulator - localAccum + currentRemaining
+ }
+ }
+
+ // if (this.refreshDecimator % LOW_RESOLUTION_TIMING_DECIMATOR === 0) {
+ // const c = document.getElementById('debug-console')
+ // if (c) c.innerHTML = debugConsole.replace(/\n/g, ' ')
+ // }
+ }
+
+ let remainingTimeOnCurrentPart: number | undefined = undefined
+ let currentPartWillAutoNext = false
+ if (currentAIndex >= 0) {
+ const currentLivePart = parts[currentAIndex]
+ const currentLivePartInstance = findPartInstanceInMapOrWrapToTemporary(partInstancesMap, currentLivePart)
+
+ const lastStartedPlayback = currentLivePartInstance.timings?.startedPlayback
+
+ let onAirPartDuration = currentLivePartInstance.timings?.duration || currentLivePart.expectedDuration || 0
+ if (currentLivePart.displayDurationGroup) {
+ onAirPartDuration =
+ this.partExpectedDurations[unprotectString(currentLivePart._id)] || onAirPartDuration
+ }
+
+ remainingTimeOnCurrentPart = lastStartedPlayback
+ ? now - (lastStartedPlayback + onAirPartDuration)
+ : onAirPartDuration * -1
+
+ currentPartWillAutoNext = !!(
+ currentLivePart.autoNext &&
+ (currentLivePart.expectedDuration !== undefined ? currentLivePart.expectedDuration !== 0 : false)
+ )
+ }
+
+ return literal({
+ totalRundownDuration,
+ remainingRundownDuration,
+ asDisplayedRundownDuration,
+ asPlayedRundownDuration,
+ partCountdown: _.object(this.linearParts),
+ partDurations: this.partDurations,
+ partPlayed: this.partPlayed,
+ partStartsAt: this.partStartsAt,
+ partDisplayStartsAt: this.partDisplayStartsAt,
+ partExpectedDurations: this.partExpectedDurations,
+ partDisplayDurations: this.partDisplayDurations,
+ currentTime: now,
+ remainingTimeOnCurrentPart,
+ currentPartWillAutoNext,
+ isLowResolution,
+ })
+ }
+
+ clearTempPartInstances() {
+ this.temporaryPartInstances.clear()
+ }
+
+ private getPartInstanceOrGetCachedTemp(partInstancesMap: Map, part: Part): PartInstance {
+ const origPartId = part._id
+ const partInstance = partInstancesMap.get(origPartId)
+ if (partInstance !== undefined) {
+ return partInstance
+ } else {
+ let tempPartInstance = this.temporaryPartInstances.get(origPartId)
+ if (tempPartInstance !== undefined) {
+ return tempPartInstance
+ } else {
+ tempPartInstance = wrapPartToTemporaryInstance(protectString(''), part)
+ this.temporaryPartInstances.set(origPartId, tempPartInstance)
+ return tempPartInstance
+ }
+ }
+ }
+}
+
+export namespace RundownTiming {
+ /**
+ * Events used by the RundownTimingProvider
+ * @export
+ * @enum {number}
+ */
+ export enum Events {
+ /** Event is emitted every now-and-then, generally to be used for simple displays */
+ 'timeupdate' = 'sofie:rundownTimeUpdate',
+ /** event is emitted with a very high frequency (60 Hz), to be used sparingly as
+ * hooking up Components to it will cause a lot of renders
+ */
+ 'timeupdateHR' = 'sofie:rundownTimeUpdateHR',
+ }
+
+ /**
+ * Context object that will be passed to listening components. The dictionaries use the Part ID as a key.
+ * @export
+ * @interface RundownTimingContext
+ */
+ export interface RundownTimingContext {
+ /** This is the total duration of the rundown as planned (using expectedDurations). */
+ totalRundownDuration?: number
+ /** This is the content remaining to be played in the rundown (based on the expectedDurations). */
+ remainingRundownDuration?: number
+ /** This is the total duration of the rundown: as planned for the unplayed (skipped & future) content, and as-run for the played-out. */
+ asDisplayedRundownDuration?: number
+ /** This is the complete duration of the rundown: as planned for the unplayed content, and as-run for the played-out, but ignoring unplayed/unplayable parts in order */
+ asPlayedRundownDuration?: number
+ /** this is the countdown to each of the parts relative to the current on air part. */
+ partCountdown?: Record
+ /** The calculated durations of each of the Parts: as-planned/as-run depending on state. */
+ partDurations?: Record
+ /** The offset of each of the Parts from the beginning of the Rundown. */
+ partStartsAt?: Record
+ /** Same as partStartsAt, but will include display duration overrides
+ * (such as minimal display width for an Part, etc.).
+ */
+ partDisplayStartsAt?: Record
+ /** Same as partDurations, but will include display duration overrides
+ * (such as minimal display width for an Part, etc.).
+ */
+ partDisplayDurations?: Record
+ /** As-played durations of each part. Will be 0, if not yet played.
+ * Will be counted from start to now if currently playing.
+ */
+ partPlayed?: Record
+ /** Expected durations of each of the parts or the as-played duration,
+ * if the Part does not have an expected duration.
+ */
+ partExpectedDurations?: Record
+ /** Remaining time on current part */
+ remainingTimeOnCurrentPart?: number | undefined
+ /** Current part will autoNext */
+ currentPartWillAutoNext?: boolean
+ /** Current time of this calculation */
+ currentTime?: number
+ /** Was this time context calculated during a high-resolution tick */
+ isLowResolution: boolean
+ }
+
+ /**
+ * This are the properties that will be injected by the withTiming HOC.
+ * @export
+ * @interface InjectedROTimingProps
+ */
+ export interface InjectedROTimingProps {
+ timingDurations: RundownTimingContext
+ }
+}
+
+/**
+ * Computes the actual (as-played fallbacking to expected) duration of a segment, consisting of given parts
+ * @export
+ * @param {RundownTiming.RundownTimingContext} timingDurations The timing durations calculated for the Rundown
+ * @param {Array} partIds The IDs of parts that are members of the segment
+ * @return number
+ */
+export function computeSegmentDuration(
+ timingDurations: RundownTiming.RundownTimingContext,
+ partIds: PartId[],
+ display?: boolean
+): number {
+ let partDurations = timingDurations.partDurations
+
+ if (partDurations === undefined) return 0
+
+ return partIds.reduce((memo, partId) => {
+ const pId = unprotectString(partId)
+ const partDuration =
+ (partDurations ? (partDurations[pId] !== undefined ? partDurations[pId] : 0) : 0) ||
+ (display ? Settings.defaultDisplayDuration : 0)
+ return memo + partDuration
+ }, 0)
+}
From eedf5211fec8d47c70e4627ee2a2cce1e4a9be9b Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 14 Jun 2021 10:53:00 +0100
Subject: [PATCH 005/112] feat: UI components for timing to next break
---
meteor/client/styles/rundownView.scss | 3 +-
.../client/ui/ClockView/PresenterScreen.tsx | 6 +-
meteor/client/ui/Prompter/OverUnderTimer.tsx | 9 +-
meteor/client/ui/RundownView.tsx | 233 ++++++++++++------
.../ui/RundownView/RundownDividerHeader.tsx | 34 ++-
.../client/ui/RundownView/RundownOverview.tsx | 2 +-
.../RundownTiming/RundownTiming.ts | 52 +---
.../RundownTiming/RundownTimingProvider.tsx | 6 +-
.../RundownView/RundownTiming/withTiming.tsx | 6 +-
.../ui/SegmentTimeline/SegmentTimeline.tsx | 5 +-
meteor/lib/collections/Rundowns.ts | 3 +
.../rundown/__tests__/rundownTiming.test.ts | 54 ++--
meteor/lib/rundown/rundownTiming.ts | 148 ++++++-----
.../blueprints-integration/src/rundown.ts | 4 +
14 files changed, 319 insertions(+), 246 deletions(-)
diff --git a/meteor/client/styles/rundownView.scss b/meteor/client/styles/rundownView.scss
index 7d9c3c5963..76b8feffce 100644
--- a/meteor/client/styles/rundownView.scss
+++ b/meteor/client/styles/rundownView.scss
@@ -2495,7 +2495,8 @@ svg.icon {
}
> .rundown-divider-timeline__expected-start,
- > .rundown-divider-timeline__expected-duration {
+ > .rundown-divider-timeline__expected-duration,
+ > .rundown-divider-timeline__expected-end {
vertical-align: bottom;
margin: 1em 0.5em 0.5em;
diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx
index cea32acc2f..2a70fcc35d 100644
--- a/meteor/client/ui/ClockView/PresenterScreen.tsx
+++ b/meteor/client/ui/ClockView/PresenterScreen.tsx
@@ -318,9 +318,9 @@ export class PresenterScreenBase extends MeteorReactComponent<
const nextSegment = this.props.nextSegment
const overUnderClock = playlist.expectedDuration
- ? (this.props.timingDurations.asPlayedRundownDuration || 0) - playlist.expectedDuration
- : (this.props.timingDurations.asPlayedRundownDuration || 0) -
- (this.props.timingDurations.totalRundownDuration || 0)
+ ? (this.props.timingDurations.asDisplayedPlaylistDuration || 0) - playlist.expectedDuration
+ : (this.props.timingDurations.asDisplayedPlaylistDuration || 0) -
+ (this.props.timingDurations.totalPlaylistDuration || 0)
return (
diff --git a/meteor/client/ui/Prompter/OverUnderTimer.tsx b/meteor/client/ui/Prompter/OverUnderTimer.tsx
index a3bef12b3e..e38fbe67d1 100644
--- a/meteor/client/ui/Prompter/OverUnderTimer.tsx
+++ b/meteor/client/ui/Prompter/OverUnderTimer.tsx
@@ -15,17 +15,18 @@ interface IProps {
export const OverUnderTimer = withTiming
()(
class OverUnderTimer extends React.Component> {
render() {
- const target = this.props.rundownPlaylist.expectedDuration || this.props.timingDurations.totalRundownDuration || 0
+ const target =
+ this.props.rundownPlaylist.expectedDuration || this.props.timingDurations.totalPlaylistDuration || 0
return target ? (
target,
+ heavy: (this.props.timingDurations.totalPlaylistDuration || 0) <= target,
+ light: (this.props.timingDurations.totalPlaylistDuration || 0) > target,
})}
>
{RundownUtils.formatDiffToTimecode(
- (this.props.timingDurations.asPlayedRundownDuration || 0) - target,
+ (this.props.timingDurations.totalPlaylistDuration || 0) - target,
true,
false,
true,
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 897b17abad..df878cf476 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -205,6 +205,7 @@ interface ITimingDisplayProps {
rundownPlaylist: RundownPlaylist
currentRundown: Rundown | undefined
rundownCount: number
+ isLastRundownInPlaylist: boolean
}
export enum RundownViewKbdShortcuts {
@@ -270,9 +271,8 @@ const TimingDisplay = withTranslation()(
)
}
-
render() {
- const { t, rundownPlaylist } = this.props
+ const { t, rundownPlaylist, currentRundown } = this.props
if (!rundownPlaylist) return null
@@ -288,6 +288,15 @@ const TimingDisplay = withTranslation()(
{t('Planned Start')}
+ ) : rundownPlaylist.expectedEnd && rundownPlaylist.expectedDuration ? (
+
+ {t('Expected Start')}
+
+
) : null}
{rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
rundownPlaylist.expectedStart ? (
@@ -341,68 +350,22 @@ const TimingDisplay = withTranslation()(
) : null}
)}
- {rundownPlaylist.expectedEnd || rundownPlaylist.expectedDuration ? (
+ {rundownPlaylist.expectedDuration ? (
- {!rundownPlaylist.loop && rundownPlaylist.expectedEnd ? (
-
- {t('Planned End')}
-
-
- ) : !rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
-
- {t('Planned End')}
-
-
+ {!rundownPlaylist.startedPlayback ||
+ this.props.isLastRundownInPlaylist ||
+ !currentRundown?.endIsBreak ? ( // TODO: Setting
+
) : null}
- {!rundownPlaylist.loop &&
- (rundownPlaylist.expectedEnd ? (
-
- {RundownUtils.formatDiffToTimecode(
- getCurrentTime() -
- rundownPlaylist.expectedEnd +
- (this.props.timingDurations.asPlayedRundownDuration ?? 0),
- true,
- true,
- true
- )}
-
- ) : rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
-
- {RundownUtils.formatDiffToTimecode(
- getCurrentTime() - (rundownPlaylist.expectedStart + rundownPlaylist.expectedDuration),
- true,
- true,
- true
- )}
-
- ) : null)}
- {rundownPlaylist.expectedDuration ? (
-
- (rundownPlaylist.expectedDuration || 0),
- })}
- >
- {t('Diff')}
- {RundownUtils.formatDiffToTimecode(
- (this.props.timingDurations.asPlayedRundownDuration || 0) - rundownPlaylist.expectedDuration,
- true,
- false,
- true,
- true,
- true,
- undefined,
- true
- )}
-
+ {rundownPlaylist.startedPlayback &&
+ currentRundown?.endIsBreak &&
+ !this.props.isLastRundownInPlaylist ? ( // TODO: Setting // TODO: Find next break in higher-order component, so next breaks reflects next rundown in playlist marked as break.
+
) : null}
) : (
@@ -413,31 +376,25 @@ const TimingDisplay = withTranslation()(
) : null}
- {!rundownPlaylist.loop && this.props.rundownPlaylist.expectedEnd ? (
-
- {t('Planned End')}
-
-
- ) : null}
{this.props.timingDurations && this.props.rundownCount < 2 ? ( // TEMPORARY: disable the diff counter for playlists longer than one rundown -- Jan Starzak, 2021-05-06
- (this.props.timingDurations.totalRundownDuration || 0),
+ (this.props.timingDurations.asPlayedPlaylistDuration || 0) >
+ (this.props.timingDurations.totalPlaylistDuration || 0),
})}
>
{t('Diff')}
{RundownUtils.formatDiffToTimecode(
- (this.props.timingDurations.asPlayedRundownDuration || 0) -
- (this.props.timingDurations.totalRundownDuration || 0),
+ (this.props.timingDurations.asPlayedPlaylistDuration || 0) -
+ (this.props.timingDurations.totalPlaylistDuration || 0),
true,
false,
true,
@@ -457,6 +414,128 @@ const TimingDisplay = withTranslation()(
)
)
+interface IEndTimingProps {
+ loop?: boolean
+ expectedStart?: number
+ expectedDuration: number
+ expectedEnd?: number
+}
+
+const PlaylistEndTiming = withTranslation()(
+ withTiming()(
+ class PlaylistEndTiming extends React.Component>> {
+ render() {
+ let { t } = this.props
+
+ return (
+
+ {!this.props.loop && this.props.expectedStart ? (
+
+ {t('Planned End')}
+
+
+ ) : !this.props.loop && this.props.expectedEnd ? (
+
+ {t('Planned End')}
+
+
+ ) : null}
+ {!this.props.loop && this.props.expectedStart && this.props.expectedDuration ? (
+
+ {RundownUtils.formatDiffToTimecode(
+ getCurrentTime() - (this.props.expectedStart + this.props.expectedDuration),
+ true,
+ true,
+ true
+ )}
+
+ ) : !this.props.loop && this.props.expectedEnd ? (
+
+ {RundownUtils.formatDiffToTimecode(getCurrentTime() - this.props.expectedEnd, true, true, true)}
+
+ ) : null}
+ {this.props.expectedDuration ? (
+ (this.props.expectedDuration || 0),
+ })}
+ >
+ {t('Diff')}
+ {RundownUtils.formatDiffToTimecode(
+ (this.props.timingDurations.asPlayedPlaylistDuration || 0) - this.props.expectedDuration,
+ true,
+ false,
+ true,
+ true,
+ true,
+ undefined,
+ true
+ )}
+
+ ) : null}
+
+ )
+ }
+ }
+ )
+)
+
+interface INextBreakTimingProps {
+ loop?: boolean
+ breakRundown: Rundown
+}
+
+const NextBreakTiming = withTranslation()(
+ withTiming()(
+ class PlaylistEndTiming extends React.Component>> {
+ render() {
+ let { t, breakRundown } = this.props
+
+ const rundownAsPlayedDuration = this.props.timingDurations.rundownAsPlayedDurations
+ ? this.props.timingDurations.rundownAsPlayedDurations[unprotectString(breakRundown._id)]
+ : undefined
+
+ return (
+
+
+ {t('Next Break')}
+
+
+ {!this.props.loop && breakRundown.expectedEnd ? (
+
+ {RundownUtils.formatDiffToTimecode(getCurrentTime() - breakRundown.expectedEnd, true, true, true)}
+
+ ) : null}
+ {breakRundown.expectedDuration ? (
+ (breakRundown.expectedDuration || 0),
+ })}
+ >
+ {t('Diff')}
+ {RundownUtils.formatDiffToTimecode(
+ (rundownAsPlayedDuration || 0) - breakRundown.expectedDuration,
+ true,
+ false,
+ true,
+ true,
+ true,
+ undefined,
+ true
+ )}
+
+ ) : null}
+
+ )
+ }
+ }
+ )
+)
+
interface HotkeyDefinition {
key: string
label: string
@@ -1360,6 +1439,10 @@ const RundownHeader = withTranslation()(
rundownPlaylist={this.props.playlist}
currentRundown={this.props.currentRundown}
rundownCount={this.props.rundownIds.length}
+ isLastRundownInPlaylist={
+ !!this.props.currentRundown?._id &&
+ this.props.rundownIds.indexOf(this.props.currentRundown._id) === this.props.rundownIds.length - 1
+ }
/>
,
{}
@@ -30,15 +30,15 @@ const RundownCountdown = withTranslation()(
})(function RundownCountdown(
props: Translated<
WithTiming<{
- expectedStart: number | undefined
+ expectedStartOrEnd: number | undefined
className?: string | undefined
}>
>
) {
const { t } = props
- if (props.expectedStart === undefined) return null
+ if (props.expectedStartOrEnd === undefined) return null
- const time = props.expectedStart - (props.timingDurations.currentTime || 0)
+ const time = props.expectedStartOrEnd - (props.timingDurations.currentTime || 0)
if (time < QUATER_DAY) {
return (
@@ -70,7 +70,7 @@ export const RundownDividerHeader = withTranslation()(function RundownDividerHea
return (
{rundown.name}
-
{playlist.name}
+ {rundown.name !== playlist.name &&
{playlist.name} }
{rundown.expectedStart ? (
{t('Planned Start')}
@@ -85,7 +85,7 @@ export const RundownDividerHeader = withTranslation()(function RundownDividerHea
) : null}
@@ -95,6 +95,24 @@ export const RundownDividerHeader = withTranslation()(function RundownDividerHea
{RundownUtils.formatDiffToTimecode(rundown.expectedDuration, false, true, true, false, true)}
) : null}
+ {rundown.expectedEnd ? (
+
+ {t('Planned End')}
+
+ {rundown.expectedEnd}
+
+
+
+
+ ) : null}
)
})
diff --git a/meteor/client/ui/RundownView/RundownOverview.tsx b/meteor/client/ui/RundownView/RundownOverview.tsx
index 3f67fc1d1e..ee560cd3a7 100644
--- a/meteor/client/ui/RundownView/RundownOverview.tsx
+++ b/meteor/client/ui/RundownView/RundownOverview.tsx
@@ -221,7 +221,7 @@ export const RundownOverview = withTracker
- /** The calculated durations of each of the Parts: as-planned/as-run depending on state. */
- partDurations?: Record
- /** The offset of each of the Parts from the beginning of the Rundown. */
- partStartsAt?: Record
- /** Same as partStartsAt, but will include display duration overrides
- * (such as minimal display width for an Part, etc.).
- */
- partDisplayStartsAt?: Record
- /** Same as partDurations, but will include display duration overrides
- * (such as minimal display width for an Part, etc.).
- */
- partDisplayDurations?: Record
- /** As-played durations of each part. Will be 0, if not yet played.
- * Will be counted from start to now if currently playing.
- */
- partPlayed?: Record
- /** Expected durations of each of the parts or the as-played duration,
- * if the Part does not have an expected duration.
- */
- partExpectedDurations?: Record
- /** Remaining time on current part */
- remainingTimeOnCurrentPart?: number | undefined
- /** Current part will autoNext */
- currentPartWillAutoNext?: boolean
- /** Current time of this calculation */
- currentTime?: number
- /** Was this time context calculated during a high-resolution tick */
- isLowResolution: boolean
- }
-
/**
* This are the properties that will be injected by the withTiming HOC.
* @export
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
index fcfad1a16b..0edb581023 100644
--- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
@@ -9,7 +9,7 @@ import { MeteorReactComponent } from '../../../lib/MeteorReactComponent'
import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists'
import { PartInstance } from '../../../../lib/collections/PartInstances'
import { RundownTiming, TimeEventArgs } from './RundownTiming'
-import { RundownTimingCalculator } from '../../../../lib/rundown/rundownTiming'
+import { RundownTimingCalculator, RundownTimingContext } from '../../../../lib/rundown/rundownTiming'
const TIMING_DEFAULT_REFRESH_INTERVAL = 1000 / 60 // the interval for high-resolution events (timeupdateHR)
const LOW_RESOLUTION_TIMING_DECIMATOR = 15 // the low-resolution events will be called every
@@ -31,7 +31,7 @@ interface IRundownTimingProviderProps {
defaultDuration?: number
}
interface IRundownTimingProviderChildContext {
- durations: RundownTiming.RundownTimingContext
+ durations: RundownTimingContext
}
interface IRundownTimingProviderState {}
interface IRundownTimingProviderTrackedProps {
@@ -111,7 +111,7 @@ export const RundownTimingProvider = withTracker<
durations: PropTypes.object.isRequired,
}
- durations: RundownTiming.RundownTimingContext = {
+ durations: RundownTimingContext = {
isLowResolution: false,
}
refreshTimer: number
diff --git a/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
index 9b9934e803..e2e2865bdc 100644
--- a/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
@@ -2,8 +2,10 @@ import * as React from 'react'
import * as PropTypes from 'prop-types'
import * as _ from 'underscore'
import { RundownTiming } from './RundownTiming'
+import { JsxEmit } from 'typescript'
+import { RundownTimingContext } from '../../../../lib/rundown/rundownTiming'
-export type TimingFilterFunction = (durations: RundownTiming.RundownTimingContext) => any
+export type TimingFilterFunction = (durations: RundownTimingContext) => any
export interface WithTimingOptions {
isHighResolution?: boolean
@@ -92,7 +94,7 @@ export function withTiming(
}
render() {
- const durations: RundownTiming.RundownTimingContext = this.context.durations
+ const durations: RundownTimingContext = this.context.durations
// If the timing HOC is supposed to be low resolution and we are rendering
// during a high resolution tick, the WrappedComponent will render using
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
index 65d9323704..c92283f6fc 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
@@ -35,8 +35,7 @@ import { WarningIconSmall, CriticalIconSmall } from '../../lib/ui/icons/notifica
import RundownViewEventBus, { RundownViewEvents, HighlightEvent } from '../RundownView/RundownViewEventBus'
import { ZoomInIcon, ZoomOutIcon, ZoomShowAll } from '../../lib/segmentZoomIcon'
-import { PartInstanceId } from '../../../lib/collections/PartInstances'
-import { SegmentTimelineSmallPartFlag } from './SmallParts/SegmentTimelineSmallPartFlag'
+import { RundownTimingContext } from '../../../lib/rundown/rundownTiming'
interface IProps {
id: string
@@ -136,7 +135,7 @@ const SegmentTimelineZoom = class SegmentTimelineZoom extends React.Component<
calculateSegmentDuration(): number {
let total = 0
if (this.context && this.context.durations) {
- const durations = this.context.durations as RundownTiming.RundownTimingContext
+ const durations = this.context.durations as RundownTimingContext
this.props.parts.forEach((item) => {
// total += durations.partDurations ? durations.partDurations[item._id] : (item.duration || item.renderedDuration || 1)
const duration = Math.max(
diff --git a/meteor/lib/collections/Rundowns.ts b/meteor/lib/collections/Rundowns.ts
index df1335204d..2103d32e43 100644
--- a/meteor/lib/collections/Rundowns.ts
+++ b/meteor/lib/collections/Rundowns.ts
@@ -67,6 +67,8 @@ export interface DBRundown
/** External id of the Rundown Playlist to put this rundown in */
playlistExternalId?: string
+ /** Whether the end of the rundown marks a commercial break */
+ endIsBreak?: boolean
/** Name (user-facing) of the external NCS this rundown came from */
externalNRCSName: string
/** The id of the Rundown Playlist this rundown is in */
@@ -105,6 +107,7 @@ export class Rundown implements DBRundown {
public notifiedCurrentPlayingPartExternalId?: string
public notes?: Array
public playlistExternalId?: string
+ public endIsBreak?: boolean
public externalNRCSName: string
public playlistId: RundownPlaylistId
public playlistIdIsSetInSofie?: boolean
diff --git a/meteor/lib/rundown/__tests__/rundownTiming.test.ts b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
index 0869c4efbb..2e923b6c0a 100644
--- a/meteor/lib/rundown/__tests__/rundownTiming.test.ts
+++ b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
@@ -53,10 +53,11 @@ describe('rundown Timing Calculator', () => {
expect(result).toEqual(
literal({
isLowResolution: false,
- asDisplayedRundownDuration: 0,
- asPlayedRundownDuration: 0,
+ asDisplayedPlaylistDuration: 0,
+ asPlayedPlaylistDuration: 0,
currentPartWillAutoNext: false,
currentTime: 0,
+ rundownExpectedDurations: {},
partCountdown: {},
partDisplayDurations: {},
partDisplayStartsAt: {},
@@ -64,8 +65,8 @@ describe('rundown Timing Calculator', () => {
partExpectedDurations: {},
partPlayed: {},
partStartsAt: {},
- remainingRundownDuration: 0,
- totalRundownDuration: 0,
+ remainingPlaylistDuration: 0,
+ totalPlaylistDuration: 0,
})
)
})
@@ -88,10 +89,13 @@ describe('rundown Timing Calculator', () => {
expect(result).toEqual(
literal({
isLowResolution: false,
- asDisplayedRundownDuration: 4000,
- asPlayedRundownDuration: 4000,
+ asDisplayedPlaylistDuration: 4000,
+ asPlayedPlaylistDuration: 4000,
currentPartWillAutoNext: false,
currentTime: 0,
+ rundownExpectedDurations: {
+ [rundownId]: 4000,
+ },
partCountdown: {
part1: 0,
part2: 1000,
@@ -134,8 +138,8 @@ describe('rundown Timing Calculator', () => {
part3: 2000,
part4: 3000,
},
- remainingRundownDuration: 4000,
- totalRundownDuration: 4000,
+ remainingPlaylistDuration: 4000,
+ totalPlaylistDuration: 4000,
})
)
})
@@ -158,10 +162,13 @@ describe('rundown Timing Calculator', () => {
expect(result).toEqual(
literal({
isLowResolution: false,
- asDisplayedRundownDuration: 4000,
- asPlayedRundownDuration: 4000,
+ asDisplayedPlaylistDuration: 4000,
+ asPlayedPlaylistDuration: 4000,
currentPartWillAutoNext: false,
currentTime: 0,
+ rundownExpectedDurations: {
+ [rundownId]: 4000,
+ },
partCountdown: {
part1: 0,
part2: 1000,
@@ -204,8 +211,8 @@ describe('rundown Timing Calculator', () => {
part3: 2000,
part4: 3000,
},
- remainingRundownDuration: 4000,
- totalRundownDuration: 4000,
+ remainingPlaylistDuration: 4000,
+ totalPlaylistDuration: 4000,
})
)
})
@@ -229,10 +236,14 @@ describe('rundown Timing Calculator', () => {
expect(result).toEqual(
literal({
isLowResolution: false,
- asDisplayedRundownDuration: 4000,
- asPlayedRundownDuration: 4000,
+ asDisplayedPlaylistDuration: 4000,
+ asPlayedPlaylistDuration: 4000,
currentPartWillAutoNext: false,
currentTime: 0,
+ rundownExpectedDurations: {
+ [rundownId1]: 2000,
+ [rundownId2]: 2000,
+ },
partCountdown: {
part1: 0,
part2: 1000,
@@ -275,8 +286,8 @@ describe('rundown Timing Calculator', () => {
part3: 2000,
part4: 3000,
},
- remainingRundownDuration: 4000,
- totalRundownDuration: 4000,
+ remainingPlaylistDuration: 4000,
+ totalPlaylistDuration: 4000,
})
)
})
@@ -323,10 +334,13 @@ describe('rundown Timing Calculator', () => {
expect(result).toEqual(
literal({
isLowResolution: false,
- asDisplayedRundownDuration: 4000,
- asPlayedRundownDuration: 4000,
+ asDisplayedPlaylistDuration: 4000,
+ asPlayedPlaylistDuration: 4000,
currentPartWillAutoNext: false,
currentTime: 0,
+ rundownExpectedDurations: {
+ [rundownId1]: 4000,
+ },
partCountdown: {
part1: 0,
part2: 2000,
@@ -369,8 +383,8 @@ describe('rundown Timing Calculator', () => {
part3: 2000,
part4: 3000,
},
- remainingRundownDuration: 4000,
- totalRundownDuration: 4000,
+ remainingPlaylistDuration: 4000,
+ totalPlaylistDuration: 4000,
})
)
})
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index 1bec59d69e..d60bdcf2de 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -44,6 +44,9 @@ export class RundownTimingCalculator {
let startsAtAccumulator = 0
let displayStartsAtAccumulator = 0
+ let rundownExpectedDurations: Record = {}
+ let rundownAsPlayedDurations: Record = {}
+
Object.keys(this.displayDurationGroups).forEach((key) => delete this.displayDurationGroups[key])
this.linearParts.length = 0
@@ -176,12 +179,25 @@ export class RundownTimingCalculator {
// Parts that are Untimed are ignored always.
// Parts that don't count are ignored, unless they are being played or have been played.
if (!partIsUntimed) {
+ let valToAddToAsPlayedDuration = 0
+
if (lastStartedPlayback && !partInstance.timings?.duration) {
- asPlayedRundownDuration += Math.max(partExpectedDuration, now - lastStartedPlayback)
+ valToAddToAsPlayedDuration = Math.max(partExpectedDuration, now - lastStartedPlayback)
} else if (partInstance.timings?.duration) {
- asPlayedRundownDuration += partInstance.timings.duration
+ valToAddToAsPlayedDuration = partInstance.timings.duration
} else if (partCounts) {
- asPlayedRundownDuration += partInstance.part.expectedDuration || 0
+ valToAddToAsPlayedDuration = partInstance.part.expectedDuration || 0
+ }
+
+ asPlayedRundownDuration += valToAddToAsPlayedDuration
+ if (!rundownAsPlayedDurations[unprotectString(partInstance.part.rundownId)]) {
+ rundownAsPlayedDurations[
+ unprotectString(partInstance.part.rundownId)
+ ] = valToAddToAsPlayedDuration
+ } else {
+ rundownAsPlayedDurations[
+ unprotectString(partInstance.part.rundownId)
+ ] += valToAddToAsPlayedDuration
}
}
@@ -256,6 +272,12 @@ export class RundownTimingCalculator {
) {
remainingRundownDuration += partExpectedDuration - (now - lastStartedPlayback)
}
+
+ if (!rundownExpectedDurations[unprotectString(partInstance.part.rundownId)]) {
+ rundownExpectedDurations[unprotectString(partInstance.part.rundownId)] = partExpectedDuration
+ } else {
+ rundownExpectedDurations[unprotectString(partInstance.part.rundownId)] += partExpectedDuration
+ }
})
// This is where the waitAccumulator-generated data in the linearSegLines is used to calculate the countdowns.
@@ -320,11 +342,13 @@ export class RundownTimingCalculator {
)
}
- return literal({
- totalRundownDuration,
- remainingRundownDuration,
- asDisplayedRundownDuration,
- asPlayedRundownDuration,
+ return literal({
+ totalPlaylistDuration: totalRundownDuration,
+ remainingPlaylistDuration: remainingRundownDuration,
+ asDisplayedPlaylistDuration: asDisplayedRundownDuration,
+ asPlayedPlaylistDuration: asPlayedRundownDuration,
+ rundownExpectedDurations,
+ rundownAsPlayedDurations,
partCountdown: _.object(this.linearParts),
partDurations: this.partDurations,
partPlayed: this.partPlayed,
@@ -361,75 +385,49 @@ export class RundownTimingCalculator {
}
}
-export namespace RundownTiming {
- /**
- * Events used by the RundownTimingProvider
- * @export
- * @enum {number}
+export interface RundownTimingContext {
+ /** This is the total duration of the palylist as planned (using expectedDurations). */
+ totalPlaylistDuration?: number
+ /** This is the content remaining to be played in the playlist (based on the expectedDurations). */
+ remainingPlaylistDuration?: number
+ /** This is the total duration of the playlist: as planned for the unplayed (skipped & future) content, and as-run for the played-out. */
+ asDisplayedPlaylistDuration?: number
+ /** This is the complete duration of the playlist: as planned for the unplayed content, and as-run for the played-out, but ignoring unplayed/unplayable parts in order */
+ asPlayedPlaylistDuration?: number
+ /** Expected duration of each rundown in playlist (based on part expected durations) */
+ rundownExpectedDurations?: Record
+ /** This is the complete duration of each rundown: as planned for the unplayed content, and as-run for the played-out, but ignoring unplayed/unplayable parts in order */
+ rundownAsPlayedDurations?: Record
+ /** this is the countdown to each of the parts relative to the current on air part. */
+ partCountdown?: Record
+ /** The calculated durations of each of the Parts: as-planned/as-run depending on state. */
+ partDurations?: Record
+ /** The offset of each of the Parts from the beginning of the Playlist. */
+ partStartsAt?: Record
+ /** Same as partStartsAt, but will include display duration overrides
+ * (such as minimal display width for an Part, etc.).
*/
- export enum Events {
- /** Event is emitted every now-and-then, generally to be used for simple displays */
- 'timeupdate' = 'sofie:rundownTimeUpdate',
- /** event is emitted with a very high frequency (60 Hz), to be used sparingly as
- * hooking up Components to it will cause a lot of renders
- */
- 'timeupdateHR' = 'sofie:rundownTimeUpdateHR',
- }
-
- /**
- * Context object that will be passed to listening components. The dictionaries use the Part ID as a key.
- * @export
- * @interface RundownTimingContext
+ partDisplayStartsAt?: Record
+ /** Same as partDurations, but will include display duration overrides
+ * (such as minimal display width for an Part, etc.).
*/
- export interface RundownTimingContext {
- /** This is the total duration of the rundown as planned (using expectedDurations). */
- totalRundownDuration?: number
- /** This is the content remaining to be played in the rundown (based on the expectedDurations). */
- remainingRundownDuration?: number
- /** This is the total duration of the rundown: as planned for the unplayed (skipped & future) content, and as-run for the played-out. */
- asDisplayedRundownDuration?: number
- /** This is the complete duration of the rundown: as planned for the unplayed content, and as-run for the played-out, but ignoring unplayed/unplayable parts in order */
- asPlayedRundownDuration?: number
- /** this is the countdown to each of the parts relative to the current on air part. */
- partCountdown?: Record
- /** The calculated durations of each of the Parts: as-planned/as-run depending on state. */
- partDurations?: Record
- /** The offset of each of the Parts from the beginning of the Rundown. */
- partStartsAt?: Record
- /** Same as partStartsAt, but will include display duration overrides
- * (such as minimal display width for an Part, etc.).
- */
- partDisplayStartsAt?: Record
- /** Same as partDurations, but will include display duration overrides
- * (such as minimal display width for an Part, etc.).
- */
- partDisplayDurations?: Record
- /** As-played durations of each part. Will be 0, if not yet played.
- * Will be counted from start to now if currently playing.
- */
- partPlayed?: Record
- /** Expected durations of each of the parts or the as-played duration,
- * if the Part does not have an expected duration.
- */
- partExpectedDurations?: Record
- /** Remaining time on current part */
- remainingTimeOnCurrentPart?: number | undefined
- /** Current part will autoNext */
- currentPartWillAutoNext?: boolean
- /** Current time of this calculation */
- currentTime?: number
- /** Was this time context calculated during a high-resolution tick */
- isLowResolution: boolean
- }
-
- /**
- * This are the properties that will be injected by the withTiming HOC.
- * @export
- * @interface InjectedROTimingProps
+ partDisplayDurations?: Record
+ /** As-played durations of each part. Will be 0, if not yet played.
+ * Will be counted from start to now if currently playing.
*/
- export interface InjectedROTimingProps {
- timingDurations: RundownTimingContext
- }
+ partPlayed?: Record
+ /** Expected durations of each of the parts or the as-played duration,
+ * if the Part does not have an expected duration.
+ */
+ partExpectedDurations?: Record
+ /** Remaining time on current part */
+ remainingTimeOnCurrentPart?: number | undefined
+ /** Current part will autoNext */
+ currentPartWillAutoNext?: boolean
+ /** Current time of this calculation */
+ currentTime?: number
+ /** Was this time context calculated during a high-resolution tick */
+ isLowResolution: boolean
}
/**
@@ -440,7 +438,7 @@ export namespace RundownTiming {
* @return number
*/
export function computeSegmentDuration(
- timingDurations: RundownTiming.RundownTimingContext,
+ timingDurations: RundownTimingContext,
partIds: PartId[],
display?: boolean
): number {
diff --git a/packages/blueprints-integration/src/rundown.ts b/packages/blueprints-integration/src/rundown.ts
index 845e6ea758..9136908005 100644
--- a/packages/blueprints-integration/src/rundown.ts
+++ b/packages/blueprints-integration/src/rundown.ts
@@ -42,7 +42,11 @@ export interface IBlueprintRundown {
/** A hint to the Core that the Rundown should be a part of a playlist */
playlistExternalId?: string
+
+ /** Whether the end of the rundown marks a commercial break */
+ endIsBreak?: boolean
}
+
/** The Rundown sent from Core */
export interface IBlueprintRundownDB
extends IBlueprintRundown,
From d6262b103d17f9f7dc6ddd6874d5140ed25b1987 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 15 Jun 2021 11:20:19 +0100
Subject: [PATCH 006/112] fix: Rebase fixes
---
.../ui/RundownView/RundownTiming/RundownTiming.ts | 9 +++++++++
.../ui/RundownView/RundownTiming/withTiming.tsx | 1 -
.../client/ui/SegmentTimeline/SegmentTimeline.tsx | 2 ++
.../ui/SegmentTimeline/SegmentTimelineContainer.tsx | 13 ++++++++-----
.../ui/SegmentTimeline/SegmentTimelinePart.tsx | 5 +++--
meteor/lib/rundown/__tests__/rundownTiming.test.ts | 12 ++++++------
meteor/lib/rundown/rundownTiming.ts | 12 +++++-------
7 files changed, 33 insertions(+), 21 deletions(-)
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts b/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
index 04482ae51b..94f03d0b57 100644
--- a/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
@@ -1,4 +1,6 @@
import { RundownTimingContext } from '../../../../lib/rundown/rundownTiming'
+import { PartUi } from '../../SegmentTimeline/SegmentTimelineContainer'
+import { SegmentTimelinePartClass } from '../../SegmentTimeline/SegmentTimelinePart'
export interface TimeEventArgs {
currentTime: number
@@ -37,3 +39,10 @@ export namespace RundownTiming {
timingDurations: RundownTimingContext
}
}
+
+export function computeSegmentDisplayDuration(timingDurations: RundownTimingContext, parts: PartUi[]): number {
+ return parts.reduce(
+ (memo, part) => memo + SegmentTimelinePartClass.getPartDisplayDuration(part, timingDurations),
+ 0
+ )
+}
diff --git a/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
index e2e2865bdc..3d4aee7eff 100644
--- a/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
@@ -2,7 +2,6 @@ import * as React from 'react'
import * as PropTypes from 'prop-types'
import * as _ from 'underscore'
import { RundownTiming } from './RundownTiming'
-import { JsxEmit } from 'typescript'
import { RundownTimingContext } from '../../../../lib/rundown/rundownTiming'
export type TimingFilterFunction = (durations: RundownTimingContext) => any
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
index c92283f6fc..574b35923c 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
@@ -36,6 +36,8 @@ import RundownViewEventBus, { RundownViewEvents, HighlightEvent } from '../Rundo
import { ZoomInIcon, ZoomOutIcon, ZoomShowAll } from '../../lib/segmentZoomIcon'
import { RundownTimingContext } from '../../../lib/rundown/rundownTiming'
+import { PartInstanceId } from '../../../lib/collections/PartInstances'
+import { SegmentTimelineSmallPartFlag } from './SmallParts/SegmentTimelineSmallPartFlag'
interface IProps {
id: string
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
index de4ace347a..8f2b6ff9f6 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
@@ -7,7 +7,7 @@ import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/reac
import { Segments, SegmentId } from '../../../lib/collections/Segments'
import { Studio } from '../../../lib/collections/Studios'
import { SegmentTimeline, SegmentTimelineClass } from './SegmentTimeline'
-import { RundownTiming, TimingEvent } from '../RundownView/RundownTiming/RundownTiming'
+import { computeSegmentDisplayDuration, RundownTiming, TimingEvent } from '../RundownView/RundownTiming/RundownTiming'
import { UIStateStorage } from '../../lib/UIStateStorage'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import {
@@ -43,7 +43,10 @@ import RundownViewEventBus, {
import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper'
import { checkPieceContentStatus, getNoteTypeForPieceStatus, ScanInfoForPackages } from '../../../lib/mediaObjects'
import { getBasicNotesForSegment } from '../../../lib/rundownNotifications'
-import { computeSegmentDuration } from '../../../lib/rundown/rundownTiming'
+import { computeSegmentDuration, RundownTimingContext } from '../../../lib/rundown/rundownTiming'
+import { SegmentTimelinePartClass } from './SegmentTimelinePart'
+import { Piece, Pieces } from '../../../lib/collections/Pieces'
+import { RundownAPI } from '../../../lib/api/rundown'
export const SIMULATED_PLAYBACK_SOFT_MARGIN = 0
export const SIMULATED_PLAYBACK_HARD_MARGIN = 2500
@@ -723,7 +726,7 @@ export const SegmentTimelineContainer = translateWithTracker {
+ onGoToPartInner = (part: PartUi, timingDurations: RundownTimingContext, zoomInToFit?: boolean) => {
let newScale: number | undefined
let scrollLeft =
@@ -751,7 +754,7 @@ export const SegmentTimelineContainer = translateWithTracker {
if (this.props.segmentId === e.segmentId) {
- const timingDurations = this.context?.durations as RundownTiming.RundownTimingContext
+ const timingDurations = this.context?.durations as RundownTimingContext
const part = this.props.parts.find((part) => part.partId === e.partId)
if (part) {
@@ -762,7 +765,7 @@ export const SegmentTimelineContainer = translateWithTracker {
if (this.props.segmentId === e.segmentId) {
- const timingDurations = this.context?.durations as RundownTiming.RundownTimingContext
+ const timingDurations = this.context?.durations as RundownTimingContext
const part = this.props.parts.find((part) => part.instance._id === e.partInstanceId)
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelinePart.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelinePart.tsx
index 6da865e9b9..a0f02e850c 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelinePart.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelinePart.tsx
@@ -36,6 +36,7 @@ import { SegmentEnd } from '../../lib/ui/icons/segment'
import { getShowHiddenSourceLayers } from '../../lib/localStorage'
import { Part } from '../../../lib/collections/Parts'
import { TFunction } from 'i18next'
+import { RundownTimingContext } from '../../../lib/rundown/rundownTiming'
export const SegmentTimelineLineElementId = 'rundown__segment__line__'
export const SegmentTimelinePartElementId = 'rundown__segment__part__'
@@ -688,7 +689,7 @@ export class SegmentTimelinePartClass extends React.Component((props: IProps) => {
return {
isHighResolution: false,
- filter: (durations: RundownTiming.RundownTimingContext) => {
+ filter: (durations: RundownTimingContext) => {
durations = durations || {}
const partId = unprotectString(props.part.instance.part._id)
diff --git a/meteor/lib/rundown/__tests__/rundownTiming.test.ts b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
index 2e923b6c0a..434b529382 100644
--- a/meteor/lib/rundown/__tests__/rundownTiming.test.ts
+++ b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
@@ -2,7 +2,7 @@ import { PartInstance } from '../../collections/PartInstances'
import { DBPart, Part, PartId } from '../../collections/Parts'
import { DBRundownPlaylist, RundownPlaylist } from '../../collections/RundownPlaylists'
import { literal, protectString } from '../../lib'
-import { RundownTiming, RundownTimingCalculator } from '../rundownTiming'
+import { RundownTimingCalculator, RundownTimingContext } from '../rundownTiming'
const DEFAULT_DURATION = 4000
@@ -51,7 +51,7 @@ describe('rundown Timing Calculator', () => {
const partInstancesMap: Map = new Map()
const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
expect(result).toEqual(
- literal({
+ literal({
isLowResolution: false,
asDisplayedPlaylistDuration: 0,
asPlayedPlaylistDuration: 0,
@@ -87,7 +87,7 @@ describe('rundown Timing Calculator', () => {
const partInstancesMap: Map = new Map()
const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
expect(result).toEqual(
- literal({
+ literal({
isLowResolution: false,
asDisplayedPlaylistDuration: 4000,
asPlayedPlaylistDuration: 4000,
@@ -160,7 +160,7 @@ describe('rundown Timing Calculator', () => {
const partInstancesMap: Map = new Map()
const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
expect(result).toEqual(
- literal({
+ literal({
isLowResolution: false,
asDisplayedPlaylistDuration: 4000,
asPlayedPlaylistDuration: 4000,
@@ -234,7 +234,7 @@ describe('rundown Timing Calculator', () => {
const partInstancesMap: Map = new Map()
const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
expect(result).toEqual(
- literal({
+ literal({
isLowResolution: false,
asDisplayedPlaylistDuration: 4000,
asPlayedPlaylistDuration: 4000,
@@ -332,7 +332,7 @@ describe('rundown Timing Calculator', () => {
const partInstancesMap: Map = new Map()
const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
expect(result).toEqual(
- literal({
+ literal({
isLowResolution: false,
asDisplayedPlaylistDuration: 4000,
asPlayedPlaylistDuration: 4000,
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index d60bdcf2de..ec563e5070 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -191,13 +191,11 @@ export class RundownTimingCalculator {
asPlayedRundownDuration += valToAddToAsPlayedDuration
if (!rundownAsPlayedDurations[unprotectString(partInstance.part.rundownId)]) {
- rundownAsPlayedDurations[
- unprotectString(partInstance.part.rundownId)
- ] = valToAddToAsPlayedDuration
+ rundownAsPlayedDurations[unprotectString(partInstance.part.rundownId)] =
+ valToAddToAsPlayedDuration
} else {
- rundownAsPlayedDurations[
- unprotectString(partInstance.part.rundownId)
- ] += valToAddToAsPlayedDuration
+ rundownAsPlayedDurations[unprotectString(partInstance.part.rundownId)] +=
+ valToAddToAsPlayedDuration
}
}
@@ -433,7 +431,7 @@ export interface RundownTimingContext {
/**
* Computes the actual (as-played fallbacking to expected) duration of a segment, consisting of given parts
* @export
- * @param {RundownTiming.RundownTimingContext} timingDurations The timing durations calculated for the Rundown
+ * @param {RundownTimingContext} timingDurations The timing durations calculated for the Rundown
* @param {Array} partIds The IDs of parts that are members of the segment
* @return number
*/
From 79bd1895b3dfa56c55ace3e124915ad3b2d5bee2 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 15 Jun 2021 14:37:21 +0100
Subject: [PATCH 007/112] feat: Settings for header customisations
---
meteor/client/ui/RundownView.tsx | 16 +++--
.../RundownHeaderLayoutSettings.tsx | 65 ++++++++++++++++++-
meteor/lib/collections/RundownLayouts.ts | 6 ++
3 files changed, 79 insertions(+), 8 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 4484796032..6baad08b8d 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -360,18 +360,20 @@ const TimingDisplay = withTranslation()(
{!rundownPlaylist.startedPlayback ||
this.props.isLastRundownInPlaylist ||
- !currentRundown?.endIsBreak ? ( // TODO: Setting
+ !(currentRundown?.endIsBreak && this.props.layout?.hideExpectedEndBeforeBreak) ? (
) : null}
{rundownPlaylist.startedPlayback &&
currentRundown?.endIsBreak &&
- !this.props.isLastRundownInPlaylist ? ( // TODO: Setting // TODO: Find next break in higher-order component, so next breaks reflects next rundown in playlist marked as break.
-
+ this.props.layout?.showNextBreakTiming &&
+ !(this.props.isLastRundownInPlaylist && this.props.layout.lastRundownIsNotBreak) ? ( // TODO: Find next break in higher-order component, so next breaks reflects next rundown in playlist marked as break.
+
) : null}
) : (
@@ -427,6 +429,7 @@ interface IEndTimingProps {
expectedStart?: number
expectedDuration: number
expectedEnd?: number
+ endLabel?: string
}
const PlaylistEndTiming = withTranslation()(
@@ -439,12 +442,12 @@ const PlaylistEndTiming = withTranslation()(
{!this.props.loop && this.props.expectedStart ? (
- {t('Planned End')}
+ {t(this.props.endLabel ?? 'Planned End')}
) : !this.props.loop && this.props.expectedEnd ? (
- {t('Planned End')}
+ {t(this.props.endLabel ?? 'Planned End')}
) : null}
@@ -494,6 +497,7 @@ const PlaylistEndTiming = withTranslation()(
interface INextBreakTimingProps {
loop?: boolean
breakRundown: Rundown
+ breakText?: string
}
const NextBreakTiming = withTranslation()(
@@ -509,7 +513,7 @@ const NextBreakTiming = withTranslation()(
return (
- {t('Next Break')}
+ {t(this.props.breakText ?? 'Next Break')}
{!this.props.loop && breakRundown.expectedEnd ? (
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
index 5fe708b4f6..c0fffd8cee 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
@@ -12,7 +12,7 @@ interface IProps {
interface IState {}
export default withTranslation()(
- class ShelfLayoutSettings extends MeteorReactComponent, IState> {
+ class RundownHeaderLayoutSettings extends MeteorReactComponent, IState> {
render() {
const { t } = this.props
@@ -20,7 +20,7 @@ export default withTranslation()(
- {t('End of Show Text')}
+ {t('Expected End text')}
+ {t('Text to show above countdown to end of show')}
+
+
+
+
+ {t('Hide Expected End timing when a break is next')}
+
+
+ {t('While there are still breaks coming up in the show, hide the Expected End timers')}
+
+
+
+
+
+ {t('Show next break timing')}
+
+ {t('Whether to show countdown to next break')}
+
+
+
+
+ {t('Last rundown is not break')}
+
+
+ {t("Don't treat the end of the last rundown in a playlist as a break")}
+
+
+
+
+
+ {t('Next Break text')}
+
+ {t('Text to show above countdown to next break')}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 6fb3ac4095..6f415b7cc3 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -193,6 +193,12 @@ export interface RundownLayoutRundownHeader extends RundownLayoutBase {
type: RundownLayoutType.RUNDOWN_HEADER_LAYOUT
expectedEndText: string
nextBreakText: string
+ /** When true, hide the Planned End timer when there is a rundown marked as a break in the future */
+ hideExpectedEndBeforeBreak: boolean
+ /** When a rundown is marked as a break, show the Next Break timing */
+ showNextBreakTiming: boolean
+ /** If true, don't treat the last rundown as a break even if it's marked as one */
+ lastRundownIsNotBreak: boolean
}
export enum ActionButtonType {
From 44328e4685b06159ea808f7be4d6e2b9f665e0c6 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 15 Jun 2021 16:15:29 +0100
Subject: [PATCH 008/112] feat: Look ahead to find rundown marked as next break
---
meteor/client/ui/RundownView.tsx | 74 +++++++++++++++++++++++++++-----
1 file changed, 63 insertions(+), 11 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 6baad08b8d..8a582f6b09 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -209,6 +209,8 @@ const WarningDisplay = withTranslation()(
interface ITimingDisplayProps {
rundownPlaylist: RundownPlaylist
currentRundown: Rundown | undefined
+ /** Rundowns between current rundown and rundown with next break (inclusive of both). Undefined if there's no break in the future. */
+ rundownsBeforeNextBreak: Rundown[] | undefined
rundownCount: number
isLastRundownInPlaylist: boolean
layout: RundownLayoutRundownHeader | undefined
@@ -360,7 +362,7 @@ const TimingDisplay = withTranslation()(
{!rundownPlaylist.startedPlayback ||
this.props.isLastRundownInPlaylist ||
- !(currentRundown?.endIsBreak && this.props.layout?.hideExpectedEndBeforeBreak) ? (
+ !(this.props.rundownsBeforeNextBreak && this.props.layout?.hideExpectedEndBeforeBreak) ? (
) : null}
{rundownPlaylist.startedPlayback &&
- currentRundown?.endIsBreak &&
+ this.props.rundownsBeforeNextBreak &&
this.props.layout?.showNextBreakTiming &&
- !(this.props.isLastRundownInPlaylist && this.props.layout.lastRundownIsNotBreak) ? ( // TODO: Find next break in higher-order component, so next breaks reflects next rundown in playlist marked as break.
-
+ !(this.props.isLastRundownInPlaylist && this.props.layout.lastRundownIsNotBreak) ? (
+
) : null}
) : (
@@ -496,7 +501,7 @@ const PlaylistEndTiming = withTranslation()(
interface INextBreakTimingProps {
loop?: boolean
- breakRundown: Rundown
+ rundownsBeforeBreak: Rundown[]
breakText?: string
}
@@ -504,12 +509,27 @@ const NextBreakTiming = withTranslation()(
withTiming()(
class PlaylistEndTiming extends React.Component>> {
render() {
- let { t, breakRundown } = this.props
+ let { t, rundownsBeforeBreak } = this.props
+ let breakRundown = rundownsBeforeBreak.length ? rundownsBeforeBreak[rundownsBeforeBreak.length - 1] : undefined
const rundownAsPlayedDuration = this.props.timingDurations.rundownAsPlayedDurations
- ? this.props.timingDurations.rundownAsPlayedDurations[unprotectString(breakRundown._id)]
+ ? rundownsBeforeBreak.reduce(
+ (prev, curr) => (prev += this.props.timingDurations.rundownAsPlayedDurations![unprotectString(curr._id)]),
+ 0
+ )
+ : undefined
+
+ const accumulatedExpectedDurations = this.props.timingDurations.rundownExpectedDurations
+ ? rundownsBeforeBreak.reduce(
+ (prev, curr) => (prev += this.props.timingDurations.rundownExpectedDurations![unprotectString(curr._id)]),
+ 0
+ )
: undefined
+ if (!breakRundown) {
+ return null
+ }
+
return (
@@ -521,16 +541,16 @@ const NextBreakTiming = withTranslation()(
{RundownUtils.formatDiffToTimecode(getCurrentTime() - breakRundown.expectedEnd, true, true, true)}
) : null}
- {breakRundown.expectedDuration ? (
+ {accumulatedExpectedDurations ? (
(breakRundown.expectedDuration || 0),
+ heavy: (rundownAsPlayedDuration || 0) < (accumulatedExpectedDurations || 0),
+ light: (rundownAsPlayedDuration || 0) > (accumulatedExpectedDurations || 0),
})}
>
{t('Diff')}
{RundownUtils.formatDiffToTimecode(
- (rundownAsPlayedDuration || 0) - breakRundown.expectedDuration,
+ (rundownAsPlayedDuration || 0) - accumulatedExpectedDurations,
true,
false,
true,
@@ -558,6 +578,8 @@ interface IRundownHeaderProps {
currentRundown: Rundown | undefined
studio: Studio
rundownIds: RundownId[]
+ /** Rundowns between current rundown and rundown with next break (inclusive of both). Undefined if there's no break in the future. */
+ rundownsBeforeNextBreak: Rundown[] | undefined
firstRundown: Rundown | undefined
onActivate?: (isRehearsal: boolean) => void
onRegisterHotkeys?: (hotkeys: Array) => void
@@ -1451,6 +1473,7 @@ const RundownHeader = withTranslation()(
((
}
}
+ private getRundownsBeforeNextBreak(
+ currentRundown: Rundown | undefined,
+ breakRundowns: Rundown[]
+ ): Rundown[] | undefined {
+ if (!currentRundown) {
+ return undefined
+ }
+
+ let currentRundownIndex = this.props.rundowns.findIndex((r) => r._id === currentRundown._id)
+
+ if (currentRundownIndex === -1) {
+ return undefined
+ }
+
+ let nextBreakIndex = this.props.rundowns.findIndex((rundown, index) => {
+ if (index < currentRundownIndex) {
+ return false
+ }
+
+ return breakRundowns.some((r) => r._id == rundown._id)
+ })
+
+ return this.props.rundowns.slice(currentRundownIndex, nextBreakIndex + 1)
+ }
+
render() {
const { t } = this.props
@@ -2868,6 +2916,10 @@ export const RundownView = translateWithTracker((
playlist={this.props.playlist}
studio={this.props.studio}
rundownIds={this.props.rundowns.map((r) => r._id)}
+ rundownsBeforeNextBreak={this.getRundownsBeforeNextBreak(
+ this.state.currentRundown || this.props.rundowns[0],
+ this.props.rundowns.filter((r) => r.endIsBreak)
+ )}
firstRundown={this.props.rundowns[0]}
onActivate={this.onActivate}
studioMode={this.state.studioMode}
From 8b537b38c6a2eb69cef91fe776f6fe8906657d04 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 16 Jun 2021 10:22:04 +0100
Subject: [PATCH 009/112] fix: Blank timer labels
---
meteor/client/ui/RundownView.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 8a582f6b09..1543e1d365 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -447,12 +447,12 @@ const PlaylistEndTiming = withTranslation()(
{!this.props.loop && this.props.expectedStart ? (
- {t(this.props.endLabel ?? 'Planned End')}
+ {t(this.props.endLabel || 'Planned End')}
) : !this.props.loop && this.props.expectedEnd ? (
- {t(this.props.endLabel ?? 'Planned End')}
+ {t(this.props.endLabel || 'Planned End')}
) : null}
@@ -533,7 +533,7 @@ const NextBreakTiming = withTranslation()(
return (
- {t(this.props.breakText ?? 'Next Break')}
+ {t(this.props.breakText || 'Next Break')}
{!this.props.loop && breakRundown.expectedEnd ? (
From ffd1257346b3d783c58944895e2c4c8bbf0db51d Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 16 Jun 2021 10:32:51 +0100
Subject: [PATCH 010/112] fix: Check for last break
---
meteor/client/ui/RundownView.tsx | 29 +++++++++++++++++++----------
1 file changed, 19 insertions(+), 10 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 1543e1d365..5dc195c1af 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -212,7 +212,8 @@ interface ITimingDisplayProps {
/** Rundowns between current rundown and rundown with next break (inclusive of both). Undefined if there's no break in the future. */
rundownsBeforeNextBreak: Rundown[] | undefined
rundownCount: number
- isLastRundownInPlaylist: boolean
+ /** Whether the next break is also the last. */
+ lastBreak: boolean
layout: RundownLayoutRundownHeader | undefined
}
@@ -361,7 +362,7 @@ const TimingDisplay = withTranslation()(
{rundownPlaylist.expectedDuration ? (
{!rundownPlaylist.startedPlayback ||
- this.props.isLastRundownInPlaylist ||
+ this.props.lastBreak ||
!(this.props.rundownsBeforeNextBreak && this.props.layout?.hideExpectedEndBeforeBreak) ? (
void
onRegisterHotkeys?: (hotkeys: Array) => void
@@ -1475,7 +1478,7 @@ const RundownHeader = withTranslation()(
currentRundown={this.props.currentRundown}
rundownsBeforeNextBreak={this.props.rundownsBeforeNextBreak}
rundownCount={this.props.rundownIds.length}
- isLastRundownInPlaylist={
+ lastBreak={
!!this.props.currentRundown?._id &&
this.props.rundownIds.indexOf(this.props.currentRundown._id) === this.props.rundownIds.length - 1
}
@@ -2764,7 +2767,7 @@ export const RundownView = translateWithTracker((
private getRundownsBeforeNextBreak(
currentRundown: Rundown | undefined,
breakRundowns: Rundown[]
- ): Rundown[] | undefined {
+ ): { rundownsBeforeNextBreak: Rundown[]; breakIsLastRundown } | undefined {
if (!currentRundown) {
return undefined
}
@@ -2783,7 +2786,10 @@ export const RundownView = translateWithTracker((
return breakRundowns.some((r) => r._id == rundown._id)
})
- return this.props.rundowns.slice(currentRundownIndex, nextBreakIndex + 1)
+ return {
+ rundownsBeforeNextBreak: this.props.rundowns.slice(currentRundownIndex, nextBreakIndex + 1),
+ breakIsLastRundown: nextBreakIndex === this.props.rundowns.length,
+ }
}
render() {
@@ -2798,6 +2804,11 @@ export const RundownView = translateWithTracker((
this.props.rundowns.find((r) => r._id === selectedPiece?.instance.rundownId)) ||
undefined
+ let breakProps = this.getRundownsBeforeNextBreak(
+ this.state.currentRundown || this.props.rundowns[0],
+ this.props.rundowns.filter((r) => r.endIsBreak)
+ )
+
return (
@@ -2916,10 +2927,8 @@ export const RundownView = translateWithTracker((
playlist={this.props.playlist}
studio={this.props.studio}
rundownIds={this.props.rundowns.map((r) => r._id)}
- rundownsBeforeNextBreak={this.getRundownsBeforeNextBreak(
- this.state.currentRundown || this.props.rundowns[0],
- this.props.rundowns.filter((r) => r.endIsBreak)
- )}
+ rundownsBeforeNextBreak={breakProps?.rundownsBeforeNextBreak}
+ lastBreak={breakProps?.breakIsLastRundown}
firstRundown={this.props.rundowns[0]}
onActivate={this.onActivate}
studioMode={this.state.studioMode}
From 1e93b78cb2a8a15f82f9f285ffef92f9e21ac587 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 21 Jun 2021 15:31:13 +0100
Subject: [PATCH 011/112] wip: Top bar dashboard
---
meteor/client/ui/RundownView.tsx | 71 ++++++++++++++-----
.../ui/Settings/RundownLayoutEditor.tsx | 1 +
.../ui/Settings/components/FilterEditor.tsx | 5 +-
.../RundownHeaderLayoutSettings.tsx | 6 +-
meteor/lib/api/rundownLayouts.ts | 17 ++---
5 files changed, 70 insertions(+), 30 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 7879e5b87f..926d2601b8 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -102,6 +102,9 @@ import RundownViewEventBus, { RundownViewEvents } from './RundownView/RundownVie
import { LoopingIcon } from '../lib/ui/icons/looping'
import StudioPackageContainersContext from './RundownView/StudioPackageContainersContext'
import { RundownLayoutsAPI } from '../../lib/api/rundownLayouts'
+import { ShelfDashboardLayout } from './Shelf/ShelfDashboardLayout'
+import { IAdLibListItem } from './Shelf/AdLibListItem'
+import { BucketAdLibItem } from './Shelf/RundownViewBuckets'
export const MAGIC_TIME_SCALE_FACTOR = 0.03
@@ -574,6 +577,7 @@ interface HotkeyDefinition {
interface IRundownHeaderProps {
playlist: RundownPlaylist
+ showStyleBase: ShowStyleBase
currentRundown: Rundown | undefined
studio: Studio
rundownIds: RundownId[]
@@ -592,6 +596,8 @@ interface IRundownHeaderProps {
interface IRundownHeaderState {
isError: boolean
errorMessage?: string
+ shouldQueue: boolean
+ selectedPiece: BucketAdLibItem | IAdLibListItem | PieceUi | undefined
}
const RundownHeader = withTranslation()(
@@ -720,6 +726,8 @@ const RundownHeader = withTranslation()(
}
this.state = {
isError: false,
+ shouldQueue: false,
+ selectedPiece: undefined,
}
}
componentDidMount() {
@@ -1376,6 +1384,18 @@ const RundownHeader = withTranslation()(
})
}
+ changeQueueAdLib = (shouldQueue: boolean) => {
+ this.setState({
+ shouldQueue,
+ })
+ }
+
+ selectPiece = (piece: BucketAdLibItem | IAdLibListItem | PieceUi | undefined) => {
+ this.setState({
+ selectedPiece: piece,
+ })
+ }
+
render() {
const { t } = this.props
return (
@@ -1471,23 +1491,40 @@ const RundownHeader = withTranslation()(
-
-
+ {this.props.layout && RundownLayoutsAPI.isDashboardLayout(this.props.layout) ? (
+
+ ) : (
+
+
+
+
+ )}
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index b6f503ee66..e7af9a3025 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -352,6 +352,7 @@ export default translateWithTracker((props: IProp
filter={tab}
index={index}
showStyleBase={this.props.showStyleBase}
+ supportedElements={layout?.supportedElements ?? []}
/>
))}
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index 3810825680..ca07ffa4ee 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -28,6 +28,7 @@ interface IProps {
filter: RundownLayoutElementBase
index: number
showStyleBase: ShowStyleBase
+ supportedElements: RundownLayoutElementType[]
}
interface ITrackedProps {}
@@ -1029,9 +1030,9 @@ export default translateWithTracker((props: IProp
modifiedClassName="bghl"
attribute={`filters.${this.props.index}.type`}
obj={this.props.item}
- options={RundownLayoutElementType}
+ options={this.props.supportedElements}
type="dropdown"
- mutateDisplayValue={(v) => (v === undefined ? RundownLayoutElementType.FILTER : v)}
+ mutateDisplayValue={(v) => (v === undefined ? this.props.supportedElements[0] : v)}
collection={RundownLayouts}
className="input text-input input-l"
>
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
index c0fffd8cee..5b9688b9fa 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
@@ -1,6 +1,6 @@
import React from 'react'
import { withTranslation } from 'react-i18next'
-import { RundownLayoutBase, RundownLayouts } from '../../../../../lib/collections/RundownLayouts'
+import { RundownLayoutBase, RundownLayouts, RundownLayoutType } from '../../../../../lib/collections/RundownLayouts'
import { EditAttribute } from '../../../../lib/EditAttribute'
import { MeteorReactComponent } from '../../../../lib/MeteorReactComponent'
import { Translated } from '../../../../lib/ReactMeteorData/ReactMeteorData'
@@ -16,7 +16,7 @@ export default withTranslation()(
render() {
const { t } = this.props
- return (
+ return this.props.item.type === RundownLayoutType.RUNDOWN_HEADER_LAYOUT ? (
@@ -93,7 +93,7 @@ export default withTranslation()(
- )
+ ) : null
}
}
)
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 587b2e742f..efe7e55799 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -102,8 +102,7 @@ class RundownLayoutsRegistry {
return literal({
_id: layoutType,
type: layoutType,
- filtersTitle: descriptor.filtersTitle,
- supportedElements: descriptor.supportedElements,
+ ...descriptor,
})
}),
},
@@ -114,8 +113,7 @@ class RundownLayoutsRegistry {
return literal({
_id: layoutType,
type: layoutType,
- filtersTitle: descriptor.filtersTitle,
- supportedElements: descriptor.supportedElements,
+ ...descriptor,
})
}),
},
@@ -126,8 +124,7 @@ class RundownLayoutsRegistry {
return literal({
_id: layoutType,
type: layoutType,
- filtersTitle: descriptor.filtersTitle,
- supportedElements: descriptor.supportedElements,
+ ...descriptor,
})
}),
},
@@ -138,8 +135,7 @@ class RundownLayoutsRegistry {
return literal({
_id: layoutType,
type: layoutType,
- filtersTitle: descriptor.filtersTitle,
- supportedElements: descriptor.supportedElements,
+ ...descriptor,
})
}),
},
@@ -181,6 +177,11 @@ export namespace RundownLayoutsAPI {
registry.RegisterRundownHeaderLayouts(RundownLayoutType.RUNDOWN_HEADER_LAYOUT, {
supportedElements: [],
})
+ registry.RegisterRundownHeaderLayouts(RundownLayoutType.DASHBOARD_LAYOUT, {
+ filtersTitle: 'Layout Elements',
+ supportsFilters: true,
+ supportedElements: [RundownLayoutElementType.PIECE_COUNTDOWN],
+ })
export function GetSettingsManifest(): CustomizableRegionSettingsManifest[] {
return registry.GetSettingsManifest()
From cecaead45babe0d9900918c8a4040fa6551834f7 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 21 Jun 2021 16:26:59 +0100
Subject: [PATCH 012/112] chore: Produce next break info inside
rundownTimingProvider
---
meteor/client/ui/RundownView.tsx | 60 +++----------------
.../RundownTiming/RundownTimingProvider.tsx | 15 ++++-
meteor/lib/rundown/rundownTiming.ts | 56 ++++++++++++++++-
3 files changed, 76 insertions(+), 55 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 5dc195c1af..1ac518d9be 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -209,11 +209,7 @@ const WarningDisplay = withTranslation()(
interface ITimingDisplayProps {
rundownPlaylist: RundownPlaylist
currentRundown: Rundown | undefined
- /** Rundowns between current rundown and rundown with next break (inclusive of both). Undefined if there's no break in the future. */
- rundownsBeforeNextBreak: Rundown[] | undefined
rundownCount: number
- /** Whether the next break is also the last. */
- lastBreak: boolean
layout: RundownLayoutRundownHeader | undefined
}
@@ -362,8 +358,10 @@ const TimingDisplay = withTranslation()(
{rundownPlaylist.expectedDuration ? (
{!rundownPlaylist.startedPlayback ||
- this.props.lastBreak ||
- !(this.props.rundownsBeforeNextBreak && this.props.layout?.hideExpectedEndBeforeBreak) ? (
+ this.props.timingDurations.breakIsLastRundown ||
+ !(
+ this.props.timingDurations.rundownsBeforeNextBreak && this.props.layout?.hideExpectedEndBeforeBreak
+ ) ? (
) : null}
{rundownPlaylist.startedPlayback &&
- this.props.rundownsBeforeNextBreak &&
+ this.props.timingDurations.rundownsBeforeNextBreak &&
this.props.layout?.showNextBreakTiming &&
- !(this.props.lastBreak && this.props.layout.lastRundownIsNotBreak) ? (
+ !(this.props.timingDurations.breakIsLastRundown && this.props.layout.lastRundownIsNotBreak) ? (
) : null}
@@ -579,10 +577,6 @@ interface IRundownHeaderProps {
currentRundown: Rundown | undefined
studio: Studio
rundownIds: RundownId[]
- /** Rundowns between current rundown and rundown with next break (inclusive of both). Undefined if there's no break in the future. */
- rundownsBeforeNextBreak: Rundown[] | undefined
- /** Whether the next break is also the last */
- lastBreak: boolean
firstRundown: Rundown | undefined
onActivate?: (isRehearsal: boolean) => void
onRegisterHotkeys?: (hotkeys: Array) => void
@@ -1476,12 +1470,7 @@ const RundownHeader = withTranslation()(
((
}
}
- private getRundownsBeforeNextBreak(
- currentRundown: Rundown | undefined,
- breakRundowns: Rundown[]
- ): { rundownsBeforeNextBreak: Rundown[]; breakIsLastRundown } | undefined {
- if (!currentRundown) {
- return undefined
- }
-
- let currentRundownIndex = this.props.rundowns.findIndex((r) => r._id === currentRundown._id)
-
- if (currentRundownIndex === -1) {
- return undefined
- }
-
- let nextBreakIndex = this.props.rundowns.findIndex((rundown, index) => {
- if (index < currentRundownIndex) {
- return false
- }
-
- return breakRundowns.some((r) => r._id == rundown._id)
- })
-
- return {
- rundownsBeforeNextBreak: this.props.rundowns.slice(currentRundownIndex, nextBreakIndex + 1),
- breakIsLastRundown: nextBreakIndex === this.props.rundowns.length,
- }
- }
-
render() {
const { t } = this.props
@@ -2804,11 +2765,6 @@ export const RundownView = translateWithTracker((
this.props.rundowns.find((r) => r._id === selectedPiece?.instance.rundownId)) ||
undefined
- let breakProps = this.getRundownsBeforeNextBreak(
- this.state.currentRundown || this.props.rundowns[0],
- this.props.rundowns.filter((r) => r.endIsBreak)
- )
-
return (
@@ -2927,8 +2883,6 @@ export const RundownView = translateWithTracker((
playlist={this.props.playlist}
studio={this.props.studio}
rundownIds={this.props.rundowns.map((r) => r._id)}
- rundownsBeforeNextBreak={breakProps?.rundownsBeforeNextBreak}
- lastBreak={breakProps?.breakIsLastRundown}
firstRundown={this.props.rundowns[0]}
onActivate={this.onActivate}
studioMode={this.state.studioMode}
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
index 0edb581023..c3cd181778 100644
--- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
@@ -10,6 +10,7 @@ import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists'
import { PartInstance } from '../../../../lib/collections/PartInstances'
import { RundownTiming, TimeEventArgs } from './RundownTiming'
import { RundownTimingCalculator, RundownTimingContext } from '../../../../lib/rundown/rundownTiming'
+import { Rundown } from '../../../../lib/collections/Rundowns'
const TIMING_DEFAULT_REFRESH_INTERVAL = 1000 / 60 // the interval for high-resolution events (timeupdateHR)
const LOW_RESOLUTION_TIMING_DECIMATOR = 15 // the low-resolution events will be called every
@@ -35,6 +36,8 @@ interface IRundownTimingProviderChildContext {
}
interface IRundownTimingProviderState {}
interface IRundownTimingProviderTrackedProps {
+ rundowns: Array
+ currentRundown: Rundown | undefined
parts: Array
partInstancesMap: Map
}
@@ -50,13 +53,19 @@ export const RundownTimingProvider = withTracker<
IRundownTimingProviderState,
IRundownTimingProviderTrackedProps
>((props) => {
+ let rundowns: Array = []
let parts: Array = []
const partInstancesMap = new Map()
+ let currentRundown: Rundown | undefined
if (props.playlist) {
+ rundowns = props.playlist.getRundowns()
const { parts: incomingParts } = props.playlist.getSegmentsAndPartsSync()
parts = incomingParts
const partInstances = props.playlist.getActivePartInstances()
+ const currentPartInstance = partInstances.find((p) => p._id === props.playlist!.currentPartInstanceId)
+ currentRundown = currentPartInstance ? rundowns.find((r) => r._id === currentPartInstance.rundownId) : rundowns[0]
+
partInstances.forEach((partInstance) => {
partInstancesMap.set(partInstance.part._id, partInstance)
@@ -96,6 +105,8 @@ export const RundownTimingProvider = withTracker<
})
}
return {
+ rundowns,
+ currentRundown,
parts,
partInstancesMap,
}
@@ -194,13 +205,15 @@ export const RundownTimingProvider = withTracker<
}
updateDurations(now: number, isLowResolution: boolean) {
- const { playlist, parts, partInstancesMap } = this.props
+ const { playlist, rundowns, currentRundown, parts, partInstancesMap } = this.props
this.durations = Object.assign(
this.durations,
this.timingCalculator.updateDurations(
now,
isLowResolution,
playlist,
+ rundowns,
+ currentRundown,
parts,
partInstancesMap,
this.props.defaultDuration
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index ec563e5070..cfdf09b7f8 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -6,6 +6,7 @@ import {
} from '../collections/PartInstances'
import { Part, PartId } from '../collections/Parts'
import { RundownPlaylist } from '../collections/RundownPlaylists'
+import { Rundown } from '../collections/Rundowns'
import { unprotectString, literal, protectString } from '../lib'
import { Settings } from '../Settings'
@@ -30,6 +31,8 @@ export class RundownTimingCalculator {
now: number,
isLowResolution: boolean,
playlist: RundownPlaylist | undefined,
+ rundowns: Rundown[],
+ currentRundown: Rundown | undefined,
parts: Part[],
partInstancesMap: Map,
/** Fallback duration for Parts that have no as-played duration of their own. */
@@ -47,13 +50,29 @@ export class RundownTimingCalculator {
let rundownExpectedDurations: Record = {}
let rundownAsPlayedDurations: Record = {}
+ let rundownsBeforeNextBreak: Rundown[] | undefined
+ let breakIsLastRundown: boolean | undefined
+
Object.keys(this.displayDurationGroups).forEach((key) => delete this.displayDurationGroups[key])
this.linearParts.length = 0
let nextAIndex = -1
let currentAIndex = -1
- if (playlist && parts) {
+ if (playlist) {
+ const breakProps = currentRundown
+ ? this.getRundownsBeforeNextBreak(
+ rundowns,
+ currentRundown,
+ rundowns.filter((r) => r.endIsBreak)
+ )
+ : undefined
+
+ if (breakProps) {
+ rundownsBeforeNextBreak = breakProps.rundownsBeforeNextBreak
+ breakIsLastRundown = breakProps.breakIsLastRundown
+ }
+
parts.forEach((origPart, itIndex) => {
const partInstance = this.getPartInstanceOrGetCachedTemp(partInstancesMap, origPart)
@@ -357,6 +376,8 @@ export class RundownTimingCalculator {
currentTime: now,
remainingTimeOnCurrentPart,
currentPartWillAutoNext,
+ rundownsBeforeNextBreak,
+ breakIsLastRundown,
isLowResolution,
})
}
@@ -381,6 +402,35 @@ export class RundownTimingCalculator {
}
}
}
+
+ private getRundownsBeforeNextBreak(
+ orderedRundowns: Rundown[],
+ currentRundown: Rundown | undefined,
+ breakRundowns: Rundown[]
+ ): { rundownsBeforeNextBreak: Rundown[]; breakIsLastRundown } | undefined {
+ if (!currentRundown) {
+ return undefined
+ }
+
+ let currentRundownIndex = orderedRundowns.findIndex((r) => r._id === currentRundown._id)
+
+ if (currentRundownIndex === -1) {
+ return undefined
+ }
+
+ let nextBreakIndex = orderedRundowns.findIndex((rundown, index) => {
+ if (index < currentRundownIndex) {
+ return false
+ }
+
+ return breakRundowns.some((r) => r._id == rundown._id)
+ })
+
+ return {
+ rundownsBeforeNextBreak: orderedRundowns.slice(currentRundownIndex, nextBreakIndex + 1),
+ breakIsLastRundown: nextBreakIndex === orderedRundowns.length,
+ }
+ }
}
export interface RundownTimingContext {
@@ -424,6 +474,10 @@ export interface RundownTimingContext {
currentPartWillAutoNext?: boolean
/** Current time of this calculation */
currentTime?: number
+ /** Rundowns between current rundown and rundown with next break (inclusive of both). Undefined if there's no break in the future. */
+ rundownsBeforeNextBreak?: Rundown[]
+ /** Whether the next break is also the last */
+ breakIsLastRundown?: boolean
/** Was this time context calculated during a high-resolution tick */
isLowResolution: boolean
}
From f4fd866524d65c822b2fc0518346873100a7e6e1 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Fri, 25 Jun 2021 11:54:11 +0100
Subject: [PATCH 013/112] feat: Endwords, Playlist start / end timer panels
---
meteor/client/styles/_header.scss | 5 +
meteor/client/styles/shelf/endTimerPanel.scss | 7 +
meteor/client/styles/shelf/endWordsPanel.scss | 22 +
.../client/styles/shelf/startTimerPanel.scss | 7 +
.../MicFloatingInspector.tsx | 24 +-
meteor/client/ui/RundownView.tsx | 347 ++---------
.../RundownTiming/NextBreakTiming.tsx | 78 +++
.../RundownTiming/PlaylistEndTiming.tsx | 132 +++++
.../RundownTiming/PlaylistStartTiming.tsx | 74 +++
.../RundownView/RundownTiming/RundownName.tsx | 98 ++++
.../RundownView/RundownTiming/TimeOfDay.tsx | 22 +
.../ui/Settings/components/FilterEditor.tsx | 545 ++++++++++++------
meteor/client/ui/Shelf/DashboardPanel.tsx | 35 +-
meteor/client/ui/Shelf/EndWordsPanel.tsx | 98 ++++
.../client/ui/Shelf/PlaylistEndTimerPanel.tsx | 72 +++
.../ui/Shelf/PlaylistStartTimerPanel.tsx | 60 ++
.../client/ui/Shelf/ShelfDashboardLayout.tsx | 9 +
meteor/client/ui/scriptPreview.ts | 32 +
meteor/lib/api/rundownLayouts.ts | 24 +-
meteor/lib/collections/RundownLayouts.ts | 48 ++
20 files changed, 1213 insertions(+), 526 deletions(-)
create mode 100644 meteor/client/styles/shelf/endTimerPanel.scss
create mode 100644 meteor/client/styles/shelf/endWordsPanel.scss
create mode 100644 meteor/client/styles/shelf/startTimerPanel.scss
create mode 100644 meteor/client/ui/RundownView/RundownTiming/NextBreakTiming.tsx
create mode 100644 meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx
create mode 100644 meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx
create mode 100644 meteor/client/ui/RundownView/RundownTiming/RundownName.tsx
create mode 100644 meteor/client/ui/RundownView/RundownTiming/TimeOfDay.tsx
create mode 100644 meteor/client/ui/Shelf/EndWordsPanel.tsx
create mode 100644 meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
create mode 100644 meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
create mode 100644 meteor/client/ui/scriptPreview.ts
diff --git a/meteor/client/styles/_header.scss b/meteor/client/styles/_header.scss
index 1e015f0243..38f060715f 100644
--- a/meteor/client/styles/_header.scss
+++ b/meteor/client/styles/_header.scss
@@ -25,6 +25,11 @@
margin-top: -0.5em;
}
}
+
+ .dashboard {
+ flex: 1 1;
+ position: unset;
+ }
}
.super-dark {
diff --git a/meteor/client/styles/shelf/endTimerPanel.scss b/meteor/client/styles/shelf/endTimerPanel.scss
new file mode 100644
index 0000000000..7ceda059b8
--- /dev/null
+++ b/meteor/client/styles/shelf/endTimerPanel.scss
@@ -0,0 +1,7 @@
+.playlist-end-time-panel {
+ position: absolute;
+
+ &.timing {
+ width: unset !important;
+ }
+}
diff --git a/meteor/client/styles/shelf/endWordsPanel.scss b/meteor/client/styles/shelf/endWordsPanel.scss
new file mode 100644
index 0000000000..a1c49da902
--- /dev/null
+++ b/meteor/client/styles/shelf/endWordsPanel.scss
@@ -0,0 +1,22 @@
+.end-words-panel {
+ padding: 1vw 1vh;
+ overflow: hidden;
+ position: absolute;
+
+ .row {
+ display: inline-block;
+ width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ .title {
+ font-weight: bold;
+ }
+
+ .text {
+ text-overflow: ellipsis;
+ text-align: left;
+ direction: rtl;
+ }
+}
diff --git a/meteor/client/styles/shelf/startTimerPanel.scss b/meteor/client/styles/shelf/startTimerPanel.scss
new file mode 100644
index 0000000000..c10006a5dc
--- /dev/null
+++ b/meteor/client/styles/shelf/startTimerPanel.scss
@@ -0,0 +1,7 @@
+.playlist-start-time-panel {
+ position: absolute;
+
+ &.timing {
+ width: unset !important;
+ }
+}
diff --git a/meteor/client/ui/FloatingInspectors/MicFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/MicFloatingInspector.tsx
index 94e7e210aa..f96bac0764 100644
--- a/meteor/client/ui/FloatingInspectors/MicFloatingInspector.tsx
+++ b/meteor/client/ui/FloatingInspectors/MicFloatingInspector.tsx
@@ -4,6 +4,7 @@ import Moment from 'react-moment'
import { FloatingInspector } from '../FloatingInspector'
import { ScriptContent } from '@sofie-automation/blueprints-integration'
+import { GetScriptPreview } from '../scriptPreview'
interface IProps {
typeClass?: string
@@ -14,31 +15,10 @@ interface IProps {
displayOn?: 'document' | 'viewport'
}
-const BREAK_SCRIPT_BREAKPOINT = 620
-const SCRIPT_PART_LENGTH = 250
-
export function MicFloatingInspector(props: IProps) {
const { t } = useTranslation()
- let startOfScript = (props.content && props.content.fullScript) || ''
- let cutLength = startOfScript.length
- if (startOfScript.length > SCRIPT_PART_LENGTH) {
- startOfScript = startOfScript.substring(0, startOfScript.substr(0, SCRIPT_PART_LENGTH).lastIndexOf(' '))
- cutLength = startOfScript.length
- }
- let endOfScript = (props.content && props.content.fullScript) || ''
- if (endOfScript.length > SCRIPT_PART_LENGTH) {
- endOfScript = endOfScript.substring(
- endOfScript.indexOf(' ', Math.max(cutLength, endOfScript.length - SCRIPT_PART_LENGTH)),
- endOfScript.length
- )
- }
-
- const breakScript = !!(
- props.content &&
- props.content.fullScript &&
- props.content.fullScript.length > BREAK_SCRIPT_BREAKPOINT
- )
+ let { startOfScript, endOfScript, breakScript } = GetScriptPreview(props.content.fullScript || '')
return (
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 4df8f335a3..9296b1ae6b 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -105,6 +105,11 @@ import { RundownLayoutsAPI } from '../../lib/api/rundownLayouts'
import { ShelfDashboardLayout } from './Shelf/ShelfDashboardLayout'
import { IAdLibListItem } from './Shelf/AdLibListItem'
import { BucketAdLibItem } from './Shelf/RundownViewBuckets'
+import { PlaylistEndTiming } from './RundownView/RundownTiming/PlaylistEndTiming'
+import { NextBreakTiming } from './RundownView/RundownTiming/NextBreakTiming'
+import { RundownName } from './RundownView/RundownTiming/RundownName'
+import { TimeOfDay } from './RundownView/RundownTiming/TimeOfDay'
+import { PlaylistStartTiming } from './RundownView/RundownTiming/PlaylistStartTiming'
export const MAGIC_TIME_SCALE_FACTOR = 0.03
@@ -243,40 +248,6 @@ export enum RundownViewKbdShortcuts {
const TimingDisplay = withTranslation()(
withTiming()(
class TimingDisplay extends React.Component>> {
- private renderRundownName() {
- const { rundownPlaylist, currentRundown, rundownCount, t } = this.props
- return currentRundown && (rundownPlaylist.name !== currentRundown.name || rundownCount > 1) ? (
-
- {rundownPlaylist.loop && } {currentRundown.name} {rundownPlaylist.name}
-
- ) : (
-
- {rundownPlaylist.loop && } {rundownPlaylist.name}
-
- )
- }
render() {
const { t, rundownPlaylist, currentRundown } = this.props
@@ -284,66 +255,13 @@ const TimingDisplay = withTranslation()(
return (
- {rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
-
- {t('Started')}
-
-
- ) : rundownPlaylist.expectedStart ? (
-
- {t('Planned Start')}
-
-
- ) : rundownPlaylist.expectedEnd && rundownPlaylist.expectedDuration ? (
-
- {t('Expected Start')}
-
-
- ) : null}
- {rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
- rundownPlaylist.expectedStart ? (
-
- {this.renderRundownName()}
- {RundownUtils.formatDiffToTimecode(
- rundownPlaylist.startedPlayback - rundownPlaylist.expectedStart,
- true,
- false,
- true,
- true,
- true
- )}
-
- ) : (
-
{this.renderRundownName()}
- )
- ) : (
- (rundownPlaylist.expectedStart ? (
-
rundownPlaylist.expectedStart,
- })}
- >
- {this.renderRundownName()}
- {RundownUtils.formatDiffToTimecode(
- getCurrentTime() - rundownPlaylist.expectedStart,
- true,
- false,
- true,
- true,
- true
- )}
-
- ) : (
-
{this.renderRundownName()}
- )) || undefined
- )}
-
-
-
+
+
+
{rundownPlaylist.currentPartInstanceId && (
)}
- {rundownPlaylist.expectedDuration ? (
-
- {!rundownPlaylist.startedPlayback ||
- this.props.timingDurations.breakIsLastRundown ||
- !(
- this.props.timingDurations.rundownsBeforeNextBreak && this.props.layout?.hideExpectedEndBeforeBreak
- ) ? (
-
- ) : null}
- {rundownPlaylist.startedPlayback &&
- this.props.timingDurations.rundownsBeforeNextBreak &&
- this.props.layout?.showNextBreakTiming &&
- !(this.props.timingDurations.breakIsLastRundown && this.props.layout.lastRundownIsNotBreak) ? (
-
- ) : null}
-
- ) : (
-
- {!rundownPlaylist.loop && this.props.timingDurations ? (
-
-
- {this.props.layout?.expectedEndText ? t(this.props.layout.expectedEndText) : t('Expected End')}
-
-
-
- ) : null}
- {this.props.timingDurations && this.props.rundownCount < 2 ? ( // TEMPORARY: disable the diff counter for playlists longer than one rundown -- Jan Starzak, 2021-05-06
-
- (this.props.timingDurations.totalPlaylistDuration || 0),
- })}
- >
- {t('Diff')}
- {RundownUtils.formatDiffToTimecode(
- (this.props.timingDurations.asPlayedPlaylistDuration || 0) -
- (this.props.timingDurations.totalPlaylistDuration || 0),
- true,
- false,
- true,
- true,
- true,
- undefined,
- true
- )}
-
- ) : null}
-
- )}
-
- )
- }
- }
- )
-)
-
-interface IEndTimingProps {
- loop?: boolean
- expectedStart?: number
- expectedDuration: number
- expectedEnd?: number
- endLabel?: string
-}
-
-const PlaylistEndTiming = withTranslation()(
- withTiming()(
- class PlaylistEndTiming extends React.Component>> {
- render() {
- let { t } = this.props
-
- return (
-
- {!this.props.loop && this.props.expectedStart ? (
-
- {t(this.props.endLabel || 'Planned End')}
-
-
- ) : !this.props.loop && this.props.expectedEnd ? (
-
- {t(this.props.endLabel || 'Planned End')}
-
-
- ) : null}
- {!this.props.loop && this.props.expectedStart && this.props.expectedDuration ? (
-
- {RundownUtils.formatDiffToTimecode(
- getCurrentTime() - (this.props.expectedStart + this.props.expectedDuration),
- true,
- true,
- true
- )}
-
- ) : !this.props.loop && this.props.expectedEnd ? (
-
- {RundownUtils.formatDiffToTimecode(getCurrentTime() - this.props.expectedEnd, true, true, true)}
-
- ) : null}
- {this.props.expectedDuration ? (
- (this.props.expectedDuration || 0),
- })}
- >
- {t('Diff')}
- {RundownUtils.formatDiffToTimecode(
- (this.props.timingDurations.asPlayedPlaylistDuration || 0) - this.props.expectedDuration,
- true,
- false,
- true,
- true,
- true,
- undefined,
- true
- )}
-
- ) : null}
-
- )
- }
- }
- )
-)
-
-interface INextBreakTimingProps {
- loop?: boolean
- rundownsBeforeBreak: Rundown[]
- breakText?: string
-}
-
-const NextBreakTiming = withTranslation()(
- withTiming()(
- class PlaylistEndTiming extends React.Component>> {
- render() {
- let { t, rundownsBeforeBreak } = this.props
- let breakRundown = rundownsBeforeBreak.length ? rundownsBeforeBreak[rundownsBeforeBreak.length - 1] : undefined
-
- const rundownAsPlayedDuration = this.props.timingDurations.rundownAsPlayedDurations
- ? rundownsBeforeBreak.reduce(
- (prev, curr) => (prev += this.props.timingDurations.rundownAsPlayedDurations![unprotectString(curr._id)]),
- 0
- )
- : undefined
-
- const accumulatedExpectedDurations = this.props.timingDurations.rundownExpectedDurations
- ? rundownsBeforeBreak.reduce(
- (prev, curr) => (prev += this.props.timingDurations.rundownExpectedDurations![unprotectString(curr._id)]),
- 0
- )
- : undefined
-
- if (!breakRundown) {
- return null
- }
-
- return (
-
-
- {t(this.props.breakText || 'Next Break')}
-
-
- {!this.props.loop && breakRundown.expectedEnd ? (
-
- {RundownUtils.formatDiffToTimecode(getCurrentTime() - breakRundown.expectedEnd, true, true, true)}
-
+ {!rundownPlaylist.startedPlayback ||
+ this.props.timingDurations.breakIsLastRundown ||
+ !(this.props.timingDurations.rundownsBeforeNextBreak && this.props.layout?.hideExpectedEndBeforeBreak) ? (
+
) : null}
- {accumulatedExpectedDurations ? (
- (accumulatedExpectedDurations || 0),
- })}
- >
- {t('Diff')}
- {RundownUtils.formatDiffToTimecode(
- (rundownAsPlayedDuration || 0) - accumulatedExpectedDurations,
- true,
- false,
- true,
- true,
- true,
- undefined,
- true
- )}
-
+ {rundownPlaylist.startedPlayback &&
+ this.props.timingDurations.rundownsBeforeNextBreak &&
+ this.props.layout?.showNextBreakTiming &&
+ !(this.props.timingDurations.breakIsLastRundown && this.props.layout.lastRundownIsNotBreak) ? (
+
) : null}
-
+
)
}
}
@@ -1463,7 +1197,7 @@ const RundownHeader = withTranslation()(
playlist={this.props.playlist}
oneMinuteBeforeAction={this.resetAndActivateRundown}
/>
-
+
-
{this.props.layout && RundownLayoutsAPI.isDashboardLayout(this.props.layout) ? (
)}
+
@@ -2924,6 +2658,7 @@ export const RundownView = translateWithTracker((
inActiveRundownView={this.props.inActiveRundownView}
currentRundown={this.state.currentRundown || this.props.rundowns[0]}
layout={this.state.rundownHeaderLayout}
+ showStyleBase={this.props.showStyleBase}
/>
diff --git a/meteor/client/ui/RundownView/RundownTiming/NextBreakTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/NextBreakTiming.tsx
new file mode 100644
index 0000000000..298198d324
--- /dev/null
+++ b/meteor/client/ui/RundownView/RundownTiming/NextBreakTiming.tsx
@@ -0,0 +1,78 @@
+import React from 'react'
+import { WithTranslation, withTranslation } from 'react-i18next'
+import Moment from 'react-moment'
+import { Rundown } from '../../../../lib/collections/Rundowns'
+import { getCurrentTime, unprotectString } from '../../../../lib/lib'
+import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData'
+import { RundownUtils } from '../../../lib/rundown'
+import { WithTiming, withTiming } from './withTiming'
+import ClassNames from 'classnames'
+
+interface INextBreakTimingProps {
+ loop?: boolean
+ rundownsBeforeBreak: Rundown[]
+ breakText?: string
+}
+
+export const NextBreakTiming = withTranslation()(
+ withTiming()(
+ class PlaylistEndTiming extends React.Component>> {
+ render() {
+ let { t, rundownsBeforeBreak } = this.props
+ let breakRundown = rundownsBeforeBreak.length ? rundownsBeforeBreak[rundownsBeforeBreak.length - 1] : undefined
+
+ const rundownAsPlayedDuration = this.props.timingDurations.rundownAsPlayedDurations
+ ? rundownsBeforeBreak.reduce(
+ (prev, curr) => (prev += this.props.timingDurations.rundownAsPlayedDurations![unprotectString(curr._id)]),
+ 0
+ )
+ : undefined
+
+ const accumulatedExpectedDurations = this.props.timingDurations.rundownExpectedDurations
+ ? rundownsBeforeBreak.reduce(
+ (prev, curr) => (prev += this.props.timingDurations.rundownExpectedDurations![unprotectString(curr._id)]),
+ 0
+ )
+ : undefined
+
+ if (!breakRundown) {
+ return null
+ }
+
+ return (
+
+
+ {t(this.props.breakText || 'Next Break')}
+
+
+ {!this.props.loop && breakRundown.expectedEnd ? (
+
+ {RundownUtils.formatDiffToTimecode(getCurrentTime() - breakRundown.expectedEnd, true, true, true)}
+
+ ) : null}
+ {accumulatedExpectedDurations ? (
+ (accumulatedExpectedDurations || 0),
+ })}
+ >
+ {t('Diff')}
+ {RundownUtils.formatDiffToTimecode(
+ (rundownAsPlayedDuration || 0) - accumulatedExpectedDurations,
+ true,
+ false,
+ true,
+ true,
+ true,
+ undefined,
+ true
+ )}
+
+ ) : null}
+
+ )
+ }
+ }
+ )
+)
diff --git a/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx
new file mode 100644
index 0000000000..ba7b98ba14
--- /dev/null
+++ b/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx
@@ -0,0 +1,132 @@
+import React from 'react'
+import { WithTranslation, withTranslation } from 'react-i18next'
+import Moment from 'react-moment'
+import { getCurrentTime } from '../../../../lib/lib'
+import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData'
+import { RundownUtils } from '../../../lib/rundown'
+import { withTiming, WithTiming } from './withTiming'
+import ClassNames from 'classnames'
+
+interface IEndTimingProps {
+ loop?: boolean
+ expectedStart?: number
+ expectedDuration?: number
+ expectedEnd?: number
+ endLabel?: string
+ hidePlannedEnd?: boolean
+ hideCountdown?: boolean
+ hideDiff?: boolean
+ rundownCount: number
+}
+
+export const PlaylistEndTiming = withTranslation()(
+ withTiming()(
+ class PlaylistEndTiming extends React.Component>> {
+ render() {
+ let { t } = this.props
+
+ return (
+
+ {this.props.expectedDuration ? (
+
+ {!this.props.hidePlannedEnd &&
+ (!this.props.loop && this.props.expectedStart ? (
+
+ {t(this.props.endLabel || 'Planned End')}
+
+
+ ) : !this.props.loop && this.props.expectedEnd ? (
+
+ {t(this.props.endLabel || 'Planned End')}
+
+
+ ) : null)}
+ {!this.props.hideCountdown &&
+ (!this.props.loop && this.props.expectedStart && this.props.expectedDuration ? (
+
+ {RundownUtils.formatDiffToTimecode(
+ getCurrentTime() - (this.props.expectedStart + this.props.expectedDuration),
+ true,
+ true,
+ true
+ )}
+
+ ) : !this.props.loop && this.props.expectedEnd ? (
+
+ {RundownUtils.formatDiffToTimecode(getCurrentTime() - this.props.expectedEnd, true, true, true)}
+
+ ) : null)}
+ {this.props.expectedDuration && !this.props.hideDiff ? (
+ (this.props.expectedDuration || 0),
+ })}
+ >
+ {t('Diff')}
+ {RundownUtils.formatDiffToTimecode(
+ (this.props.timingDurations.asPlayedPlaylistDuration || 0) - this.props.expectedDuration,
+ true,
+ false,
+ true,
+ true,
+ true,
+ undefined,
+ true
+ )}
+
+ ) : null}
+
+ ) : (
+
+ {!this.props.loop && this.props.timingDurations ? (
+
+
+ {this.props.endLabel ? t(this.props.endLabel) : t('Expected End')}
+
+
+
+ ) : null}
+ {this.props.timingDurations && this.props.rundownCount < 2 ? ( // TEMPORARY: disable the diff counter for playlists longer than one rundown -- Jan Starzak, 2021-05-06
+
+ (this.props.timingDurations.totalPlaylistDuration || 0),
+ })}
+ >
+ {t('Diff')}
+ {RundownUtils.formatDiffToTimecode(
+ (this.props.timingDurations.asPlayedPlaylistDuration || 0) -
+ (this.props.timingDurations.totalPlaylistDuration || 0),
+ true,
+ false,
+ true,
+ true,
+ true,
+ undefined,
+ true
+ )}
+
+ ) : null}
+
+ )}
+
+ )
+ }
+ }
+ )
+)
diff --git a/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx
new file mode 100644
index 0000000000..dd61559890
--- /dev/null
+++ b/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx
@@ -0,0 +1,74 @@
+import React from 'react'
+import { WithTranslation, withTranslation } from 'react-i18next'
+import Moment from 'react-moment'
+import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData'
+import { withTiming, WithTiming } from './withTiming'
+import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists'
+import { RundownUtils } from '../../../lib/rundown'
+import { getCurrentTime } from '../../../../lib/lib'
+import ClassNames from 'classnames'
+
+interface IEndTimingProps {
+ rundownPlaylist: RundownPlaylist
+ hideDiff?: boolean
+}
+
+export const PlaylistStartTiming = withTranslation()(
+ withTiming()(
+ class PlaylistStartTiming extends React.Component>> {
+ render() {
+ let { t, rundownPlaylist } = this.props
+ let expectedStart = rundownPlaylist.expectedStart
+ ? rundownPlaylist.expectedStart
+ : rundownPlaylist.expectedDuration && rundownPlaylist.expectedEnd
+ ? rundownPlaylist.expectedEnd - rundownPlaylist.expectedDuration
+ : undefined
+
+ return (
+
+ {rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
+
+ {t('Started')}
+
+
+ ) : rundownPlaylist.expectedStart ? (
+
+ {t('Planned Start')}
+
+
+ ) : rundownPlaylist.expectedEnd && rundownPlaylist.expectedDuration ? (
+
+ {t('Expected Start')}
+
+
+ ) : null}
+ {!this.props.hideDiff && expectedStart && (
+ expectedStart,
+ light: getCurrentTime() <= expectedStart,
+ })}
+ >
+ {t('Diff')}
+ {rundownPlaylist.startedPlayback
+ ? RundownUtils.formatDiffToTimecode(
+ rundownPlaylist.startedPlayback - expectedStart,
+ true,
+ false,
+ true,
+ true,
+ true
+ )
+ : RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)}
+
+ )}
+
+ )
+ }
+ }
+ )
+)
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownName.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownName.tsx
new file mode 100644
index 0000000000..06e3d0a4e0
--- /dev/null
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownName.tsx
@@ -0,0 +1,98 @@
+import React from 'react'
+import { WithTranslation, withTranslation } from 'react-i18next'
+import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData'
+import { withTiming, WithTiming } from './withTiming'
+import ClassNames from 'classnames'
+import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists'
+import { LoopingIcon } from '../../../lib/ui/icons/looping'
+import { Rundown } from '../../../../lib/collections/Rundowns'
+import { RundownUtils } from '../../../lib/rundown'
+import { getCurrentTime } from '../../../../lib/lib'
+
+interface IRundownNameProps {
+ rundownPlaylist: RundownPlaylist
+ currentRundown?: Rundown
+ rundownCount: number
+ hideDiff?: boolean
+}
+
+export const RundownName = withTranslation()(
+ withTiming()(
+ class RundownName extends React.Component>> {
+ render() {
+ const { rundownPlaylist, currentRundown, rundownCount, t } = this.props
+ return (
+ rundownPlaylist.expectedStart,
+ })}
+ >
+ {currentRundown && (rundownPlaylist.name !== currentRundown.name || rundownCount > 1) ? (
+
+ {rundownPlaylist.loop && } {currentRundown.name} {rundownPlaylist.name}
+
+ ) : (
+
+ {rundownPlaylist.loop && } {rundownPlaylist.name}
+
+ )}
+ {!this.props.hideDiff &&
+ rundownPlaylist.startedPlayback &&
+ rundownPlaylist.activationId &&
+ !rundownPlaylist.rehearsal
+ ? rundownPlaylist.expectedStart &&
+ RundownUtils.formatDiffToTimecode(
+ rundownPlaylist.startedPlayback - rundownPlaylist.expectedStart,
+ true,
+ false,
+ true,
+ true,
+ true
+ )
+ : rundownPlaylist.expectedStart &&
+ RundownUtils.formatDiffToTimecode(
+ getCurrentTime() - rundownPlaylist.expectedStart,
+ true,
+ false,
+ true,
+ true,
+ true
+ )}
+
+ )
+ }
+ }
+ )
+)
diff --git a/meteor/client/ui/RundownView/RundownTiming/TimeOfDay.tsx b/meteor/client/ui/RundownView/RundownTiming/TimeOfDay.tsx
new file mode 100644
index 0000000000..34e90b6786
--- /dev/null
+++ b/meteor/client/ui/RundownView/RundownTiming/TimeOfDay.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import { WithTranslation, withTranslation } from 'react-i18next'
+import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData'
+import { withTiming, WithTiming } from './withTiming'
+import { getCurrentTime } from '../../../../lib/lib'
+import Moment from 'react-moment'
+
+interface ITimeOfDayProps {}
+
+export const TimeOfDay = withTranslation()(
+ withTiming()(
+ class RundownName extends React.Component>> {
+ render() {
+ return (
+
+
+
+ )
+ }
+ }
+ )
+)
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index ca07ffa4ee..2b732f47b2 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -12,9 +12,12 @@ import {
RundownLayoutBase,
RundownLayoutElementBase,
RundownLayoutElementType,
+ RundownLayoutEndWords,
RundownLayoutExternalFrame,
RundownLayoutFilterBase,
RundownLayoutPieceCountdown,
+ RundownLayoutPlaylistEndTimer,
+ RundownLayoutPlaylistStartTimer,
RundownLayouts,
} from '../../../../lib/collections/RundownLayouts'
import { EditAttribute } from '../../../lib/EditAttribute'
@@ -616,73 +619,9 @@ export default translateWithTracker((props: IProp
/>
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
{isDashboardLayout && (
-
-
- {t('X')}
-
-
-
-
-
- {t('Y')}
-
-
-
-
-
- {t('Width')}
-
-
-
-
-
- {t('Height')}
-
-
-
-
-
- {t('Scale')}
-
-
-
{t('Display Rank')}
@@ -806,77 +745,22 @@ export default translateWithTracker((props: IProp
/>
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
{isDashboardLayout && (
- {t('X')}
-
-
-
-
-
- {t('Y')}
-
-
-
-
-
- {t('Width')}
-
-
-
-
-
- {t('Height')}
+ {t('Register Shortcuts for this Panel')}
- {isDashboardLayout && (
-
-
-
- {t('Register Shortcuts for this Panel')}
-
-
-
-
- )}
)}
@@ -889,6 +773,62 @@ export default translateWithTracker((props: IProp
index: number,
isRundownLayout: boolean,
isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+ {t('Source Layers')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+ {
+ return { name: l.name, value: l._id }
+ })}
+ type="multiselect"
+ label={t('Disabled')}
+ collection={RundownLayouts}
+ className="input text-input input-l dropdown"
+ mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
+ />
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
+ {isDashboardLayout && (
+
+
+ {t('Scale')}
+
+
+
+ )}
+
+ )
+ }
+
+ renderPlaylistStartTimer(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPlaylistStartTimer,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
) {
const { t } = this.props
return (
@@ -907,10 +847,160 @@ export default translateWithTracker((props: IProp
- {t('Source Layers')}
+
+ {t('Hide Diff')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
+ {isDashboardLayout && (
+
+
+ {t('Scale')}
+
+
+
+ )}
+
+ )
+ }
+
+ renderPlaylistEndTimer(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPlaylistEndTimer,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+
+ {t('Expected End text')}
+ {t('Text to show above countdown to end of show')}
+
+
+
+ {t('Hide Diff')}
+
+
+
+
+
+ {t('Hide Countdown')}
+
+
+
+
+
+ {t('Hide End Time')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
+ {isDashboardLayout && (
+
+
+ {t('Scale')}
+
+
+
+ )}
+
+ )
+ }
+
+ renderEndWords(
+ item: RundownLayoutBase,
+ tab: RundownLayoutEndWords,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+
+ {t('Script Source Layers')}
+ ((props: IProp
/>
{
+ return { name: l.name, value: l._id }
+ })}
+ type="multiselect"
+ label={t('Disabled')}
+ collection={RundownLayouts}
+ className="input text-input input-l dropdown"
+ mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
+ />
+ {t('Source layers containing script')}
+
+
+ {t('Required Source Layers')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+ {
return { name: l.name, value: l._id }
@@ -931,67 +1048,107 @@ export default translateWithTracker((props: IProp
className="input text-input input-l dropdown"
mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
/>
+
+ {t('Specify layers where at least one layer must have an active piece for end words to be shown')}
+
+
+
+
+ {t('Require All Sourcelayers')}
+
+
+ {t('All required source layers must have active pieces to show end words')}
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
{isDashboardLayout && (
-
-
-
- {t('X')}
-
-
-
-
-
- {t('Y')}
-
-
-
-
-
- {t('Width')}
-
-
-
-
-
- {t('Scale')}
-
-
-
-
+
+
+ {t('Scale')}
+
+
+
)}
)
}
+ renderDashboardLayoutSettings(item: RundownLayoutBase, index: number) {
+ let { t } = this.props
+
+ return (
+
+
+
+ {t('X')}
+
+
+
+
+
+ {t('Y')}
+
+
+
+
+
+ {t('Width')}
+
+
+
+
+
+ {t('Height')}
+
+
+
+
+ )
+ }
+
render() {
const { t } = this.props
@@ -1065,6 +1222,30 @@ export default translateWithTracker((props: IProp
isRundownLayout,
isDashboardLayout
)
+ : RundownLayoutsAPI.isPlaylistStartTimer(this.props.filter)
+ ? this.renderPlaylistStartTimer(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
+ : RundownLayoutsAPI.isPlaylistEndTimer(this.props.filter)
+ ? this.renderPlaylistEndTimer(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
+ : RundownLayoutsAPI.isEndWords(this.props.filter)
+ ? this.renderEndWords(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
: undefined}
)
diff --git a/meteor/client/ui/Shelf/DashboardPanel.tsx b/meteor/client/ui/Shelf/DashboardPanel.tsx
index 075e14ab42..9bbff1c841 100644
--- a/meteor/client/ui/Shelf/DashboardPanel.tsx
+++ b/meteor/client/ui/Shelf/DashboardPanel.tsx
@@ -676,7 +676,10 @@ export class DashboardPanelInner extends MeteorReactComponent<
}
}
-export function getUnfinishedPieceInstancesReactive(currentPartInstanceId: PartInstanceId | null) {
+export function getUnfinishedPieceInstancesReactive(
+ currentPartInstanceId: PartInstanceId | null,
+ includeNonAdLibPieces?: boolean
+) {
let prospectivePieces: PieceInstance[] = []
const now = getCurrentTime()
if (currentPartInstanceId) {
@@ -699,20 +702,22 @@ export function getUnfinishedPieceInstancesReactive(currentPartInstanceId: PartI
},
],
},
- {
- $or: [
- {
- adLibSourceId: {
- $exists: true,
- },
- },
- {
- 'piece.tags': {
- $exists: true,
- },
- },
- ],
- },
+ !includeNonAdLibPieces
+ ? {
+ $or: [
+ {
+ adLibSourceId: {
+ $exists: true,
+ },
+ },
+ {
+ 'piece.tags': {
+ $exists: true,
+ },
+ },
+ ],
+ }
+ : {},
{
$or: [
{
diff --git a/meteor/client/ui/Shelf/EndWordsPanel.tsx b/meteor/client/ui/Shelf/EndWordsPanel.tsx
new file mode 100644
index 0000000000..9443785789
--- /dev/null
+++ b/meteor/client/ui/Shelf/EndWordsPanel.tsx
@@ -0,0 +1,98 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutEndsWords,
+ RundownLayoutBase,
+ RundownLayoutEndWords,
+} from '../../../lib/collections/RundownLayouts'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { dashboardElementPosition, getUnfinishedPieceInstancesReactive } from './DashboardPanel'
+import { Translated, translateWithTracker, withTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { PieceInstance } from '../../../lib/collections/PieceInstances'
+import { ScriptContent } from '@sofie-automation/blueprints-integration'
+import { GetScriptPreview } from '../scriptPreview'
+
+interface IEndsWordsPanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutEndWords
+ playlist: RundownPlaylist
+}
+
+interface IEndsWordsPanelTrackedProps {
+ livePieceInstance?: PieceInstance
+}
+
+interface IState {}
+
+export class EndWordsPanelInner extends MeteorReactComponent<
+ Translated,
+ IState
+> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+
+ let { t, livePieceInstance, panel } = this.props
+ let content = livePieceInstance?.piece.content as Partial | undefined
+
+ let { endOfScript } = GetScriptPreview(content?.fullScript || '')
+
+ return (
+
+ {t('End Words')}
+ {endOfScript}
+
+ )
+ }
+}
+
+export const EndWordsPanel = translateWithTracker(
+ (props: IEndsWordsPanelProps & IEndsWordsPanelTrackedProps) => {
+ const unfinishedPieces = getUnfinishedPieceInstancesReactive(props.playlist.currentPartInstanceId, true)
+ let livePieceInstance: PieceInstance | undefined
+ let activeLayers = unfinishedPieces.map((p) => p.piece.sourceLayerId)
+ let containsEveryRequiredLayer = props.panel.requireAllSourcelayers
+ ? props.panel.requiredLayers?.every((s) => activeLayers.includes(s))
+ : false
+ let containsRequiredLayer = containsEveryRequiredLayer
+ ? true
+ : props.panel.requiredLayers && props.panel.requiredLayers.length
+ ? props.panel.requiredLayers.some((s) => activeLayers.includes(s))
+ : false
+
+ if (
+ (!props.panel.requireAllSourcelayers || containsEveryRequiredLayer) &&
+ (!props.panel.requiredLayers?.length || containsRequiredLayer)
+ ) {
+ livePieceInstance =
+ props.panel.scriptSourceLayerIds && props.panel.scriptSourceLayerIds.length
+ ? _.flatten(Object.values(unfinishedPieces)).find((piece: PieceInstance) => {
+ return (
+ (props.panel.scriptSourceLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 &&
+ piece.partInstanceId === props.playlist.currentPartInstanceId
+ )
+ })
+ : undefined
+ }
+ return { livePieceInstance }
+ },
+ (_data, props: IEndsWordsPanelProps, nextProps: IEndsWordsPanelProps) => {
+ return !_.isEqual(props, nextProps)
+ }
+)(EndWordsPanelInner)
diff --git a/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx b/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
new file mode 100644
index 0000000000..549a55456e
--- /dev/null
+++ b/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
@@ -0,0 +1,72 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutPlaylistEndTimer,
+ DashboardLayoutPlaylistStartTimer,
+ RundownLayoutBase,
+ RundownLayoutPlaylistEndTimer,
+} from '../../../lib/collections/RundownLayouts'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { dashboardElementPosition } from './DashboardPanel'
+import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { withTranslation } from 'react-i18next'
+import { PlaylistStartTiming } from '../RundownView/RundownTiming/PlaylistStartTiming'
+import { PlaylistEndTiming } from '../RundownView/RundownTiming/PlaylistEndTiming'
+
+interface IPlaylistStartTimerPanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutPlaylistEndTimer
+ playlist: RundownPlaylist
+}
+
+interface IState {}
+
+export class PlaylistEndTimerPanelInner extends MeteorReactComponent<
+ Translated,
+ IState
+> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+
+ let { playlist, panel } = this.props
+
+ if (!playlist.expectedDuration) {
+ return null
+ }
+
+ return (
+
+ )
+ }
+}
+
+export const PlaylistEndTimerPanel = withTranslation()(PlaylistEndTimerPanelInner)
diff --git a/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx b/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
new file mode 100644
index 0000000000..3874389c66
--- /dev/null
+++ b/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
@@ -0,0 +1,60 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutPlaylistStartTimer,
+ RundownLayoutBase,
+ RundownLayoutPlaylistStartTimer,
+} from '../../../lib/collections/RundownLayouts'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { dashboardElementPosition } from './DashboardPanel'
+import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { withTranslation } from 'react-i18next'
+import { PlaylistStartTiming } from '../RundownView/RundownTiming/PlaylistStartTiming'
+
+interface IPlaylistStartTimerPanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutPlaylistStartTimer
+ playlist: RundownPlaylist
+}
+
+interface IState {}
+
+export class PlaylistStartTimerPanelInner extends MeteorReactComponent<
+ Translated,
+ IState
+> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+
+ let { playlist } = this.props
+
+ if (!playlist.expectedDuration) {
+ return null
+ }
+
+ return (
+
+ )
+ }
+}
+
+export const PlaylistStartTimerPanel = withTranslation()(PlaylistStartTimerPanelInner)
diff --git a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
index c0afb5df16..bac44b4e35 100644
--- a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
+++ b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
@@ -14,6 +14,9 @@ import { BucketAdLibItem } from './RundownViewBuckets'
import { IAdLibListItem } from './AdLibListItem'
import { PieceUi } from '../SegmentTimeline/SegmentTimelineContainer'
import { AdLibPieceUi } from './AdLibPanel'
+import { PlaylistStartTimerPanel } from './PlaylistStartTimerPanel'
+import { EndWordsPanel } from './EndWordsPanel'
+import { PlaylistEndTimerPanel } from './PlaylistEndTimerPanel'
export interface IShelfDashboardLayoutProps {
rundownLayout: DashboardLayout
@@ -103,6 +106,12 @@ export function ShelfDashboardLayout(props: IShelfDashboardLayoutProps) {
playlist={props.playlist}
visible={true}
/>
+ ) : RundownLayoutsAPI.isPlaylistStartTimer(panel) ? (
+
+ ) : RundownLayoutsAPI.isPlaylistEndTimer(panel) ? (
+
+ ) : RundownLayoutsAPI.isEndWords(panel) ? (
+
) : undefined
)}
{rundownLayout.actionButtons && (
diff --git a/meteor/client/ui/scriptPreview.ts b/meteor/client/ui/scriptPreview.ts
new file mode 100644
index 0000000000..e028a90406
--- /dev/null
+++ b/meteor/client/ui/scriptPreview.ts
@@ -0,0 +1,32 @@
+const BREAK_SCRIPT_BREAKPOINT = 620
+const SCRIPT_PART_LENGTH = 250
+
+interface ScriptPreview {
+ startOfScript: string
+ endOfScript: string
+ breakScript: boolean
+}
+
+export function GetScriptPreview(fullScript: string): ScriptPreview {
+ let startOfScript = fullScript
+ let cutLength = startOfScript.length
+ if (startOfScript.length > SCRIPT_PART_LENGTH) {
+ startOfScript = startOfScript.substring(0, startOfScript.substr(0, SCRIPT_PART_LENGTH).lastIndexOf(' '))
+ cutLength = startOfScript.length
+ }
+ let endOfScript = fullScript
+ if (endOfScript.length > SCRIPT_PART_LENGTH) {
+ endOfScript = endOfScript.substring(
+ endOfScript.indexOf(' ', Math.max(cutLength, endOfScript.length - SCRIPT_PART_LENGTH)),
+ endOfScript.length
+ )
+ }
+
+ const breakScript = fullScript.length > BREAK_SCRIPT_BREAKPOINT
+
+ return {
+ startOfScript,
+ endOfScript,
+ breakScript,
+ }
+}
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index d8edb8ed95..b5242355d3 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -15,6 +15,9 @@ import {
RundownLayoutRundownHeader,
RundownLayoutShelfBase,
CustomizableRegions,
+ RundownLayoutPlaylistStartTimer,
+ RundownLayoutPlaylistEndTimer,
+ RundownLayoutEndWords,
} from '../collections/RundownLayouts'
import { ShowStyleBaseId } from '../collections/ShowStyleBases'
import * as _ from 'underscore'
@@ -168,7 +171,12 @@ export namespace RundownLayoutsAPI {
registry.RegisterRundownHeaderLayouts(RundownLayoutType.DASHBOARD_LAYOUT, {
filtersTitle: 'Layout Elements',
supportsFilters: true,
- supportedElements: [RundownLayoutElementType.PIECE_COUNTDOWN],
+ supportedElements: [
+ RundownLayoutElementType.PIECE_COUNTDOWN,
+ RundownLayoutElementType.PLAYLIST_START_TIMER,
+ RundownLayoutElementType.PLAYLIST_END_TIMER,
+ RundownLayoutElementType.END_WORDS,
+ ],
})
export function GetSettingsManifest(): CustomizableRegionSettingsManifest[] {
@@ -223,6 +231,20 @@ export namespace RundownLayoutsAPI {
return element.type === RundownLayoutElementType.PIECE_COUNTDOWN
}
+ export function isPlaylistStartTimer(
+ element: RundownLayoutElementBase
+ ): element is RundownLayoutPlaylistStartTimer {
+ return element.type === RundownLayoutElementType.PLAYLIST_START_TIMER
+ }
+
+ export function isPlaylistEndTimer(element: RundownLayoutElementBase): element is RundownLayoutPlaylistEndTimer {
+ return element.type === RundownLayoutElementType.PLAYLIST_END_TIMER
+ }
+
+ export function isEndWords(element: RundownLayoutElementBase): element is RundownLayoutEndWords {
+ return element.type === RundownLayoutElementType.END_WORDS
+ }
+
export function adLibRegionToFilter(element: RundownLayoutAdLibRegion): RundownLayoutFilterBase {
return {
..._.pick(element, '_id', 'name', 'rank', 'tags'),
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 6f415b7cc3..d5fdcaefa9 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -45,6 +45,9 @@ export enum RundownLayoutElementType {
EXTERNAL_FRAME = 'external_frame',
ADLIB_REGION = 'adlib_region',
PIECE_COUNTDOWN = 'piece_countdown',
+ PLAYLIST_START_TIMER = 'playlist_start_timer',
+ PLAYLIST_END_TIMER = 'playlist_end_timer',
+ END_WORDS = 'end_words',
}
export interface RundownLayoutElementBase {
@@ -79,6 +82,30 @@ export interface RundownLayoutPieceCountdown extends RundownLayoutElementBase {
sourceLayerIds: string[] | undefined
}
+export interface RundownLayoutPlaylistStartTimer extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.PLAYLIST_START_TIMER
+ hideDiff: boolean
+}
+
+export interface RundownLayoutPlaylistEndTimer extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.PLAYLIST_END_TIMER
+ expectedEndText: string
+ hideCountdown: boolean
+ hideDiff: boolean
+ hidePlannedEnd: boolean
+}
+
+export interface RundownLayoutEndWords extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.PLAYLIST_END_TIMER
+ scriptSourceLayerIds?: string[]
+ requiredLayers?: string[]
+ /**
+ * Require that all required sourcelayers be active in order to show end words.
+ * This allows end words to be tied to a combination of e.g. script + VT.
+ */
+ requireAllSourcelayers: boolean
+}
+
/**
* A filter to be applied against the AdLib Pieces. If a member is undefined, the pool is not tested
* against that filter. A member must match all of the sub-filters to be included in a filter view
@@ -132,6 +159,27 @@ export interface DashboardLayoutPieceCountdown extends RundownLayoutPieceCountdo
scale: number
}
+export interface DashboardLayoutPlaylistStartTimer extends RundownLayoutPlaylistStartTimer {
+ x: number
+ y: number
+ width: number
+ scale: number
+}
+
+export interface DashboardLayoutPlaylistEndTimer extends RundownLayoutPlaylistEndTimer {
+ x: number
+ y: number
+ width: number
+ scale: number
+}
+
+export interface DashboardLayoutEndsWords extends RundownLayoutEndWords {
+ x: number
+ y: number
+ width: number
+ scale: number
+}
+
export interface DashboardLayoutFilter extends RundownLayoutFilterBase {
x: number
y: number
From 1e9324a1ba4845733b89e5683c4869f255f5a5e6 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Thu, 3 Jun 2021 17:26:51 +0100
Subject: [PATCH 014/112] feat: Playlist/Rundown expectedEnd display in lobby /
header
---
meteor/client/styles/rundownList.scss | 10 ++++---
.../client/ui/ClockView/PresenterScreen.tsx | 1 +
meteor/client/ui/RundownList.tsx | 3 +++
.../ui/RundownList/RundownListItemView.tsx | 15 +++++++++--
.../ui/RundownList/RundownPlaylistUi.tsx | 15 +++++++++--
meteor/client/ui/RundownView.tsx | 26 ++++++++++++-------
.../StudioScreenSaver/StudioScreenSaver.tsx | 1 +
meteor/lib/collections/RundownPlaylists.ts | 4 +++
meteor/lib/collections/Rundowns.ts | 1 +
meteor/server/api/rundownPlaylist.ts | 3 +++
.../migration/deprecatedDataTypes/1_0_1.ts | 2 ++
.../blueprints-integration/src/rundown.ts | 4 +++
12 files changed, 68 insertions(+), 17 deletions(-)
diff --git a/meteor/client/styles/rundownList.scss b/meteor/client/styles/rundownList.scss
index 9f4fbe7e97..89fa18937d 100644
--- a/meteor/client/styles/rundownList.scss
+++ b/meteor/client/styles/rundownList.scss
@@ -10,7 +10,7 @@
* Note that the standard size is used for several columns, which is why these
* percentages do not add up to a perfect 100% (ie. 1.0)
*/
- --nameColumnSize: 0.4;
+ --nameColumnSize: 0.3;
--showStyleColumnSize: 0.17;
--standardColumnSize: 0.11;
--layoutSelectionColumnSize: 0.075;
@@ -23,6 +23,7 @@
--showStyleColumnWidth: calc(var(--componentWidth) * var(--showStyleColumnSize));
--airTimeColumnWidth: calc(var(--componentWidth) * var(--standardColumnSize));
--durationColumnWidth: calc(var(--componentWidth) * var(--standardColumnSize));
+ --expectedEndColumnWidth: calc(var(--componentWidth) * var(--standardColumnSize));
--lastModifiedColumnWidth: calc(var(--componentWidth) * var(--standardColumnSize));
--layoutSelectionColumnWidth: calc(var(--componentWidth) * var(--layoutSelectionColumnSize));
--actionsColumnWidth: calc(var(--componentWidth) * var(--actionsColumnSize));
@@ -36,7 +37,7 @@
display: grid;
grid-template-columns:
var(--nameColumnWidth) var(--showStyleColumnWidth) var(--airTimeColumnWidth)
- var(--durationColumnWidth) var(--lastModifiedColumnWidth) var(--layoutSelectionColumnWidth);
+ var(--durationColumnWidth) var(--expectedEndColumnWidth) var(--lastModifiedColumnWidth) var(--layoutSelectionColumnWidth);
background-color: #898989;
color: #fff;
@@ -173,7 +174,7 @@
// for the header, the first column spans the first four item columns
grid-template-columns:
calc(var(--nameColumnWidth) + var(--showStyleColumnWidth)) var(--airTimeColumnWidth)
- var(--durationColumnWidth) var(--lastModifiedColumnWidth);
+ var(--durationColumnWidth) var(--expectedEndColumnWidth) var(--lastModifiedColumnWidth);
> span {
align-self: center;
@@ -246,6 +247,7 @@
--showStyleColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--showStyleColumnSize));
--airTimeColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--standardColumnSize));
--durationColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--standardColumnSize));
+ --expectedEndColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--standardColumnSize));
--lastModifiedColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--standardColumnSize));
--actionsColumnWidth: calc((var(--componentWidth) + var(--leftGutterWidth)) * var(--actionsColumnSize));
@@ -304,7 +306,7 @@
display: grid;
grid-template-columns:
var(--nameColumnWidth) var(--showStyleColumnWidth) var(--airTimeColumnWidth)
- var(--durationColumnWidth) var(--lastModifiedColumnWidth) var(--layoutSelectionColumnWidth);
+ var(--durationColumnWidth) var(--expectedEndColumnWidth) var(--lastModifiedColumnWidth) var(--layoutSelectionColumnWidth);
.rundown-list-item__actions {
padding: 0 0 3px; // cater for icons being larger than the font
diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx
index d401c4b376..cea32acc2f 100644
--- a/meteor/client/ui/ClockView/PresenterScreen.tsx
+++ b/meteor/client/ui/ClockView/PresenterScreen.tsx
@@ -74,6 +74,7 @@ function getShowStyleBaseIdSegmentPartUi(
name: 1,
expectedStart: 1,
expectedDuration: 1,
+ expectedEnd: 1,
},
})
showStyleBaseId = currentRundown?.showStyleBaseId
diff --git a/meteor/client/ui/RundownList.tsx b/meteor/client/ui/RundownList.tsx
index b6c2117edc..0038423be2 100644
--- a/meteor/client/ui/RundownList.tsx
+++ b/meteor/client/ui/RundownList.tsx
@@ -303,6 +303,9 @@ export const RundownList = translateWithTracker((): IRundownsListProps => {
{t('Show Style')}
{t('On Air Start Time')}
{t('Duration')}
+ {this.props.rundownPlaylists.some(
+ (p) => !!p.expectedEnd || p.rundowns.some((r) => r.expectedEnd)
+ ) && {t('Expected End Time')} }
{t('Last Updated')}
{this.props.rundownLayouts.some((l) => l.exposeAsShelf || l.exposeAsStandalone) && (
{t('Shelf Layout')}
diff --git a/meteor/client/ui/RundownList/RundownListItemView.tsx b/meteor/client/ui/RundownList/RundownListItemView.tsx
index 6b544cb2fa..a9362d6e05 100644
--- a/meteor/client/ui/RundownList/RundownListItemView.tsx
+++ b/meteor/client/ui/RundownList/RundownListItemView.tsx
@@ -107,7 +107,9 @@ export default withTranslation()(function RundownListItemView(props: Translated<
{rundown.expectedStart ? (
-
+
+ ) : rundown.expectedEnd && rundown.expectedDuration ? (
+
) : (
{t('Not set')}
)}
@@ -138,7 +140,16 @@ export default withTranslation()(function RundownListItemView(props: Translated<
)}
-
+ {rundown.expectedEnd ? (
+
+ ) : rundown.expectedStart && rundown.expectedDuration ? (
+
+ ) : (
+ {t('Not set')}
+ )}
+
+
+
{rundownLayouts.some((l) => l.exposeAsShelf || l.exposeAsStandalone) && (
diff --git a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
index fefd48073d..13466246d0 100644
--- a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
+++ b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
@@ -340,7 +340,9 @@ export const RundownPlaylistUi = DropTarget(
{playlist.expectedStart ? (
-
+
+ ) : playlist.expectedEnd && playlist.expectedDuration ? (
+
) : (
{t('Not set')}
)}
@@ -357,7 +359,16 @@ export const RundownPlaylistUi = DropTarget(
)}
-
+ {playlist.expectedEnd ? (
+
+ ) : playlist.expectedStart && playlist.expectedDuration ? (
+
+ ) : (
+ {t('Not set')}
+ )}
+
+
+
{rundownLayouts.some((l) => l.exposeAsShelf || l.exposeAsStandalone) && (
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index c1a0e25970..7953eba643 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -338,7 +338,7 @@ const TimingDisplay = withTranslation()(
) : null}
)}
- {rundownPlaylist.expectedDuration ? (
+ {rundownPlaylist.expectedEnd || rundownPlaylist.expectedDuration ? (
{!rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
@@ -349,6 +349,11 @@ const TimingDisplay = withTranslation()(
date={rundownPlaylist.expectedStart + rundownPlaylist.expectedDuration}
/>
+ ) : !rundownPlaylist.loop && rundownPlaylist.expectedEnd ? (
+
+ {t('Planned End')}
+
+
) : null}
{!rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
@@ -407,14 +412,17 @@ const TimingDisplay = withTranslation()(
) : null
) : (
-
- {t('Expected End')}
-
-
+
+ {t('Expected End')}
+
+
)
) : null}
{this.props.timingDurations && this.props.rundownCount < 2 ? ( // TEMPORARY: disable the diff counter for playlists longer than one rundown -- Jan Starzak, 2021-05-06
diff --git a/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx b/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx
index 027cfe1aa4..2090bdeeed 100644
--- a/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx
+++ b/meteor/client/ui/StudioScreenSaver/StudioScreenSaver.tsx
@@ -58,6 +58,7 @@ export const findNextPlaylist = (props: IProps) => {
name: 1,
expectedStart: 1,
expectedDuration: 1,
+ expectedEnd: 1,
studioId: 1,
},
}
diff --git a/meteor/lib/collections/RundownPlaylists.ts b/meteor/lib/collections/RundownPlaylists.ts
index 26b577560f..87c888f5ad 100644
--- a/meteor/lib/collections/RundownPlaylists.ts
+++ b/meteor/lib/collections/RundownPlaylists.ts
@@ -65,6 +65,8 @@ export interface DBRundownPlaylist {
expectedStart?: Time
/** How long the playlist is expected to take ON AIR */
expectedDuration?: number
+ /** When the playlist is expected to end */
+ expectedEnd?: Time
/** Is the playlist in rehearsal mode (can be used, when active: true) */
rehearsal?: boolean
/** Playout hold state */
@@ -122,6 +124,7 @@ export class RundownPlaylist implements DBRundownPlaylist {
public rundownsStartedPlayback?: Record
public expectedStart?: Time
public expectedDuration?: number
+ public expectedEnd?: Time
public rehearsal?: boolean
public holdState?: RundownHoldState
public activationId?: RundownPlaylistActivationId
@@ -213,6 +216,7 @@ export class RundownPlaylist implements DBRundownPlaylist {
playlistId: 1,
expectedStart: 1,
expectedDuration: 1,
+ expectedEnd: 1,
showStyleBaseId: 1,
},
})
diff --git a/meteor/lib/collections/Rundowns.ts b/meteor/lib/collections/Rundowns.ts
index c8013fe2f5..df1335204d 100644
--- a/meteor/lib/collections/Rundowns.ts
+++ b/meteor/lib/collections/Rundowns.ts
@@ -86,6 +86,7 @@ export class Rundown implements DBRundown {
public description?: string
public expectedStart?: Time
public expectedDuration?: number
+ public expectedEnd?: Time
public metaData?: unknown
// From IBlueprintRundownDB:
public _id: RundownId
diff --git a/meteor/server/api/rundownPlaylist.ts b/meteor/server/api/rundownPlaylist.ts
index 7a0c2107fb..ff88d46c31 100644
--- a/meteor/server/api/rundownPlaylist.ts
+++ b/meteor/server/api/rundownPlaylist.ts
@@ -171,6 +171,7 @@ export function produceRundownPlaylistInfoFromRundown(
name: playlistInfo.playlist.name,
expectedStart: playlistInfo.playlist.expectedStart,
expectedDuration: playlistInfo.playlist.expectedDuration,
+ expectedEnd: playlistInfo.playlist.expectedEnd,
loop: playlistInfo.playlist.loop,
@@ -213,6 +214,7 @@ function defaultPlaylistForRundown(
name: rundown.name,
expectedStart: rundown.expectedStart,
expectedDuration: rundown.expectedDuration,
+ expectedEnd: rundown.expectedEnd,
modified: getCurrentTime(),
}
@@ -487,6 +489,7 @@ function sortDefaultRundownInPlaylistOrder(rundowns: ReadonlyDeep, ReadonlyDeep>(rundowns, {
sort: {
expectedStart: 1,
+ expectedEnd: 1,
name: 1,
_id: 1,
},
diff --git a/meteor/server/migration/deprecatedDataTypes/1_0_1.ts b/meteor/server/migration/deprecatedDataTypes/1_0_1.ts
index 7c7174f357..8c386680ad 100644
--- a/meteor/server/migration/deprecatedDataTypes/1_0_1.ts
+++ b/meteor/server/migration/deprecatedDataTypes/1_0_1.ts
@@ -14,6 +14,7 @@ export interface Rundown {
name: string
expectedStart?: Time
expectedDuration?: number
+ expectedEnd?: Time
metaData?: {
[key: string]: any
}
@@ -61,6 +62,7 @@ export function makePlaylistFromRundown_1_0_0(
nextPartInstanceId: null,
expectedDuration: rundown.expectedDuration,
expectedStart: rundown.expectedStart,
+ expectedEnd: rundown.expectedEnd,
holdState: rundown.holdState,
name: rundown.name,
nextPartManual: rundown.nextPartManual,
diff --git a/packages/blueprints-integration/src/rundown.ts b/packages/blueprints-integration/src/rundown.ts
index 56bc6ccea7..85da97b1ac 100644
--- a/packages/blueprints-integration/src/rundown.ts
+++ b/packages/blueprints-integration/src/rundown.ts
@@ -13,6 +13,8 @@ export interface IBlueprintRundownPlaylistInfo {
expectedStart?: Time
/** Expected duration of the rundown playlist */
expectedDuration?: number
+ /** Expected end time of the rundown playlist */
+ expectedEnd?: Time
/** Should the rundown playlist use out-of-order timing mode (unplayed content will be played eventually) as opposed to normal timing mode (unplayed content behind the OnAir line has been skipped) */
outOfOrderTiming?: boolean
/** Should the rundown playlist loop at the end */
@@ -34,6 +36,8 @@ export interface IBlueprintRundown {
expectedStart?: Time
/** Expected duration of the rundown */
expectedDuration?: number
+ /** Expected end time of the rundown */
+ expectedEnd?: Time
/** Arbitrary data storage for plugins */
metaData?: TMetadata
From f8cc7b7908b02340d3971a799359e761d5b666b6 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Fri, 4 Jun 2021 13:33:04 +0100
Subject: [PATCH 015/112] feat: End time diff
---
meteor/client/ui/RundownView.tsx | 72 ++++++++++++++++++++++----------
1 file changed, 49 insertions(+), 23 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 7953eba643..809f5750be 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -268,6 +268,20 @@ const TimingDisplay = withTranslation()(
)
}
+
+ private getEndTimeDiff(expectedDuration: number) {
+ let diff = 0
+ if (this.props.rundownPlaylist.expectedEnd) {
+ let nowDiff = getCurrentTime() - this.props.rundownPlaylist.expectedEnd
+ let durationDiff = expectedDuration - (this.props.timingDurations.asPlayedRundownDuration ?? 0)
+ diff = nowDiff + durationDiff
+ } else {
+ diff = (this.props.timingDurations.asPlayedRundownDuration || 0) - expectedDuration
+ }
+
+ return diff
+ }
+
render() {
const { t, rundownPlaylist } = this.props
@@ -340,7 +354,12 @@ const TimingDisplay = withTranslation()(
)}
{rundownPlaylist.expectedEnd || rundownPlaylist.expectedDuration ? (
- {!rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
+ {!rundownPlaylist.loop && rundownPlaylist.expectedEnd ? (
+
+ {t('Planned End')}
+
+
+ ) : !rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
{t('Planned End')}
- ) : !rundownPlaylist.loop && rundownPlaylist.expectedEnd ? (
-
- {t('Planned End')}
-
-
) : null}
- {!rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
-
- {RundownUtils.formatDiffToTimecode(
- getCurrentTime() - (rundownPlaylist.expectedStart + rundownPlaylist.expectedDuration),
- true,
- true,
- true
- )}
-
- ) : null}
- {rundownPlaylist.expectedDuration && this.props.rundownCount < 2 ? ( // TEMPORARY: disable the diff counter for playlists longer than one rundown -- Jan Starzak, 2021-05-06
+ {!rundownPlaylist.loop &&
+ (rundownPlaylist.expectedEnd ? (
+
+ {RundownUtils.formatDiffToTimecode(
+ getCurrentTime() - rundownPlaylist.expectedEnd,
+ true,
+ true,
+ true
+ )}
+
+ ) : rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
+
+ {RundownUtils.formatDiffToTimecode(
+ getCurrentTime() - (rundownPlaylist.expectedStart + rundownPlaylist.expectedDuration),
+ true,
+ true,
+ true
+ )}
+
+ ) : null)}
+ {rundownPlaylist.expectedDuration ? (
{t('Diff')}
{RundownUtils.formatDiffToTimecode(
- (this.props.timingDurations.asPlayedRundownDuration || 0) - rundownPlaylist.expectedDuration,
+ this.getEndTimeDiff(rundownPlaylist.expectedDuration),
true,
false,
true,
@@ -417,13 +441,15 @@ const TimingDisplay = withTranslation()(
- )
+ ) : null}
+ {!rundownPlaylist.loop && this.props.rundownPlaylist.expectedEnd ? (
+
+ {t('Planned End')}
+
+
) : null}
{this.props.timingDurations && this.props.rundownCount < 2 ? ( // TEMPORARY: disable the diff counter for playlists longer than one rundown -- Jan Starzak, 2021-05-06
Date: Fri, 4 Jun 2021 14:33:21 +0100
Subject: [PATCH 016/112] fix: End time diff
---
meteor/client/ui/RundownView.tsx | 19 ++++---------------
1 file changed, 4 insertions(+), 15 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 809f5750be..7c7e998969 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -269,19 +269,6 @@ const TimingDisplay = withTranslation()(
)
}
- private getEndTimeDiff(expectedDuration: number) {
- let diff = 0
- if (this.props.rundownPlaylist.expectedEnd) {
- let nowDiff = getCurrentTime() - this.props.rundownPlaylist.expectedEnd
- let durationDiff = expectedDuration - (this.props.timingDurations.asPlayedRundownDuration ?? 0)
- diff = nowDiff + durationDiff
- } else {
- diff = (this.props.timingDurations.asPlayedRundownDuration || 0) - expectedDuration
- }
-
- return diff
- }
-
render() {
const { t, rundownPlaylist } = this.props
@@ -373,7 +360,9 @@ const TimingDisplay = withTranslation()(
(rundownPlaylist.expectedEnd ? (
{RundownUtils.formatDiffToTimecode(
- getCurrentTime() - rundownPlaylist.expectedEnd,
+ getCurrentTime() -
+ rundownPlaylist.expectedEnd +
+ (this.props.timingDurations.asPlayedRundownDuration ?? 0),
true,
true,
true
@@ -402,7 +391,7 @@ const TimingDisplay = withTranslation()(
>
{t('Diff')}
{RundownUtils.formatDiffToTimecode(
- this.getEndTimeDiff(rundownPlaylist.expectedDuration),
+ (this.props.timingDurations.asPlayedRundownDuration || 0) - rundownPlaylist.expectedDuration,
true,
false,
true,
From 757082214d90bf1cf518350227f685626960a893 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 9 Jun 2021 15:54:35 +0100
Subject: [PATCH 017/112] wip: Move rundown timing to lib and add tests
---
.../RundownTiming/RundownTiming.ts | 35 --
.../RundownTiming/RundownTimingProvider.tsx | 342 +------------
.../SegmentTimelineContainer.tsx | 11 +-
.../rundown/__tests__/rundownTiming.test.ts | 377 ++++++++++++++
meteor/lib/rundown/rundownTiming.ts | 458 ++++++++++++++++++
5 files changed, 849 insertions(+), 374 deletions(-)
create mode 100644 meteor/lib/rundown/__tests__/rundownTiming.test.ts
create mode 100644 meteor/lib/rundown/rundownTiming.ts
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts b/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
index 98fe3c934d..16524cf1e0 100644
--- a/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
@@ -87,38 +87,3 @@ export namespace RundownTiming {
timingDurations: RundownTimingContext
}
}
-
-/**
- * Computes the actual (as-played fallbacking to expected) duration of a segment, consisting of given parts
- * @export
- * @param {RundownTiming.RundownTimingContext} timingDurations The timing durations calculated for the Rundown
- * @param {Array} partIds The IDs of parts that are members of the segment
- * @return number
- */
-export function computeSegmentDuration(
- timingDurations: RundownTiming.RundownTimingContext,
- partIds: PartId[],
- display?: boolean
-): number {
- const partDurations = timingDurations.partDurations
-
- if (partDurations === undefined) return 0
-
- return partIds.reduce((memo, partId) => {
- const pId = unprotectString(partId)
- const partDuration =
- (partDurations ? (partDurations[pId] !== undefined ? partDurations[pId] : 0) : 0) ||
- (display ? Settings.defaultDisplayDuration : 0)
- return memo + partDuration
- }, 0)
-}
-
-export function computeSegmentDisplayDuration(
- timingDurations: RundownTiming.RundownTimingContext,
- parts: PartUi[]
-): number {
- return parts.reduce(
- (memo, part) => memo + SegmentTimelinePartClass.getPartDisplayDuration(part, timingDurations),
- 0
- )
-}
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
index 248c5dd3fc..fcfad1a16b 100644
--- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
@@ -4,19 +4,12 @@ import * as PropTypes from 'prop-types'
import * as _ from 'underscore'
import { withTracker } from '../../../lib/ReactMeteorData/react-meteor-data'
import { Part, PartId } from '../../../../lib/collections/Parts'
-import { getCurrentTime, literal, protectString, unprotectString } from '../../../../lib/lib'
+import { getCurrentTime } from '../../../../lib/lib'
import { MeteorReactComponent } from '../../../lib/MeteorReactComponent'
import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists'
-import {
- PartInstance,
- wrapPartToTemporaryInstance,
- findPartInstanceInMapOrWrapToTemporary,
-} from '../../../../lib/collections/PartInstances'
-import { Settings } from '../../../../lib/Settings'
+import { PartInstance } from '../../../../lib/collections/PartInstances'
import { RundownTiming, TimeEventArgs } from './RundownTiming'
-
-// Minimum duration that a part can be assigned. Used by gap parts to allow them to "compress" to indicate time running out.
-const MINIMAL_NONZERO_DURATION = 1
+import { RundownTimingCalculator } from '../../../../lib/rundown/rundownTiming'
const TIMING_DEFAULT_REFRESH_INTERVAL = 1000 / 60 // the interval for high-resolution events (timeupdateHR)
const LOW_RESOLUTION_TIMING_DECIMATOR = 15 // the low-resolution events will be called every
@@ -125,18 +118,7 @@ export const RundownTimingProvider = withTracker<
refreshTimerInterval: number
refreshDecimator: number
- private temporaryPartInstances: Map = new Map()
-
- private linearParts: Array<[PartId, number | null]> = []
- // look at the comments on RundownTimingContext to understand what these do
- private partDurations: Record = {}
- private partExpectedDurations: Record = {}
- private partPlayed: Record = {}
- private partStartsAt: Record = {}
- private partDisplayStartsAt: Record = {}
- private partDisplayDurations: Record = {}
- private partDisplayDurationsNoPlayback: Record = {}
- private displayDurationGroups: Record = {}
+ private timingCalculator: RundownTimingCalculator = new RundownTimingCalculator()
constructor(props: IRundownTimingProviderProps & IRundownTimingProviderTrackedProps) {
super(props)
@@ -180,7 +162,7 @@ export const RundownTimingProvider = withTracker<
}
if (prevProps.parts !== this.props.parts) {
// empty the temporary Part Instances cache
- this.temporaryPartInstances.clear()
+ this.timingCalculator.clearTempPartInstances()
this.onRefreshTimer()
}
}
@@ -211,318 +193,18 @@ export const RundownTimingProvider = withTracker<
window.dispatchEvent(event)
}
- private getPartInstanceOrGetCachedTemp(partInstancesMap: Map, part: Part): PartInstance {
- const origPartId = part._id
- const partInstance = partInstancesMap.get(origPartId)
- if (partInstance !== undefined) {
- return partInstance
- } else {
- let tempPartInstance = this.temporaryPartInstances.get(origPartId)
- if (tempPartInstance !== undefined) {
- return tempPartInstance
- } else {
- tempPartInstance = wrapPartToTemporaryInstance(protectString(''), part)
- this.temporaryPartInstances.set(origPartId, tempPartInstance)
- return tempPartInstance
- }
- }
- }
-
updateDurations(now: number, isLowResolution: boolean) {
- let totalRundownDuration = 0
- let remainingRundownDuration = 0
- let asPlayedRundownDuration = 0
- let asDisplayedRundownDuration = 0
- let waitAccumulator = 0
- let currentRemaining = 0
- let startsAtAccumulator = 0
- let displayStartsAtAccumulator = 0
-
- Object.keys(this.displayDurationGroups).forEach((key) => delete this.displayDurationGroups[key])
- this.linearParts.length = 0
-
const { playlist, parts, partInstancesMap } = this.props
-
- let nextAIndex = -1
- let currentAIndex = -1
-
- if (playlist && parts) {
- parts.forEach((origPart, itIndex) => {
- const partInstance = this.getPartInstanceOrGetCachedTemp(partInstancesMap, origPart)
-
- // add piece to accumulator
- const aIndex = this.linearParts.push([partInstance.part._id, waitAccumulator]) - 1
-
- // if this is next segementLine, clear previous countdowns and clear accumulator
- if (playlist.nextPartInstanceId === partInstance._id) {
- nextAIndex = aIndex
- } else if (playlist.currentPartInstanceId === partInstance._id) {
- currentAIndex = aIndex
- }
-
- const partCounts =
- playlist.outOfOrderTiming ||
- !playlist.activationId ||
- (itIndex >= currentAIndex && currentAIndex >= 0) ||
- (itIndex >= nextAIndex && nextAIndex >= 0 && currentAIndex === -1)
-
- const partIsUntimed = partInstance.part.untimed || false
-
- // expected is just a sum of expectedDurations
- totalRundownDuration += partInstance.part.expectedDuration || 0
-
- const lastStartedPlayback = partInstance.timings?.startedPlayback
- const playOffset = partInstance.timings?.playOffset || 0
-
- let partDuration = 0
- let partExpectedDuration = 0
- let partDisplayDuration = 0
- let partDisplayDurationNoPlayback = 0
-
- let displayDurationFromGroup = 0
-
- partExpectedDuration = partInstance.part.expectedDuration || partInstance.timings?.duration || 0
-
- // Display Duration groups are groups of two or more Parts, where some of them have an
- // expectedDuration and some have 0.
- // Then, some of them will have a displayDuration. The expectedDurations are pooled together, the parts with
- // display durations will take up that much time in the Rundown. The left-over time from the display duration group
- // will be used by Parts without expectedDurations.
- let memberOfDisplayDurationGroup = false
- // using a separate displayDurationGroup processing flag simplifies implementation
- if (
- partInstance.part.displayDurationGroup &&
- // either this is not the first element of the displayDurationGroup
- (this.displayDurationGroups[partInstance.part.displayDurationGroup] !== undefined ||
- // or there is a following member of this displayDurationGroup
- (parts[itIndex + 1] &&
- parts[itIndex + 1].displayDurationGroup === partInstance.part.displayDurationGroup)) &&
- !partInstance.part.floated &&
- !partIsUntimed
- ) {
- this.displayDurationGroups[partInstance.part.displayDurationGroup] =
- (this.displayDurationGroups[partInstance.part.displayDurationGroup] || 0) +
- (partInstance.part.expectedDuration || 0)
- displayDurationFromGroup =
- partInstance.part.displayDuration ||
- Math.max(
- 0,
- this.displayDurationGroups[partInstance.part.displayDurationGroup],
- partInstance.part.gap
- ? MINIMAL_NONZERO_DURATION
- : this.props.defaultDuration || Settings.defaultDisplayDuration
- )
- partExpectedDuration =
- partExpectedDuration || this.displayDurationGroups[partInstance.part.displayDurationGroup] || 0
- memberOfDisplayDurationGroup = true
- }
-
- // This is where we actually calculate all the various variants of duration of a part
- if (lastStartedPlayback && !partInstance.timings?.duration) {
- // if duration isn't available, check if `takeOut` has already been set and use the difference
- // between startedPlayback and takeOut as a temporary duration
- const duration =
- partInstance.timings?.duration ||
- (partInstance.timings?.takeOut ? lastStartedPlayback - partInstance.timings?.takeOut : undefined)
- currentRemaining = Math.max(
- 0,
- (duration ||
- (memberOfDisplayDurationGroup ? displayDurationFromGroup : partInstance.part.expectedDuration) ||
- 0) -
- (now - lastStartedPlayback)
- )
- partDuration =
- Math.max(duration || partInstance.part.expectedDuration || 0, now - lastStartedPlayback) - playOffset
- // because displayDurationGroups have no actual timing on them, we need to have a copy of the
- // partDisplayDuration, but calculated as if it's not playing, so that the countdown can be
- // calculated
- partDisplayDurationNoPlayback =
- duration ||
- (memberOfDisplayDurationGroup ? displayDurationFromGroup : partInstance.part.expectedDuration) ||
- this.props.defaultDuration ||
- Settings.defaultDisplayDuration
- partDisplayDuration = Math.max(partDisplayDurationNoPlayback, now - lastStartedPlayback)
- this.partPlayed[unprotectString(partInstance.part._id)] = now - lastStartedPlayback
- } else {
- partDuration = (partInstance.timings?.duration || partInstance.part.expectedDuration || 0) - playOffset
- partDisplayDurationNoPlayback = Math.max(
- 0,
- (partInstance.timings?.duration && partInstance.timings?.duration + playOffset) ||
- displayDurationFromGroup ||
- partInstance.part.expectedDuration ||
- this.props.defaultDuration ||
- Settings.defaultDisplayDuration
- )
- partDisplayDuration = partDisplayDurationNoPlayback
- this.partPlayed[unprotectString(partInstance.part._id)] = (partInstance.timings?.duration || 0) - playOffset
- }
-
- // asPlayed is the actual duration so far and expected durations in unplayed lines.
- // If item is onAir right now, it's duration is counted as expected duration or current
- // playback duration whichever is larger.
- // Parts that are Untimed are ignored always.
- // Parts that don't count are ignored, unless they are being played or have been played.
- if (!partIsUntimed) {
- if (lastStartedPlayback && !partInstance.timings?.duration) {
- asPlayedRundownDuration += Math.max(partExpectedDuration, now - lastStartedPlayback)
- } else if (partInstance.timings?.duration) {
- asPlayedRundownDuration += partInstance.timings.duration
- } else if (partCounts) {
- asPlayedRundownDuration += partInstance.part.expectedDuration || 0
- }
- }
-
- // asDisplayed is the actual duration so far and expected durations in unplayed lines
- // If item is onAir right now, it's duration is counted as expected duration or current
- // playback duration whichever is larger.
- // All parts are counted.
- if (lastStartedPlayback && !partInstance.timings?.duration) {
- asDisplayedRundownDuration += Math.max(
- memberOfDisplayDurationGroup
- ? Math.max(partExpectedDuration, partInstance.part.expectedDuration || 0)
- : partInstance.part.expectedDuration || 0,
- now - lastStartedPlayback
- )
- } else {
- asDisplayedRundownDuration += partInstance.timings?.duration || partInstance.part.expectedDuration || 0
- }
-
- // the part is the current part but has not yet started playback
- if (playlist.currentPartInstanceId === partInstance._id && !lastStartedPlayback) {
- currentRemaining = partDisplayDuration
- }
-
- // Handle invalid parts by overriding the values to preset values for Invalid parts
- if (partInstance.part.invalid && !partInstance.part.gap) {
- partDisplayDuration = this.props.defaultDuration || Settings.defaultDisplayDuration
- this.partPlayed[unprotectString(partInstance.part._id)] = 0
- }
-
- if (
- memberOfDisplayDurationGroup &&
- partInstance.part.displayDurationGroup &&
- !partInstance.part.floated &&
- !partInstance.part.invalid &&
- !partIsUntimed &&
- (partInstance.timings?.duration || partInstance.timings?.takeOut || partCounts)
- ) {
- this.displayDurationGroups[partInstance.part.displayDurationGroup] =
- this.displayDurationGroups[partInstance.part.displayDurationGroup] - partDisplayDuration
- }
- const partInstancePartId = unprotectString(partInstance.part._id)
- this.partExpectedDurations[partInstancePartId] = partExpectedDuration
- this.partStartsAt[partInstancePartId] = startsAtAccumulator
- this.partDisplayStartsAt[partInstancePartId] = displayStartsAtAccumulator
- this.partDurations[partInstancePartId] = partDuration
- this.partDisplayDurations[partInstancePartId] = partDisplayDuration
- this.partDisplayDurationsNoPlayback[partInstancePartId] = partDisplayDurationNoPlayback
- startsAtAccumulator += this.partDurations[partInstancePartId]
- displayStartsAtAccumulator += this.partDisplayDurations[partInstancePartId] // || this.props.defaultDuration || 3000
- // waitAccumulator is used to calculate the countdowns for Parts relative to the current Part
- // always add the full duration, in case by some manual intervention this segment should play twice
- if (memberOfDisplayDurationGroup) {
- waitAccumulator +=
- partInstance.timings?.duration || partDisplayDuration || partInstance.part.expectedDuration || 0
- } else {
- waitAccumulator += partInstance.timings?.duration || partInstance.part.expectedDuration || 0
- }
-
- // remaining is the sum of unplayed lines + whatever is left of the current segment
- // if outOfOrderTiming is true, count parts before current part towards remaining rundown duration
- // if false (default), past unplayed parts will not count towards remaining time
- if (!lastStartedPlayback && !partInstance.part.floated && partCounts && !partIsUntimed) {
- remainingRundownDuration += partExpectedDuration || 0
- // item is onAir right now, and it's is currently shorter than expectedDuration
- } else if (
- lastStartedPlayback &&
- !partInstance.timings?.duration &&
- playlist.currentPartInstanceId === partInstance._id &&
- lastStartedPlayback + partExpectedDuration > now &&
- !partIsUntimed
- ) {
- remainingRundownDuration += partExpectedDuration - (now - lastStartedPlayback)
- }
- })
-
- // This is where the waitAccumulator-generated data in the linearSegLines is used to calculate the countdowns.
- let localAccum = 0
- for (let i = 0; i < this.linearParts.length; i++) {
- if (i < nextAIndex) {
- // this is a line before next line
- localAccum = this.linearParts[i][1] || 0
- // only null the values if not looping, if looping, these will be offset by the countdown for the last part
- if (!playlist.loop) {
- this.linearParts[i][1] = null // we use null to express 'will not probably be played out, if played in order'
- }
- } else if (i === nextAIndex) {
- // this is a calculation for the next line, which is basically how much there is left of the current line
- localAccum = this.linearParts[i][1] || 0 // if there is no current line, rebase following lines to the next line
- this.linearParts[i][1] = currentRemaining
- } else {
- // these are lines after next line
- // we take whatever value this line has, subtract the value as set on the Next Part
- // (note that the Next Part value will be using currentRemaining as the countdown)
- // and add the currentRemaining countdown, since we are currentRemaining + diff between next and
- // this away from this line.
- this.linearParts[i][1] = (this.linearParts[i][1] || 0) - localAccum + currentRemaining
- }
- }
- // contiunation of linearParts calculations for looping playlists
- if (playlist.loop) {
- for (let i = 0; i < nextAIndex; i++) {
- // offset the parts before the on air line by the countdown for the end of the rundown
- this.linearParts[i][1] = (this.linearParts[i][1] || 0) + waitAccumulator - localAccum + currentRemaining
- }
- }
-
- // if (this.refreshDecimator % LOW_RESOLUTION_TIMING_DECIMATOR === 0) {
- // const c = document.getElementById('debug-console')
- // if (c) c.innerHTML = debugConsole.replace(/\n/g, ' ')
- // }
- }
-
- let remainingTimeOnCurrentPart: number | undefined = undefined
- let currentPartWillAutoNext = false
- if (currentAIndex >= 0) {
- const currentLivePart = parts[currentAIndex]
- const currentLivePartInstance = findPartInstanceInMapOrWrapToTemporary(partInstancesMap, currentLivePart)
-
- const lastStartedPlayback = currentLivePartInstance.timings?.startedPlayback
-
- let onAirPartDuration = currentLivePartInstance.timings?.duration || currentLivePart.expectedDuration || 0
- if (currentLivePart.displayDurationGroup) {
- onAirPartDuration = this.partExpectedDurations[unprotectString(currentLivePart._id)] || onAirPartDuration
- }
-
- remainingTimeOnCurrentPart = lastStartedPlayback
- ? now - (lastStartedPlayback + onAirPartDuration)
- : onAirPartDuration * -1
-
- currentPartWillAutoNext = !!(
- currentLivePart.autoNext &&
- (currentLivePart.expectedDuration !== undefined ? currentLivePart.expectedDuration !== 0 : false)
- )
- }
-
this.durations = Object.assign(
this.durations,
- literal({
- totalRundownDuration,
- remainingRundownDuration,
- asDisplayedRundownDuration,
- asPlayedRundownDuration,
- partCountdown: _.object(this.linearParts),
- partDurations: this.partDurations,
- partPlayed: this.partPlayed,
- partStartsAt: this.partStartsAt,
- partDisplayStartsAt: this.partDisplayStartsAt,
- partExpectedDurations: this.partExpectedDurations,
- partDisplayDurations: this.partDisplayDurations,
- currentTime: now,
- remainingTimeOnCurrentPart,
- currentPartWillAutoNext,
+ this.timingCalculator.updateDurations(
+ now,
isLowResolution,
- })
+ playlist,
+ parts,
+ partInstancesMap,
+ this.props.defaultDuration
+ )
)
}
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
index 023aa3baad..de4ace347a 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
@@ -7,12 +7,7 @@ import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/reac
import { Segments, SegmentId } from '../../../lib/collections/Segments'
import { Studio } from '../../../lib/collections/Studios'
import { SegmentTimeline, SegmentTimelineClass } from './SegmentTimeline'
-import {
- RundownTiming,
- computeSegmentDuration,
- TimingEvent,
- computeSegmentDisplayDuration,
-} from '../RundownView/RundownTiming/RundownTiming'
+import { RundownTiming, TimingEvent } from '../RundownView/RundownTiming/RundownTiming'
import { UIStateStorage } from '../../lib/UIStateStorage'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import {
@@ -48,9 +43,7 @@ import RundownViewEventBus, {
import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper'
import { checkPieceContentStatus, getNoteTypeForPieceStatus, ScanInfoForPackages } from '../../../lib/mediaObjects'
import { getBasicNotesForSegment } from '../../../lib/rundownNotifications'
-import { SegmentTimelinePartClass } from './SegmentTimelinePart'
-import { RundownAPI } from '../../../lib/api/rundown'
-import { Piece, Pieces } from '../../../lib/collections/Pieces'
+import { computeSegmentDuration } from '../../../lib/rundown/rundownTiming'
export const SIMULATED_PLAYBACK_SOFT_MARGIN = 0
export const SIMULATED_PLAYBACK_HARD_MARGIN = 2500
diff --git a/meteor/lib/rundown/__tests__/rundownTiming.test.ts b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
new file mode 100644
index 0000000000..0869c4efbb
--- /dev/null
+++ b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
@@ -0,0 +1,377 @@
+import { PartInstance } from '../../collections/PartInstances'
+import { DBPart, Part, PartId } from '../../collections/Parts'
+import { DBRundownPlaylist, RundownPlaylist } from '../../collections/RundownPlaylists'
+import { literal, protectString } from '../../lib'
+import { RundownTiming, RundownTimingCalculator } from '../rundownTiming'
+
+const DEFAULT_DURATION = 4000
+
+function makeMockPlaylist(): RundownPlaylist {
+ return new RundownPlaylist(
+ literal({
+ _id: protectString('mock-playlist'),
+ externalId: 'mock-playlist',
+ organizationId: protectString('test'),
+ studioId: protectString('studio0'),
+ name: 'Mock Playlist',
+ created: 0,
+ modified: 0,
+ currentPartInstanceId: null,
+ nextPartInstanceId: null,
+ previousPartInstanceId: null,
+ })
+ )
+}
+
+function makeMockPart(
+ id: string,
+ rank: number,
+ rundownId: string,
+ segmentId: string,
+ durations: { expectedDuration?: number; displayDuration?: number; displayDurationGroup?: string }
+): Part {
+ return new Part(
+ literal({
+ _id: protectString(id),
+ externalId: id,
+ title: '',
+ segmentId: protectString(segmentId),
+ _rank: rank,
+ rundownId: protectString(rundownId),
+ ...durations,
+ })
+ )
+}
+
+describe('rundown Timing Calculator', () => {
+ it('Provides output for empty playlist', () => {
+ const timing = new RundownTimingCalculator()
+ const playlist: RundownPlaylist = makeMockPlaylist()
+ const parts: Part[] = []
+ const partInstancesMap: Map = new Map()
+ const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ expect(result).toEqual(
+ literal({
+ isLowResolution: false,
+ asDisplayedRundownDuration: 0,
+ asPlayedRundownDuration: 0,
+ currentPartWillAutoNext: false,
+ currentTime: 0,
+ partCountdown: {},
+ partDisplayDurations: {},
+ partDisplayStartsAt: {},
+ partDurations: {},
+ partExpectedDurations: {},
+ partPlayed: {},
+ partStartsAt: {},
+ remainingRundownDuration: 0,
+ totalRundownDuration: 0,
+ })
+ )
+ })
+
+ it('Calculates time for unplayed playlist with start time and duration', () => {
+ const timing = new RundownTimingCalculator()
+ const playlist: RundownPlaylist = makeMockPlaylist()
+ playlist.expectedStart = 0
+ playlist.expectedDuration = 4000
+ const rundownId = 'rundown1'
+ const segmentId1 = 'segment1'
+ const segmentId2 = 'segment2'
+ const parts: Part[] = []
+ parts.push(makeMockPart('part1', 0, rundownId, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part4', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
+ const partInstancesMap: Map = new Map()
+ const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ expect(result).toEqual(
+ literal({
+ isLowResolution: false,
+ asDisplayedRundownDuration: 4000,
+ asPlayedRundownDuration: 4000,
+ currentPartWillAutoNext: false,
+ currentTime: 0,
+ partCountdown: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDisplayDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partDisplayStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partExpectedDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partPlayed: {
+ part1: 0,
+ part2: 0,
+ part3: 0,
+ part4: 0,
+ },
+ partStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ remainingRundownDuration: 4000,
+ totalRundownDuration: 4000,
+ })
+ )
+ })
+
+ it('Calculates time for unplayed playlist with end time and duration', () => {
+ const timing = new RundownTimingCalculator()
+ const playlist: RundownPlaylist = makeMockPlaylist()
+ playlist.expectedDuration = 4000
+ playlist.expectedEnd = 4000
+ const rundownId = 'rundown1'
+ const segmentId1 = 'segment1'
+ const segmentId2 = 'segment2'
+ const parts: Part[] = []
+ parts.push(makeMockPart('part1', 0, rundownId, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part2', 0, rundownId, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part4', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
+ const partInstancesMap: Map = new Map()
+ const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ expect(result).toEqual(
+ literal({
+ isLowResolution: false,
+ asDisplayedRundownDuration: 4000,
+ asPlayedRundownDuration: 4000,
+ currentPartWillAutoNext: false,
+ currentTime: 0,
+ partCountdown: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDisplayDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partDisplayStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partExpectedDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partPlayed: {
+ part1: 0,
+ part2: 0,
+ part3: 0,
+ part4: 0,
+ },
+ partStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ remainingRundownDuration: 4000,
+ totalRundownDuration: 4000,
+ })
+ )
+ })
+
+ it('Produces timing per rundown with start time and duration', () => {
+ const timing = new RundownTimingCalculator()
+ const playlist: RundownPlaylist = makeMockPlaylist()
+ playlist.expectedStart = 0
+ playlist.expectedDuration = 4000
+ const rundownId1 = 'rundown1'
+ const rundownId2 = 'rundown2'
+ const segmentId1 = 'segment1'
+ const segmentId2 = 'segment2'
+ const parts: Part[] = []
+ parts.push(makeMockPart('part1', 0, rundownId1, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part2', 0, rundownId1, segmentId1, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part3', 0, rundownId2, segmentId2, { expectedDuration: 1000 }))
+ parts.push(makeMockPart('part4', 0, rundownId2, segmentId2, { expectedDuration: 1000 }))
+ const partInstancesMap: Map = new Map()
+ const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ expect(result).toEqual(
+ literal({
+ isLowResolution: false,
+ asDisplayedRundownDuration: 4000,
+ asPlayedRundownDuration: 4000,
+ currentPartWillAutoNext: false,
+ currentTime: 0,
+ partCountdown: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDisplayDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partDisplayStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ partDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partExpectedDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partPlayed: {
+ part1: 0,
+ part2: 0,
+ part3: 0,
+ part4: 0,
+ },
+ partStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ remainingRundownDuration: 4000,
+ totalRundownDuration: 4000,
+ })
+ )
+ })
+
+ it('Handles display duration groups', () => {
+ const timing = new RundownTimingCalculator()
+ const playlist: RundownPlaylist = makeMockPlaylist()
+ playlist.expectedStart = 0
+ playlist.expectedDuration = 4000
+ const rundownId1 = 'rundown1'
+ const segmentId1 = 'segment1'
+ const segmentId2 = 'segment2'
+ const parts: Part[] = []
+ parts.push(
+ makeMockPart('part1', 0, rundownId1, segmentId1, {
+ expectedDuration: 1000,
+ displayDuration: 2000,
+ displayDurationGroup: 'test',
+ })
+ )
+ parts.push(
+ makeMockPart('part2', 0, rundownId1, segmentId1, {
+ expectedDuration: 1000,
+ displayDuration: 3000,
+ displayDurationGroup: 'test',
+ })
+ )
+ parts.push(
+ makeMockPart('part3', 0, rundownId1, segmentId2, {
+ expectedDuration: 1000,
+ displayDuration: 4000,
+ displayDurationGroup: 'test',
+ })
+ )
+ parts.push(
+ makeMockPart('part4', 0, rundownId1, segmentId2, {
+ expectedDuration: 1000,
+ displayDuration: 5000,
+ displayDurationGroup: 'test',
+ })
+ )
+ const partInstancesMap: Map = new Map()
+ const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ expect(result).toEqual(
+ literal({
+ isLowResolution: false,
+ asDisplayedRundownDuration: 4000,
+ asPlayedRundownDuration: 4000,
+ currentPartWillAutoNext: false,
+ currentTime: 0,
+ partCountdown: {
+ part1: 0,
+ part2: 2000,
+ part3: 5000,
+ part4: 9000,
+ },
+ partDisplayDurations: {
+ part1: 2000,
+ part2: 3000,
+ part3: 4000,
+ part4: 5000,
+ },
+ partDisplayStartsAt: {
+ part1: 0,
+ part2: 2000,
+ part3: 5000,
+ part4: 9000,
+ },
+ partDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partExpectedDurations: {
+ part1: 1000,
+ part2: 1000,
+ part3: 1000,
+ part4: 1000,
+ },
+ partPlayed: {
+ part1: 0,
+ part2: 0,
+ part3: 0,
+ part4: 0,
+ },
+ partStartsAt: {
+ part1: 0,
+ part2: 1000,
+ part3: 2000,
+ part4: 3000,
+ },
+ remainingRundownDuration: 4000,
+ totalRundownDuration: 4000,
+ })
+ )
+ })
+})
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
new file mode 100644
index 0000000000..1bec59d69e
--- /dev/null
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -0,0 +1,458 @@
+import _ from 'underscore'
+import {
+ findPartInstanceInMapOrWrapToTemporary,
+ PartInstance,
+ wrapPartToTemporaryInstance,
+} from '../collections/PartInstances'
+import { Part, PartId } from '../collections/Parts'
+import { RundownPlaylist } from '../collections/RundownPlaylists'
+import { unprotectString, literal, protectString } from '../lib'
+import { Settings } from '../Settings'
+
+// Minimum duration that a part can be assigned. Used by gap parts to allow them to "compress" to indicate time running out.
+const MINIMAL_NONZERO_DURATION = 1
+
+export class RundownTimingCalculator {
+ private temporaryPartInstances: Map = new Map()
+
+ private linearParts: Array<[PartId, number | null]> = []
+ // look at the comments on RundownTimingContext to understand what these do
+ private partDurations: Record = {}
+ private partExpectedDurations: Record = {}
+ private partPlayed: Record = {}
+ private partStartsAt: Record = {}
+ private partDisplayStartsAt: Record = {}
+ private partDisplayDurations: Record = {}
+ private partDisplayDurationsNoPlayback: Record = {}
+ private displayDurationGroups: Record = {}
+
+ updateDurations(
+ now: number,
+ isLowResolution: boolean,
+ playlist: RundownPlaylist | undefined,
+ parts: Part[],
+ partInstancesMap: Map,
+ /** Fallback duration for Parts that have no as-played duration of their own. */
+ defaultDuration?: number
+ ) {
+ let totalRundownDuration = 0
+ let remainingRundownDuration = 0
+ let asPlayedRundownDuration = 0
+ let asDisplayedRundownDuration = 0
+ let waitAccumulator = 0
+ let currentRemaining = 0
+ let startsAtAccumulator = 0
+ let displayStartsAtAccumulator = 0
+
+ Object.keys(this.displayDurationGroups).forEach((key) => delete this.displayDurationGroups[key])
+ this.linearParts.length = 0
+
+ let nextAIndex = -1
+ let currentAIndex = -1
+
+ if (playlist && parts) {
+ parts.forEach((origPart, itIndex) => {
+ const partInstance = this.getPartInstanceOrGetCachedTemp(partInstancesMap, origPart)
+
+ // add piece to accumulator
+ const aIndex = this.linearParts.push([partInstance.part._id, waitAccumulator]) - 1
+
+ // if this is next segementLine, clear previous countdowns and clear accumulator
+ if (playlist.nextPartInstanceId === partInstance._id) {
+ nextAIndex = aIndex
+ } else if (playlist.currentPartInstanceId === partInstance._id) {
+ currentAIndex = aIndex
+ }
+
+ const partCounts =
+ playlist.outOfOrderTiming ||
+ !playlist.activationId ||
+ (itIndex >= currentAIndex && currentAIndex >= 0) ||
+ (itIndex >= nextAIndex && nextAIndex >= 0 && currentAIndex === -1)
+
+ const partIsUntimed = partInstance.part.untimed || false
+
+ // expected is just a sum of expectedDurations
+ totalRundownDuration += partInstance.part.expectedDuration || 0
+
+ const lastStartedPlayback = partInstance.timings?.startedPlayback
+ const playOffset = partInstance.timings?.playOffset || 0
+
+ let partDuration = 0
+ let partExpectedDuration = 0
+ let partDisplayDuration = 0
+ let partDisplayDurationNoPlayback = 0
+
+ let displayDurationFromGroup = 0
+
+ partExpectedDuration = partInstance.part.expectedDuration || partInstance.timings?.duration || 0
+
+ // Display Duration groups are groups of two or more Parts, where some of them have an
+ // expectedDuration and some have 0.
+ // Then, some of them will have a displayDuration. The expectedDurations are pooled together, the parts with
+ // display durations will take up that much time in the Rundown. The left-over time from the display duration group
+ // will be used by Parts without expectedDurations.
+ let memberOfDisplayDurationGroup = false
+ // using a separate displayDurationGroup processing flag simplifies implementation
+ if (
+ partInstance.part.displayDurationGroup &&
+ // either this is not the first element of the displayDurationGroup
+ (this.displayDurationGroups[partInstance.part.displayDurationGroup] !== undefined ||
+ // or there is a following member of this displayDurationGroup
+ (parts[itIndex + 1] &&
+ parts[itIndex + 1].displayDurationGroup === partInstance.part.displayDurationGroup)) &&
+ !partInstance.part.floated &&
+ !partIsUntimed
+ ) {
+ this.displayDurationGroups[partInstance.part.displayDurationGroup] =
+ (this.displayDurationGroups[partInstance.part.displayDurationGroup] || 0) +
+ (partInstance.part.expectedDuration || 0)
+ displayDurationFromGroup =
+ partInstance.part.displayDuration ||
+ Math.max(
+ 0,
+ this.displayDurationGroups[partInstance.part.displayDurationGroup],
+ partInstance.part.gap
+ ? MINIMAL_NONZERO_DURATION
+ : defaultDuration || Settings.defaultDisplayDuration
+ )
+ partExpectedDuration =
+ partExpectedDuration || this.displayDurationGroups[partInstance.part.displayDurationGroup] || 0
+ memberOfDisplayDurationGroup = true
+ }
+
+ // This is where we actually calculate all the various variants of duration of a part
+ if (lastStartedPlayback && !partInstance.timings?.duration) {
+ // if duration isn't available, check if `takeOut` has already been set and use the difference
+ // between startedPlayback and takeOut as a temporary duration
+ const duration =
+ partInstance.timings?.duration ||
+ (partInstance.timings?.takeOut
+ ? lastStartedPlayback - partInstance.timings?.takeOut
+ : undefined)
+ currentRemaining = Math.max(
+ 0,
+ (duration ||
+ (memberOfDisplayDurationGroup
+ ? displayDurationFromGroup
+ : partInstance.part.expectedDuration) ||
+ 0) -
+ (now - lastStartedPlayback)
+ )
+ partDuration =
+ Math.max(duration || partInstance.part.expectedDuration || 0, now - lastStartedPlayback) -
+ playOffset
+ // because displayDurationGroups have no actual timing on them, we need to have a copy of the
+ // partDisplayDuration, but calculated as if it's not playing, so that the countdown can be
+ // calculated
+ partDisplayDurationNoPlayback =
+ duration ||
+ (memberOfDisplayDurationGroup
+ ? displayDurationFromGroup
+ : partInstance.part.expectedDuration) ||
+ defaultDuration ||
+ Settings.defaultDisplayDuration
+ partDisplayDuration = Math.max(partDisplayDurationNoPlayback, now - lastStartedPlayback)
+ this.partPlayed[unprotectString(partInstance.part._id)] = now - lastStartedPlayback
+ } else {
+ partDuration =
+ (partInstance.timings?.duration || partInstance.part.expectedDuration || 0) - playOffset
+ partDisplayDurationNoPlayback = Math.max(
+ 0,
+ (partInstance.timings?.duration && partInstance.timings?.duration + playOffset) ||
+ displayDurationFromGroup ||
+ partInstance.part.expectedDuration ||
+ defaultDuration ||
+ Settings.defaultDisplayDuration
+ )
+ partDisplayDuration = partDisplayDurationNoPlayback
+ this.partPlayed[unprotectString(partInstance.part._id)] =
+ (partInstance.timings?.duration || 0) - playOffset
+ }
+
+ // asPlayed is the actual duration so far and expected durations in unplayed lines.
+ // If item is onAir right now, it's duration is counted as expected duration or current
+ // playback duration whichever is larger.
+ // Parts that are Untimed are ignored always.
+ // Parts that don't count are ignored, unless they are being played or have been played.
+ if (!partIsUntimed) {
+ if (lastStartedPlayback && !partInstance.timings?.duration) {
+ asPlayedRundownDuration += Math.max(partExpectedDuration, now - lastStartedPlayback)
+ } else if (partInstance.timings?.duration) {
+ asPlayedRundownDuration += partInstance.timings.duration
+ } else if (partCounts) {
+ asPlayedRundownDuration += partInstance.part.expectedDuration || 0
+ }
+ }
+
+ // asDisplayed is the actual duration so far and expected durations in unplayed lines
+ // If item is onAir right now, it's duration is counted as expected duration or current
+ // playback duration whichever is larger.
+ // All parts are counted.
+ if (lastStartedPlayback && !partInstance.timings?.duration) {
+ asDisplayedRundownDuration += Math.max(
+ memberOfDisplayDurationGroup
+ ? Math.max(partExpectedDuration, partInstance.part.expectedDuration || 0)
+ : partInstance.part.expectedDuration || 0,
+ now - lastStartedPlayback
+ )
+ } else {
+ asDisplayedRundownDuration +=
+ partInstance.timings?.duration || partInstance.part.expectedDuration || 0
+ }
+
+ // the part is the current part but has not yet started playback
+ if (playlist.currentPartInstanceId === partInstance._id && !lastStartedPlayback) {
+ currentRemaining = partDisplayDuration
+ }
+
+ // Handle invalid parts by overriding the values to preset values for Invalid parts
+ if (partInstance.part.invalid && !partInstance.part.gap) {
+ partDisplayDuration = defaultDuration || Settings.defaultDisplayDuration
+ this.partPlayed[unprotectString(partInstance.part._id)] = 0
+ }
+
+ if (
+ memberOfDisplayDurationGroup &&
+ partInstance.part.displayDurationGroup &&
+ !partInstance.part.floated &&
+ !partInstance.part.invalid &&
+ !partIsUntimed &&
+ (partInstance.timings?.duration || partInstance.timings?.takeOut || partCounts)
+ ) {
+ this.displayDurationGroups[partInstance.part.displayDurationGroup] =
+ this.displayDurationGroups[partInstance.part.displayDurationGroup] - partDisplayDuration
+ }
+ const partInstancePartId = unprotectString(partInstance.part._id)
+ this.partExpectedDurations[partInstancePartId] = partExpectedDuration
+ this.partStartsAt[partInstancePartId] = startsAtAccumulator
+ this.partDisplayStartsAt[partInstancePartId] = displayStartsAtAccumulator
+ this.partDurations[partInstancePartId] = partDuration
+ this.partDisplayDurations[partInstancePartId] = partDisplayDuration
+ this.partDisplayDurationsNoPlayback[partInstancePartId] = partDisplayDurationNoPlayback
+ startsAtAccumulator += this.partDurations[partInstancePartId]
+ displayStartsAtAccumulator += this.partDisplayDurations[partInstancePartId] // || this.props.defaultDuration || 3000
+ // waitAccumulator is used to calculate the countdowns for Parts relative to the current Part
+ // always add the full duration, in case by some manual intervention this segment should play twice
+ if (memberOfDisplayDurationGroup) {
+ waitAccumulator +=
+ partInstance.timings?.duration || partDisplayDuration || partInstance.part.expectedDuration || 0
+ } else {
+ waitAccumulator += partInstance.timings?.duration || partInstance.part.expectedDuration || 0
+ }
+
+ // remaining is the sum of unplayed lines + whatever is left of the current segment
+ // if outOfOrderTiming is true, count parts before current part towards remaining rundown duration
+ // if false (default), past unplayed parts will not count towards remaining time
+ if (!lastStartedPlayback && !partInstance.part.floated && partCounts && !partIsUntimed) {
+ remainingRundownDuration += partExpectedDuration || 0
+ // item is onAir right now, and it's is currently shorter than expectedDuration
+ } else if (
+ lastStartedPlayback &&
+ !partInstance.timings?.duration &&
+ playlist.currentPartInstanceId === partInstance._id &&
+ lastStartedPlayback + partExpectedDuration > now &&
+ !partIsUntimed
+ ) {
+ remainingRundownDuration += partExpectedDuration - (now - lastStartedPlayback)
+ }
+ })
+
+ // This is where the waitAccumulator-generated data in the linearSegLines is used to calculate the countdowns.
+ let localAccum = 0
+ for (let i = 0; i < this.linearParts.length; i++) {
+ if (i < nextAIndex) {
+ // this is a line before next line
+ localAccum = this.linearParts[i][1] || 0
+ // only null the values if not looping, if looping, these will be offset by the countdown for the last part
+ if (!playlist.loop) {
+ this.linearParts[i][1] = null // we use null to express 'will not probably be played out, if played in order'
+ }
+ } else if (i === nextAIndex) {
+ // this is a calculation for the next line, which is basically how much there is left of the current line
+ localAccum = this.linearParts[i][1] || 0 // if there is no current line, rebase following lines to the next line
+ this.linearParts[i][1] = currentRemaining
+ } else {
+ // these are lines after next line
+ // we take whatever value this line has, subtract the value as set on the Next Part
+ // (note that the Next Part value will be using currentRemaining as the countdown)
+ // and add the currentRemaining countdown, since we are currentRemaining + diff between next and
+ // this away from this line.
+ this.linearParts[i][1] = (this.linearParts[i][1] || 0) - localAccum + currentRemaining
+ }
+ }
+ // contiunation of linearParts calculations for looping playlists
+ if (playlist.loop) {
+ for (let i = 0; i < nextAIndex; i++) {
+ // offset the parts before the on air line by the countdown for the end of the rundown
+ this.linearParts[i][1] =
+ (this.linearParts[i][1] || 0) + waitAccumulator - localAccum + currentRemaining
+ }
+ }
+
+ // if (this.refreshDecimator % LOW_RESOLUTION_TIMING_DECIMATOR === 0) {
+ // const c = document.getElementById('debug-console')
+ // if (c) c.innerHTML = debugConsole.replace(/\n/g, ' ')
+ // }
+ }
+
+ let remainingTimeOnCurrentPart: number | undefined = undefined
+ let currentPartWillAutoNext = false
+ if (currentAIndex >= 0) {
+ const currentLivePart = parts[currentAIndex]
+ const currentLivePartInstance = findPartInstanceInMapOrWrapToTemporary(partInstancesMap, currentLivePart)
+
+ const lastStartedPlayback = currentLivePartInstance.timings?.startedPlayback
+
+ let onAirPartDuration = currentLivePartInstance.timings?.duration || currentLivePart.expectedDuration || 0
+ if (currentLivePart.displayDurationGroup) {
+ onAirPartDuration =
+ this.partExpectedDurations[unprotectString(currentLivePart._id)] || onAirPartDuration
+ }
+
+ remainingTimeOnCurrentPart = lastStartedPlayback
+ ? now - (lastStartedPlayback + onAirPartDuration)
+ : onAirPartDuration * -1
+
+ currentPartWillAutoNext = !!(
+ currentLivePart.autoNext &&
+ (currentLivePart.expectedDuration !== undefined ? currentLivePart.expectedDuration !== 0 : false)
+ )
+ }
+
+ return literal({
+ totalRundownDuration,
+ remainingRundownDuration,
+ asDisplayedRundownDuration,
+ asPlayedRundownDuration,
+ partCountdown: _.object(this.linearParts),
+ partDurations: this.partDurations,
+ partPlayed: this.partPlayed,
+ partStartsAt: this.partStartsAt,
+ partDisplayStartsAt: this.partDisplayStartsAt,
+ partExpectedDurations: this.partExpectedDurations,
+ partDisplayDurations: this.partDisplayDurations,
+ currentTime: now,
+ remainingTimeOnCurrentPart,
+ currentPartWillAutoNext,
+ isLowResolution,
+ })
+ }
+
+ clearTempPartInstances() {
+ this.temporaryPartInstances.clear()
+ }
+
+ private getPartInstanceOrGetCachedTemp(partInstancesMap: Map, part: Part): PartInstance {
+ const origPartId = part._id
+ const partInstance = partInstancesMap.get(origPartId)
+ if (partInstance !== undefined) {
+ return partInstance
+ } else {
+ let tempPartInstance = this.temporaryPartInstances.get(origPartId)
+ if (tempPartInstance !== undefined) {
+ return tempPartInstance
+ } else {
+ tempPartInstance = wrapPartToTemporaryInstance(protectString(''), part)
+ this.temporaryPartInstances.set(origPartId, tempPartInstance)
+ return tempPartInstance
+ }
+ }
+ }
+}
+
+export namespace RundownTiming {
+ /**
+ * Events used by the RundownTimingProvider
+ * @export
+ * @enum {number}
+ */
+ export enum Events {
+ /** Event is emitted every now-and-then, generally to be used for simple displays */
+ 'timeupdate' = 'sofie:rundownTimeUpdate',
+ /** event is emitted with a very high frequency (60 Hz), to be used sparingly as
+ * hooking up Components to it will cause a lot of renders
+ */
+ 'timeupdateHR' = 'sofie:rundownTimeUpdateHR',
+ }
+
+ /**
+ * Context object that will be passed to listening components. The dictionaries use the Part ID as a key.
+ * @export
+ * @interface RundownTimingContext
+ */
+ export interface RundownTimingContext {
+ /** This is the total duration of the rundown as planned (using expectedDurations). */
+ totalRundownDuration?: number
+ /** This is the content remaining to be played in the rundown (based on the expectedDurations). */
+ remainingRundownDuration?: number
+ /** This is the total duration of the rundown: as planned for the unplayed (skipped & future) content, and as-run for the played-out. */
+ asDisplayedRundownDuration?: number
+ /** This is the complete duration of the rundown: as planned for the unplayed content, and as-run for the played-out, but ignoring unplayed/unplayable parts in order */
+ asPlayedRundownDuration?: number
+ /** this is the countdown to each of the parts relative to the current on air part. */
+ partCountdown?: Record
+ /** The calculated durations of each of the Parts: as-planned/as-run depending on state. */
+ partDurations?: Record
+ /** The offset of each of the Parts from the beginning of the Rundown. */
+ partStartsAt?: Record
+ /** Same as partStartsAt, but will include display duration overrides
+ * (such as minimal display width for an Part, etc.).
+ */
+ partDisplayStartsAt?: Record
+ /** Same as partDurations, but will include display duration overrides
+ * (such as minimal display width for an Part, etc.).
+ */
+ partDisplayDurations?: Record
+ /** As-played durations of each part. Will be 0, if not yet played.
+ * Will be counted from start to now if currently playing.
+ */
+ partPlayed?: Record
+ /** Expected durations of each of the parts or the as-played duration,
+ * if the Part does not have an expected duration.
+ */
+ partExpectedDurations?: Record
+ /** Remaining time on current part */
+ remainingTimeOnCurrentPart?: number | undefined
+ /** Current part will autoNext */
+ currentPartWillAutoNext?: boolean
+ /** Current time of this calculation */
+ currentTime?: number
+ /** Was this time context calculated during a high-resolution tick */
+ isLowResolution: boolean
+ }
+
+ /**
+ * This are the properties that will be injected by the withTiming HOC.
+ * @export
+ * @interface InjectedROTimingProps
+ */
+ export interface InjectedROTimingProps {
+ timingDurations: RundownTimingContext
+ }
+}
+
+/**
+ * Computes the actual (as-played fallbacking to expected) duration of a segment, consisting of given parts
+ * @export
+ * @param {RundownTiming.RundownTimingContext} timingDurations The timing durations calculated for the Rundown
+ * @param {Array} partIds The IDs of parts that are members of the segment
+ * @return number
+ */
+export function computeSegmentDuration(
+ timingDurations: RundownTiming.RundownTimingContext,
+ partIds: PartId[],
+ display?: boolean
+): number {
+ let partDurations = timingDurations.partDurations
+
+ if (partDurations === undefined) return 0
+
+ return partIds.reduce((memo, partId) => {
+ const pId = unprotectString(partId)
+ const partDuration =
+ (partDurations ? (partDurations[pId] !== undefined ? partDurations[pId] : 0) : 0) ||
+ (display ? Settings.defaultDisplayDuration : 0)
+ return memo + partDuration
+ }, 0)
+}
From f1ddbe5a6514b6a18b3dc028f08ad11d3f5dbb55 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 14 Jun 2021 10:53:00 +0100
Subject: [PATCH 018/112] feat: UI components for timing to next break
---
meteor/client/styles/rundownView.scss | 3 +-
.../client/ui/ClockView/PresenterScreen.tsx | 6 +-
meteor/client/ui/Prompter/OverUnderTimer.tsx | 9 +-
meteor/client/ui/RundownView.tsx | 233 ++++++++++++------
.../ui/RundownView/RundownDividerHeader.tsx | 34 ++-
.../client/ui/RundownView/RundownOverview.tsx | 2 +-
.../RundownTiming/RundownTiming.ts | 52 +---
.../RundownTiming/RundownTimingProvider.tsx | 6 +-
.../RundownView/RundownTiming/withTiming.tsx | 6 +-
.../ui/SegmentTimeline/SegmentTimeline.tsx | 3 +-
meteor/lib/collections/Rundowns.ts | 3 +
.../rundown/__tests__/rundownTiming.test.ts | 54 ++--
meteor/lib/rundown/rundownTiming.ts | 148 ++++++-----
.../blueprints-integration/src/rundown.ts | 4 +
14 files changed, 319 insertions(+), 244 deletions(-)
diff --git a/meteor/client/styles/rundownView.scss b/meteor/client/styles/rundownView.scss
index 9985695a66..1c4ad01c30 100644
--- a/meteor/client/styles/rundownView.scss
+++ b/meteor/client/styles/rundownView.scss
@@ -2530,7 +2530,8 @@ svg.icon {
}
> .rundown-divider-timeline__expected-start,
- > .rundown-divider-timeline__expected-duration {
+ > .rundown-divider-timeline__expected-duration,
+ > .rundown-divider-timeline__expected-end {
vertical-align: bottom;
margin: 1em 0.5em 0.5em;
diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx
index cea32acc2f..2a70fcc35d 100644
--- a/meteor/client/ui/ClockView/PresenterScreen.tsx
+++ b/meteor/client/ui/ClockView/PresenterScreen.tsx
@@ -318,9 +318,9 @@ export class PresenterScreenBase extends MeteorReactComponent<
const nextSegment = this.props.nextSegment
const overUnderClock = playlist.expectedDuration
- ? (this.props.timingDurations.asPlayedRundownDuration || 0) - playlist.expectedDuration
- : (this.props.timingDurations.asPlayedRundownDuration || 0) -
- (this.props.timingDurations.totalRundownDuration || 0)
+ ? (this.props.timingDurations.asDisplayedPlaylistDuration || 0) - playlist.expectedDuration
+ : (this.props.timingDurations.asDisplayedPlaylistDuration || 0) -
+ (this.props.timingDurations.totalPlaylistDuration || 0)
return (
diff --git a/meteor/client/ui/Prompter/OverUnderTimer.tsx b/meteor/client/ui/Prompter/OverUnderTimer.tsx
index a3bef12b3e..e38fbe67d1 100644
--- a/meteor/client/ui/Prompter/OverUnderTimer.tsx
+++ b/meteor/client/ui/Prompter/OverUnderTimer.tsx
@@ -15,17 +15,18 @@ interface IProps {
export const OverUnderTimer = withTiming
()(
class OverUnderTimer extends React.Component> {
render() {
- const target = this.props.rundownPlaylist.expectedDuration || this.props.timingDurations.totalRundownDuration || 0
+ const target =
+ this.props.rundownPlaylist.expectedDuration || this.props.timingDurations.totalPlaylistDuration || 0
return target ? (
target,
+ heavy: (this.props.timingDurations.totalPlaylistDuration || 0) <= target,
+ light: (this.props.timingDurations.totalPlaylistDuration || 0) > target,
})}
>
{RundownUtils.formatDiffToTimecode(
- (this.props.timingDurations.asPlayedRundownDuration || 0) - target,
+ (this.props.timingDurations.totalPlaylistDuration || 0) - target,
true,
false,
true,
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 7c7e998969..fb46103ee3 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -203,6 +203,7 @@ interface ITimingDisplayProps {
rundownPlaylist: RundownPlaylist
currentRundown: Rundown | undefined
rundownCount: number
+ isLastRundownInPlaylist: boolean
}
export enum RundownViewKbdShortcuts {
@@ -268,9 +269,8 @@ const TimingDisplay = withTranslation()(
)
}
-
render() {
- const { t, rundownPlaylist } = this.props
+ const { t, rundownPlaylist, currentRundown } = this.props
if (!rundownPlaylist) return null
@@ -286,6 +286,15 @@ const TimingDisplay = withTranslation()(
{t('Planned Start')}
+ ) : rundownPlaylist.expectedEnd && rundownPlaylist.expectedDuration ? (
+
+ {t('Expected Start')}
+
+
) : null}
{rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
rundownPlaylist.expectedStart ? (
@@ -339,68 +348,22 @@ const TimingDisplay = withTranslation()(
) : null}
)}
- {rundownPlaylist.expectedEnd || rundownPlaylist.expectedDuration ? (
+ {rundownPlaylist.expectedDuration ? (
- {!rundownPlaylist.loop && rundownPlaylist.expectedEnd ? (
-
- {t('Planned End')}
-
-
- ) : !rundownPlaylist.loop && rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
-
- {t('Planned End')}
-
-
+ {!rundownPlaylist.startedPlayback ||
+ this.props.isLastRundownInPlaylist ||
+ !currentRundown?.endIsBreak ? ( // TODO: Setting
+
) : null}
- {!rundownPlaylist.loop &&
- (rundownPlaylist.expectedEnd ? (
-
- {RundownUtils.formatDiffToTimecode(
- getCurrentTime() -
- rundownPlaylist.expectedEnd +
- (this.props.timingDurations.asPlayedRundownDuration ?? 0),
- true,
- true,
- true
- )}
-
- ) : rundownPlaylist.expectedStart && rundownPlaylist.expectedDuration ? (
-
- {RundownUtils.formatDiffToTimecode(
- getCurrentTime() - (rundownPlaylist.expectedStart + rundownPlaylist.expectedDuration),
- true,
- true,
- true
- )}
-
- ) : null)}
- {rundownPlaylist.expectedDuration ? (
-
- (rundownPlaylist.expectedDuration || 0),
- })}
- >
- {t('Diff')}
- {RundownUtils.formatDiffToTimecode(
- (this.props.timingDurations.asPlayedRundownDuration || 0) - rundownPlaylist.expectedDuration,
- true,
- false,
- true,
- true,
- true,
- undefined,
- true
- )}
-
+ {rundownPlaylist.startedPlayback &&
+ currentRundown?.endIsBreak &&
+ !this.props.isLastRundownInPlaylist ? ( // TODO: Setting // TODO: Find next break in higher-order component, so next breaks reflects next rundown in playlist marked as break.
+
) : null}
) : (
@@ -430,31 +393,25 @@ const TimingDisplay = withTranslation()(
) : null}
- {!rundownPlaylist.loop && this.props.rundownPlaylist.expectedEnd ? (
-
- {t('Planned End')}
-
-
- ) : null}
{this.props.timingDurations && this.props.rundownCount < 2 ? ( // TEMPORARY: disable the diff counter for playlists longer than one rundown -- Jan Starzak, 2021-05-06
- (this.props.timingDurations.totalRundownDuration || 0),
+ (this.props.timingDurations.asPlayedPlaylistDuration || 0) >
+ (this.props.timingDurations.totalPlaylistDuration || 0),
})}
>
{t('Diff')}
{RundownUtils.formatDiffToTimecode(
- (this.props.timingDurations.asPlayedRundownDuration || 0) -
- (this.props.timingDurations.totalRundownDuration || 0),
+ (this.props.timingDurations.asPlayedPlaylistDuration || 0) -
+ (this.props.timingDurations.totalPlaylistDuration || 0),
true,
false,
true,
@@ -474,6 +431,128 @@ const TimingDisplay = withTranslation()(
)
)
+interface IEndTimingProps {
+ loop?: boolean
+ expectedStart?: number
+ expectedDuration: number
+ expectedEnd?: number
+}
+
+const PlaylistEndTiming = withTranslation()(
+ withTiming()(
+ class PlaylistEndTiming extends React.Component>> {
+ render() {
+ let { t } = this.props
+
+ return (
+
+ {!this.props.loop && this.props.expectedStart ? (
+
+ {t('Planned End')}
+
+
+ ) : !this.props.loop && this.props.expectedEnd ? (
+
+ {t('Planned End')}
+
+
+ ) : null}
+ {!this.props.loop && this.props.expectedStart && this.props.expectedDuration ? (
+
+ {RundownUtils.formatDiffToTimecode(
+ getCurrentTime() - (this.props.expectedStart + this.props.expectedDuration),
+ true,
+ true,
+ true
+ )}
+
+ ) : !this.props.loop && this.props.expectedEnd ? (
+
+ {RundownUtils.formatDiffToTimecode(getCurrentTime() - this.props.expectedEnd, true, true, true)}
+
+ ) : null}
+ {this.props.expectedDuration ? (
+ (this.props.expectedDuration || 0),
+ })}
+ >
+ {t('Diff')}
+ {RundownUtils.formatDiffToTimecode(
+ (this.props.timingDurations.asPlayedPlaylistDuration || 0) - this.props.expectedDuration,
+ true,
+ false,
+ true,
+ true,
+ true,
+ undefined,
+ true
+ )}
+
+ ) : null}
+
+ )
+ }
+ }
+ )
+)
+
+interface INextBreakTimingProps {
+ loop?: boolean
+ breakRundown: Rundown
+}
+
+const NextBreakTiming = withTranslation()(
+ withTiming()(
+ class PlaylistEndTiming extends React.Component>> {
+ render() {
+ let { t, breakRundown } = this.props
+
+ const rundownAsPlayedDuration = this.props.timingDurations.rundownAsPlayedDurations
+ ? this.props.timingDurations.rundownAsPlayedDurations[unprotectString(breakRundown._id)]
+ : undefined
+
+ return (
+
+
+ {t('Next Break')}
+
+
+ {!this.props.loop && breakRundown.expectedEnd ? (
+
+ {RundownUtils.formatDiffToTimecode(getCurrentTime() - breakRundown.expectedEnd, true, true, true)}
+
+ ) : null}
+ {breakRundown.expectedDuration ? (
+ (breakRundown.expectedDuration || 0),
+ })}
+ >
+ {t('Diff')}
+ {RundownUtils.formatDiffToTimecode(
+ (rundownAsPlayedDuration || 0) - breakRundown.expectedDuration,
+ true,
+ false,
+ true,
+ true,
+ true,
+ undefined,
+ true
+ )}
+
+ ) : null}
+
+ )
+ }
+ }
+ )
+)
+
interface HotkeyDefinition {
key: string
label: string
@@ -1377,6 +1456,10 @@ const RundownHeader = withTranslation()(
rundownPlaylist={this.props.playlist}
currentRundown={this.props.currentRundown}
rundownCount={this.props.rundownIds.length}
+ isLastRundownInPlaylist={
+ !!this.props.currentRundown?._id &&
+ this.props.rundownIds.indexOf(this.props.currentRundown._id) === this.props.rundownIds.length - 1
+ }
/>
,
{}
@@ -30,15 +30,15 @@ const RundownCountdown = withTranslation()(
})(function RundownCountdown(
props: Translated<
WithTiming<{
- expectedStart: number | undefined
+ expectedStartOrEnd: number | undefined
className?: string | undefined
}>
>
) {
const { t } = props
- if (props.expectedStart === undefined) return null
+ if (props.expectedStartOrEnd === undefined) return null
- const time = props.expectedStart - (props.timingDurations.currentTime || 0)
+ const time = props.expectedStartOrEnd - (props.timingDurations.currentTime || 0)
if (time < QUATER_DAY) {
return (
@@ -70,7 +70,7 @@ export const RundownDividerHeader = withTranslation()(function RundownDividerHea
return (
{rundown.name}
-
{playlist.name}
+ {rundown.name !== playlist.name &&
{playlist.name} }
{rundown.expectedStart ? (
{t('Planned Start')}
@@ -85,7 +85,7 @@ export const RundownDividerHeader = withTranslation()(function RundownDividerHea
) : null}
@@ -95,6 +95,24 @@ export const RundownDividerHeader = withTranslation()(function RundownDividerHea
{RundownUtils.formatDiffToTimecode(rundown.expectedDuration, false, true, true, false, true)}
) : null}
+ {rundown.expectedEnd ? (
+
+ {t('Planned End')}
+
+ {rundown.expectedEnd}
+
+
+
+
+ ) : null}
)
})
diff --git a/meteor/client/ui/RundownView/RundownOverview.tsx b/meteor/client/ui/RundownView/RundownOverview.tsx
index 3f67fc1d1e..ee560cd3a7 100644
--- a/meteor/client/ui/RundownView/RundownOverview.tsx
+++ b/meteor/client/ui/RundownView/RundownOverview.tsx
@@ -221,7 +221,7 @@ export const RundownOverview = withTracker
- /** The calculated durations of each of the Parts: as-planned/as-run depending on state. */
- partDurations?: Record
- /** The offset of each of the Parts from the beginning of the Rundown. */
- partStartsAt?: Record
- /** Same as partStartsAt, but will include display duration overrides
- * (such as minimal display width for an Part, etc.).
- */
- partDisplayStartsAt?: Record
- /** Same as partDurations, but will include display duration overrides
- * (such as minimal display width for an Part, etc.).
- */
- partDisplayDurations?: Record
- /** As-played durations of each part. Will be 0, if not yet played.
- * Will be counted from start to now if currently playing.
- */
- partPlayed?: Record
- /** Expected durations of each of the parts or the as-played duration,
- * if the Part does not have an expected duration.
- */
- partExpectedDurations?: Record
- /** Remaining time on current part */
- remainingTimeOnCurrentPart?: number | undefined
- /** Current part will autoNext */
- currentPartWillAutoNext?: boolean
- /** Current time of this calculation */
- currentTime?: number
- /** Was this time context calculated during a high-resolution tick */
- isLowResolution: boolean
- }
-
/**
* This are the properties that will be injected by the withTiming HOC.
* @export
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
index fcfad1a16b..0edb581023 100644
--- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
@@ -9,7 +9,7 @@ import { MeteorReactComponent } from '../../../lib/MeteorReactComponent'
import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists'
import { PartInstance } from '../../../../lib/collections/PartInstances'
import { RundownTiming, TimeEventArgs } from './RundownTiming'
-import { RundownTimingCalculator } from '../../../../lib/rundown/rundownTiming'
+import { RundownTimingCalculator, RundownTimingContext } from '../../../../lib/rundown/rundownTiming'
const TIMING_DEFAULT_REFRESH_INTERVAL = 1000 / 60 // the interval for high-resolution events (timeupdateHR)
const LOW_RESOLUTION_TIMING_DECIMATOR = 15 // the low-resolution events will be called every
@@ -31,7 +31,7 @@ interface IRundownTimingProviderProps {
defaultDuration?: number
}
interface IRundownTimingProviderChildContext {
- durations: RundownTiming.RundownTimingContext
+ durations: RundownTimingContext
}
interface IRundownTimingProviderState {}
interface IRundownTimingProviderTrackedProps {
@@ -111,7 +111,7 @@ export const RundownTimingProvider = withTracker<
durations: PropTypes.object.isRequired,
}
- durations: RundownTiming.RundownTimingContext = {
+ durations: RundownTimingContext = {
isLowResolution: false,
}
refreshTimer: number
diff --git a/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
index 9b9934e803..e2e2865bdc 100644
--- a/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
@@ -2,8 +2,10 @@ import * as React from 'react'
import * as PropTypes from 'prop-types'
import * as _ from 'underscore'
import { RundownTiming } from './RundownTiming'
+import { JsxEmit } from 'typescript'
+import { RundownTimingContext } from '../../../../lib/rundown/rundownTiming'
-export type TimingFilterFunction = (durations: RundownTiming.RundownTimingContext) => any
+export type TimingFilterFunction = (durations: RundownTimingContext) => any
export interface WithTimingOptions {
isHighResolution?: boolean
@@ -92,7 +94,7 @@ export function withTiming(
}
render() {
- const durations: RundownTiming.RundownTimingContext = this.context.durations
+ const durations: RundownTimingContext = this.context.durations
// If the timing HOC is supposed to be low resolution and we are rendering
// during a high resolution tick, the WrappedComponent will render using
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
index 1fb6bf7099..695c5b783e 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
@@ -38,6 +38,7 @@ import { ZoomInIcon, ZoomOutIcon, ZoomShowAll } from '../../lib/segmentZoomIcon'
import { PartInstanceId } from '../../../lib/collections/PartInstances'
import { SegmentTimelineSmallPartFlag } from './SmallParts/SegmentTimelineSmallPartFlag'
import { UIStateStorage } from '../../lib/UIStateStorage'
+import { RundownTimingContext } from '../../../lib/rundown/rundownTiming'
interface IProps {
id: string
@@ -138,7 +139,7 @@ const SegmentTimelineZoom = class SegmentTimelineZoom extends React.Component<
calculateSegmentDuration(): number {
let total = 0
if (this.context && this.context.durations) {
- const durations = this.context.durations as RundownTiming.RundownTimingContext
+ const durations = this.context.durations as RundownTimingContext
this.props.parts.forEach((item) => {
// total += durations.partDurations ? durations.partDurations[item._id] : (item.duration || item.renderedDuration || 1)
const duration = Math.max(
diff --git a/meteor/lib/collections/Rundowns.ts b/meteor/lib/collections/Rundowns.ts
index df1335204d..2103d32e43 100644
--- a/meteor/lib/collections/Rundowns.ts
+++ b/meteor/lib/collections/Rundowns.ts
@@ -67,6 +67,8 @@ export interface DBRundown
/** External id of the Rundown Playlist to put this rundown in */
playlistExternalId?: string
+ /** Whether the end of the rundown marks a commercial break */
+ endIsBreak?: boolean
/** Name (user-facing) of the external NCS this rundown came from */
externalNRCSName: string
/** The id of the Rundown Playlist this rundown is in */
@@ -105,6 +107,7 @@ export class Rundown implements DBRundown {
public notifiedCurrentPlayingPartExternalId?: string
public notes?: Array
public playlistExternalId?: string
+ public endIsBreak?: boolean
public externalNRCSName: string
public playlistId: RundownPlaylistId
public playlistIdIsSetInSofie?: boolean
diff --git a/meteor/lib/rundown/__tests__/rundownTiming.test.ts b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
index 0869c4efbb..2e923b6c0a 100644
--- a/meteor/lib/rundown/__tests__/rundownTiming.test.ts
+++ b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
@@ -53,10 +53,11 @@ describe('rundown Timing Calculator', () => {
expect(result).toEqual(
literal({
isLowResolution: false,
- asDisplayedRundownDuration: 0,
- asPlayedRundownDuration: 0,
+ asDisplayedPlaylistDuration: 0,
+ asPlayedPlaylistDuration: 0,
currentPartWillAutoNext: false,
currentTime: 0,
+ rundownExpectedDurations: {},
partCountdown: {},
partDisplayDurations: {},
partDisplayStartsAt: {},
@@ -64,8 +65,8 @@ describe('rundown Timing Calculator', () => {
partExpectedDurations: {},
partPlayed: {},
partStartsAt: {},
- remainingRundownDuration: 0,
- totalRundownDuration: 0,
+ remainingPlaylistDuration: 0,
+ totalPlaylistDuration: 0,
})
)
})
@@ -88,10 +89,13 @@ describe('rundown Timing Calculator', () => {
expect(result).toEqual(
literal({
isLowResolution: false,
- asDisplayedRundownDuration: 4000,
- asPlayedRundownDuration: 4000,
+ asDisplayedPlaylistDuration: 4000,
+ asPlayedPlaylistDuration: 4000,
currentPartWillAutoNext: false,
currentTime: 0,
+ rundownExpectedDurations: {
+ [rundownId]: 4000,
+ },
partCountdown: {
part1: 0,
part2: 1000,
@@ -134,8 +138,8 @@ describe('rundown Timing Calculator', () => {
part3: 2000,
part4: 3000,
},
- remainingRundownDuration: 4000,
- totalRundownDuration: 4000,
+ remainingPlaylistDuration: 4000,
+ totalPlaylistDuration: 4000,
})
)
})
@@ -158,10 +162,13 @@ describe('rundown Timing Calculator', () => {
expect(result).toEqual(
literal({
isLowResolution: false,
- asDisplayedRundownDuration: 4000,
- asPlayedRundownDuration: 4000,
+ asDisplayedPlaylistDuration: 4000,
+ asPlayedPlaylistDuration: 4000,
currentPartWillAutoNext: false,
currentTime: 0,
+ rundownExpectedDurations: {
+ [rundownId]: 4000,
+ },
partCountdown: {
part1: 0,
part2: 1000,
@@ -204,8 +211,8 @@ describe('rundown Timing Calculator', () => {
part3: 2000,
part4: 3000,
},
- remainingRundownDuration: 4000,
- totalRundownDuration: 4000,
+ remainingPlaylistDuration: 4000,
+ totalPlaylistDuration: 4000,
})
)
})
@@ -229,10 +236,14 @@ describe('rundown Timing Calculator', () => {
expect(result).toEqual(
literal({
isLowResolution: false,
- asDisplayedRundownDuration: 4000,
- asPlayedRundownDuration: 4000,
+ asDisplayedPlaylistDuration: 4000,
+ asPlayedPlaylistDuration: 4000,
currentPartWillAutoNext: false,
currentTime: 0,
+ rundownExpectedDurations: {
+ [rundownId1]: 2000,
+ [rundownId2]: 2000,
+ },
partCountdown: {
part1: 0,
part2: 1000,
@@ -275,8 +286,8 @@ describe('rundown Timing Calculator', () => {
part3: 2000,
part4: 3000,
},
- remainingRundownDuration: 4000,
- totalRundownDuration: 4000,
+ remainingPlaylistDuration: 4000,
+ totalPlaylistDuration: 4000,
})
)
})
@@ -323,10 +334,13 @@ describe('rundown Timing Calculator', () => {
expect(result).toEqual(
literal({
isLowResolution: false,
- asDisplayedRundownDuration: 4000,
- asPlayedRundownDuration: 4000,
+ asDisplayedPlaylistDuration: 4000,
+ asPlayedPlaylistDuration: 4000,
currentPartWillAutoNext: false,
currentTime: 0,
+ rundownExpectedDurations: {
+ [rundownId1]: 4000,
+ },
partCountdown: {
part1: 0,
part2: 2000,
@@ -369,8 +383,8 @@ describe('rundown Timing Calculator', () => {
part3: 2000,
part4: 3000,
},
- remainingRundownDuration: 4000,
- totalRundownDuration: 4000,
+ remainingPlaylistDuration: 4000,
+ totalPlaylistDuration: 4000,
})
)
})
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index 1bec59d69e..d60bdcf2de 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -44,6 +44,9 @@ export class RundownTimingCalculator {
let startsAtAccumulator = 0
let displayStartsAtAccumulator = 0
+ let rundownExpectedDurations: Record = {}
+ let rundownAsPlayedDurations: Record = {}
+
Object.keys(this.displayDurationGroups).forEach((key) => delete this.displayDurationGroups[key])
this.linearParts.length = 0
@@ -176,12 +179,25 @@ export class RundownTimingCalculator {
// Parts that are Untimed are ignored always.
// Parts that don't count are ignored, unless they are being played or have been played.
if (!partIsUntimed) {
+ let valToAddToAsPlayedDuration = 0
+
if (lastStartedPlayback && !partInstance.timings?.duration) {
- asPlayedRundownDuration += Math.max(partExpectedDuration, now - lastStartedPlayback)
+ valToAddToAsPlayedDuration = Math.max(partExpectedDuration, now - lastStartedPlayback)
} else if (partInstance.timings?.duration) {
- asPlayedRundownDuration += partInstance.timings.duration
+ valToAddToAsPlayedDuration = partInstance.timings.duration
} else if (partCounts) {
- asPlayedRundownDuration += partInstance.part.expectedDuration || 0
+ valToAddToAsPlayedDuration = partInstance.part.expectedDuration || 0
+ }
+
+ asPlayedRundownDuration += valToAddToAsPlayedDuration
+ if (!rundownAsPlayedDurations[unprotectString(partInstance.part.rundownId)]) {
+ rundownAsPlayedDurations[
+ unprotectString(partInstance.part.rundownId)
+ ] = valToAddToAsPlayedDuration
+ } else {
+ rundownAsPlayedDurations[
+ unprotectString(partInstance.part.rundownId)
+ ] += valToAddToAsPlayedDuration
}
}
@@ -256,6 +272,12 @@ export class RundownTimingCalculator {
) {
remainingRundownDuration += partExpectedDuration - (now - lastStartedPlayback)
}
+
+ if (!rundownExpectedDurations[unprotectString(partInstance.part.rundownId)]) {
+ rundownExpectedDurations[unprotectString(partInstance.part.rundownId)] = partExpectedDuration
+ } else {
+ rundownExpectedDurations[unprotectString(partInstance.part.rundownId)] += partExpectedDuration
+ }
})
// This is where the waitAccumulator-generated data in the linearSegLines is used to calculate the countdowns.
@@ -320,11 +342,13 @@ export class RundownTimingCalculator {
)
}
- return literal({
- totalRundownDuration,
- remainingRundownDuration,
- asDisplayedRundownDuration,
- asPlayedRundownDuration,
+ return literal({
+ totalPlaylistDuration: totalRundownDuration,
+ remainingPlaylistDuration: remainingRundownDuration,
+ asDisplayedPlaylistDuration: asDisplayedRundownDuration,
+ asPlayedPlaylistDuration: asPlayedRundownDuration,
+ rundownExpectedDurations,
+ rundownAsPlayedDurations,
partCountdown: _.object(this.linearParts),
partDurations: this.partDurations,
partPlayed: this.partPlayed,
@@ -361,75 +385,49 @@ export class RundownTimingCalculator {
}
}
-export namespace RundownTiming {
- /**
- * Events used by the RundownTimingProvider
- * @export
- * @enum {number}
+export interface RundownTimingContext {
+ /** This is the total duration of the palylist as planned (using expectedDurations). */
+ totalPlaylistDuration?: number
+ /** This is the content remaining to be played in the playlist (based on the expectedDurations). */
+ remainingPlaylistDuration?: number
+ /** This is the total duration of the playlist: as planned for the unplayed (skipped & future) content, and as-run for the played-out. */
+ asDisplayedPlaylistDuration?: number
+ /** This is the complete duration of the playlist: as planned for the unplayed content, and as-run for the played-out, but ignoring unplayed/unplayable parts in order */
+ asPlayedPlaylistDuration?: number
+ /** Expected duration of each rundown in playlist (based on part expected durations) */
+ rundownExpectedDurations?: Record
+ /** This is the complete duration of each rundown: as planned for the unplayed content, and as-run for the played-out, but ignoring unplayed/unplayable parts in order */
+ rundownAsPlayedDurations?: Record
+ /** this is the countdown to each of the parts relative to the current on air part. */
+ partCountdown?: Record
+ /** The calculated durations of each of the Parts: as-planned/as-run depending on state. */
+ partDurations?: Record
+ /** The offset of each of the Parts from the beginning of the Playlist. */
+ partStartsAt?: Record
+ /** Same as partStartsAt, but will include display duration overrides
+ * (such as minimal display width for an Part, etc.).
*/
- export enum Events {
- /** Event is emitted every now-and-then, generally to be used for simple displays */
- 'timeupdate' = 'sofie:rundownTimeUpdate',
- /** event is emitted with a very high frequency (60 Hz), to be used sparingly as
- * hooking up Components to it will cause a lot of renders
- */
- 'timeupdateHR' = 'sofie:rundownTimeUpdateHR',
- }
-
- /**
- * Context object that will be passed to listening components. The dictionaries use the Part ID as a key.
- * @export
- * @interface RundownTimingContext
+ partDisplayStartsAt?: Record
+ /** Same as partDurations, but will include display duration overrides
+ * (such as minimal display width for an Part, etc.).
*/
- export interface RundownTimingContext {
- /** This is the total duration of the rundown as planned (using expectedDurations). */
- totalRundownDuration?: number
- /** This is the content remaining to be played in the rundown (based on the expectedDurations). */
- remainingRundownDuration?: number
- /** This is the total duration of the rundown: as planned for the unplayed (skipped & future) content, and as-run for the played-out. */
- asDisplayedRundownDuration?: number
- /** This is the complete duration of the rundown: as planned for the unplayed content, and as-run for the played-out, but ignoring unplayed/unplayable parts in order */
- asPlayedRundownDuration?: number
- /** this is the countdown to each of the parts relative to the current on air part. */
- partCountdown?: Record
- /** The calculated durations of each of the Parts: as-planned/as-run depending on state. */
- partDurations?: Record
- /** The offset of each of the Parts from the beginning of the Rundown. */
- partStartsAt?: Record
- /** Same as partStartsAt, but will include display duration overrides
- * (such as minimal display width for an Part, etc.).
- */
- partDisplayStartsAt?: Record
- /** Same as partDurations, but will include display duration overrides
- * (such as minimal display width for an Part, etc.).
- */
- partDisplayDurations?: Record
- /** As-played durations of each part. Will be 0, if not yet played.
- * Will be counted from start to now if currently playing.
- */
- partPlayed?: Record
- /** Expected durations of each of the parts or the as-played duration,
- * if the Part does not have an expected duration.
- */
- partExpectedDurations?: Record
- /** Remaining time on current part */
- remainingTimeOnCurrentPart?: number | undefined
- /** Current part will autoNext */
- currentPartWillAutoNext?: boolean
- /** Current time of this calculation */
- currentTime?: number
- /** Was this time context calculated during a high-resolution tick */
- isLowResolution: boolean
- }
-
- /**
- * This are the properties that will be injected by the withTiming HOC.
- * @export
- * @interface InjectedROTimingProps
+ partDisplayDurations?: Record
+ /** As-played durations of each part. Will be 0, if not yet played.
+ * Will be counted from start to now if currently playing.
*/
- export interface InjectedROTimingProps {
- timingDurations: RundownTimingContext
- }
+ partPlayed?: Record
+ /** Expected durations of each of the parts or the as-played duration,
+ * if the Part does not have an expected duration.
+ */
+ partExpectedDurations?: Record
+ /** Remaining time on current part */
+ remainingTimeOnCurrentPart?: number | undefined
+ /** Current part will autoNext */
+ currentPartWillAutoNext?: boolean
+ /** Current time of this calculation */
+ currentTime?: number
+ /** Was this time context calculated during a high-resolution tick */
+ isLowResolution: boolean
}
/**
@@ -440,7 +438,7 @@ export namespace RundownTiming {
* @return number
*/
export function computeSegmentDuration(
- timingDurations: RundownTiming.RundownTimingContext,
+ timingDurations: RundownTimingContext,
partIds: PartId[],
display?: boolean
): number {
diff --git a/packages/blueprints-integration/src/rundown.ts b/packages/blueprints-integration/src/rundown.ts
index 85da97b1ac..c22d0b5322 100644
--- a/packages/blueprints-integration/src/rundown.ts
+++ b/packages/blueprints-integration/src/rundown.ts
@@ -44,7 +44,11 @@ export interface IBlueprintRundown {
/** A hint to the Core that the Rundown should be a part of a playlist */
playlistExternalId?: string
+
+ /** Whether the end of the rundown marks a commercial break */
+ endIsBreak?: boolean
}
+
/** The Rundown sent from Core */
export interface IBlueprintRundownDB
extends IBlueprintRundown,
From e094c56d60c6eb9b043919e7c97547d2af44da56 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 15 Jun 2021 11:20:19 +0100
Subject: [PATCH 019/112] fix: Rebase fixes
---
.../ui/RundownView/RundownTiming/RundownTiming.ts | 9 +++++++++
.../ui/RundownView/RundownTiming/withTiming.tsx | 1 -
.../client/ui/SegmentTimeline/SegmentTimeline.tsx | 2 ++
.../ui/SegmentTimeline/SegmentTimelineContainer.tsx | 13 ++++++++-----
.../ui/SegmentTimeline/SegmentTimelinePart.tsx | 5 +++--
meteor/lib/rundown/__tests__/rundownTiming.test.ts | 12 ++++++------
meteor/lib/rundown/rundownTiming.ts | 12 +++++-------
7 files changed, 33 insertions(+), 21 deletions(-)
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts b/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
index 04482ae51b..94f03d0b57 100644
--- a/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownTiming.ts
@@ -1,4 +1,6 @@
import { RundownTimingContext } from '../../../../lib/rundown/rundownTiming'
+import { PartUi } from '../../SegmentTimeline/SegmentTimelineContainer'
+import { SegmentTimelinePartClass } from '../../SegmentTimeline/SegmentTimelinePart'
export interface TimeEventArgs {
currentTime: number
@@ -37,3 +39,10 @@ export namespace RundownTiming {
timingDurations: RundownTimingContext
}
}
+
+export function computeSegmentDisplayDuration(timingDurations: RundownTimingContext, parts: PartUi[]): number {
+ return parts.reduce(
+ (memo, part) => memo + SegmentTimelinePartClass.getPartDisplayDuration(part, timingDurations),
+ 0
+ )
+}
diff --git a/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
index e2e2865bdc..3d4aee7eff 100644
--- a/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/withTiming.tsx
@@ -2,7 +2,6 @@ import * as React from 'react'
import * as PropTypes from 'prop-types'
import * as _ from 'underscore'
import { RundownTiming } from './RundownTiming'
-import { JsxEmit } from 'typescript'
import { RundownTimingContext } from '../../../../lib/rundown/rundownTiming'
export type TimingFilterFunction = (durations: RundownTimingContext) => any
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
index 695c5b783e..b4053b9817 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
@@ -39,6 +39,8 @@ import { PartInstanceId } from '../../../lib/collections/PartInstances'
import { SegmentTimelineSmallPartFlag } from './SmallParts/SegmentTimelineSmallPartFlag'
import { UIStateStorage } from '../../lib/UIStateStorage'
import { RundownTimingContext } from '../../../lib/rundown/rundownTiming'
+import { PartInstanceId } from '../../../lib/collections/PartInstances'
+import { SegmentTimelineSmallPartFlag } from './SmallParts/SegmentTimelineSmallPartFlag'
interface IProps {
id: string
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
index de4ace347a..8f2b6ff9f6 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
@@ -7,7 +7,7 @@ import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/reac
import { Segments, SegmentId } from '../../../lib/collections/Segments'
import { Studio } from '../../../lib/collections/Studios'
import { SegmentTimeline, SegmentTimelineClass } from './SegmentTimeline'
-import { RundownTiming, TimingEvent } from '../RundownView/RundownTiming/RundownTiming'
+import { computeSegmentDisplayDuration, RundownTiming, TimingEvent } from '../RundownView/RundownTiming/RundownTiming'
import { UIStateStorage } from '../../lib/UIStateStorage'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import {
@@ -43,7 +43,10 @@ import RundownViewEventBus, {
import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper'
import { checkPieceContentStatus, getNoteTypeForPieceStatus, ScanInfoForPackages } from '../../../lib/mediaObjects'
import { getBasicNotesForSegment } from '../../../lib/rundownNotifications'
-import { computeSegmentDuration } from '../../../lib/rundown/rundownTiming'
+import { computeSegmentDuration, RundownTimingContext } from '../../../lib/rundown/rundownTiming'
+import { SegmentTimelinePartClass } from './SegmentTimelinePart'
+import { Piece, Pieces } from '../../../lib/collections/Pieces'
+import { RundownAPI } from '../../../lib/api/rundown'
export const SIMULATED_PLAYBACK_SOFT_MARGIN = 0
export const SIMULATED_PLAYBACK_HARD_MARGIN = 2500
@@ -723,7 +726,7 @@ export const SegmentTimelineContainer = translateWithTracker {
+ onGoToPartInner = (part: PartUi, timingDurations: RundownTimingContext, zoomInToFit?: boolean) => {
let newScale: number | undefined
let scrollLeft =
@@ -751,7 +754,7 @@ export const SegmentTimelineContainer = translateWithTracker {
if (this.props.segmentId === e.segmentId) {
- const timingDurations = this.context?.durations as RundownTiming.RundownTimingContext
+ const timingDurations = this.context?.durations as RundownTimingContext
const part = this.props.parts.find((part) => part.partId === e.partId)
if (part) {
@@ -762,7 +765,7 @@ export const SegmentTimelineContainer = translateWithTracker {
if (this.props.segmentId === e.segmentId) {
- const timingDurations = this.context?.durations as RundownTiming.RundownTimingContext
+ const timingDurations = this.context?.durations as RundownTimingContext
const part = this.props.parts.find((part) => part.instance._id === e.partInstanceId)
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelinePart.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelinePart.tsx
index 6da865e9b9..a0f02e850c 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelinePart.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelinePart.tsx
@@ -36,6 +36,7 @@ import { SegmentEnd } from '../../lib/ui/icons/segment'
import { getShowHiddenSourceLayers } from '../../lib/localStorage'
import { Part } from '../../../lib/collections/Parts'
import { TFunction } from 'i18next'
+import { RundownTimingContext } from '../../../lib/rundown/rundownTiming'
export const SegmentTimelineLineElementId = 'rundown__segment__line__'
export const SegmentTimelinePartElementId = 'rundown__segment__part__'
@@ -688,7 +689,7 @@ export class SegmentTimelinePartClass extends React.Component((props: IProps) => {
return {
isHighResolution: false,
- filter: (durations: RundownTiming.RundownTimingContext) => {
+ filter: (durations: RundownTimingContext) => {
durations = durations || {}
const partId = unprotectString(props.part.instance.part._id)
diff --git a/meteor/lib/rundown/__tests__/rundownTiming.test.ts b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
index 2e923b6c0a..434b529382 100644
--- a/meteor/lib/rundown/__tests__/rundownTiming.test.ts
+++ b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
@@ -2,7 +2,7 @@ import { PartInstance } from '../../collections/PartInstances'
import { DBPart, Part, PartId } from '../../collections/Parts'
import { DBRundownPlaylist, RundownPlaylist } from '../../collections/RundownPlaylists'
import { literal, protectString } from '../../lib'
-import { RundownTiming, RundownTimingCalculator } from '../rundownTiming'
+import { RundownTimingCalculator, RundownTimingContext } from '../rundownTiming'
const DEFAULT_DURATION = 4000
@@ -51,7 +51,7 @@ describe('rundown Timing Calculator', () => {
const partInstancesMap: Map = new Map()
const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
expect(result).toEqual(
- literal({
+ literal({
isLowResolution: false,
asDisplayedPlaylistDuration: 0,
asPlayedPlaylistDuration: 0,
@@ -87,7 +87,7 @@ describe('rundown Timing Calculator', () => {
const partInstancesMap: Map = new Map()
const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
expect(result).toEqual(
- literal({
+ literal({
isLowResolution: false,
asDisplayedPlaylistDuration: 4000,
asPlayedPlaylistDuration: 4000,
@@ -160,7 +160,7 @@ describe('rundown Timing Calculator', () => {
const partInstancesMap: Map = new Map()
const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
expect(result).toEqual(
- literal({
+ literal({
isLowResolution: false,
asDisplayedPlaylistDuration: 4000,
asPlayedPlaylistDuration: 4000,
@@ -234,7 +234,7 @@ describe('rundown Timing Calculator', () => {
const partInstancesMap: Map = new Map()
const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
expect(result).toEqual(
- literal({
+ literal({
isLowResolution: false,
asDisplayedPlaylistDuration: 4000,
asPlayedPlaylistDuration: 4000,
@@ -332,7 +332,7 @@ describe('rundown Timing Calculator', () => {
const partInstancesMap: Map = new Map()
const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
expect(result).toEqual(
- literal({
+ literal({
isLowResolution: false,
asDisplayedPlaylistDuration: 4000,
asPlayedPlaylistDuration: 4000,
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index d60bdcf2de..ec563e5070 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -191,13 +191,11 @@ export class RundownTimingCalculator {
asPlayedRundownDuration += valToAddToAsPlayedDuration
if (!rundownAsPlayedDurations[unprotectString(partInstance.part.rundownId)]) {
- rundownAsPlayedDurations[
- unprotectString(partInstance.part.rundownId)
- ] = valToAddToAsPlayedDuration
+ rundownAsPlayedDurations[unprotectString(partInstance.part.rundownId)] =
+ valToAddToAsPlayedDuration
} else {
- rundownAsPlayedDurations[
- unprotectString(partInstance.part.rundownId)
- ] += valToAddToAsPlayedDuration
+ rundownAsPlayedDurations[unprotectString(partInstance.part.rundownId)] +=
+ valToAddToAsPlayedDuration
}
}
@@ -433,7 +431,7 @@ export interface RundownTimingContext {
/**
* Computes the actual (as-played fallbacking to expected) duration of a segment, consisting of given parts
* @export
- * @param {RundownTiming.RundownTimingContext} timingDurations The timing durations calculated for the Rundown
+ * @param {RundownTimingContext} timingDurations The timing durations calculated for the Rundown
* @param {Array} partIds The IDs of parts that are members of the segment
* @return number
*/
From 162384e4c44d0a8fa108a5becd299f82b544db0c Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 19 Apr 2021 17:01:52 +0100
Subject: [PATCH 020/112] feat: Skeleton of rundown layout registry
---
meteor/lib/api/rundownLayouts.ts | 107 +++++++++++++++++++++++
meteor/lib/collections/RundownLayouts.ts | 17 +++-
2 files changed, 123 insertions(+), 1 deletion(-)
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 1560f24ab6..0c01e4ed75 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -11,6 +11,9 @@ import {
RundownLayoutAdLibRegion,
PieceDisplayStyle,
RundownLayoutPieceCountdown,
+ RundownViewLayout,
+ RundownLayoutTopBar,
+ LayoutFactory,
} from '../collections/RundownLayouts'
import { ShowStyleBaseId } from '../collections/ShowStyleBases'
import * as _ from 'underscore'
@@ -29,7 +32,107 @@ export enum RundownLayoutsAPIMethods {
'createRundownLayout' = 'rundownLayout.createRundownLayout',
}
+interface LayoutDescriptor {
+ factory: LayoutFactory
+ supportedElements: RundownLayoutElementType[]
+}
+
+class RundownLayoutsRegistry {
+ private shelfLayouts: Map> = new Map()
+ private rundownViewLayouts: Map> = new Map()
+ private miniShelfLayouts: Map> = new Map()
+ private topBarLayouts: Map> = new Map()
+
+ public RegisterShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) {
+ this.shelfLayouts.set(id, description)
+ }
+
+ public RegisterRundownViewLayout(id: RundownLayoutType, description: LayoutDescriptor) {
+ this.rundownViewLayouts.set(id, description)
+ }
+
+ public RegisterMiniShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) {
+ this.miniShelfLayouts.set(id, description)
+ }
+
+ public RegisterTopBarLayouts(id: RundownLayoutType, description: LayoutDescriptor) {
+ this.topBarLayouts.set(id, description)
+ }
+
+ public IsShelfLayout(id: RundownLayoutType) {
+ return this.shelfLayouts.has(id)
+ }
+
+ public IsRudownViewLayout(id: RundownLayoutType) {
+ return this.rundownViewLayouts.has(id)
+ }
+
+ public IsMiniShelfLayout(id: RundownLayoutType) {
+ return this.miniShelfLayouts.has(id)
+ }
+
+ public IsTopBarLayout(id: RundownLayoutType) {
+ return this.topBarLayouts.has(id)
+ }
+}
+
export namespace RundownLayoutsAPI {
+ const registry = new RundownLayoutsRegistry()
+ registry.RegisterShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
+ factory: { createLayout: () => undefined },
+ supportedElements: [
+ RundownLayoutElementType.ADLIB_REGION,
+ RundownLayoutElementType.EXTERNAL_FRAME,
+ RundownLayoutElementType.FILTER,
+ RundownLayoutElementType.PIECE_COUNTDOWN,
+ ],
+ })
+ registry.RegisterShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
+ factory: { createLayout: () => undefined },
+ supportedElements: [
+ RundownLayoutElementType.ADLIB_REGION,
+ RundownLayoutElementType.EXTERNAL_FRAME,
+ RundownLayoutElementType.FILTER,
+ RundownLayoutElementType.PIECE_COUNTDOWN,
+ ],
+ })
+ registry.RegisterMiniShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
+ factory: { createLayout: () => undefined },
+ supportedElements: [],
+ })
+ registry.RegisterMiniShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
+ factory: { createLayout: () => undefined },
+ supportedElements: [],
+ })
+ registry.RegisterRundownViewLayout(RundownLayoutType.RUNDOWN_VIEW_LAYOUT, {
+ factory: { createLayout: () => undefined },
+ supportedElements: [],
+ })
+ registry.RegisterTopBarLayouts(RundownLayoutType.TOP_BAR_LAYOUT, {
+ factory: { createLayout: () => undefined },
+ supportedElements: [],
+ })
+
+ export function IsLayoutForShelf(layout: RundownLayoutBase): layout is RundownLayoutBase {
+ return registry.IsShelfLayout(layout.type)
+ }
+
+ export function IsLayoutForRundownView(layout: RundownLayoutBase): layout is RundownLayoutBase {
+ return registry.IsRudownViewLayout(layout.type)
+ }
+
+ export function IsLayoutForMiniShelf(layout: RundownLayoutBase): layout is RundownLayoutBase {
+ return registry.IsMiniShelfLayout(layout.type)
+ }
+
+ export function IsLayoutForTopBar(layout: RundownLayoutBase): layout is RundownLayoutBase {
+ return registry.IsTopBarLayout(layout.type)
+ }
+
+ export function isRundownViewLayout(layout: RundownLayoutBase): layout is RundownViewLayout {
+ return layout.type === RundownLayoutType.RUNDOWN_VIEW_LAYOUT
+ }
+
export function isRundownLayout(layout: RundownLayoutBase): layout is RundownLayout {
return layout.type === RundownLayoutType.RUNDOWN_LAYOUT
}
@@ -38,6 +141,10 @@ export namespace RundownLayoutsAPI {
return layout.type === RundownLayoutType.DASHBOARD_LAYOUT
}
+ export function isTopBarLayout(layout: RundownLayoutBase): layout is RundownLayoutTopBar {
+ return layout.type === RundownLayoutType.TOP_BAR_LAYOUT
+ }
+
export function isFilter(element: RundownLayoutElementBase): element is RundownLayoutFilterBase {
return element.type === undefined || element.type === RundownLayoutElementType.FILTER
}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 3b1bd80536..31c92ee3d9 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -6,6 +6,10 @@ import { ShowStyleBaseId } from './ShowStyleBases'
import { UserId } from './Users'
import { registerIndex } from '../database'
+export interface LayoutFactory {
+ createLayout(...args: any[]): T | undefined
+}
+
/**
* The view targeted by this layout:
* RUNDOWN_LAYOUT: a Rundown view for highly scripted shows: a show split into Segments and Parts,
@@ -16,8 +20,10 @@ import { registerIndex } from '../database'
* @enum {string}
*/
export enum RundownLayoutType {
+ RUNDOWN_VIEW_LAYOUT = 'rundown_view_layout',
RUNDOWN_LAYOUT = 'rundown_layout',
DASHBOARD_LAYOUT = 'dashboard_layout',
+ TOP_BAR_LAYOUT = 'top_bar_layout',
}
/**
@@ -152,7 +158,7 @@ export interface RundownLayoutBase {
blueprintId?: BlueprintId
userId?: UserId
name: string
- type: RundownLayoutType.RUNDOWN_LAYOUT | RundownLayoutType.DASHBOARD_LAYOUT
+ type: RundownLayoutType
filters: RundownLayoutElementBase[]
exposeAsStandalone: boolean
exposeAsShelf: boolean
@@ -162,11 +168,20 @@ export interface RundownLayoutBase {
startingHeight?: number
}
+export interface RundownViewLayout extends RundownLayoutBase {
+ type: RundownLayoutType.RUNDOWN_VIEW_LAYOUT
+ expectedEndText: string
+}
+
export interface RundownLayout extends RundownLayoutBase {
type: RundownLayoutType.RUNDOWN_LAYOUT
filters: RundownLayoutElementBase[]
}
+export interface RundownLayoutTopBar extends RundownLayoutBase {
+ type: RundownLayoutType.TOP_BAR_LAYOUT
+}
+
export enum ActionButtonType {
TAKE = 'take',
HOLD = 'hold',
From 9bdfe2f1abf1bb8b90b67a797fd2ed324ad25bb8 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 10 May 2021 17:16:46 +0100
Subject: [PATCH 021/112] chore: Move setting for filters to separate component
for reuse
---
.../ui/Settings/RundownLayoutEditor.tsx | 76 +-
.../ui/Settings/components/FilterEditor.tsx | 1030 +++++++++++++++++
2 files changed, 1039 insertions(+), 67 deletions(-)
create mode 100644 meteor/client/ui/Settings/components/FilterEditor.tsx
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index ead902ac97..1471ab3410 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -5,32 +5,24 @@ import { EditAttribute } from '../../lib/EditAttribute'
import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data'
import { ShowStyleBase } from '../../../lib/collections/ShowStyleBases'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
-import { faStar, faUpload, faPlus, faCheck, faPencilAlt, faDownload, faTrash } from '@fortawesome/free-solid-svg-icons'
+import { faUpload, faPlus, faCheck, faPencilAlt, faDownload, faTrash } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
RundownLayouts,
- RundownLayout,
RundownLayoutType,
RundownLayoutBase,
RundownLayoutFilter,
PieceDisplayStyle,
- RundownLayoutFilterBase,
DashboardLayout,
ActionButtonType,
DashboardLayoutActionButton,
RundownLayoutElementType,
- RundownLayoutElementBase,
- RundownLayoutExternalFrame,
- RundownLayoutAdLibRegion,
- RundownLayoutAdLibRegionRole,
RundownLayoutId,
- RundownLayoutPieceCountdown,
} from '../../../lib/collections/RundownLayouts'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
import { PubSub } from '../../../lib/api/pubsub'
import { literal, unprotectString } from '../../../lib/lib'
import { Random } from 'meteor/random'
-import { SourceLayerType } from '@sofie-automation/blueprints-integration'
import { UploadButton } from '../../lib/uploadButton'
import { doModalDialog } from '../../lib/ModalDialog'
import { NotificationCenter, Notification, NoticeLevel } from '../../lib/notifications/notifications'
@@ -39,6 +31,7 @@ import { Studio } from '../../../lib/collections/Studios'
import { Link } from 'react-router-dom'
import { MeteorCall } from '../../../lib/api/methods'
import { defaultColorPickerPalette } from '../../lib/colorPicker'
+import FilterEditor from './components/FilterEditor'
export interface IProps {
showStyleBase: ShowStyleBase
@@ -136,13 +129,6 @@ export default translateWithTracker((props: IProp
})
}
- onToggleDefault = (item: RundownLayout, index: number, value: boolean) => {
- const obj = _.object(item.filters.map((item, i) => [`filters.${i}.default`, i === index ? value : false]))
- RundownLayouts.update(item._id, {
- $set: obj,
- })
- }
-
onRemoveButton = (item: RundownLayoutBase, button: DashboardLayoutActionButton) => {
RundownLayouts.update(item._id, {
$pull: {
@@ -153,16 +139,6 @@ export default translateWithTracker((props: IProp
})
}
- onRemoveElement = (item: RundownLayoutBase, filter: RundownLayoutElementBase) => {
- RundownLayouts.update(item._id, {
- $pull: {
- filters: {
- _id: filter._id,
- },
- },
- })
- }
-
isItemEdited = (layoutBase: RundownLayoutBase) => {
return this.state.editedItems.indexOf(layoutBase._id) >= 0
}
@@ -1336,47 +1312,13 @@ export default translateWithTracker((props: IProp
{t('There are no filters set up yet')}
) : null}
{item.filters.map((tab, index) => (
-
-
this.onRemoveElement(item, tab)}>
-
-
- {isRundownLayout && (
-
this.onToggleDefault(item as RundownLayout, index, !(tab as any).default)}
- >
-
-
- )}
-
-
-
- {t('Type')}
- (v === undefined ? RundownLayoutElementType.FILTER : v)}
- collection={RundownLayouts}
- className="input text-input input-l"
- >
-
-
-
- {RundownLayoutsAPI.isFilter(tab)
- ? this.renderFilter(item, tab, index, isRundownLayout, isDashboardLayout)
- : RundownLayoutsAPI.isExternalFrame(tab)
- ? this.renderFrame(item, tab, index, isRundownLayout, isDashboardLayout)
- : RundownLayoutsAPI.isAdLibRegion(tab)
- ? this.renderAdLibRegion(item, tab, index, isRundownLayout, isDashboardLayout)
- : RundownLayoutsAPI.isPieceCountdown(tab)
- ? this.renderPieceCountdown(item, tab, index, isRundownLayout, isDashboardLayout)
- : undefined}
-
+
))}
)
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
new file mode 100644
index 0000000000..154f3d0370
--- /dev/null
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -0,0 +1,1030 @@
+import { faTrash, faStar } from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import React from 'react'
+import ClassNames from 'classnames'
+import _ from 'underscore'
+import { RundownLayoutsAPI } from '../../../../lib/api/rundownLayouts'
+import {
+ PieceDisplayStyle,
+ RundownLayout,
+ RundownLayoutAdLibRegion,
+ RundownLayoutAdLibRegionRole,
+ RundownLayoutBase,
+ RundownLayoutElementBase,
+ RundownLayoutElementType,
+ RundownLayoutExternalFrame,
+ RundownLayoutFilterBase,
+ RundownLayoutPieceCountdown,
+ RundownLayouts,
+} from '../../../../lib/collections/RundownLayouts'
+import { EditAttribute } from '../../../lib/EditAttribute'
+import { MeteorReactComponent } from '../../../lib/MeteorReactComponent'
+import { Translated, translateWithTracker } from '../../../lib/ReactMeteorData/react-meteor-data'
+import { ShowStyleBase } from '../../../../lib/collections/ShowStyleBases'
+import { SourceLayerType } from '@sofie-automation/blueprints-integration'
+
+interface IProps {
+ item: RundownLayoutBase
+ filter: RundownLayoutElementBase
+ index: number
+ showStyleBase: ShowStyleBase
+}
+
+interface ITrackedProps {}
+
+interface IState {}
+
+export default translateWithTracker((props: IProps) => {
+ return {}
+})(
+ class FilterEditor extends MeteorReactComponent, IState> {
+ onToggleDefault = (item: RundownLayout, index: number, value: boolean) => {
+ const obj = _.object(item.filters.map((item, i) => [`filters.${i}.default`, i === index ? value : false]))
+ RundownLayouts.update(item._id, {
+ $set: obj,
+ })
+ }
+
+ onRemoveElement = (item: RundownLayoutBase, filter: RundownLayoutElementBase) => {
+ RundownLayouts.update(item._id, {
+ $pull: {
+ filters: {
+ _id: filter._id,
+ },
+ },
+ })
+ }
+
+ renderFilter(
+ item: RundownLayoutBase,
+ tab: RundownLayoutFilterBase,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ const isList = tab.displayStyle === PieceDisplayStyle.LIST
+ const rundownBaselineOptions = [
+ {
+ name: t('Yes'),
+ value: true,
+ },
+ {
+ name: t('No'),
+ value: false,
+ },
+ {
+ name: t('Only Match Global AdLibs'),
+ value: 'only',
+ },
+ ]
+
+ return (
+
+
+
+ {t('Name')}
+
+
+
+ {isDashboardLayout && (
+
+
+
+ {t('Display Style')}
+
+
+
+ {isList && (
+
+
+ {t('Show thumbnails next to list items')}
+
+
+
+ )}
+
+
+ {t('X')}
+
+
+
+
+
+ {t('Y')}
+
+
+
+
+
+ {t('Width')}
+
+
+
+
+
+ {t('Height')}
+
+
+
+ {!isList && (
+
+
+
+ {t('Button width scale factor')}
+
+
+
+
+
+ {t('Button height scale factor')}
+
+
+
+
+ )}
+
+ )}
+
+
+ {t('Display Rank')}
+
+
+
+
+
+ {t('Only Display AdLibs from Current Segment')}
+
+
+
+
+ {t('Include Global AdLibs')}
+
+
+ {isDashboardLayout && (
+
+
+
+ {t('Include Clear Source Layer in Ad-Libs')}
+
+
+
+
+ )}
+
+ {t('Source Layers')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+ {
+ return { name: l.name, value: l._id }
+ })}
+ type="multiselect"
+ label={t('Filter Disabled')}
+ collection={RundownLayouts}
+ className="input text-input input-l dropdown"
+ mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
+ />
+
+
+ {t('Source Layer Types')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+
+ v && v.length > 0 ? v.map((a) => parseInt(a, 10)) : undefined
+ }
+ />
+
+
+ {t('Output Channels')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+ {
+ return { name: l.name, value: l._id }
+ })}
+ type="multiselect"
+ label={t('Filter Disabled')}
+ collection={RundownLayouts}
+ className="input text-input input-l dropdown"
+ mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
+ />
+
+
+
+ {t('Label contains')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+ (v === undefined || v.length === 0 ? undefined : v.join(', '))}
+ mutateUpdateValue={(v) =>
+ v === undefined || v.length === 0 ? undefined : v.split(',').map((i) => i.trim())
+ }
+ />
+
+
+
+
+ {t('Tags must contain')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+ (v === undefined || v.length === 0 ? undefined : v.join(', '))}
+ mutateUpdateValue={(v) =>
+ v === undefined || v.length === 0 ? undefined : v.split(',').map((i) => i.trim())
+ }
+ />
+
+
+ {isDashboardLayout && (
+
+
+
+ {t('Register Shortcuts for this Panel')}
+
+
+
+
+
+ {t('Hide Panel from view')}
+
+
+
+
+ )}
+ {isDashboardLayout && (
+
+
+
+ {t('Show panel as a timeline')}
+
+
+
+
+ )}
+
+
+ {t('Enable search toolbar')}
+
+
+
+ {isDashboardLayout && (
+
+
+
+ {t('Include Clear Source Layer in Ad-Libs')}
+
+
+
+
+ )}
+ {isDashboardLayout && (
+
+
+
+ {t('Overflow horizontally')}
+
+
+
+
+
+ {t('Display Take buttons')}
+
+
+
+
+
+ {t('Queue all adlibs')}
+
+
+
+
+
+ {t('Toggle AdLibs on single mouse click')}
+
+
+
+
+ )}
+
+ )
+ }
+
+ renderFrame(
+ item: RundownLayoutBase,
+ tab: RundownLayoutExternalFrame,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+
+
+ {t('URL')}
+
+
+
+ {isDashboardLayout && (
+
+
+
+ {t('X')}
+
+
+
+
+
+ {t('Y')}
+
+
+
+
+
+ {t('Width')}
+
+
+
+
+
+ {t('Height')}
+
+
+
+
+
+ {t('Scale')}
+
+
+
+
+
+ {t('Display Rank')}
+
+
+
+
+ )}
+
+
+ {t('Scale')}
+
+
+
+
+ )
+ }
+
+ renderAdLibRegion(
+ item: RundownLayoutBase,
+ tab: RundownLayoutAdLibRegion,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+
+
+ {t('Role')}
+
+
+
+
+
+ {t('Adlib Rank')}
+
+
+
+
+
+ {t('Tags must contain')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+ (v === undefined || v.length === 0 ? undefined : v.join(', '))}
+ mutateUpdateValue={(v) =>
+ v === undefined || v.length === 0 ? undefined : v.split(',').map((i) => i.trim())
+ }
+ />
+
+
+
+
+ {t('Place label below panel')}
+
+
+
+ {isDashboardLayout && (
+
+
+
+ {t('X')}
+
+
+
+
+
+ {t('Y')}
+
+
+
+
+
+ {t('Width')}
+
+
+
+
+
+ {t('Height')}
+
+
+
+ {isDashboardLayout && (
+
+
+
+ {t('Register Shortcuts for this Panel')}
+
+
+
+
+ )}
+
+ )}
+
+ )
+ }
+
+ renderPieceCountdown(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPieceCountdown,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+
+ {t('Source Layers')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+ {
+ return { name: l.name, value: l._id }
+ })}
+ type="multiselect"
+ label={t('Disabled')}
+ collection={RundownLayouts}
+ className="input text-input input-l dropdown"
+ mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
+ />
+
+ {isDashboardLayout && (
+
+
+
+ {t('X')}
+
+
+
+
+
+ {t('Y')}
+
+
+
+
+
+ {t('Width')}
+
+
+
+
+
+ {t('Scale')}
+
+
+
+
+ )}
+
+ )
+ }
+
+ render() {
+ const { t } = this.props
+
+ const isRundownLayout = RundownLayoutsAPI.isRundownLayout(this.props.item)
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.item)
+
+ return (
+
+
this.onRemoveElement(this.props.item, this.props.filter)}
+ >
+
+
+ {isRundownLayout && (
+
+ this.onToggleDefault(
+ this.props.item as RundownLayout,
+ this.props.index,
+ !(this.props.filter as any).default
+ )
+ }
+ >
+
+
+ )}
+
+
+
+ {t('Type')}
+ (v === undefined ? RundownLayoutElementType.FILTER : v)}
+ collection={RundownLayouts}
+ className="input text-input input-l"
+ >
+
+
+
+ {RundownLayoutsAPI.isFilter(this.props.filter)
+ ? this.renderFilter(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
+ : RundownLayoutsAPI.isExternalFrame(this.props.filter)
+ ? this.renderFrame(this.props.item, this.props.filter, this.props.index, isRundownLayout, isDashboardLayout)
+ : RundownLayoutsAPI.isAdLibRegion(this.props.filter)
+ ? this.renderAdLibRegion(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
+ : RundownLayoutsAPI.isPieceCountdown(this.props.filter)
+ ? this.renderPieceCountdown(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
+ : undefined}
+
+ )
+ }
+ }
+)
From 72295d16c1de7ee49ce560509a3db26809463d7c Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 12 May 2021 13:04:53 +0100
Subject: [PATCH 022/112] feat: Describe rundown view and shelf layouts through
manifests
---
meteor/client/ui/RundownList.tsx | 9 +-
.../ui/RundownList/RundownListItemView.tsx | 5 +-
.../ui/RundownList/RundownPlaylistUi.tsx | 5 +-
.../RundownShelfLayoutSelection.tsx | 5 +-
meteor/client/ui/RundownView.tsx | 70 +++--
.../ui/Settings/RundownLayoutEditor.tsx | 295 ++++++++----------
.../ui/Settings/ShowStyleBaseSettings.tsx | 21 +-
.../rundownLayouts/ShelfLayoutSettings.tsx | 77 +++++
meteor/client/ui/Shelf/Shelf.tsx | 4 +-
meteor/lib/api/rundownLayouts.ts | 97 ++++--
meteor/lib/collections/RundownLayouts.ts | 28 +-
meteor/server/api/rundownLayouts.ts | 8 +-
12 files changed, 371 insertions(+), 253 deletions(-)
create mode 100644 meteor/client/ui/Settings/components/rundownLayouts/ShelfLayoutSettings.tsx
diff --git a/meteor/client/ui/RundownList.tsx b/meteor/client/ui/RundownList.tsx
index 0038423be2..01deee519d 100644
--- a/meteor/client/ui/RundownList.tsx
+++ b/meteor/client/ui/RundownList.tsx
@@ -32,6 +32,7 @@ import { RundownListFooter } from './RundownList/RundownListFooter'
import RundownPlaylistDragLayer from './RundownList/RundownPlaylistDragLayer'
import { RundownPlaylistUi } from './RundownList/RundownPlaylistUi'
import { doUserAction, UserAction } from '../lib/userAction'
+import { RundownLayoutsAPI } from '../../lib/api/rundownLayouts'
export enum ToolTipStep {
TOOLTIP_START_HERE = 'TOOLTIP_START_HERE',
@@ -306,10 +307,10 @@ export const RundownList = translateWithTracker((): IRundownsListProps => {
{this.props.rundownPlaylists.some(
(p) => !!p.expectedEnd || p.rundowns.some((r) => r.expectedEnd)
) && {t('Expected End Time')} }
- {t('Last Updated')}
- {this.props.rundownLayouts.some((l) => l.exposeAsShelf || l.exposeAsStandalone) && (
- {t('Shelf Layout')}
- )}
+ {t('Last updated')}
+ {this.props.rundownLayouts.some(
+ (l) => RundownLayoutsAPI.IsLayoutForShelf(l) && (l.exposeAsShelf || l.exposeAsStandalone)
+ ) && {t('Shelf Layout')} }
{this.renderRundownPlaylists(rundownPlaylists)}
diff --git a/meteor/client/ui/RundownList/RundownListItemView.tsx b/meteor/client/ui/RundownList/RundownListItemView.tsx
index a9362d6e05..d6edf0dfce 100644
--- a/meteor/client/ui/RundownList/RundownListItemView.tsx
+++ b/meteor/client/ui/RundownList/RundownListItemView.tsx
@@ -12,6 +12,7 @@ import { EyeIcon } from '../../lib/ui/icons/rundownList'
import { LoopingIcon } from '../../lib/ui/icons/looping'
import { RundownShelfLayoutSelection } from './RundownShelfLayoutSelection'
import { RundownLayoutBase } from '../../../lib/collections/RundownLayouts'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
interface IRundownListItemViewProps {
isActive: boolean
@@ -151,7 +152,9 @@ export default withTranslation()(function RundownListItemView(props: Translated<
- {rundownLayouts.some((l) => l.exposeAsShelf || l.exposeAsStandalone) && (
+ {rundownLayouts.some(
+ (l) => RundownLayoutsAPI.IsLayoutForShelf(l) && (l.exposeAsShelf || l.exposeAsStandalone)
+ ) && (
{isOnlyRundownInPlaylist && (
- {rundownLayouts.some((l) => l.exposeAsShelf || l.exposeAsStandalone) && (
+ {rundownLayouts.some(
+ (l) => RundownLayoutsAPI.IsLayoutForShelf(l) && (l.exposeAsShelf || l.exposeAsStandalone)
+ ) && (
layout.exposeAsStandalone)
+ .filter((layout) => RundownLayoutsAPI.IsLayoutForShelf(layout) && layout.exposeAsStandalone)
.map((layout) => {
return this.renderLinkItem(layout, getShelfLink(this.props.playlistId, layout._id), `standalone${layout._id}`)
})
const shelfLayouts = layoutsInRundown
- .filter((layout) => layout.exposeAsShelf)
+ .filter((layout) => RundownLayoutsAPI.IsLayoutForShelf(layout) && layout.exposeAsShelf)
.map((layout) => {
return this.renderLinkItem(
layout,
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index fb46103ee3..e0595c81fb 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -77,6 +77,9 @@ import {
RundownLayoutType,
RundownLayoutBase,
RundownLayoutId,
+ RundownViewLayout,
+ DashboardLayout,
+ RundownLayoutShelfBase,
} from '../../lib/collections/RundownLayouts'
import { VirtualElement } from '../lib/VirtualElement'
import { SEGMENT_TIMELINE_ELEMENT_ID } from './SegmentTimeline/SegmentTimeline'
@@ -97,6 +100,7 @@ import { memoizedIsolatedAutorun } from '../lib/reactiveData/reactiveDataHelper'
import RundownViewEventBus, { RundownViewEvents } from './RundownView/RundownViewEventBus'
import { LoopingIcon } from '../lib/ui/icons/looping'
import StudioPackageContainersContext from './RundownView/StudioPackageContainersContext'
+import { RundownLayoutsAPI } from '../../lib/api/rundownLayouts'
export const MAGIC_TIME_SCALE_FACTOR = 0.03
@@ -1520,7 +1524,8 @@ interface IState {
isInspectorShelfExpanded: boolean
isClipTrimmerOpen: boolean
selectedPiece: AdLibPieceUi | PieceUi | undefined
- rundownLayout: RundownLayoutBase | undefined
+ shelfLayout: RundownLayoutShelfBase | undefined
+ rundownViewLayout: RundownLayoutBase | undefined
currentRundown: Rundown | undefined
/** Tracks whether the user has resized the shelf to prevent using default shelf settings */
wasShelfResizedByUser: boolean
@@ -1543,7 +1548,9 @@ interface ITrackedProps {
rundownLayouts?: Array
buckets: Bucket[]
casparCGPlayoutDevices?: PeripheralDevice[]
- rundownLayoutId?: RundownLayoutId
+ shelfLayoutId?: RundownLayoutId
+ rundownViewLayoutId?: RundownLayoutId
+ orderedPartsIds: PartId[]
shelfDisplayOptions: {
buckets: boolean
layout: boolean
@@ -1642,7 +1649,9 @@ export const RundownView = translateWithTracker((
subType: TSR.DeviceType.CASPARCG,
}).fetch()) ||
undefined,
- rundownLayoutId: protectString((params['layout'] as string) || ''),
+ shelfLayoutId: protectString((params['layout'] as string) || (params['shelfLayout'] as string) || ''), // 'layout' kept for backwards compatibility
+ rundownViewLayoutId: protectString((params['rundownViewLayout'] as string) || ''),
+ orderedPartsIds: allParts,
shelfDisplayOptions: {
buckets: displayOptions.includes('buckets'),
layout: displayOptions.includes('layout'),
@@ -1694,7 +1703,12 @@ export const RundownView = translateWithTracker((
})
}
- const rundownLayout = this.props.rundownLayouts?.find((layout) => layout._id === this.props.rundownLayoutId)
+ const shelfLayout = this.props.rundownLayouts?.find((layout) => layout._id === this.props.shelfLayoutId)
+ let isInspectorShelfExpanded = false
+
+ if (shelfLayout && RundownLayoutsAPI.IsLayoutForShelf(shelfLayout)) {
+ isInspectorShelfExpanded = shelfLayout.openByDefault
+ }
this.state = {
timeScale: MAGIC_TIME_SCALE_FACTOR * Settings.defaultTimeScale,
@@ -1717,39 +1731,51 @@ export const RundownView = translateWithTracker((
]),
isNotificationsCenterOpen: undefined,
isSupportPanelOpen: false,
- isInspectorShelfExpanded: rundownLayout?.openByDefault ?? false,
+ isInspectorShelfExpanded,
isClipTrimmerOpen: false,
selectedPiece: undefined,
- rundownLayout: undefined,
+ shelfLayout: undefined,
+ rundownViewLayout: undefined,
currentRundown: undefined,
wasShelfResizedByUser: false,
}
}
static getDerivedStateFromProps(props: Translated): Partial {
- let selectedLayout: RundownLayoutBase | undefined = undefined
+ let selectedShelfLayout: RundownLayoutBase | undefined = undefined
+ let selectedViewLayout: RundownLayoutBase | undefined = undefined
if (props.rundownLayouts) {
// first try to use the one selected by the user
- if (props.rundownLayoutId) {
- selectedLayout = props.rundownLayouts.find((i) => i._id === props.rundownLayoutId)
+ if (props.shelfLayoutId) {
+ selectedShelfLayout = props.rundownLayouts.find((i) => i._id === props.shelfLayoutId)
+ }
+
+ if (props.rundownViewLayoutId) {
+ selectedViewLayout = props.rundownLayouts.find((i) => i._id === props.rundownViewLayoutId)
}
// if couldn't find based on id, try matching part of the name
- if (props.rundownLayoutId && !selectedLayout) {
- selectedLayout = props.rundownLayouts.find(
- (i) => i.name.indexOf(unprotectString(props.rundownLayoutId!)) >= 0
+ if (props.shelfLayoutId && !selectedShelfLayout) {
+ selectedShelfLayout = props.rundownLayouts.find(
+ (i) => i.name.indexOf(unprotectString(props.shelfLayoutId!)) >= 0
+ )
+ }
+
+ if (props.rundownViewLayoutId && !selectedViewLayout) {
+ selectedViewLayout = props.rundownLayouts.find(
+ (i) => i.name.indexOf(unprotectString(props.rundownViewLayoutId!)) >= 0
)
}
// if not, try the first RUNDOWN_LAYOUT available
- if (!selectedLayout) {
- selectedLayout = props.rundownLayouts.find((i) => i.type === RundownLayoutType.RUNDOWN_LAYOUT)
+ if (!selectedShelfLayout) {
+ selectedShelfLayout = props.rundownLayouts.find((i) => i.type === RundownLayoutType.RUNDOWN_LAYOUT)
}
// if still not found, use the first one
- if (!selectedLayout) {
- selectedLayout = props.rundownLayouts[0]
+ if (!selectedShelfLayout) {
+ selectedShelfLayout = props.rundownLayouts[0]
}
}
@@ -1762,7 +1788,11 @@ export const RundownView = translateWithTracker((
}
return {
- rundownLayout: selectedLayout,
+ shelfLayout:
+ selectedShelfLayout && RundownLayoutsAPI.IsLayoutForShelf(selectedShelfLayout)
+ ? selectedShelfLayout
+ : undefined,
+ rundownViewLayout: selectedViewLayout,
currentRundown,
}
}
@@ -2847,7 +2877,7 @@ export const RundownView = translateWithTracker((
buckets={this.props.buckets}
isExpanded={
this.state.isInspectorShelfExpanded ||
- (!this.state.wasShelfResizedByUser && this.state.rundownLayout?.openByDefault)
+ (!this.state.wasShelfResizedByUser && this.state.shelfLayout?.openByDefault)
}
onChangeExpanded={this.onShelfChangeExpanded}
hotkeys={this.state.usedHotkeys}
@@ -2856,7 +2886,7 @@ export const RundownView = translateWithTracker((
studioMode={this.state.studioMode}
onChangeBottomMargin={this.onChangeBottomMargin}
onRegisterHotkeys={this.onRegisterHotkeys}
- rundownLayout={this.state.rundownLayout}
+ rundownLayout={this.state.shelfLayout}
shelfDisplayOptions={this.props.shelfDisplayOptions}
bucketDisplayFilter={this.props.bucketDisplayFilter}
studio={this.props.studio}
@@ -2905,7 +2935,7 @@ export const RundownView = translateWithTracker((
studioMode={this.state.studioMode}
onChangeBottomMargin={this.onChangeBottomMargin}
onRegisterHotkeys={this.onRegisterHotkeys}
- rundownLayout={this.state.rundownLayout}
+ rundownLayout={this.state.shelfLayout}
studio={this.props.studio}
fullViewport={true}
shelfDisplayOptions={this.props.shelfDisplayOptions}
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index 1471ab3410..0b8a48e51b 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -19,7 +19,11 @@ import {
RundownLayoutElementType,
RundownLayoutId,
} from '../../../lib/collections/RundownLayouts'
-import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import {
+ CustomizableRegionLayout,
+ CustomizableRegionSettingsManifest,
+ RundownLayoutsAPI,
+} from '../../../lib/api/rundownLayouts'
import { PubSub } from '../../../lib/api/pubsub'
import { literal, unprotectString } from '../../../lib/lib'
import { Random } from 'meteor/random'
@@ -32,10 +36,12 @@ import { Link } from 'react-router-dom'
import { MeteorCall } from '../../../lib/api/methods'
import { defaultColorPickerPalette } from '../../lib/colorPicker'
import FilterEditor from './components/FilterEditor'
+import ShelfLayoutSettings from './components/rundownLayouts/ShelfLayoutSettings'
export interface IProps {
showStyleBase: ShowStyleBase
studios: Studio[]
+ customRegion: CustomizableRegionSettingsManifest
}
interface IState {
@@ -45,16 +51,21 @@ interface IState {
interface ITrackedProps {
rundownLayouts: RundownLayoutBase[]
+ layoutTypes: RundownLayoutType[]
}
export default translateWithTracker((props: IProps) => {
+ const layoutTypes = props.customRegion.layouts.map((l) => l.type)
+
const rundownLayouts = RundownLayouts.find({
showStyleBaseId: props.showStyleBase._id,
userId: { $exists: false },
+ type: { $in: layoutTypes },
}).fetch()
return {
rundownLayouts,
+ layoutTypes,
}
})(
class RundownLayoutEditor extends MeteorReactComponent, IState> {
@@ -76,7 +87,7 @@ export default translateWithTracker((props: IProp
onAddLayout = () => {
const { t, showStyleBase } = this.props
MeteorCall.rundownLayout
- .createRundownLayout(t('New Layout'), RundownLayoutType.RUNDOWN_LAYOUT, showStyleBase._id)
+ .createRundownLayout(t('New Layout'), this.props.layoutTypes[0], showStyleBase._id)
.catch(console.error)
}
@@ -101,15 +112,19 @@ export default translateWithTracker((props: IProp
onAddElement = (item: RundownLayoutBase) => {
const { t } = this.props
- const isRundownLayout = RundownLayoutsAPI.isRundownLayout(item)
- const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(item)
+ const layout = this.props.customRegion.layouts.find((l) => l.type === item.type)
+ const filtersTitle = layout?.filtersTitle ? t(layout.filtersTitle) : t('New Filter')
+
+ if (!layout?.supportedElements.length) {
+ return
+ }
RundownLayouts.update(item._id, {
$push: {
filters: literal({
_id: Random.id(),
type: RundownLayoutElementType.FILTER,
- name: isRundownLayout ? t('New Tab') : isDashboardLayout ? t('New Panel') : t('New Item'),
+ name: filtersTitle,
currentSegment: false,
displayStyle: PieceDisplayStyle.BUTTONS,
label: undefined,
@@ -1220,39 +1235,10 @@ export default translateWithTracker((props: IProp
renderElements(item: RundownLayoutBase) {
const { t } = this.props
- const isRundownLayout = RundownLayoutsAPI.isRundownLayout(item)
- const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(item)
+ const isShelfLayout = RundownLayoutsAPI.IsLayoutForShelf(item)
return (
-
-
- {t('Expose layout as a standalone page')}
-
-
-
-
-
- {t('Expose as a layout for the shelf')}
-
-
-
{t('Icon')}
@@ -1280,34 +1266,8 @@ export default translateWithTracker((props: IProp
>
-
-
- {t('Open shelf by default')}
-
-
-
-
-
- {t('Default shelf height')}
-
-
-
- {isRundownLayout ? t('Tabs') : isDashboardLayout ? t('Panels') : null}
+ {isShelfLayout && }
+ {layout?.filtersTitle ? t(`${layout?.filtersTitle}`) : t('Filters')}
{item.filters.length === 0 ? (
{t('There are no filters set up yet')}
) : null}
@@ -1326,118 +1286,113 @@ export default translateWithTracker((props: IProp
renderItems() {
const { t } = this.props
- return (this.props.rundownLayouts || []).map((item) => (
-
-
- {item.name || t('Default Layout')}
- {item.type}
-
- {this.props.studios.map((studio) => (
-
-
- {studio.name}
-
-
- ))}
-
-
- this.downloadItem(item)}>
-
-
- this.editItem(item)}>
-
-
- this.onDeleteLayout(e, item)}>
-
-
-
-
- {this.isItemEdited(item) && (
-
-
-
-
-
- {t('Name')}
-
-
-
-
-
- {t('Type')}
-
-
+ return (this.props.rundownLayouts || []).map((item, index) => {
+ const layout = this.props.customRegion.layouts.find((l) => l.type === item.type)
+ return (
+
+
+ {item.name || t('Default Layout')}
+ {item.type}
+
+ {this.props.studios.map((studio) => (
+
+
+ {studio.name}
+
+
+ ))}
+
+
+ this.downloadItem(item)}>
+
+
+ this.editItem(item)}>
+
+
+ this.onDeleteLayout(e, item)}>
+
+
+
+
+ {this.isItemEdited(item) && (
+
+
+
+
+
+ {t('Name')}
+
+
+
+
+
+ {t('Type')}
+
+
+
-
-
- {item.type === RundownLayoutType.RUNDOWN_LAYOUT
- ? this.renderElements(item)
- : item.type === RundownLayoutType.DASHBOARD_LAYOUT
- ? this.renderElements(item)
- : null}
-
-
- this.onAddElement(item)}>
-
-
- {item.type === RundownLayoutType.RUNDOWN_LAYOUT
- ? t('Add tab')
- : item.type === RundownLayoutType.DASHBOARD_LAYOUT
- ? t('Add panel')
- : null}
-
-
- {item.type === RundownLayoutType.DASHBOARD_LAYOUT ? (
- <>
- {RundownLayoutsAPI.isDashboardLayout(item) ? this.renderActionButtons(item) : null}
+ {this.renderElements(item, layout)}
+ {layout?.supportedElements.length ? (
- this.finishEditItem(item)}>
-
-
- this.onAddButton(item)}>
+ this.onAddElement(item)}>
- {t('Add button')}
-
-
- >
- ) : (
- <>
-
- this.finishEditItem(item)}>
-
+ {t(`Add ${layout?.filtersTitle?.toLowerCase() ?? 'filter'}`)}
- >
- )}
-
-
- )}
-
- ))
+ ) : null}
+ {item.type === RundownLayoutType.DASHBOARD_LAYOUT ? (
+ <>
+
{RundownLayoutsAPI.isDashboardLayout(item) ? this.renderActionButtons(item) : null}
+
+ this.finishEditItem(item)}>
+
+
+ this.onAddButton(item)}>
+
+
+ {t('Add button')}
+
+
+ >
+ ) : (
+ <>
+
+ this.finishEditItem(item)}>
+
+
+
+ >
+ )}
+
+
+ )}
+
+ )
+ })
}
onUploadFile(e) {
@@ -1518,7 +1473,7 @@ export default translateWithTracker((props: IProp
return (
-
{t('Shelf Layouts')}
+
{this.props.customRegion.title}
diff --git a/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx b/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx
index a7c4d44837..f79b24defa 100644
--- a/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx
+++ b/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx
@@ -36,6 +36,9 @@ import RundownLayoutEditor from './RundownLayoutEditor'
import { getHelpMode } from '../../lib/localStorage'
import { SettingsNavigation } from '../../lib/SettingsNavigation'
import { MeteorCall } from '../../../lib/api/methods'
+import { Settings } from '../../../lib/Settings'
+import { RundownLayoutType } from '../../../lib/collections/RundownLayouts'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
interface IProps {
match: {
@@ -223,11 +226,19 @@ export default translateWithTracker
((props: IProp
-
+ {RundownLayoutsAPI.GetSettingsManifest().map((region) => {
+ return (
+
+ )
+ })}
, IState> {
+ render() {
+ const { t } = this.props
+
+ return (
+
+
+
+ {t('Expose layout as a standalone page')}
+
+
+
+
+
+ {t('Expose as a layout for the shelf')}
+
+
+
+
+
+ {t('Open shelf by default')}
+
+
+
+
+
+ {t('Default shelf height')}
+
+
+
+
+ )
+ }
+ }
+)
diff --git a/meteor/client/ui/Shelf/Shelf.tsx b/meteor/client/ui/Shelf/Shelf.tsx
index 4205cd0efe..b5ab2f0e45 100644
--- a/meteor/client/ui/Shelf/Shelf.tsx
+++ b/meteor/client/ui/Shelf/Shelf.tsx
@@ -14,7 +14,7 @@ import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
import { RundownViewKbdShortcuts } from '../RundownView'
import { ShowStyleBase } from '../../../lib/collections/ShowStyleBases'
import { getElementDocumentOffset } from '../../utils/positions'
-import { RundownLayoutBase, RundownLayoutFilter } from '../../../lib/collections/RundownLayouts'
+import { RundownLayoutFilter, RundownLayoutShelfBase } from '../../../lib/collections/RundownLayouts'
import { UIStateStorage } from '../../lib/UIStateStorage'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
import { contextMenuHoldToDisplayTime } from '../../lib/lib'
@@ -51,7 +51,7 @@ export interface IShelfProps extends React.ComponentPropsWithRef {
key: string
label: string
}>
- rundownLayout?: RundownLayoutBase
+ rundownLayout?: RundownLayoutShelfBase
fullViewport?: boolean
shelfDisplayOptions: {
buckets: boolean
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 0c01e4ed75..9039bc0210 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -12,11 +12,12 @@ import {
PieceDisplayStyle,
RundownLayoutPieceCountdown,
RundownViewLayout,
- RundownLayoutTopBar,
- LayoutFactory,
+ RundownLayoutRundownHeader,
+ RundownLayoutShelfBase,
} from '../collections/RundownLayouts'
import { ShowStyleBaseId } from '../collections/ShowStyleBases'
import * as _ from 'underscore'
+import { literal } from '../lib'
export interface NewRundownLayoutsAPI {
createRundownLayout(
@@ -32,18 +33,31 @@ export enum RundownLayoutsAPIMethods {
'createRundownLayout' = 'rundownLayout.createRundownLayout',
}
-interface LayoutDescriptor {
- factory: LayoutFactory
+export interface LayoutDescriptor {
+ supportedElements: RundownLayoutElementType[]
+ filtersTitle?: string // e.g. tabs/panels
+}
+
+export interface CustomizableRegionSettingsManifest {
+ _id: string
+ title: string
+ layouts: Array
+}
+
+export interface CustomizableRegionLayout {
+ _id: string
+ type: RundownLayoutType
+ filtersTitle?: string
supportedElements: RundownLayoutElementType[]
}
class RundownLayoutsRegistry {
- private shelfLayouts: Map> = new Map()
- private rundownViewLayouts: Map> = new Map()
- private miniShelfLayouts: Map> = new Map()
- private topBarLayouts: Map> = new Map()
+ private shelfLayouts: Map> = new Map()
+ private rundownViewLayouts: Map> = new Map()
+ private miniShelfLayouts: Map> = new Map()
+ private rundownHeaderLayouts: Map> = new Map()
- public RegisterShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) {
+ public RegisterShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) {
this.shelfLayouts.set(id, description)
}
@@ -55,8 +69,8 @@ class RundownLayoutsRegistry {
this.miniShelfLayouts.set(id, description)
}
- public RegisterTopBarLayouts(id: RundownLayoutType, description: LayoutDescriptor) {
- this.topBarLayouts.set(id, description)
+ public RegisterRundownHeaderLayouts(id: RundownLayoutType, description: LayoutDescriptor) {
+ this.rundownHeaderLayouts.set(id, description)
}
public IsShelfLayout(id: RundownLayoutType) {
@@ -71,15 +85,44 @@ class RundownLayoutsRegistry {
return this.miniShelfLayouts.has(id)
}
- public IsTopBarLayout(id: RundownLayoutType) {
- return this.topBarLayouts.has(id)
+ public IsRundownHeaderLayout(id: RundownLayoutType) {
+ return this.rundownHeaderLayouts.has(id)
+ }
+
+ public GetSettingsManifest(): CustomizableRegionSettingsManifest[] {
+ return [
+ {
+ _id: 'shelf_layouts',
+ title: 'Shelf Layouts',
+ layouts: Array.from(this.shelfLayouts.entries()).map(([layoutType, descriptor]) => {
+ return literal({
+ _id: layoutType,
+ type: layoutType,
+ filtersTitle: descriptor.filtersTitle,
+ supportedElements: descriptor.supportedElements,
+ })
+ }),
+ },
+ {
+ _id: 'rundown_view_layouts',
+ title: 'Rundown View Layouts',
+ layouts: Array.from(this.rundownViewLayouts.entries()).map(([layoutType, descriptor]) => {
+ return literal({
+ _id: layoutType,
+ type: layoutType,
+ filtersTitle: descriptor.filtersTitle,
+ supportedElements: descriptor.supportedElements,
+ })
+ }),
+ },
+ ]
}
}
export namespace RundownLayoutsAPI {
const registry = new RundownLayoutsRegistry()
- registry.RegisterShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
- factory: { createLayout: () => undefined },
+ registry.RegisterShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
+ filtersTitle: 'Panels',
supportedElements: [
RundownLayoutElementType.ADLIB_REGION,
RundownLayoutElementType.EXTERNAL_FRAME,
@@ -87,8 +130,8 @@ export namespace RundownLayoutsAPI {
RundownLayoutElementType.PIECE_COUNTDOWN,
],
})
- registry.RegisterShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
- factory: { createLayout: () => undefined },
+ registry.RegisterShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
+ filtersTitle: 'Tabs',
supportedElements: [
RundownLayoutElementType.ADLIB_REGION,
RundownLayoutElementType.EXTERNAL_FRAME,
@@ -97,23 +140,23 @@ export namespace RundownLayoutsAPI {
],
})
registry.RegisterMiniShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
- factory: { createLayout: () => undefined },
supportedElements: [],
})
registry.RegisterMiniShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
- factory: { createLayout: () => undefined },
supportedElements: [],
})
registry.RegisterRundownViewLayout(RundownLayoutType.RUNDOWN_VIEW_LAYOUT, {
- factory: { createLayout: () => undefined },
supportedElements: [],
})
- registry.RegisterTopBarLayouts(RundownLayoutType.TOP_BAR_LAYOUT, {
- factory: { createLayout: () => undefined },
+ registry.RegisterRundownHeaderLayouts(RundownLayoutType.RUNDOWN_HEADER_LAYOUT, {
supportedElements: [],
})
- export function IsLayoutForShelf(layout: RundownLayoutBase): layout is RundownLayoutBase {
+ export function GetSettingsManifest(): CustomizableRegionSettingsManifest[] {
+ return registry.GetSettingsManifest()
+ }
+
+ export function IsLayoutForShelf(layout: RundownLayoutBase): layout is RundownLayoutShelfBase {
return registry.IsShelfLayout(layout.type)
}
@@ -125,8 +168,8 @@ export namespace RundownLayoutsAPI {
return registry.IsMiniShelfLayout(layout.type)
}
- export function IsLayoutForTopBar(layout: RundownLayoutBase): layout is RundownLayoutBase {
- return registry.IsTopBarLayout(layout.type)
+ export function IsLayoutForRundownHeader(layout: RundownLayoutBase): layout is RundownLayoutBase {
+ return registry.IsRundownHeaderLayout(layout.type)
}
export function isRundownViewLayout(layout: RundownLayoutBase): layout is RundownViewLayout {
@@ -141,8 +184,8 @@ export namespace RundownLayoutsAPI {
return layout.type === RundownLayoutType.DASHBOARD_LAYOUT
}
- export function isTopBarLayout(layout: RundownLayoutBase): layout is RundownLayoutTopBar {
- return layout.type === RundownLayoutType.TOP_BAR_LAYOUT
+ export function isRundownHeaderLayout(layout: RundownLayoutBase): layout is RundownLayoutRundownHeader {
+ return layout.type === RundownLayoutType.RUNDOWN_HEADER_LAYOUT
}
export function isFilter(element: RundownLayoutElementBase): element is RundownLayoutFilterBase {
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 31c92ee3d9..7c72c44227 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -6,10 +6,6 @@ import { ShowStyleBaseId } from './ShowStyleBases'
import { UserId } from './Users'
import { registerIndex } from '../database'
-export interface LayoutFactory {
- createLayout(...args: any[]): T | undefined
-}
-
/**
* The view targeted by this layout:
* RUNDOWN_LAYOUT: a Rundown view for highly scripted shows: a show split into Segments and Parts,
@@ -23,7 +19,7 @@ export enum RundownLayoutType {
RUNDOWN_VIEW_LAYOUT = 'rundown_view_layout',
RUNDOWN_LAYOUT = 'rundown_layout',
DASHBOARD_LAYOUT = 'dashboard_layout',
- TOP_BAR_LAYOUT = 'top_bar_layout',
+ RUNDOWN_HEADER_LAYOUT = 'top_bar_layout',
}
/**
@@ -160,12 +156,8 @@ export interface RundownLayoutBase {
name: string
type: RundownLayoutType
filters: RundownLayoutElementBase[]
- exposeAsStandalone: boolean
- exposeAsShelf: boolean
icon: string
iconColor: string
- openByDefault: boolean
- startingHeight?: number
}
export interface RundownViewLayout extends RundownLayoutBase {
@@ -173,13 +165,21 @@ export interface RundownViewLayout extends RundownLayoutBase {
expectedEndText: string
}
-export interface RundownLayout extends RundownLayoutBase {
+export interface RundownLayoutShelfBase extends RundownLayoutBase {
+ exposeAsStandalone: boolean
+ exposeAsShelf: boolean
+ openByDefault: boolean
+ startingHeight?: number
+}
+
+export interface RundownLayout extends RundownLayoutShelfBase {
type: RundownLayoutType.RUNDOWN_LAYOUT
- filters: RundownLayoutElementBase[]
}
-export interface RundownLayoutTopBar extends RundownLayoutBase {
- type: RundownLayoutType.TOP_BAR_LAYOUT
+export interface RundownLayoutRundownHeader extends RundownLayoutBase {
+ type: RundownLayoutType.RUNDOWN_HEADER_LAYOUT
+ endOfShowText: string
+ nextBreakText: string
}
export enum ActionButtonType {
@@ -207,7 +207,7 @@ export interface DashboardLayoutActionButton {
label: string
}
-export interface DashboardLayout extends RundownLayoutBase {
+export interface DashboardLayout extends RundownLayoutShelfBase {
type: RundownLayoutType.DASHBOARD_LAYOUT
filters: RundownLayoutElementBase[]
actionButtons?: DashboardLayoutActionButton[]
diff --git a/meteor/server/api/rundownLayouts.ts b/meteor/server/api/rundownLayouts.ts
index ea078e9f31..556763a956 100644
--- a/meteor/server/api/rundownLayouts.ts
+++ b/meteor/server/api/rundownLayouts.ts
@@ -23,10 +23,7 @@ export function createRundownLayout(
type: RundownLayoutType,
showStyleBaseId: ShowStyleBaseId,
blueprintId: BlueprintId | undefined,
- userId?: UserId | undefined,
- exposeAsStandalone?: boolean,
- exposeAsShelf?: boolean,
- openByDefault?: boolean
+ userId?: UserId | undefined
) {
const id: RundownLayoutId = getRandomId()
RundownLayouts.insert(
@@ -38,11 +35,8 @@ export function createRundownLayout(
filters: [],
type,
userId,
- exposeAsStandalone: !!exposeAsStandalone,
- exposeAsShelf: !!exposeAsShelf,
icon: '',
iconColor: '#ffffff',
- openByDefault: openByDefault ?? false,
})
)
return id
From 5f9261af6a07a39bbeb92844d87d081fca432daf Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 12 May 2021 15:03:09 +0100
Subject: [PATCH 023/112] feat: Apply rundown header / view layouts to rundown
---
.../RundownShelfLayoutSelection.tsx | 4 +-
meteor/client/ui/RundownList/util.ts | 9 ++--
meteor/client/ui/RundownView.tsx | 45 +++++++++++++++----
.../ui/Settings/RundownLayoutEditor.tsx | 5 +++
.../RundownHeaderLayoutSettings.tsx | 38 ++++++++++++++++
meteor/lib/api/rundownLayouts.ts | 14 +++++-
meteor/lib/collections/RundownLayouts.ts | 4 +-
7 files changed, 102 insertions(+), 17 deletions(-)
create mode 100644 meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
diff --git a/meteor/client/ui/RundownList/RundownShelfLayoutSelection.tsx b/meteor/client/ui/RundownList/RundownShelfLayoutSelection.tsx
index 3a1efc4239..8ab72ac13f 100644
--- a/meteor/client/ui/RundownList/RundownShelfLayoutSelection.tsx
+++ b/meteor/client/ui/RundownList/RundownShelfLayoutSelection.tsx
@@ -3,7 +3,7 @@ import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData'
import { UIStateStorage } from '../../lib/UIStateStorage'
import { Link } from 'react-router-dom'
import { SplitDropdown } from '../../lib/SplitDropdown'
-import { getRundownPlaylistLink, getRundownWithLayoutLink, getShelfLink } from './util'
+import { getRundownPlaylistLink, getRundownWithShelfLayoutLink, getShelfLink } from './util'
import { Rundown } from '../../../lib/collections/Rundowns'
import { RundownLayoutBase } from '../../../lib/collections/RundownLayouts'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@@ -77,7 +77,7 @@ export const RundownShelfLayoutSelection = withTranslation()(
.map((layout) => {
return this.renderLinkItem(
layout,
- getRundownWithLayoutLink(this.props.playlistId, layout._id),
+ getRundownWithShelfLayoutLink(this.props.playlistId, layout._id),
`shelf${layout._id}`
)
})
diff --git a/meteor/client/ui/RundownList/util.ts b/meteor/client/ui/RundownList/util.ts
index 5956c756f5..d06a05c0ef 100644
--- a/meteor/client/ui/RundownList/util.ts
+++ b/meteor/client/ui/RundownList/util.ts
@@ -36,15 +36,18 @@ export function getShelfLink(rundownId: RundownId | RundownPlaylistId, layoutId:
const encodedRundownId = encodeURIComponent(encodeURIComponent(unprotectString(rundownId)))
const encodedLayoutId = encodeURIComponent(encodeURIComponent(unprotectString(layoutId)))
- return `/rundown/${encodedRundownId}/shelf/?layout=${encodedLayoutId}`
+ return `/rundown/${encodedRundownId}/shelf/?shelfLayout=${encodedLayoutId}`
}
-export function getRundownWithLayoutLink(rundownId: RundownId | RundownPlaylistId, layoutId: RundownLayoutId): string {
+export function getRundownWithShelfLayoutLink(
+ rundownId: RundownId | RundownPlaylistId,
+ layoutId: RundownLayoutId
+): string {
// double encoding so that "/" are handled correctly
const encodedRundownId = encodeURIComponent(encodeURIComponent(unprotectString(rundownId)))
const encodedLayoutId = encodeURIComponent(encodeURIComponent(unprotectString(layoutId)))
- return `/rundown/${encodedRundownId}?layout=${encodedLayoutId}`
+ return `/rundown/${encodedRundownId}?shelfLayout=${encodedLayoutId}`
}
export function confirmDeleteRundown(rundown: Rundown, t: TFunction) {
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index e0595c81fb..1fb02adafd 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -80,6 +80,7 @@ import {
RundownViewLayout,
DashboardLayout,
RundownLayoutShelfBase,
+ RundownLayoutRundownHeader,
} from '../../lib/collections/RundownLayouts'
import { VirtualElement } from '../lib/VirtualElement'
import { SEGMENT_TIMELINE_ELEMENT_ID } from './SegmentTimeline/SegmentTimeline'
@@ -207,7 +208,7 @@ interface ITimingDisplayProps {
rundownPlaylist: RundownPlaylist
currentRundown: Rundown | undefined
rundownCount: number
- isLastRundownInPlaylist: boolean
+ layout: RundownLayoutRundownHeader | undefined
}
export enum RundownViewKbdShortcuts {
@@ -393,7 +394,7 @@ const TimingDisplay = withTranslation()(
) : null
) : (
- {t('Expected End')}
+ {this.props.layout?.expectedEndText ?? t('Expected End')}
) => void
studioMode: boolean
inActiveRundownView?: boolean
+ layout: RundownLayoutRundownHeader | undefined
}
interface IRundownHeaderState {
@@ -1460,10 +1462,7 @@ const RundownHeader = withTranslation()(
rundownPlaylist={this.props.playlist}
currentRundown={this.props.currentRundown}
rundownCount={this.props.rundownIds.length}
- isLastRundownInPlaylist={
- !!this.props.currentRundown?._id &&
- this.props.rundownIds.indexOf(this.props.currentRundown._id) === this.props.rundownIds.length - 1
- }
+ layout={this.props.layout}
/>
((
const params = queryStringParse(location.search)
- const displayOptions = ((params['display'] as string) || 'buckets,layout,inspector').split(',')
+ const displayOptions = ((params['display'] as string) || 'buckets,layout,shelfLayout,inspector').split(',')
const bucketDisplayFilter = !(params['buckets'] as string)
? undefined
: (params['buckets'] as string).split(',').map((v) => parseInt(v))
@@ -1651,10 +1652,11 @@ export const RundownView = translateWithTracker((
undefined,
shelfLayoutId: protectString((params['layout'] as string) || (params['shelfLayout'] as string) || ''), // 'layout' kept for backwards compatibility
rundownViewLayoutId: protectString((params['rundownViewLayout'] as string) || ''),
+ rundownHeaderLayoutId: protectString((params['rundownHeaderLayout'] as string) || ''),
orderedPartsIds: allParts,
shelfDisplayOptions: {
buckets: displayOptions.includes('buckets'),
- layout: displayOptions.includes('layout'),
+ layout: displayOptions.includes('layout') || displayOptions.includes('shelfLayout'),
inspector: displayOptions.includes('inspector'),
},
bucketDisplayFilter,
@@ -1736,6 +1738,7 @@ export const RundownView = translateWithTracker((
selectedPiece: undefined,
shelfLayout: undefined,
rundownViewLayout: undefined,
+ rundownHeaderLayout: undefined,
currentRundown: undefined,
wasShelfResizedByUser: false,
}
@@ -1744,6 +1747,7 @@ export const RundownView = translateWithTracker((
static getDerivedStateFromProps(props: Translated): Partial {
let selectedShelfLayout: RundownLayoutBase | undefined = undefined
let selectedViewLayout: RundownLayoutBase | undefined = undefined
+ let selectedHeaderLayout: RundownLayoutBase | undefined = undefined
if (props.rundownLayouts) {
// first try to use the one selected by the user
@@ -1755,6 +1759,10 @@ export const RundownView = translateWithTracker((
selectedViewLayout = props.rundownLayouts.find((i) => i._id === props.rundownViewLayoutId)
}
+ if (props.rundownHeaderLayoutId) {
+ selectedHeaderLayout = props.rundownLayouts.find((i) => i._id === props.rundownHeaderLayoutId)
+ }
+
// if couldn't find based on id, try matching part of the name
if (props.shelfLayoutId && !selectedShelfLayout) {
selectedShelfLayout = props.rundownLayouts.find(
@@ -1768,6 +1776,12 @@ export const RundownView = translateWithTracker((
)
}
+ if (props.rundownHeaderLayoutId && !selectedHeaderLayout) {
+ selectedHeaderLayout = props.rundownLayouts.find(
+ (i) => i.name.indexOf(unprotectString(props.rundownHeaderLayoutId!)) >= 0
+ )
+ }
+
// if not, try the first RUNDOWN_LAYOUT available
if (!selectedShelfLayout) {
selectedShelfLayout = props.rundownLayouts.find((i) => i.type === RundownLayoutType.RUNDOWN_LAYOUT)
@@ -1775,7 +1789,15 @@ export const RundownView = translateWithTracker((
// if still not found, use the first one
if (!selectedShelfLayout) {
- selectedShelfLayout = props.rundownLayouts[0]
+ selectedShelfLayout = props.rundownLayouts.filter((i) => RundownLayoutsAPI.IsLayoutForShelf(i))[0]
+ }
+
+ if (!selectedViewLayout) {
+ selectedViewLayout = props.rundownLayouts.filter((i) => RundownLayoutsAPI.IsLayoutForRundownView(i))[0]
+ }
+
+ if (!selectedHeaderLayout) {
+ selectedHeaderLayout = props.rundownLayouts.filter((i) => RundownLayoutsAPI.IsLayoutForRundownHeader(i))[0]
}
}
@@ -1793,6 +1815,10 @@ export const RundownView = translateWithTracker((
? selectedShelfLayout
: undefined,
rundownViewLayout: selectedViewLayout,
+ rundownHeaderLayout:
+ selectedHeaderLayout && RundownLayoutsAPI.IsLayoutForRundownHeader(selectedHeaderLayout)
+ ? selectedHeaderLayout
+ : undefined,
currentRundown,
}
}
@@ -2821,6 +2847,7 @@ export const RundownView = translateWithTracker((
onRegisterHotkeys={this.onRegisterHotkeys}
inActiveRundownView={this.props.inActiveRundownView}
currentRundown={this.state.currentRundown || this.props.rundowns[0]}
+ layout={this.state.rundownHeaderLayout}
/>
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index 0b8a48e51b..0b59123fbb 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -37,6 +37,7 @@ import { MeteorCall } from '../../../lib/api/methods'
import { defaultColorPickerPalette } from '../../lib/colorPicker'
import FilterEditor from './components/FilterEditor'
import ShelfLayoutSettings from './components/rundownLayouts/ShelfLayoutSettings'
+import RundownHeaderLayoutSettings from './components/rundownLayouts/RundownHeaderLayoutSettings'
export interface IProps {
showStyleBase: ShowStyleBase
@@ -1236,6 +1237,9 @@ export default translateWithTracker((props: IProp
const { t } = this.props
const isShelfLayout = RundownLayoutsAPI.IsLayoutForShelf(item)
+ const isRundownViewLayout = RundownLayoutsAPI.IsLayoutForRundownView(item)
+ const isRundownHeaderLayout = RundownLayoutsAPI.IsLayoutForRundownHeader(item)
+ const isMiniShelfLayout = RundownLayoutsAPI.IsLayoutForMiniShelf(item)
return (
@@ -1267,6 +1271,7 @@ export default translateWithTracker((props: IProp
{isShelfLayout &&
}
+ {isRundownHeaderLayout &&
}
{layout?.filtersTitle ? t(`${layout?.filtersTitle}`) : t('Filters')}
{item.filters.length === 0 ? (
{t('There are no filters set up yet')}
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
new file mode 100644
index 0000000000..5fe708b4f6
--- /dev/null
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
@@ -0,0 +1,38 @@
+import React from 'react'
+import { withTranslation } from 'react-i18next'
+import { RundownLayoutBase, RundownLayouts } from '../../../../../lib/collections/RundownLayouts'
+import { EditAttribute } from '../../../../lib/EditAttribute'
+import { MeteorReactComponent } from '../../../../lib/MeteorReactComponent'
+import { Translated } from '../../../../lib/ReactMeteorData/ReactMeteorData'
+
+interface IProps {
+ item: RundownLayoutBase
+}
+
+interface IState {}
+
+export default withTranslation()(
+ class ShelfLayoutSettings extends MeteorReactComponent
, IState> {
+ render() {
+ const { t } = this.props
+
+ return (
+
+
+
+ {t('End of Show Text')}
+
+
+
+
+ )
+ }
+ }
+)
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 9039bc0210..3df0432158 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -115,6 +115,18 @@ class RundownLayoutsRegistry {
})
}),
},
+ {
+ _id: 'rundown_header_layouts',
+ title: 'Rundown Header Layouts',
+ layouts: Array.from(this.rundownHeaderLayouts.entries()).map(([layoutType, descriptor]) => {
+ return literal({
+ _id: layoutType,
+ type: layoutType,
+ filtersTitle: descriptor.filtersTitle,
+ supportedElements: descriptor.supportedElements,
+ })
+ }),
+ },
]
}
}
@@ -168,7 +180,7 @@ export namespace RundownLayoutsAPI {
return registry.IsMiniShelfLayout(layout.type)
}
- export function IsLayoutForRundownHeader(layout: RundownLayoutBase): layout is RundownLayoutBase {
+ export function IsLayoutForRundownHeader(layout: RundownLayoutBase): layout is RundownLayoutRundownHeader {
return registry.IsRundownHeaderLayout(layout.type)
}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 7c72c44227..7d1193be05 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -19,7 +19,7 @@ export enum RundownLayoutType {
RUNDOWN_VIEW_LAYOUT = 'rundown_view_layout',
RUNDOWN_LAYOUT = 'rundown_layout',
DASHBOARD_LAYOUT = 'dashboard_layout',
- RUNDOWN_HEADER_LAYOUT = 'top_bar_layout',
+ RUNDOWN_HEADER_LAYOUT = 'rundown_header_layout',
}
/**
@@ -178,7 +178,7 @@ export interface RundownLayout extends RundownLayoutShelfBase {
export interface RundownLayoutRundownHeader extends RundownLayoutBase {
type: RundownLayoutType.RUNDOWN_HEADER_LAYOUT
- endOfShowText: string
+ expectedEndText: string
nextBreakText: string
}
From 4ca95926c91441d12aa42f85380484f917acb20b Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 12 May 2021 15:56:23 +0100
Subject: [PATCH 024/112] fix: Post-rebase
---
meteor/client/ui/RundownView.tsx | 2 -
.../ui/Settings/RundownLayoutEditor.tsx | 938 +-----------------
.../ui/Settings/components/FilterEditor.tsx | 16 +
3 files changed, 17 insertions(+), 939 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 1fb02adafd..45951614d2 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -1551,7 +1551,6 @@ interface ITrackedProps {
shelfLayoutId?: RundownLayoutId
rundownViewLayoutId?: RundownLayoutId
rundownHeaderLayoutId?: RundownLayoutId
- orderedPartsIds: PartId[]
shelfDisplayOptions: {
buckets: boolean
layout: boolean
@@ -1653,7 +1652,6 @@ export const RundownView = translateWithTracker((
shelfLayoutId: protectString((params['layout'] as string) || (params['shelfLayout'] as string) || ''), // 'layout' kept for backwards compatibility
rundownViewLayoutId: protectString((params['rundownViewLayout'] as string) || ''),
rundownHeaderLayoutId: protectString((params['rundownHeaderLayout'] as string) || ''),
- orderedPartsIds: allParts,
shelfDisplayOptions: {
buckets: displayOptions.includes('buckets'),
layout: displayOptions.includes('layout') || displayOptions.includes('shelfLayout'),
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index 0b59123fbb..3f7cade82e 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -297,943 +297,7 @@ export default translateWithTracker((props: IProp
)
}
- renderFilter(
- item: RundownLayoutBase,
- tab: RundownLayoutFilterBase,
- index: number,
- isRundownLayout: boolean,
- isDashboardLayout: boolean
- ) {
- const { t } = this.props
- const isList = tab.displayStyle === PieceDisplayStyle.LIST
- const rundownBaselineOptions = [
- {
- name: t('Yes'),
- value: true,
- },
- {
- name: t('No'),
- value: false,
- },
- {
- name: t('Only Match Global AdLibs'),
- value: 'only',
- },
- ]
-
- return (
-
-
-
- {t('Name')}
-
-
-
- {isDashboardLayout && (
-
-
-
- {t('Display Style')}
-
-
-
- {isList && (
-
-
- {t('Show thumbnails next to list items')}
-
-
-
- )}
-
-
- {t('X')}
-
-
-
-
-
- {t('Y')}
-
-
-
-
-
- {t('Width')}
-
-
-
-
-
- {t('Height')}
-
-
-
- {!isList && (
-
-
-
- {t('Button width scale factor')}
-
-
-
-
-
- {t('Button height scale factor')}
-
-
-
-
- )}
-
- )}
-
-
- {t('Display Rank')}
-
-
-
-
-
- {t('Only Display AdLibs from Current Segment')}
-
-
-
-
- {t('Include Global AdLibs')}
-
-
- {isDashboardLayout && (
-
-
-
- {t('Include Clear Source Layer in Ad-Libs')}
-
-
-
-
- )}
-
- {t('Source Layers')}
- (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={() => undefined}
- />
- {
- return { name: l.name, value: l._id }
- })}
- type="multiselect"
- label={t('Filter Disabled')}
- collection={RundownLayouts}
- className="input text-input input-l dropdown"
- mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
- />
-
-
- {t('Source Layer Types')}
- (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={() => undefined}
- />
-
- v && v.length > 0 ? v.map((a) => parseInt(a, 10)) : undefined
- }
- />
-
-
- {t('Output Channels')}
- (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={() => undefined}
- />
- {
- return { name: l.name, value: l._id }
- })}
- type="multiselect"
- label={t('Filter Disabled')}
- collection={RundownLayouts}
- className="input text-input input-l dropdown"
- mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
- />
-
-
-
- {t('Label contains')}
- (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={() => undefined}
- />
- (v === undefined || v.length === 0 ? undefined : v.join(', '))}
- mutateUpdateValue={(v) =>
- v === undefined || v.length === 0 ? undefined : v.split(',').map((i) => i.trim())
- }
- />
-
-
-
-
- {t('Tags must contain')}
- (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={() => undefined}
- />
- (v === undefined || v.length === 0 ? undefined : v.join(', '))}
- mutateUpdateValue={(v) =>
- v === undefined || v.length === 0 ? undefined : v.split(',').map((i) => i.trim())
- }
- />
-
-
- {isDashboardLayout && (
-
-
-
- {t('Register Shortcuts for this Panel')}
-
-
-
-
-
- {t('Hide Panel from view')}
-
-
-
-
- )}
- {isDashboardLayout && (
-
-
-
- {t('Show panel as a timeline')}
-
-
-
-
- )}
-
-
- {t('Enable search toolbar')}
-
-
-
- {isDashboardLayout && (
-
-
-
- {t('Include Clear Source Layer in Ad-Libs')}
-
-
-
-
- )}
- {isDashboardLayout && (
-
-
-
- {t('Overflow horizontally')}
-
-
-
-
-
- {t('Display Take buttons')}
-
-
-
-
-
- {t('Queue all adlibs')}
-
-
-
-
-
- {t('Toggle AdLibs on single mouse click')}
-
-
-
-
-
- {t('Current part can contain next pieces')}
-
-
-
-
-
- {t('Indicate only one next piece per source layer')}
-
-
-
-
-
- {t('Hide duplicated AdLibs')}
-
-
- {t('Picks the first instance of an adLib per rundown, identified by uniqueness Id')}
-
-
-
-
- )}
-
- )
- }
-
- renderFrame(
- item: RundownLayoutBase,
- tab: RundownLayoutExternalFrame,
- index: number,
- isRundownLayout: boolean,
- isDashboardLayout: boolean
- ) {
- const { t } = this.props
- return (
-
-
-
- {t('Name')}
-
-
-
-
-
- {t('URL')}
-
-
-
- {isDashboardLayout && (
-
-
-
- {t('X')}
-
-
-
-
-
- {t('Y')}
-
-
-
-
-
- {t('Width')}
-
-
-
-
-
- {t('Height')}
-
-
-
-
-
- {t('Scale')}
-
-
-
-
-
- {t('Display Rank')}
-
-
-
-
- )}
-
-
- {t('Scale')}
-
-
-
-
- )
- }
-
- renderAdLibRegion(
- item: RundownLayoutBase,
- tab: RundownLayoutAdLibRegion,
- index: number,
- isRundownLayout: boolean,
- isDashboardLayout: boolean
- ) {
- const { t } = this.props
- return (
-
-
-
- {t('Name')}
-
-
-
-
-
- {t('Role')}
-
-
-
-
-
- {t('Adlib Rank')}
-
-
-
-
-
- {t('Tags must contain')}
- (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={() => undefined}
- />
- (v === undefined || v.length === 0 ? undefined : v.join(', '))}
- mutateUpdateValue={(v) =>
- v === undefined || v.length === 0 ? undefined : v.split(',').map((i) => i.trim())
- }
- />
-
-
-
-
- {t('Place label below panel')}
-
-
-
- {isDashboardLayout && (
-
-
-
- {t('X')}
-
-
-
-
-
- {t('Y')}
-
-
-
-
-
- {t('Width')}
-
-
-
-
-
- {t('Height')}
-
-
-
- {isDashboardLayout && (
-
-
-
- {t('Register Shortcuts for this Panel')}
-
-
-
-
- )}
-
- )}
-
- )
- }
-
- renderPieceCountdown(
- item: RundownLayoutBase,
- tab: RundownLayoutPieceCountdown,
- index: number,
- isRundownLayout: boolean,
- isDashboardLayout: boolean
- ) {
- const { t } = this.props
- return (
-
-
-
- {t('Name')}
-
-
-
-
- {t('Source Layers')}
- (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={() => undefined}
- />
- {
- return { name: l.name, value: l._id }
- })}
- type="multiselect"
- label={t('Disabled')}
- collection={RundownLayouts}
- className="input text-input input-l dropdown"
- mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
- />
-
- {isDashboardLayout && (
-
-
-
- {t('X')}
-
-
-
-
-
- {t('Y')}
-
-
-
-
-
- {t('Width')}
-
-
-
-
-
- {t('Scale')}
-
-
-
-
- )}
-
- )
- }
-
- renderElements(item: RundownLayoutBase) {
+ renderElements(item: RundownLayoutBase, layout: CustomizableRegionLayout | undefined) {
const { t } = this.props
const isShelfLayout = RundownLayoutsAPI.IsLayoutForShelf(item)
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index 154f3d0370..c4aa568793 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -531,6 +531,22 @@ export default translateWithTracker((props: IProp
/>
+
+
+ {t('Hide duplicated AdLibs')}
+
+
+ {t('Picks the first instance of an adLib per rundown, identified by uniqueness Id')}
+
+
+
)}
From 437a730fc365258dc3160ab52bb19c694fc104a4 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 17 May 2021 16:29:29 +0100
Subject: [PATCH 025/112] feat: Scope layouts to their settings section
---
meteor/client/ui/Settings/RundownLayoutEditor.tsx | 3 ++-
meteor/lib/api/rundownLayouts.ts | 3 ++-
meteor/lib/collections/RundownLayouts.ts | 2 ++
meteor/server/api/rundownLayouts.ts | 11 +++++++----
meteor/server/migration/X_X_X.ts | 2 ++
5 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index 3f7cade82e..a02453dc7f 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -62,6 +62,7 @@ export default translateWithTracker((props: IProp
showStyleBaseId: props.showStyleBase._id,
userId: { $exists: false },
type: { $in: layoutTypes },
+ regionId: props.customRegion._id,
}).fetch()
return {
@@ -88,7 +89,7 @@ export default translateWithTracker((props: IProp
onAddLayout = () => {
const { t, showStyleBase } = this.props
MeteorCall.rundownLayout
- .createRundownLayout(t('New Layout'), this.props.layoutTypes[0], showStyleBase._id)
+ .createRundownLayout(t('New Layout'), this.props.layoutTypes[0], showStyleBase._id, this.props.customRegion._id)
.catch(console.error)
}
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 3df0432158..0f98da9f33 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -23,7 +23,8 @@ export interface NewRundownLayoutsAPI {
createRundownLayout(
name: string,
type: RundownLayoutType,
- showStyleBaseId: ShowStyleBaseId
+ showStyleBaseId: ShowStyleBaseId,
+ regionId: string
): Promise
removeRundownLayout(id: RundownLayoutId): Promise
}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 7d1193be05..186701f92d 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -158,6 +158,8 @@ export interface RundownLayoutBase {
filters: RundownLayoutElementBase[]
icon: string
iconColor: string
+ /* Customizable region that the layout modifies. */
+ regionId: string
}
export interface RundownViewLayout extends RundownLayoutBase {
diff --git a/meteor/server/api/rundownLayouts.ts b/meteor/server/api/rundownLayouts.ts
index 556763a956..75d166d143 100644
--- a/meteor/server/api/rundownLayouts.ts
+++ b/meteor/server/api/rundownLayouts.ts
@@ -22,6 +22,7 @@ export function createRundownLayout(
name: string,
type: RundownLayoutType,
showStyleBaseId: ShowStyleBaseId,
+ regionId: string,
blueprintId: BlueprintId | undefined,
userId?: UserId | undefined
) {
@@ -37,6 +38,7 @@ export function createRundownLayout(
userId,
icon: '',
iconColor: '#ffffff',
+ regionId,
})
)
return id
@@ -118,7 +120,8 @@ function apiCreateRundownLayout(
context: MethodContext,
name: string,
type: RundownLayoutType,
- showStyleBaseId: ShowStyleBaseId
+ showStyleBaseId: ShowStyleBaseId,
+ regionId: string
) {
check(name, String)
check(type, String)
@@ -126,7 +129,7 @@ function apiCreateRundownLayout(
const access = ShowStyleContentWriteAccess.anyContent(context, showStyleBaseId)
- return createRundownLayout(name, type, showStyleBaseId, undefined, access.userId || undefined)
+ return createRundownLayout(name, type, showStyleBaseId, regionId, undefined, access.userId || undefined)
}
function apiRemoveRundownLayout(context: MethodContext, id: RundownLayoutId) {
check(id, String)
@@ -139,8 +142,8 @@ function apiRemoveRundownLayout(context: MethodContext, id: RundownLayoutId) {
}
class ServerRundownLayoutsAPI extends MethodContextAPI implements NewRundownLayoutsAPI {
- createRundownLayout(name: string, type: RundownLayoutType, showStyleBaseId: ShowStyleBaseId) {
- return makePromise(() => apiCreateRundownLayout(this, name, type, showStyleBaseId))
+ createRundownLayout(name: string, type: RundownLayoutType, showStyleBaseId: ShowStyleBaseId, regionId: string) {
+ return makePromise(() => apiCreateRundownLayout(this, name, type, showStyleBaseId, regionId))
}
removeRundownLayout(rundownLayoutId: RundownLayoutId) {
return makePromise(() => apiRemoveRundownLayout(this, rundownLayoutId))
diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts
index 8e93e7b731..53e792480a 100644
--- a/meteor/server/migration/X_X_X.ts
+++ b/meteor/server/migration/X_X_X.ts
@@ -1,6 +1,7 @@
import { addMigrationSteps } from './databaseMigration'
import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion'
import { removeCollectionProperty } from './lib'
+import { ensureCollectionProperty } from './lib'
/*
* **************************************************************************************
@@ -29,4 +30,5 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [
//
removeCollectionProperty('PeripheralDevices', {}, 'expectedVersion'),
+ ensureCollectionProperty('RundownLayouts', { regionId: { $exists: false } }, 'regionId', 'shelf_layouts'),
])
From 3c9aef28e6613c414cc3beca8857fe8004597d4b Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 17 May 2021 16:30:01 +0100
Subject: [PATCH 026/112] feat: Select mini shelf layout in rundown view
---
meteor/client/ui/RundownView.tsx | 23 +++++++++++++++++++++++
meteor/lib/api/rundownLayouts.ts | 14 +++++++++++++-
2 files changed, 36 insertions(+), 1 deletion(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 45951614d2..382a8549a0 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -1526,6 +1526,7 @@ interface IState {
shelfLayout: RundownLayoutShelfBase | undefined
rundownViewLayout: RundownLayoutBase | undefined
rundownHeaderLayout: RundownLayoutRundownHeader | undefined
+ miniShelfLayout: RundownLayoutShelfBase | undefined
currentRundown: Rundown | undefined
/** Tracks whether the user has resized the shelf to prevent using default shelf settings */
wasShelfResizedByUser: boolean
@@ -1551,6 +1552,7 @@ interface ITrackedProps {
shelfLayoutId?: RundownLayoutId
rundownViewLayoutId?: RundownLayoutId
rundownHeaderLayoutId?: RundownLayoutId
+ miniShelfLayoutId?: RundownLayoutId
shelfDisplayOptions: {
buckets: boolean
layout: boolean
@@ -1652,6 +1654,7 @@ export const RundownView = translateWithTracker((
shelfLayoutId: protectString((params['layout'] as string) || (params['shelfLayout'] as string) || ''), // 'layout' kept for backwards compatibility
rundownViewLayoutId: protectString((params['rundownViewLayout'] as string) || ''),
rundownHeaderLayoutId: protectString((params['rundownHeaderLayout'] as string) || ''),
+ miniShelfLayoutId: protectString((params['miniShelfLayout'] as string) || ''),
shelfDisplayOptions: {
buckets: displayOptions.includes('buckets'),
layout: displayOptions.includes('layout') || displayOptions.includes('shelfLayout'),
@@ -1737,6 +1740,7 @@ export const RundownView = translateWithTracker((
shelfLayout: undefined,
rundownViewLayout: undefined,
rundownHeaderLayout: undefined,
+ miniShelfLayout: undefined,
currentRundown: undefined,
wasShelfResizedByUser: false,
}
@@ -1746,6 +1750,7 @@ export const RundownView = translateWithTracker((
let selectedShelfLayout: RundownLayoutBase | undefined = undefined
let selectedViewLayout: RundownLayoutBase | undefined = undefined
let selectedHeaderLayout: RundownLayoutBase | undefined = undefined
+ let selectedMiniShelfLayout: RundownLayoutBase | undefined = undefined
if (props.rundownLayouts) {
// first try to use the one selected by the user
@@ -1761,6 +1766,10 @@ export const RundownView = translateWithTracker((
selectedHeaderLayout = props.rundownLayouts.find((i) => i._id === props.rundownHeaderLayoutId)
}
+ if (props.miniShelfLayoutId) {
+ selectedMiniShelfLayout = props.rundownLayouts.find((i) => i._id === props.miniShelfLayoutId)
+ }
+
// if couldn't find based on id, try matching part of the name
if (props.shelfLayoutId && !selectedShelfLayout) {
selectedShelfLayout = props.rundownLayouts.find(
@@ -1780,6 +1789,12 @@ export const RundownView = translateWithTracker((
)
}
+ if (props.miniShelfLayoutId && !selectedMiniShelfLayout) {
+ selectedMiniShelfLayout = props.rundownLayouts.find(
+ (i) => i.name.indexOf(unprotectString(props.miniShelfLayoutId!)) >= 0
+ )
+ }
+
// if not, try the first RUNDOWN_LAYOUT available
if (!selectedShelfLayout) {
selectedShelfLayout = props.rundownLayouts.find((i) => i.type === RundownLayoutType.RUNDOWN_LAYOUT)
@@ -1797,6 +1812,10 @@ export const RundownView = translateWithTracker((
if (!selectedHeaderLayout) {
selectedHeaderLayout = props.rundownLayouts.filter((i) => RundownLayoutsAPI.IsLayoutForRundownHeader(i))[0]
}
+
+ if (!selectedMiniShelfLayout) {
+ selectedMiniShelfLayout = props.rundownLayouts.filter((i) => RundownLayoutsAPI.IsLayoutForMiniShelf(i))[0]
+ }
}
let currentRundown: Rundown | undefined = undefined
@@ -1817,6 +1836,10 @@ export const RundownView = translateWithTracker((
selectedHeaderLayout && RundownLayoutsAPI.IsLayoutForRundownHeader(selectedHeaderLayout)
? selectedHeaderLayout
: undefined,
+ miniShelfLayout:
+ selectedMiniShelfLayout && RundownLayoutsAPI.IsLayoutForMiniShelf(selectedMiniShelfLayout)
+ ? selectedMiniShelfLayout
+ : undefined,
currentRundown,
}
}
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 0f98da9f33..34fab9e82b 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -116,6 +116,18 @@ class RundownLayoutsRegistry {
})
}),
},
+ {
+ _id: 'mini_shelf_layouts',
+ title: 'Mini Shelf Layouts',
+ layouts: Array.from(this.miniShelfLayouts.entries()).map(([layoutType, descriptor]) => {
+ return literal({
+ _id: layoutType,
+ type: layoutType,
+ filtersTitle: descriptor.filtersTitle,
+ supportedElements: descriptor.supportedElements,
+ })
+ }),
+ },
{
_id: 'rundown_header_layouts',
title: 'Rundown Header Layouts',
@@ -177,7 +189,7 @@ export namespace RundownLayoutsAPI {
return registry.IsRudownViewLayout(layout.type)
}
- export function IsLayoutForMiniShelf(layout: RundownLayoutBase): layout is RundownLayoutBase {
+ export function IsLayoutForMiniShelf(layout: RundownLayoutBase): layout is RundownLayoutShelfBase {
return registry.IsMiniShelfLayout(layout.type)
}
From 2b504af29a984640b14e34494a41c59604b099c4 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 14 Jun 2021 12:16:33 +0100
Subject: [PATCH 027/112] fix: Fixes after rebase
---
.../ui/Settings/components/FilterEditor.tsx | 26 +++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index c4aa568793..3810825680 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -531,6 +531,32 @@ export default translateWithTracker((props: IProp
/>
+
+
+ {t('Current part can contain next pieces')}
+
+
+
+
+
+ {t('Indicate only one next piece per source layer')}
+
+
+
{t('Hide duplicated AdLibs')}
From 4d615f211f71c950a7f1cb96410af5a8f0664fd8 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 15 Jun 2021 10:03:11 +0100
Subject: [PATCH 028/112] feat: Select rundown layout from lobby and use
selected layouts as defaults
---
meteor/client/ui/RundownList.tsx | 9 +-
.../ui/RundownList/RundownListItemView.tsx | 8 +-
.../ui/RundownList/RundownPlaylistUi.tsx | 8 +-
...ion.tsx => RundownViewLayoutSelection.tsx} | 16 +-
meteor/client/ui/RundownList/util.ts | 2 +-
meteor/client/ui/RundownView.tsx | 24 +-
.../ui/Settings/RundownLayoutEditor.tsx | 218 +++++++++---------
.../RundownViewLayoutSettings.tsx | 90 ++++++++
.../rundownLayouts/ShelfLayoutSettings.tsx | 13 --
meteor/lib/api/rundownLayouts.ts | 47 ++--
meteor/lib/collections/RundownLayouts.ts | 13 +-
.../api/__tests__/rundownLayouts.test.ts | 2 +-
12 files changed, 286 insertions(+), 164 deletions(-)
rename meteor/client/ui/RundownList/{RundownShelfLayoutSelection.tsx => RundownViewLayoutSelection.tsx} (86%)
create mode 100644 meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
diff --git a/meteor/client/ui/RundownList.tsx b/meteor/client/ui/RundownList.tsx
index 01deee519d..e756eeb3a8 100644
--- a/meteor/client/ui/RundownList.tsx
+++ b/meteor/client/ui/RundownList.tsx
@@ -97,7 +97,7 @@ export const RundownList = translateWithTracker((): IRundownsListProps => {
const showStyleBases = ShowStyleBases.find().fetch()
const showStyleVariants = ShowStyleVariants.find().fetch()
const rundownLayouts = RundownLayouts.find({
- $or: [{ exposeAsStandalone: true }, { exposeAsShelf: true }],
+ $or: [{ exposeAsSelectableLayout: true }, { exposeAsShelf: true }],
}).fetch()
return {
@@ -309,8 +309,11 @@ export const RundownList = translateWithTracker((): IRundownsListProps => {
) && {t('Expected End Time')} }
{t('Last updated')}
{this.props.rundownLayouts.some(
- (l) => RundownLayoutsAPI.IsLayoutForShelf(l) && (l.exposeAsShelf || l.exposeAsStandalone)
- ) && {t('Shelf Layout')} }
+ (l) =>
+ (RundownLayoutsAPI.IsLayoutForShelf(l) && l.exposeAsStandalone) ||
+ (RundownLayoutsAPI.IsLayoutForRundownView(l) && l.exposeAsSelectableLayout)
+ ) && {t('View Layout')} }
+
{this.renderRundownPlaylists(rundownPlaylists)}
diff --git a/meteor/client/ui/RundownList/RundownListItemView.tsx b/meteor/client/ui/RundownList/RundownListItemView.tsx
index d6edf0dfce..903d53dc43 100644
--- a/meteor/client/ui/RundownList/RundownListItemView.tsx
+++ b/meteor/client/ui/RundownList/RundownListItemView.tsx
@@ -10,7 +10,7 @@ import { iconDragHandle, iconRemove, iconResync } from './icons'
import { DisplayFormattedTime } from './DisplayFormattedTime'
import { EyeIcon } from '../../lib/ui/icons/rundownList'
import { LoopingIcon } from '../../lib/ui/icons/looping'
-import { RundownShelfLayoutSelection } from './RundownShelfLayoutSelection'
+import { RundownViewLayoutSelection } from './RundownViewLayoutSelection'
import { RundownLayoutBase } from '../../../lib/collections/RundownLayouts'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
@@ -153,11 +153,13 @@ export default withTranslation()(function RundownListItemView(props: Translated<
{rundownLayouts.some(
- (l) => RundownLayoutsAPI.IsLayoutForShelf(l) && (l.exposeAsShelf || l.exposeAsStandalone)
+ (l) =>
+ (RundownLayoutsAPI.IsLayoutForShelf(l) && l.exposeAsStandalone) ||
+ (RundownLayoutsAPI.IsLayoutForRundownView(l) && l.exposeAsSelectableLayout)
) && (
{isOnlyRundownInPlaylist && (
-
{rundownLayouts.some(
- (l) => RundownLayoutsAPI.IsLayoutForShelf(l) && (l.exposeAsShelf || l.exposeAsStandalone)
+ (l) =>
+ (RundownLayoutsAPI.IsLayoutForShelf(l) && l.exposeAsStandalone) ||
+ (RundownLayoutsAPI.IsLayoutForRundownView(l) && l.exposeAsSelectableLayout)
) && (
- ,
IRundownShelfLayoutSelectionState
@@ -43,7 +43,7 @@ export const RundownShelfLayoutSelection = withTranslation()(
private renderLinkItem(layout: RundownLayoutBase, link: string, key: string) {
return {
key,
- children: (
+ node: (
this.saveViewChoice(key)}>
{
return this.renderLinkItem(layout, getShelfLink(this.props.playlistId, layout._id), `standalone${layout._id}`)
})
- const shelfLayouts = layoutsInRundown
- .filter((layout) => RundownLayoutsAPI.IsLayoutForShelf(layout) && layout.exposeAsShelf)
+ const rundownViewLayouts = layoutsInRundown
+ .filter((layout) => RundownLayoutsAPI.IsLayoutForRundownView(layout) && layout.exposeAsSelectableLayout)
.map((layout) => {
return this.renderLinkItem(
layout,
- getRundownWithShelfLayoutLink(this.props.playlistId, layout._id),
+ getRundownWithLayoutLink(this.props.playlistId, layout._id),
`shelf${layout._id}`
)
})
- return shelfLayouts.length > 0 || standaloneLayouts.length > 0 ? (
+ return rundownViewLayouts.length > 0 || standaloneLayouts.length > 0 ? (
{t('Standalone Shelf')}
},
...standaloneLayouts,
{ node:
{t('Rundown & Shelf')}
},
- ...shelfLayouts,
+ ...rundownViewLayouts,
{ node:
},
{
key: 'default',
diff --git a/meteor/client/ui/RundownList/util.ts b/meteor/client/ui/RundownList/util.ts
index d06a05c0ef..2676516a62 100644
--- a/meteor/client/ui/RundownList/util.ts
+++ b/meteor/client/ui/RundownList/util.ts
@@ -47,7 +47,7 @@ export function getRundownWithShelfLayoutLink(
const encodedRundownId = encodeURIComponent(encodeURIComponent(unprotectString(rundownId)))
const encodedLayoutId = encodeURIComponent(encodeURIComponent(unprotectString(layoutId)))
- return `/rundown/${encodedRundownId}?shelfLayout=${encodedLayoutId}`
+ return `/rundown/${encodedRundownId}?rundownViewLayout=${encodedLayoutId}`
}
export function confirmDeleteRundown(rundown: Rundown, t: TFunction) {
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 382a8549a0..974c6352b3 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -1795,6 +1795,22 @@ export const RundownView = translateWithTracker
((
)
}
+ // Try to load defaults from rundown view layouts
+ if (selectedViewLayout && RundownLayoutsAPI.IsLayoutForRundownView(selectedViewLayout)) {
+ let rundownLayout = selectedViewLayout as RundownViewLayout
+ if (!selectedShelfLayout && rundownLayout.shelfLayout) {
+ selectedShelfLayout = props.rundownLayouts.find((i) => i._id === rundownLayout.shelfLayout)
+ }
+
+ if (!selectedMiniShelfLayout && rundownLayout.miniShelfLayout) {
+ selectedMiniShelfLayout = props.rundownLayouts.find((i) => i._id === rundownLayout.miniShelfLayout)
+ }
+
+ if (!selectedHeaderLayout && rundownLayout.rundownHeaderLayout) {
+ selectedHeaderLayout = props.rundownLayouts.find((i) => i._id === rundownLayout.rundownHeaderLayout)
+ }
+ }
+
// if not, try the first RUNDOWN_LAYOUT available
if (!selectedShelfLayout) {
selectedShelfLayout = props.rundownLayouts.find((i) => i.type === RundownLayoutType.RUNDOWN_LAYOUT)
@@ -1802,19 +1818,19 @@ export const RundownView = translateWithTracker((
// if still not found, use the first one
if (!selectedShelfLayout) {
- selectedShelfLayout = props.rundownLayouts.filter((i) => RundownLayoutsAPI.IsLayoutForShelf(i))[0]
+ selectedShelfLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.IsLayoutForShelf(i))
}
if (!selectedViewLayout) {
- selectedViewLayout = props.rundownLayouts.filter((i) => RundownLayoutsAPI.IsLayoutForRundownView(i))[0]
+ selectedViewLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.IsLayoutForRundownView(i))
}
if (!selectedHeaderLayout) {
- selectedHeaderLayout = props.rundownLayouts.filter((i) => RundownLayoutsAPI.IsLayoutForRundownHeader(i))[0]
+ selectedHeaderLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.IsLayoutForRundownHeader(i))
}
if (!selectedMiniShelfLayout) {
- selectedMiniShelfLayout = props.rundownLayouts.filter((i) => RundownLayoutsAPI.IsLayoutForMiniShelf(i))[0]
+ selectedMiniShelfLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.IsLayoutForMiniShelf(i))
}
}
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index a02453dc7f..b6f503ee66 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -38,6 +38,7 @@ import { defaultColorPickerPalette } from '../../lib/colorPicker'
import FilterEditor from './components/FilterEditor'
import ShelfLayoutSettings from './components/rundownLayouts/ShelfLayoutSettings'
import RundownHeaderLayoutSettings from './components/rundownLayouts/RundownHeaderLayoutSettings'
+import RundownViewLayoutSettings from './components/rundownLayouts/RundownViewLayoutSettings'
export interface IProps {
showStyleBase: ShowStyleBase
@@ -61,8 +62,6 @@ export default translateWithTracker((props: IProp
const rundownLayouts = RundownLayouts.find({
showStyleBaseId: props.showStyleBase._id,
userId: { $exists: false },
- type: { $in: layoutTypes },
- regionId: props.customRegion._id,
}).fetch()
return {
@@ -337,9 +336,14 @@ export default translateWithTracker((props: IProp
{isShelfLayout && }
{isRundownHeaderLayout && }
- {layout?.filtersTitle ? t(`${layout?.filtersTitle}`) : t('Filters')}
- {item.filters.length === 0 ? (
- {t('There are no filters set up yet')}
+ {isRundownViewLayout && }
+ {layout?.supportsFilters ? (
+
+ {layout?.filtersTitle ? t(`${layout?.filtersTitle}`) : t('Filters')}
+ {item.filters.length === 0 ? (
+ {t('There are no filters set up yet')}
+ ) : null}
+
) : null}
{item.filters.map((tab, index) => (
((props: IProp
renderItems() {
const { t } = this.props
- return (this.props.rundownLayouts || []).map((item, index) => {
- const layout = this.props.customRegion.layouts.find((l) => l.type === item.type)
- return (
-
-
- {item.name || t('Default Layout')}
- {item.type}
-
- {this.props.studios.map((studio) => (
-
-
- {studio.name}
-
-
- ))}
-
-
- this.downloadItem(item)}>
-
-
- this.editItem(item)}>
-
-
- this.onDeleteLayout(e, item)}>
-
-
-
-
- {this.isItemEdited(item) && (
-
-
-
-
-
- {t('Name')}
-
-
-
-
-
- {t('Type')}
-
-
-
-
- {this.renderElements(item, layout)}
- {layout?.supportedElements.length ? (
-
-
this.onAddElement(item)}>
-
-
- {t(`Add ${layout?.filtersTitle?.toLowerCase() ?? 'filter'}`)}
-
+ return (this.props.rundownLayouts || [])
+ .filter((l) => l.regionId === this.props.customRegion._id && this.props.layoutTypes.includes(l.type))
+ .map((item, index) => {
+ const layout = this.props.customRegion.layouts.find((l) => l.type === item.type)
+ return (
+
+
+ {item.name || t('Default Layout')}
+ {item.type}
+
+ {this.props.studios.map((studio) => (
+
+
+ {studio.name}
+
+
+ ))}
+
+
+ this.downloadItem(item)}>
+
+
+ this.editItem(item)}>
+
+
+ this.onDeleteLayout(e, item)}>
+
+
+
+
+ {this.isItemEdited(item) && (
+
+
+
+
+
+ {t('Name')}
+
+
+
+
+
+ {t('Type')}
+
+
+
- ) : null}
- {item.type === RundownLayoutType.DASHBOARD_LAYOUT ? (
- <>
- {RundownLayoutsAPI.isDashboardLayout(item) ? this.renderActionButtons(item) : null}
+ {this.renderElements(item, layout)}
+ {layout?.supportedElements.length ? (
- this.finishEditItem(item)}>
-
-
- this.onAddButton(item)}>
+ this.onAddElement(item)}>
- {t('Add button')}
-
-
- >
- ) : (
- <>
-
- this.finishEditItem(item)}>
-
+ {t(`Add ${layout?.filtersTitle?.toLowerCase() ?? 'filter'}`)}
- >
- )}
-
-
- )}
-
- )
- })
+ ) : null}
+ {item.type === RundownLayoutType.DASHBOARD_LAYOUT ? (
+ <>
+
{RundownLayoutsAPI.isDashboardLayout(item) ? this.renderActionButtons(item) : null}
+
+ this.finishEditItem(item)}>
+
+
+ this.onAddButton(item)}>
+
+
+ {t('Add button')}
+
+
+ >
+ ) : (
+ <>
+
+ this.finishEditItem(item)}>
+
+
+
+ >
+ )}
+
+
+ )}
+
+ )
+ })
}
onUploadFile(e) {
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
new file mode 100644
index 0000000000..fdc42052da
--- /dev/null
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -0,0 +1,90 @@
+import React from 'react'
+import { withTranslation } from 'react-i18next'
+import { RundownLayoutsAPI } from '../../../../../lib/api/rundownLayouts'
+import { RundownLayoutBase, RundownLayoutId, RundownLayouts } from '../../../../../lib/collections/RundownLayouts'
+import { unprotectString } from '../../../../../lib/lib'
+import { EditAttribute } from '../../../../lib/EditAttribute'
+import { MeteorReactComponent } from '../../../../lib/MeteorReactComponent'
+import { Translated } from '../../../../lib/ReactMeteorData/ReactMeteorData'
+
+function filterLayouts(
+ rundownLayouts: RundownLayoutBase[],
+ testFunc: (l: RundownLayoutBase) => boolean
+): Array<{ name: string; value: string }> {
+ return rundownLayouts.filter(testFunc).map((l) => ({ name: l.name, value: unprotectString(l._id) }))
+}
+
+interface IProps {
+ item: RundownLayoutBase
+ layouts: RundownLayoutBase[]
+}
+
+interface IState {}
+
+export default withTranslation()(
+ class RundownViewLayoutSettings extends MeteorReactComponent, IState> {
+ render() {
+ let { t } = this.props
+
+ return (
+
+
+
+ {t('Expose as user selectable layout')}
+
+
+
+
+
+ {t('Shelf Layout')}
+
+
+
+
+
+ {t('Mini Shelf Layout')}
+
+
+
+
+
+ {t('Rundown Header Layout')}
+
+
+
+
+ )
+ }
+ }
+)
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/ShelfLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/ShelfLayoutSettings.tsx
index 349f340c3c..f08ceeb4fd 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/ShelfLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/ShelfLayoutSettings.tsx
@@ -31,19 +31,6 @@ export default withTranslation()(
>
-
-
- {t('Expose as a layout for the shelf')}
-
-
-
{t('Open shelf by default')}
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 34fab9e82b..587b2e742f 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -14,6 +14,7 @@ import {
RundownViewLayout,
RundownLayoutRundownHeader,
RundownLayoutShelfBase,
+ CustomizableRegions,
} from '../collections/RundownLayouts'
import { ShowStyleBaseId } from '../collections/ShowStyleBases'
import * as _ from 'underscore'
@@ -37,6 +38,7 @@ export enum RundownLayoutsAPIMethods {
export interface LayoutDescriptor {
supportedElements: RundownLayoutElementType[]
filtersTitle?: string // e.g. tabs/panels
+ supportsFilters?: boolean
}
export interface CustomizableRegionSettingsManifest {
@@ -49,6 +51,7 @@ export interface CustomizableRegionLayout {
_id: string
type: RundownLayoutType
filtersTitle?: string
+ supportsFilters?: boolean
supportedElements: RundownLayoutElementType[]
}
@@ -74,28 +77,28 @@ class RundownLayoutsRegistry {
this.rundownHeaderLayouts.set(id, description)
}
- public IsShelfLayout(id: RundownLayoutType) {
- return this.shelfLayouts.has(id)
+ public IsShelfLayout(regionId: string) {
+ return regionId === CustomizableRegions.Shelf
}
- public IsRudownViewLayout(id: RundownLayoutType) {
- return this.rundownViewLayouts.has(id)
+ public IsRudownViewLayout(regionId: string) {
+ return regionId === CustomizableRegions.RundownView
}
- public IsMiniShelfLayout(id: RundownLayoutType) {
- return this.miniShelfLayouts.has(id)
+ public IsMiniShelfLayout(regionId: string) {
+ return regionId === CustomizableRegions.MiniShelf
}
- public IsRundownHeaderLayout(id: RundownLayoutType) {
- return this.rundownHeaderLayouts.has(id)
+ public IsRundownHeaderLayout(regionId: string) {
+ return regionId === CustomizableRegions.RundownHeader
}
public GetSettingsManifest(): CustomizableRegionSettingsManifest[] {
return [
{
- _id: 'shelf_layouts',
- title: 'Shelf Layouts',
- layouts: Array.from(this.shelfLayouts.entries()).map(([layoutType, descriptor]) => {
+ _id: CustomizableRegions.RundownView,
+ title: 'Rundown View Layouts',
+ layouts: Array.from(this.rundownViewLayouts.entries()).map(([layoutType, descriptor]) => {
return literal({
_id: layoutType,
type: layoutType,
@@ -105,9 +108,9 @@ class RundownLayoutsRegistry {
}),
},
{
- _id: 'rundown_view_layouts',
- title: 'Rundown View Layouts',
- layouts: Array.from(this.rundownViewLayouts.entries()).map(([layoutType, descriptor]) => {
+ _id: CustomizableRegions.Shelf,
+ title: 'Shelf Layouts',
+ layouts: Array.from(this.shelfLayouts.entries()).map(([layoutType, descriptor]) => {
return literal({
_id: layoutType,
type: layoutType,
@@ -117,7 +120,7 @@ class RundownLayoutsRegistry {
}),
},
{
- _id: 'mini_shelf_layouts',
+ _id: CustomizableRegions.MiniShelf,
title: 'Mini Shelf Layouts',
layouts: Array.from(this.miniShelfLayouts.entries()).map(([layoutType, descriptor]) => {
return literal({
@@ -129,7 +132,7 @@ class RundownLayoutsRegistry {
}),
},
{
- _id: 'rundown_header_layouts',
+ _id: CustomizableRegions.RundownHeader,
title: 'Rundown Header Layouts',
layouts: Array.from(this.rundownHeaderLayouts.entries()).map(([layoutType, descriptor]) => {
return literal({
@@ -148,6 +151,7 @@ export namespace RundownLayoutsAPI {
const registry = new RundownLayoutsRegistry()
registry.RegisterShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
filtersTitle: 'Panels',
+ supportsFilters: true,
supportedElements: [
RundownLayoutElementType.ADLIB_REGION,
RundownLayoutElementType.EXTERNAL_FRAME,
@@ -157,6 +161,7 @@ export namespace RundownLayoutsAPI {
})
registry.RegisterShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
filtersTitle: 'Tabs',
+ supportsFilters: true,
supportedElements: [
RundownLayoutElementType.ADLIB_REGION,
RundownLayoutElementType.EXTERNAL_FRAME,
@@ -182,19 +187,19 @@ export namespace RundownLayoutsAPI {
}
export function IsLayoutForShelf(layout: RundownLayoutBase): layout is RundownLayoutShelfBase {
- return registry.IsShelfLayout(layout.type)
+ return registry.IsShelfLayout(layout.regionId)
}
- export function IsLayoutForRundownView(layout: RundownLayoutBase): layout is RundownLayoutBase {
- return registry.IsRudownViewLayout(layout.type)
+ export function IsLayoutForRundownView(layout: RundownLayoutBase): layout is RundownViewLayout {
+ return registry.IsRudownViewLayout(layout.regionId)
}
export function IsLayoutForMiniShelf(layout: RundownLayoutBase): layout is RundownLayoutShelfBase {
- return registry.IsMiniShelfLayout(layout.type)
+ return registry.IsMiniShelfLayout(layout.regionId)
}
export function IsLayoutForRundownHeader(layout: RundownLayoutBase): layout is RundownLayoutRundownHeader {
- return registry.IsRundownHeaderLayout(layout.type)
+ return registry.IsRundownHeaderLayout(layout.regionId)
}
export function isRundownViewLayout(layout: RundownLayoutBase): layout is RundownViewLayout {
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 186701f92d..6fb3ac4095 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -22,6 +22,13 @@ export enum RundownLayoutType {
RUNDOWN_HEADER_LAYOUT = 'rundown_header_layout',
}
+export enum CustomizableRegions {
+ RundownView = 'rundown_view_layouts',
+ Shelf = 'shelf_layouts',
+ MiniShelf = 'mini_shelf_layouts',
+ RundownHeader = 'rundown_header_layouts',
+}
+
/**
* Display style to be used by this filter
*
@@ -165,11 +172,15 @@ export interface RundownLayoutBase {
export interface RundownViewLayout extends RundownLayoutBase {
type: RundownLayoutType.RUNDOWN_VIEW_LAYOUT
expectedEndText: string
+ /** Expose as a layout that can be selected by the user in the lobby view */
+ exposeAsSelectableLayout: boolean
+ shelfLayout: RundownLayoutId
+ miniShelfLayout: RundownLayoutId
+ rundownHeaderLayout: RundownLayoutId
}
export interface RundownLayoutShelfBase extends RundownLayoutBase {
exposeAsStandalone: boolean
- exposeAsShelf: boolean
openByDefault: boolean
startingHeight?: number
}
diff --git a/meteor/server/api/__tests__/rundownLayouts.test.ts b/meteor/server/api/__tests__/rundownLayouts.test.ts
index e69e1be283..5f6daede9a 100644
--- a/meteor/server/api/__tests__/rundownLayouts.test.ts
+++ b/meteor/server/api/__tests__/rundownLayouts.test.ts
@@ -58,11 +58,11 @@ describe('Rundown Layouts', () => {
filters: [],
showStyleBaseId: env.showStyleBaseId,
type: RundownLayoutType.RUNDOWN_LAYOUT,
- exposeAsShelf: false,
exposeAsStandalone: false,
icon: '',
iconColor: '',
openByDefault: false,
+ regionId: 'shelf_layouts'
})
return { rundownLayout: mockLayout, rundownLayoutId }
}
From 8e14b2620839d633d5895f1a40b14f62ae435fc8 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 15 Jun 2021 12:27:21 +0100
Subject: [PATCH 029/112] fix: Lint errors
---
meteor/client/ui/RundownView.tsx | 2 +-
.../components/rundownLayouts/RundownViewLayoutSettings.tsx | 2 +-
meteor/server/api/__tests__/rundownLayouts.test.ts | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 974c6352b3..7955303c0f 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -1797,7 +1797,7 @@ export const RundownView = translateWithTracker((
// Try to load defaults from rundown view layouts
if (selectedViewLayout && RundownLayoutsAPI.IsLayoutForRundownView(selectedViewLayout)) {
- let rundownLayout = selectedViewLayout as RundownViewLayout
+ const rundownLayout = selectedViewLayout as RundownViewLayout
if (!selectedShelfLayout && rundownLayout.shelfLayout) {
selectedShelfLayout = props.rundownLayouts.find((i) => i._id === rundownLayout.shelfLayout)
}
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index fdc42052da..d32b24a308 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -24,7 +24,7 @@ interface IState {}
export default withTranslation()(
class RundownViewLayoutSettings extends MeteorReactComponent, IState> {
render() {
- let { t } = this.props
+ const { t } = this.props
return (
diff --git a/meteor/server/api/__tests__/rundownLayouts.test.ts b/meteor/server/api/__tests__/rundownLayouts.test.ts
index 5f6daede9a..c821eb6540 100644
--- a/meteor/server/api/__tests__/rundownLayouts.test.ts
+++ b/meteor/server/api/__tests__/rundownLayouts.test.ts
@@ -62,7 +62,7 @@ describe('Rundown Layouts', () => {
icon: '',
iconColor: '',
openByDefault: false,
- regionId: 'shelf_layouts'
+ regionId: 'shelf_layouts',
})
return { rundownLayout: mockLayout, rundownLayoutId }
}
From 7b4a17866389396aa1a28ce7ed32c1f3cdfb85d9 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 15 Jun 2021 14:37:21 +0100
Subject: [PATCH 030/112] feat: Settings for header customisations
---
meteor/client/ui/RundownView.tsx | 16 +++--
.../RundownHeaderLayoutSettings.tsx | 65 ++++++++++++++++++-
meteor/lib/collections/RundownLayouts.ts | 6 ++
3 files changed, 79 insertions(+), 8 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 7955303c0f..246286438f 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -357,18 +357,20 @@ const TimingDisplay = withTranslation()(
{!rundownPlaylist.startedPlayback ||
this.props.isLastRundownInPlaylist ||
- !currentRundown?.endIsBreak ? ( // TODO: Setting
+ !(currentRundown?.endIsBreak && this.props.layout?.hideExpectedEndBeforeBreak) ? (
) : null}
{rundownPlaylist.startedPlayback &&
currentRundown?.endIsBreak &&
- !this.props.isLastRundownInPlaylist ? ( // TODO: Setting // TODO: Find next break in higher-order component, so next breaks reflects next rundown in playlist marked as break.
-
+ this.props.layout?.showNextBreakTiming &&
+ !(this.props.isLastRundownInPlaylist && this.props.layout.lastRundownIsNotBreak) ? ( // TODO: Find next break in higher-order component, so next breaks reflects next rundown in playlist marked as break.
+
) : null}
) : (
@@ -441,6 +443,7 @@ interface IEndTimingProps {
expectedStart?: number
expectedDuration: number
expectedEnd?: number
+ endLabel?: string
}
const PlaylistEndTiming = withTranslation()(
@@ -453,12 +456,12 @@ const PlaylistEndTiming = withTranslation()(
{!this.props.loop && this.props.expectedStart ? (
- {t('Planned End')}
+ {t(this.props.endLabel ?? 'Planned End')}
) : !this.props.loop && this.props.expectedEnd ? (
- {t('Planned End')}
+ {t(this.props.endLabel ?? 'Planned End')}
) : null}
@@ -508,6 +511,7 @@ const PlaylistEndTiming = withTranslation()(
interface INextBreakTimingProps {
loop?: boolean
breakRundown: Rundown
+ breakText?: string
}
const NextBreakTiming = withTranslation()(
@@ -523,7 +527,7 @@ const NextBreakTiming = withTranslation()(
return (
- {t('Next Break')}
+ {t(this.props.breakText ?? 'Next Break')}
{!this.props.loop && breakRundown.expectedEnd ? (
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
index 5fe708b4f6..c0fffd8cee 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
@@ -12,7 +12,7 @@ interface IProps {
interface IState {}
export default withTranslation()(
- class ShelfLayoutSettings extends MeteorReactComponent, IState> {
+ class RundownHeaderLayoutSettings extends MeteorReactComponent, IState> {
render() {
const { t } = this.props
@@ -20,7 +20,7 @@ export default withTranslation()(
- {t('End of Show Text')}
+ {t('Expected End text')}
+ {t('Text to show above countdown to end of show')}
+
+
+
+
+ {t('Hide Expected End timing when a break is next')}
+
+
+ {t('While there are still breaks coming up in the show, hide the Expected End timers')}
+
+
+
+
+
+ {t('Show next break timing')}
+
+ {t('Whether to show countdown to next break')}
+
+
+
+
+ {t('Last rundown is not break')}
+
+
+ {t("Don't treat the end of the last rundown in a playlist as a break")}
+
+
+
+
+
+ {t('Next Break text')}
+
+ {t('Text to show above countdown to next break')}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 6fb3ac4095..6f415b7cc3 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -193,6 +193,12 @@ export interface RundownLayoutRundownHeader extends RundownLayoutBase {
type: RundownLayoutType.RUNDOWN_HEADER_LAYOUT
expectedEndText: string
nextBreakText: string
+ /** When true, hide the Planned End timer when there is a rundown marked as a break in the future */
+ hideExpectedEndBeforeBreak: boolean
+ /** When a rundown is marked as a break, show the Next Break timing */
+ showNextBreakTiming: boolean
+ /** If true, don't treat the last rundown as a break even if it's marked as one */
+ lastRundownIsNotBreak: boolean
}
export enum ActionButtonType {
From b6b0e876e0f52ba408bfdc686c801708facb6cda Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 15 Jun 2021 16:15:29 +0100
Subject: [PATCH 031/112] feat: Look ahead to find rundown marked as next break
---
meteor/client/ui/RundownView.tsx | 74 +++++++++++++++++++++++++++-----
1 file changed, 63 insertions(+), 11 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 246286438f..d5618fa23f 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -207,6 +207,8 @@ const WarningDisplay = withTranslation()(
interface ITimingDisplayProps {
rundownPlaylist: RundownPlaylist
currentRundown: Rundown | undefined
+ /** Rundowns between current rundown and rundown with next break (inclusive of both). Undefined if there's no break in the future. */
+ rundownsBeforeNextBreak: Rundown[] | undefined
rundownCount: number
layout: RundownLayoutRundownHeader | undefined
}
@@ -357,7 +359,7 @@ const TimingDisplay = withTranslation()(
{!rundownPlaylist.startedPlayback ||
this.props.isLastRundownInPlaylist ||
- !(currentRundown?.endIsBreak && this.props.layout?.hideExpectedEndBeforeBreak) ? (
+ !(this.props.rundownsBeforeNextBreak && this.props.layout?.hideExpectedEndBeforeBreak) ? (
) : null}
{rundownPlaylist.startedPlayback &&
- currentRundown?.endIsBreak &&
+ this.props.rundownsBeforeNextBreak &&
this.props.layout?.showNextBreakTiming &&
- !(this.props.isLastRundownInPlaylist && this.props.layout.lastRundownIsNotBreak) ? ( // TODO: Find next break in higher-order component, so next breaks reflects next rundown in playlist marked as break.
-
+ !(this.props.isLastRundownInPlaylist && this.props.layout.lastRundownIsNotBreak) ? (
+
) : null}
) : (
@@ -510,7 +515,7 @@ const PlaylistEndTiming = withTranslation()(
interface INextBreakTimingProps {
loop?: boolean
- breakRundown: Rundown
+ rundownsBeforeBreak: Rundown[]
breakText?: string
}
@@ -518,12 +523,27 @@ const NextBreakTiming = withTranslation()(
withTiming()(
class PlaylistEndTiming extends React.Component>> {
render() {
- let { t, breakRundown } = this.props
+ let { t, rundownsBeforeBreak } = this.props
+ let breakRundown = rundownsBeforeBreak.length ? rundownsBeforeBreak[rundownsBeforeBreak.length - 1] : undefined
const rundownAsPlayedDuration = this.props.timingDurations.rundownAsPlayedDurations
- ? this.props.timingDurations.rundownAsPlayedDurations[unprotectString(breakRundown._id)]
+ ? rundownsBeforeBreak.reduce(
+ (prev, curr) => (prev += this.props.timingDurations.rundownAsPlayedDurations![unprotectString(curr._id)]),
+ 0
+ )
+ : undefined
+
+ const accumulatedExpectedDurations = this.props.timingDurations.rundownExpectedDurations
+ ? rundownsBeforeBreak.reduce(
+ (prev, curr) => (prev += this.props.timingDurations.rundownExpectedDurations![unprotectString(curr._id)]),
+ 0
+ )
: undefined
+ if (!breakRundown) {
+ return null
+ }
+
return (
@@ -535,16 +555,16 @@ const NextBreakTiming = withTranslation()(
{RundownUtils.formatDiffToTimecode(getCurrentTime() - breakRundown.expectedEnd, true, true, true)}
) : null}
- {breakRundown.expectedDuration ? (
+ {accumulatedExpectedDurations ? (
(breakRundown.expectedDuration || 0),
+ heavy: (rundownAsPlayedDuration || 0) < (accumulatedExpectedDurations || 0),
+ light: (rundownAsPlayedDuration || 0) > (accumulatedExpectedDurations || 0),
})}
>
{t('Diff')}
{RundownUtils.formatDiffToTimecode(
- (rundownAsPlayedDuration || 0) - breakRundown.expectedDuration,
+ (rundownAsPlayedDuration || 0) - accumulatedExpectedDurations,
true,
false,
true,
@@ -572,6 +592,8 @@ interface IRundownHeaderProps {
currentRundown: Rundown | undefined
studio: Studio
rundownIds: RundownId[]
+ /** Rundowns between current rundown and rundown with next break (inclusive of both). Undefined if there's no break in the future. */
+ rundownsBeforeNextBreak: Rundown[] | undefined
firstRundown: Rundown | undefined
onActivate?: (isRehearsal: boolean) => void
onRegisterHotkeys?: (hotkeys: Array) => void
@@ -1465,6 +1487,7 @@ const RundownHeader = withTranslation()(
@@ -2752,6 +2775,31 @@ export const RundownView = translateWithTracker((
}
}
+ private getRundownsBeforeNextBreak(
+ currentRundown: Rundown | undefined,
+ breakRundowns: Rundown[]
+ ): Rundown[] | undefined {
+ if (!currentRundown) {
+ return undefined
+ }
+
+ let currentRundownIndex = this.props.rundowns.findIndex((r) => r._id === currentRundown._id)
+
+ if (currentRundownIndex === -1) {
+ return undefined
+ }
+
+ let nextBreakIndex = this.props.rundowns.findIndex((rundown, index) => {
+ if (index < currentRundownIndex) {
+ return false
+ }
+
+ return breakRundowns.some((r) => r._id == rundown._id)
+ })
+
+ return this.props.rundowns.slice(currentRundownIndex, nextBreakIndex + 1)
+ }
+
render() {
const { t } = this.props
@@ -2882,6 +2930,10 @@ export const RundownView = translateWithTracker((
playlist={this.props.playlist}
studio={this.props.studio}
rundownIds={this.props.rundowns.map((r) => r._id)}
+ rundownsBeforeNextBreak={this.getRundownsBeforeNextBreak(
+ this.state.currentRundown || this.props.rundowns[0],
+ this.props.rundowns.filter((r) => r.endIsBreak)
+ )}
firstRundown={this.props.rundowns[0]}
onActivate={this.onActivate}
studioMode={this.state.studioMode}
From 3374fd3aa795358d15c0c4d400b5e3867a9aeff0 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 16 Jun 2021 10:22:04 +0100
Subject: [PATCH 032/112] fix: Blank timer labels
---
meteor/client/ui/RundownView.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index d5618fa23f..c2fb331db1 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -461,12 +461,12 @@ const PlaylistEndTiming = withTranslation()(
{!this.props.loop && this.props.expectedStart ? (
- {t(this.props.endLabel ?? 'Planned End')}
+ {t(this.props.endLabel || 'Planned End')}
) : !this.props.loop && this.props.expectedEnd ? (
- {t(this.props.endLabel ?? 'Planned End')}
+ {t(this.props.endLabel || 'Planned End')}
) : null}
@@ -547,7 +547,7 @@ const NextBreakTiming = withTranslation()(
return (
- {t(this.props.breakText ?? 'Next Break')}
+ {t(this.props.breakText || 'Next Break')}
{!this.props.loop && breakRundown.expectedEnd ? (
From f42235e340b7e2474bb1aef91f14fc44c9467139 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 16 Jun 2021 10:32:51 +0100
Subject: [PATCH 033/112] fix: Check for last break
---
meteor/client/ui/RundownView.tsx | 24 ++++++++++++++++--------
1 file changed, 16 insertions(+), 8 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index c2fb331db1..096f6b6d8a 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -358,7 +358,7 @@ const TimingDisplay = withTranslation()(
{rundownPlaylist.expectedDuration ? (
{!rundownPlaylist.startedPlayback ||
- this.props.isLastRundownInPlaylist ||
+ this.props.lastBreak ||
!(this.props.rundownsBeforeNextBreak && this.props.layout?.hideExpectedEndBeforeBreak) ? (
void
onRegisterHotkeys?: (hotkeys: Array) => void
@@ -2778,7 +2780,7 @@ export const RundownView = translateWithTracker((
private getRundownsBeforeNextBreak(
currentRundown: Rundown | undefined,
breakRundowns: Rundown[]
- ): Rundown[] | undefined {
+ ): { rundownsBeforeNextBreak: Rundown[]; breakIsLastRundown } | undefined {
if (!currentRundown) {
return undefined
}
@@ -2797,7 +2799,10 @@ export const RundownView = translateWithTracker((
return breakRundowns.some((r) => r._id == rundown._id)
})
- return this.props.rundowns.slice(currentRundownIndex, nextBreakIndex + 1)
+ return {
+ rundownsBeforeNextBreak: this.props.rundowns.slice(currentRundownIndex, nextBreakIndex + 1),
+ breakIsLastRundown: nextBreakIndex === this.props.rundowns.length,
+ }
}
render() {
@@ -2812,6 +2817,11 @@ export const RundownView = translateWithTracker((
this.props.rundowns.find((r) => r._id === selectedPiece?.instance.rundownId)) ||
undefined
+ let breakProps = this.getRundownsBeforeNextBreak(
+ this.state.currentRundown || this.props.rundowns[0],
+ this.props.rundowns.filter((r) => r.endIsBreak)
+ )
+
return (
@@ -2930,10 +2940,8 @@ export const RundownView = translateWithTracker((
playlist={this.props.playlist}
studio={this.props.studio}
rundownIds={this.props.rundowns.map((r) => r._id)}
- rundownsBeforeNextBreak={this.getRundownsBeforeNextBreak(
- this.state.currentRundown || this.props.rundowns[0],
- this.props.rundowns.filter((r) => r.endIsBreak)
- )}
+ rundownsBeforeNextBreak={breakProps?.rundownsBeforeNextBreak}
+ lastBreak={breakProps?.breakIsLastRundown}
firstRundown={this.props.rundowns[0]}
onActivate={this.onActivate}
studioMode={this.state.studioMode}
From 68156cf91ede5de4af4a662766477d82ee9dc449 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 21 Jun 2021 15:42:07 +0100
Subject: [PATCH 034/112] fix: Filter supported elements
---
.../ui/Settings/RundownLayoutEditor.tsx | 1 +
.../ui/Settings/components/FilterEditor.tsx | 5 +-
meteor/lib/api/rundownLayouts.ts | 48 +++++++------------
3 files changed, 20 insertions(+), 34 deletions(-)
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index b6f503ee66..e7af9a3025 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -352,6 +352,7 @@ export default translateWithTracker((props: IProp
filter={tab}
index={index}
showStyleBase={this.props.showStyleBase}
+ supportedElements={layout?.supportedElements ?? []}
/>
))}
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index 3810825680..ca07ffa4ee 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -28,6 +28,7 @@ interface IProps {
filter: RundownLayoutElementBase
index: number
showStyleBase: ShowStyleBase
+ supportedElements: RundownLayoutElementType[]
}
interface ITrackedProps {}
@@ -1029,9 +1030,9 @@ export default translateWithTracker((props: IProp
modifiedClassName="bghl"
attribute={`filters.${this.props.index}.type`}
obj={this.props.item}
- options={RundownLayoutElementType}
+ options={this.props.supportedElements}
type="dropdown"
- mutateDisplayValue={(v) => (v === undefined ? RundownLayoutElementType.FILTER : v)}
+ mutateDisplayValue={(v) => (v === undefined ? this.props.supportedElements[0] : v)}
collection={RundownLayouts}
className="input text-input input-l"
>
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 587b2e742f..71c5a1bcf9 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -93,55 +93,39 @@ class RundownLayoutsRegistry {
return regionId === CustomizableRegions.RundownHeader
}
+ private wrapToCustomizableRegionLayout(
+ layouts: Map>
+ ): CustomizableRegionLayout[] {
+ return Array.from(layouts.entries()).map(([layoutType, descriptor]) => {
+ return literal({
+ _id: layoutType,
+ type: layoutType,
+ ...descriptor,
+ })
+ })
+ }
+
public GetSettingsManifest(): CustomizableRegionSettingsManifest[] {
return [
{
_id: CustomizableRegions.RundownView,
title: 'Rundown View Layouts',
- layouts: Array.from(this.rundownViewLayouts.entries()).map(([layoutType, descriptor]) => {
- return literal({
- _id: layoutType,
- type: layoutType,
- filtersTitle: descriptor.filtersTitle,
- supportedElements: descriptor.supportedElements,
- })
- }),
+ layouts: this.wrapToCustomizableRegionLayout(this.rundownViewLayouts),
},
{
_id: CustomizableRegions.Shelf,
title: 'Shelf Layouts',
- layouts: Array.from(this.shelfLayouts.entries()).map(([layoutType, descriptor]) => {
- return literal({
- _id: layoutType,
- type: layoutType,
- filtersTitle: descriptor.filtersTitle,
- supportedElements: descriptor.supportedElements,
- })
- }),
+ layouts: this.wrapToCustomizableRegionLayout(this.shelfLayouts),
},
{
_id: CustomizableRegions.MiniShelf,
title: 'Mini Shelf Layouts',
- layouts: Array.from(this.miniShelfLayouts.entries()).map(([layoutType, descriptor]) => {
- return literal({
- _id: layoutType,
- type: layoutType,
- filtersTitle: descriptor.filtersTitle,
- supportedElements: descriptor.supportedElements,
- })
- }),
+ layouts: this.wrapToCustomizableRegionLayout(this.miniShelfLayouts),
},
{
_id: CustomizableRegions.RundownHeader,
title: 'Rundown Header Layouts',
- layouts: Array.from(this.rundownHeaderLayouts.entries()).map(([layoutType, descriptor]) => {
- return literal({
- _id: layoutType,
- type: layoutType,
- filtersTitle: descriptor.filtersTitle,
- supportedElements: descriptor.supportedElements,
- })
- }),
+ layouts: this.wrapToCustomizableRegionLayout(this.rundownHeaderLayouts),
},
]
}
From 26f72e872e38302ddcf176d756c4b896cc5d2b30 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 21 Jun 2021 16:26:59 +0100
Subject: [PATCH 035/112] chore: Produce next break info inside
rundownTimingProvider
---
meteor/client/ui/RundownView.tsx | 54 +++---------------
.../RundownTiming/RundownTimingProvider.tsx | 15 ++++-
meteor/lib/rundown/rundownTiming.ts | 56 ++++++++++++++++++-
3 files changed, 76 insertions(+), 49 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 096f6b6d8a..a28bf3c00c 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -207,8 +207,6 @@ const WarningDisplay = withTranslation()(
interface ITimingDisplayProps {
rundownPlaylist: RundownPlaylist
currentRundown: Rundown | undefined
- /** Rundowns between current rundown and rundown with next break (inclusive of both). Undefined if there's no break in the future. */
- rundownsBeforeNextBreak: Rundown[] | undefined
rundownCount: number
layout: RundownLayoutRundownHeader | undefined
}
@@ -358,8 +356,10 @@ const TimingDisplay = withTranslation()(
{rundownPlaylist.expectedDuration ? (
{!rundownPlaylist.startedPlayback ||
- this.props.lastBreak ||
- !(this.props.rundownsBeforeNextBreak && this.props.layout?.hideExpectedEndBeforeBreak) ? (
+ this.props.timingDurations.breakIsLastRundown ||
+ !(
+ this.props.timingDurations.rundownsBeforeNextBreak && this.props.layout?.hideExpectedEndBeforeBreak
+ ) ? (
) : null}
{rundownPlaylist.startedPlayback &&
- this.props.rundownsBeforeNextBreak &&
+ this.props.timingDurations.rundownsBeforeNextBreak &&
this.props.layout?.showNextBreakTiming &&
- !(this.props.lastBreak && this.props.layout.lastRundownIsNotBreak) ? (
+ !(this.props.timingDurations.breakIsLastRundown && this.props.layout.lastRundownIsNotBreak) ? (
) : null}
@@ -592,10 +592,6 @@ interface IRundownHeaderProps {
currentRundown: Rundown | undefined
studio: Studio
rundownIds: RundownId[]
- /** Rundowns between current rundown and rundown with next break (inclusive of both). Undefined if there's no break in the future. */
- rundownsBeforeNextBreak: Rundown[] | undefined
- /** Whether the next break is also the last */
- lastBreak: boolean
firstRundown: Rundown | undefined
onActivate?: (isRehearsal: boolean) => void
onRegisterHotkeys?: (hotkeys: Array) => void
@@ -1489,7 +1485,6 @@ const RundownHeader = withTranslation()(
@@ -2777,34 +2772,6 @@ export const RundownView = translateWithTracker((
}
}
- private getRundownsBeforeNextBreak(
- currentRundown: Rundown | undefined,
- breakRundowns: Rundown[]
- ): { rundownsBeforeNextBreak: Rundown[]; breakIsLastRundown } | undefined {
- if (!currentRundown) {
- return undefined
- }
-
- let currentRundownIndex = this.props.rundowns.findIndex((r) => r._id === currentRundown._id)
-
- if (currentRundownIndex === -1) {
- return undefined
- }
-
- let nextBreakIndex = this.props.rundowns.findIndex((rundown, index) => {
- if (index < currentRundownIndex) {
- return false
- }
-
- return breakRundowns.some((r) => r._id == rundown._id)
- })
-
- return {
- rundownsBeforeNextBreak: this.props.rundowns.slice(currentRundownIndex, nextBreakIndex + 1),
- breakIsLastRundown: nextBreakIndex === this.props.rundowns.length,
- }
- }
-
render() {
const { t } = this.props
@@ -2817,11 +2784,6 @@ export const RundownView = translateWithTracker((
this.props.rundowns.find((r) => r._id === selectedPiece?.instance.rundownId)) ||
undefined
- let breakProps = this.getRundownsBeforeNextBreak(
- this.state.currentRundown || this.props.rundowns[0],
- this.props.rundowns.filter((r) => r.endIsBreak)
- )
-
return (
@@ -2940,8 +2902,6 @@ export const RundownView = translateWithTracker((
playlist={this.props.playlist}
studio={this.props.studio}
rundownIds={this.props.rundowns.map((r) => r._id)}
- rundownsBeforeNextBreak={breakProps?.rundownsBeforeNextBreak}
- lastBreak={breakProps?.breakIsLastRundown}
firstRundown={this.props.rundowns[0]}
onActivate={this.onActivate}
studioMode={this.state.studioMode}
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
index 0edb581023..c3cd181778 100644
--- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
@@ -10,6 +10,7 @@ import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists'
import { PartInstance } from '../../../../lib/collections/PartInstances'
import { RundownTiming, TimeEventArgs } from './RundownTiming'
import { RundownTimingCalculator, RundownTimingContext } from '../../../../lib/rundown/rundownTiming'
+import { Rundown } from '../../../../lib/collections/Rundowns'
const TIMING_DEFAULT_REFRESH_INTERVAL = 1000 / 60 // the interval for high-resolution events (timeupdateHR)
const LOW_RESOLUTION_TIMING_DECIMATOR = 15 // the low-resolution events will be called every
@@ -35,6 +36,8 @@ interface IRundownTimingProviderChildContext {
}
interface IRundownTimingProviderState {}
interface IRundownTimingProviderTrackedProps {
+ rundowns: Array
+ currentRundown: Rundown | undefined
parts: Array
partInstancesMap: Map
}
@@ -50,13 +53,19 @@ export const RundownTimingProvider = withTracker<
IRundownTimingProviderState,
IRundownTimingProviderTrackedProps
>((props) => {
+ let rundowns: Array = []
let parts: Array = []
const partInstancesMap = new Map()
+ let currentRundown: Rundown | undefined
if (props.playlist) {
+ rundowns = props.playlist.getRundowns()
const { parts: incomingParts } = props.playlist.getSegmentsAndPartsSync()
parts = incomingParts
const partInstances = props.playlist.getActivePartInstances()
+ const currentPartInstance = partInstances.find((p) => p._id === props.playlist!.currentPartInstanceId)
+ currentRundown = currentPartInstance ? rundowns.find((r) => r._id === currentPartInstance.rundownId) : rundowns[0]
+
partInstances.forEach((partInstance) => {
partInstancesMap.set(partInstance.part._id, partInstance)
@@ -96,6 +105,8 @@ export const RundownTimingProvider = withTracker<
})
}
return {
+ rundowns,
+ currentRundown,
parts,
partInstancesMap,
}
@@ -194,13 +205,15 @@ export const RundownTimingProvider = withTracker<
}
updateDurations(now: number, isLowResolution: boolean) {
- const { playlist, parts, partInstancesMap } = this.props
+ const { playlist, rundowns, currentRundown, parts, partInstancesMap } = this.props
this.durations = Object.assign(
this.durations,
this.timingCalculator.updateDurations(
now,
isLowResolution,
playlist,
+ rundowns,
+ currentRundown,
parts,
partInstancesMap,
this.props.defaultDuration
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index ec563e5070..cfdf09b7f8 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -6,6 +6,7 @@ import {
} from '../collections/PartInstances'
import { Part, PartId } from '../collections/Parts'
import { RundownPlaylist } from '../collections/RundownPlaylists'
+import { Rundown } from '../collections/Rundowns'
import { unprotectString, literal, protectString } from '../lib'
import { Settings } from '../Settings'
@@ -30,6 +31,8 @@ export class RundownTimingCalculator {
now: number,
isLowResolution: boolean,
playlist: RundownPlaylist | undefined,
+ rundowns: Rundown[],
+ currentRundown: Rundown | undefined,
parts: Part[],
partInstancesMap: Map,
/** Fallback duration for Parts that have no as-played duration of their own. */
@@ -47,13 +50,29 @@ export class RundownTimingCalculator {
let rundownExpectedDurations: Record = {}
let rundownAsPlayedDurations: Record = {}
+ let rundownsBeforeNextBreak: Rundown[] | undefined
+ let breakIsLastRundown: boolean | undefined
+
Object.keys(this.displayDurationGroups).forEach((key) => delete this.displayDurationGroups[key])
this.linearParts.length = 0
let nextAIndex = -1
let currentAIndex = -1
- if (playlist && parts) {
+ if (playlist) {
+ const breakProps = currentRundown
+ ? this.getRundownsBeforeNextBreak(
+ rundowns,
+ currentRundown,
+ rundowns.filter((r) => r.endIsBreak)
+ )
+ : undefined
+
+ if (breakProps) {
+ rundownsBeforeNextBreak = breakProps.rundownsBeforeNextBreak
+ breakIsLastRundown = breakProps.breakIsLastRundown
+ }
+
parts.forEach((origPart, itIndex) => {
const partInstance = this.getPartInstanceOrGetCachedTemp(partInstancesMap, origPart)
@@ -357,6 +376,8 @@ export class RundownTimingCalculator {
currentTime: now,
remainingTimeOnCurrentPart,
currentPartWillAutoNext,
+ rundownsBeforeNextBreak,
+ breakIsLastRundown,
isLowResolution,
})
}
@@ -381,6 +402,35 @@ export class RundownTimingCalculator {
}
}
}
+
+ private getRundownsBeforeNextBreak(
+ orderedRundowns: Rundown[],
+ currentRundown: Rundown | undefined,
+ breakRundowns: Rundown[]
+ ): { rundownsBeforeNextBreak: Rundown[]; breakIsLastRundown } | undefined {
+ if (!currentRundown) {
+ return undefined
+ }
+
+ let currentRundownIndex = orderedRundowns.findIndex((r) => r._id === currentRundown._id)
+
+ if (currentRundownIndex === -1) {
+ return undefined
+ }
+
+ let nextBreakIndex = orderedRundowns.findIndex((rundown, index) => {
+ if (index < currentRundownIndex) {
+ return false
+ }
+
+ return breakRundowns.some((r) => r._id == rundown._id)
+ })
+
+ return {
+ rundownsBeforeNextBreak: orderedRundowns.slice(currentRundownIndex, nextBreakIndex + 1),
+ breakIsLastRundown: nextBreakIndex === orderedRundowns.length,
+ }
+ }
}
export interface RundownTimingContext {
@@ -424,6 +474,10 @@ export interface RundownTimingContext {
currentPartWillAutoNext?: boolean
/** Current time of this calculation */
currentTime?: number
+ /** Rundowns between current rundown and rundown with next break (inclusive of both). Undefined if there's no break in the future. */
+ rundownsBeforeNextBreak?: Rundown[]
+ /** Whether the next break is also the last */
+ breakIsLastRundown?: boolean
/** Was this time context calculated during a high-resolution tick */
isLowResolution: boolean
}
From 2f9924087916a80ccb36d32a26f662478295e6de Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 29 Jun 2021 10:57:55 +0100
Subject: [PATCH 036/112] chore: Rename rundown layout functions to camel case
---
meteor/client/ui/RundownList.tsx | 4 +-
.../ui/RundownList/RundownListItemView.tsx | 4 +-
.../ui/RundownList/RundownPlaylistUi.tsx | 4 +-
.../RundownViewLayoutSelection.tsx | 4 +-
meteor/client/ui/RundownView.tsx | 18 +++----
.../ui/Settings/RundownLayoutEditor.tsx | 8 +--
.../ui/Settings/ShowStyleBaseSettings.tsx | 2 +-
.../RundownViewLayoutSettings.tsx | 6 +--
meteor/lib/api/rundownLayouts.ts | 50 +++++++++----------
9 files changed, 50 insertions(+), 50 deletions(-)
diff --git a/meteor/client/ui/RundownList.tsx b/meteor/client/ui/RundownList.tsx
index e756eeb3a8..7909d96cb0 100644
--- a/meteor/client/ui/RundownList.tsx
+++ b/meteor/client/ui/RundownList.tsx
@@ -310,8 +310,8 @@ export const RundownList = translateWithTracker((): IRundownsListProps => {
{t('Last updated')}
{this.props.rundownLayouts.some(
(l) =>
- (RundownLayoutsAPI.IsLayoutForShelf(l) && l.exposeAsStandalone) ||
- (RundownLayoutsAPI.IsLayoutForRundownView(l) && l.exposeAsSelectableLayout)
+ (RundownLayoutsAPI.isLayoutForShelf(l) && l.exposeAsStandalone) ||
+ (RundownLayoutsAPI.isLayoutForRundownView(l) && l.exposeAsSelectableLayout)
) && {t('View Layout')} }
diff --git a/meteor/client/ui/RundownList/RundownListItemView.tsx b/meteor/client/ui/RundownList/RundownListItemView.tsx
index 903d53dc43..5bd22e9230 100644
--- a/meteor/client/ui/RundownList/RundownListItemView.tsx
+++ b/meteor/client/ui/RundownList/RundownListItemView.tsx
@@ -154,8 +154,8 @@ export default withTranslation()(function RundownListItemView(props: Translated<
{rundownLayouts.some(
(l) =>
- (RundownLayoutsAPI.IsLayoutForShelf(l) && l.exposeAsStandalone) ||
- (RundownLayoutsAPI.IsLayoutForRundownView(l) && l.exposeAsSelectableLayout)
+ (RundownLayoutsAPI.isLayoutForShelf(l) && l.exposeAsStandalone) ||
+ (RundownLayoutsAPI.isLayoutForRundownView(l) && l.exposeAsSelectableLayout)
) && (
{isOnlyRundownInPlaylist && (
diff --git a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
index c1da7ac806..f8e58b86e2 100644
--- a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
+++ b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
@@ -373,8 +373,8 @@ export const RundownPlaylistUi = DropTarget(
{rundownLayouts.some(
(l) =>
- (RundownLayoutsAPI.IsLayoutForShelf(l) && l.exposeAsStandalone) ||
- (RundownLayoutsAPI.IsLayoutForRundownView(l) && l.exposeAsSelectableLayout)
+ (RundownLayoutsAPI.isLayoutForShelf(l) && l.exposeAsStandalone) ||
+ (RundownLayoutsAPI.isLayoutForRundownView(l) && l.exposeAsSelectableLayout)
) && (
RundownLayoutsAPI.IsLayoutForShelf(layout) && layout.exposeAsStandalone)
+ .filter((layout) => RundownLayoutsAPI.isLayoutForShelf(layout) && layout.exposeAsStandalone)
.map((layout) => {
return this.renderLinkItem(layout, getShelfLink(this.props.playlistId, layout._id), `standalone${layout._id}`)
})
const rundownViewLayouts = layoutsInRundown
- .filter((layout) => RundownLayoutsAPI.IsLayoutForRundownView(layout) && layout.exposeAsSelectableLayout)
+ .filter((layout) => RundownLayoutsAPI.isLayoutForRundownView(layout) && layout.exposeAsSelectableLayout)
.map((layout) => {
return this.renderLinkItem(
layout,
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index a28bf3c00c..590d3c8ccb 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -1733,7 +1733,7 @@ export const RundownView = translateWithTracker((
const shelfLayout = this.props.rundownLayouts?.find((layout) => layout._id === this.props.shelfLayoutId)
let isInspectorShelfExpanded = false
- if (shelfLayout && RundownLayoutsAPI.IsLayoutForShelf(shelfLayout)) {
+ if (shelfLayout && RundownLayoutsAPI.isLayoutForShelf(shelfLayout)) {
isInspectorShelfExpanded = shelfLayout.openByDefault
}
@@ -1820,7 +1820,7 @@ export const RundownView = translateWithTracker((
}
// Try to load defaults from rundown view layouts
- if (selectedViewLayout && RundownLayoutsAPI.IsLayoutForRundownView(selectedViewLayout)) {
+ if (selectedViewLayout && RundownLayoutsAPI.isLayoutForRundownView(selectedViewLayout)) {
const rundownLayout = selectedViewLayout as RundownViewLayout
if (!selectedShelfLayout && rundownLayout.shelfLayout) {
selectedShelfLayout = props.rundownLayouts.find((i) => i._id === rundownLayout.shelfLayout)
@@ -1842,19 +1842,19 @@ export const RundownView = translateWithTracker((
// if still not found, use the first one
if (!selectedShelfLayout) {
- selectedShelfLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.IsLayoutForShelf(i))
+ selectedShelfLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.isLayoutForShelf(i))
}
if (!selectedViewLayout) {
- selectedViewLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.IsLayoutForRundownView(i))
+ selectedViewLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.isLayoutForRundownView(i))
}
if (!selectedHeaderLayout) {
- selectedHeaderLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.IsLayoutForRundownHeader(i))
+ selectedHeaderLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.isLayoutForRundownHeader(i))
}
if (!selectedMiniShelfLayout) {
- selectedMiniShelfLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.IsLayoutForMiniShelf(i))
+ selectedMiniShelfLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.isLayoutForMiniShelf(i))
}
}
@@ -1868,16 +1868,16 @@ export const RundownView = translateWithTracker((
return {
shelfLayout:
- selectedShelfLayout && RundownLayoutsAPI.IsLayoutForShelf(selectedShelfLayout)
+ selectedShelfLayout && RundownLayoutsAPI.isLayoutForShelf(selectedShelfLayout)
? selectedShelfLayout
: undefined,
rundownViewLayout: selectedViewLayout,
rundownHeaderLayout:
- selectedHeaderLayout && RundownLayoutsAPI.IsLayoutForRundownHeader(selectedHeaderLayout)
+ selectedHeaderLayout && RundownLayoutsAPI.isLayoutForRundownHeader(selectedHeaderLayout)
? selectedHeaderLayout
: undefined,
miniShelfLayout:
- selectedMiniShelfLayout && RundownLayoutsAPI.IsLayoutForMiniShelf(selectedMiniShelfLayout)
+ selectedMiniShelfLayout && RundownLayoutsAPI.isLayoutForMiniShelf(selectedMiniShelfLayout)
? selectedMiniShelfLayout
: undefined,
currentRundown,
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index e7af9a3025..c5c5f0284f 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -300,10 +300,10 @@ export default translateWithTracker((props: IProp
renderElements(item: RundownLayoutBase, layout: CustomizableRegionLayout | undefined) {
const { t } = this.props
- const isShelfLayout = RundownLayoutsAPI.IsLayoutForShelf(item)
- const isRundownViewLayout = RundownLayoutsAPI.IsLayoutForRundownView(item)
- const isRundownHeaderLayout = RundownLayoutsAPI.IsLayoutForRundownHeader(item)
- const isMiniShelfLayout = RundownLayoutsAPI.IsLayoutForMiniShelf(item)
+ const isShelfLayout = RundownLayoutsAPI.isLayoutForShelf(item)
+ const isRundownViewLayout = RundownLayoutsAPI.isLayoutForRundownView(item)
+ const isRundownHeaderLayout = RundownLayoutsAPI.isLayoutForRundownHeader(item)
+ const isMiniShelfLayout = RundownLayoutsAPI.isLayoutForMiniShelf(item)
return (
diff --git a/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx b/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx
index f79b24defa..f26d696232 100644
--- a/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx
+++ b/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx
@@ -226,7 +226,7 @@ export default translateWithTracker((props: IProp
- {RundownLayoutsAPI.GetSettingsManifest().map((region) => {
+ {RundownLayoutsAPI.getSettingsManifest().map((region) => {
return (
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index d32b24a308..80cd9b8896 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -48,7 +48,7 @@ export default withTranslation()(
modifiedClassName="bghl"
attribute={'shelfLayout'}
obj={this.props.item}
- options={filterLayouts(this.props.layouts, RundownLayoutsAPI.IsLayoutForShelf)}
+ options={filterLayouts(this.props.layouts, RundownLayoutsAPI.isLayoutForShelf)}
type="dropdown"
collection={RundownLayouts}
className="input text-input input-l dropdown"
@@ -62,7 +62,7 @@ export default withTranslation()(
modifiedClassName="bghl"
attribute={'miniShelfLayout'}
obj={this.props.item}
- options={filterLayouts(this.props.layouts, RundownLayoutsAPI.IsLayoutForMiniShelf)}
+ options={filterLayouts(this.props.layouts, RundownLayoutsAPI.isLayoutForMiniShelf)}
type="dropdown"
collection={RundownLayouts}
className="input text-input input-l dropdown"
@@ -76,7 +76,7 @@ export default withTranslation()(
modifiedClassName="bghl"
attribute={'rundownHeaderLayout'}
obj={this.props.item}
- options={filterLayouts(this.props.layouts, RundownLayoutsAPI.IsLayoutForRundownHeader)}
+ options={filterLayouts(this.props.layouts, RundownLayoutsAPI.isLayoutForRundownHeader)}
type="dropdown"
collection={RundownLayouts}
className="input text-input input-l dropdown"
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 71c5a1bcf9..bfded4fde9 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -61,35 +61,35 @@ class RundownLayoutsRegistry {
private miniShelfLayouts: Map> = new Map()
private rundownHeaderLayouts: Map> = new Map()
- public RegisterShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) {
+ public registerShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) {
this.shelfLayouts.set(id, description)
}
- public RegisterRundownViewLayout(id: RundownLayoutType, description: LayoutDescriptor) {
+ public registerRundownViewLayout(id: RundownLayoutType, description: LayoutDescriptor) {
this.rundownViewLayouts.set(id, description)
}
- public RegisterMiniShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) {
+ public registerMiniShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) {
this.miniShelfLayouts.set(id, description)
}
- public RegisterRundownHeaderLayouts(id: RundownLayoutType, description: LayoutDescriptor) {
+ public registerRundownHeaderLayouts(id: RundownLayoutType, description: LayoutDescriptor) {
this.rundownHeaderLayouts.set(id, description)
}
- public IsShelfLayout(regionId: string) {
+ public isShelfLayout(regionId: string) {
return regionId === CustomizableRegions.Shelf
}
- public IsRudownViewLayout(regionId: string) {
+ public isRudownViewLayout(regionId: string) {
return regionId === CustomizableRegions.RundownView
}
- public IsMiniShelfLayout(regionId: string) {
+ public isMiniShelfLayout(regionId: string) {
return regionId === CustomizableRegions.MiniShelf
}
- public IsRundownHeaderLayout(regionId: string) {
+ public isRundownHeaderLayout(regionId: string) {
return regionId === CustomizableRegions.RundownHeader
}
@@ -105,7 +105,7 @@ class RundownLayoutsRegistry {
})
}
- public GetSettingsManifest(): CustomizableRegionSettingsManifest[] {
+ public getSettingsManifest(): CustomizableRegionSettingsManifest[] {
return [
{
_id: CustomizableRegions.RundownView,
@@ -133,7 +133,7 @@ class RundownLayoutsRegistry {
export namespace RundownLayoutsAPI {
const registry = new RundownLayoutsRegistry()
- registry.RegisterShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
+ registry.registerShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
filtersTitle: 'Panels',
supportsFilters: true,
supportedElements: [
@@ -143,7 +143,7 @@ export namespace RundownLayoutsAPI {
RundownLayoutElementType.PIECE_COUNTDOWN,
],
})
- registry.RegisterShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
+ registry.registerShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
filtersTitle: 'Tabs',
supportsFilters: true,
supportedElements: [
@@ -153,37 +153,37 @@ export namespace RundownLayoutsAPI {
RundownLayoutElementType.PIECE_COUNTDOWN,
],
})
- registry.RegisterMiniShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
+ registry.registerMiniShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
supportedElements: [],
})
- registry.RegisterMiniShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
+ registry.registerMiniShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
supportedElements: [],
})
- registry.RegisterRundownViewLayout(RundownLayoutType.RUNDOWN_VIEW_LAYOUT, {
+ registry.registerRundownViewLayout(RundownLayoutType.RUNDOWN_VIEW_LAYOUT, {
supportedElements: [],
})
- registry.RegisterRundownHeaderLayouts(RundownLayoutType.RUNDOWN_HEADER_LAYOUT, {
+ registry.registerRundownHeaderLayouts(RundownLayoutType.RUNDOWN_HEADER_LAYOUT, {
supportedElements: [],
})
- export function GetSettingsManifest(): CustomizableRegionSettingsManifest[] {
- return registry.GetSettingsManifest()
+ export function getSettingsManifest(): CustomizableRegionSettingsManifest[] {
+ return registry.getSettingsManifest()
}
- export function IsLayoutForShelf(layout: RundownLayoutBase): layout is RundownLayoutShelfBase {
- return registry.IsShelfLayout(layout.regionId)
+ export function isLayoutForShelf(layout: RundownLayoutBase): layout is RundownLayoutShelfBase {
+ return registry.isShelfLayout(layout.regionId)
}
- export function IsLayoutForRundownView(layout: RundownLayoutBase): layout is RundownViewLayout {
- return registry.IsRudownViewLayout(layout.regionId)
+ export function isLayoutForRundownView(layout: RundownLayoutBase): layout is RundownViewLayout {
+ return registry.isRudownViewLayout(layout.regionId)
}
- export function IsLayoutForMiniShelf(layout: RundownLayoutBase): layout is RundownLayoutShelfBase {
- return registry.IsMiniShelfLayout(layout.regionId)
+ export function isLayoutForMiniShelf(layout: RundownLayoutBase): layout is RundownLayoutShelfBase {
+ return registry.isMiniShelfLayout(layout.regionId)
}
- export function IsLayoutForRundownHeader(layout: RundownLayoutBase): layout is RundownLayoutRundownHeader {
- return registry.IsRundownHeaderLayout(layout.regionId)
+ export function isLayoutForRundownHeader(layout: RundownLayoutBase): layout is RundownLayoutRundownHeader {
+ return registry.isRundownHeaderLayout(layout.regionId)
}
export function isRundownViewLayout(layout: RundownLayoutBase): layout is RundownViewLayout {
From ea9cb269889d1276fd14cc6221ba6734d70e7f65 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 29 Jun 2021 10:58:37 +0100
Subject: [PATCH 037/112] fix: Translate in getSettingsManifest
---
.../client/ui/Settings/ShowStyleBaseSettings.tsx | 2 +-
meteor/lib/api/rundownLayouts.ts | 15 ++++++++-------
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx b/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx
index f26d696232..a1c19f9f51 100644
--- a/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx
+++ b/meteor/client/ui/Settings/ShowStyleBaseSettings.tsx
@@ -226,7 +226,7 @@ export default translateWithTracker((props: IProp
- {RundownLayoutsAPI.getSettingsManifest().map((region) => {
+ {RundownLayoutsAPI.getSettingsManifest(t).map((region) => {
return (
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index bfded4fde9..cb45aa1992 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -19,6 +19,7 @@ import {
import { ShowStyleBaseId } from '../collections/ShowStyleBases'
import * as _ from 'underscore'
import { literal } from '../lib'
+import { TFunction } from 'i18next'
export interface NewRundownLayoutsAPI {
createRundownLayout(
@@ -105,26 +106,26 @@ class RundownLayoutsRegistry {
})
}
- public getSettingsManifest(): CustomizableRegionSettingsManifest[] {
+ public getSettingsManifest(t: TFunction): CustomizableRegionSettingsManifest[] {
return [
{
_id: CustomizableRegions.RundownView,
- title: 'Rundown View Layouts',
+ title: t('Rundown View Layouts'),
layouts: this.wrapToCustomizableRegionLayout(this.rundownViewLayouts),
},
{
_id: CustomizableRegions.Shelf,
- title: 'Shelf Layouts',
+ title: t('Shelf Layouts'),
layouts: this.wrapToCustomizableRegionLayout(this.shelfLayouts),
},
{
_id: CustomizableRegions.MiniShelf,
- title: 'Mini Shelf Layouts',
+ title: t('Mini Shelf Layouts'),
layouts: this.wrapToCustomizableRegionLayout(this.miniShelfLayouts),
},
{
_id: CustomizableRegions.RundownHeader,
- title: 'Rundown Header Layouts',
+ title: t('Rundown Header Layouts'),
layouts: this.wrapToCustomizableRegionLayout(this.rundownHeaderLayouts),
},
]
@@ -166,8 +167,8 @@ export namespace RundownLayoutsAPI {
supportedElements: [],
})
- export function getSettingsManifest(): CustomizableRegionSettingsManifest[] {
- return registry.getSettingsManifest()
+ export function getSettingsManifest(t: TFunction): CustomizableRegionSettingsManifest[] {
+ return registry.getSettingsManifest(t)
}
export function isLayoutForShelf(layout: RundownLayoutBase): layout is RundownLayoutShelfBase {
From 0d9b95698b9e6c5b9242bf22ba6a589a8c6635da Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 29 Jun 2021 11:10:52 +0100
Subject: [PATCH 038/112] fix: Don't translate user defined strings
---
meteor/client/ui/RundownView.tsx | 2 +-
meteor/client/ui/Settings/RundownLayoutEditor.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 590d3c8ccb..ad211fcb6a 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -547,7 +547,7 @@ const NextBreakTiming = withTranslation()(
return (
- {t(this.props.breakText || 'Next Break')}
+ {this.props.breakText ?? t('Next Break')}
{!this.props.loop && breakRundown.expectedEnd ? (
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index c5c5f0284f..6a8296fd86 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -437,7 +437,7 @@ export default translateWithTracker((props: IProp
this.onAddElement(item)}>
- {t(`Add ${layout?.filtersTitle?.toLowerCase() ?? 'filter'}`)}
+ {layout?.filtersTitle ?? t(`Add filter`)}
) : null}
From c79d69e24c7d8721639ab4d735c46d8b967fe09b Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 29 Jun 2021 11:28:22 +0100
Subject: [PATCH 039/112] chore: Rename RundownCountdown to MarkerCountdownText
---
.../ui/RundownView/RundownDividerHeader.tsx | 20 +++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/meteor/client/ui/RundownView/RundownDividerHeader.tsx b/meteor/client/ui/RundownView/RundownDividerHeader.tsx
index 112655ce5e..41e0a5527f 100644
--- a/meteor/client/ui/RundownView/RundownDividerHeader.tsx
+++ b/meteor/client/ui/RundownView/RundownDividerHeader.tsx
@@ -18,27 +18,27 @@ const QUATER_DAY = 6 * 60 * 60 * 1000
* This is a countdown to the rundown's Expected Start or Expected End time. It shows nothing if the expectedStart is undefined
* or the time to Expected Start/End from now is larger than 6 hours.
*/
-const RundownCountdown = withTranslation()(
+const MarkerCountdownText = withTranslation()(
withTiming<
Translated<{
- expectedStartOrEnd: number | undefined
+ markerTimestamp: number | undefined
className?: string | undefined
}>,
{}
>({
filter: 'currentTime',
- })(function RundownCountdown(
+ })(function MarkerCountdown(
props: Translated<
WithTiming<{
- expectedStartOrEnd: number | undefined
+ markerTimestamp: number | undefined
className?: string | undefined
}>
>
) {
const { t } = props
- if (props.expectedStartOrEnd === undefined) return null
+ if (props.markerTimestamp === undefined) return null
- const time = props.expectedStartOrEnd - (props.timingDurations.currentTime || 0)
+ const time = props.markerTimestamp - (props.timingDurations.currentTime || 0)
if (time < QUATER_DAY) {
return (
@@ -83,9 +83,9 @@ export const RundownDividerHeader = withTranslation()(function RundownDividerHea
{rundown.expectedStart}
-
) : null}
@@ -107,9 +107,9 @@ export const RundownDividerHeader = withTranslation()(function RundownDividerHea
{rundown.expectedEnd}
-
) : null}
From c65bc3fadf9f6d568e0fac527eaec642b626356c Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 29 Jun 2021 11:33:50 +0100
Subject: [PATCH 040/112] chore: Remove references to miniShelf
---
meteor/client/ui/RundownView.tsx | 27 -------------------
.../ui/Settings/RundownLayoutEditor.tsx | 1 -
.../RundownViewLayoutSettings.tsx | 14 ----------
meteor/lib/api/rundownLayouts.ts | 24 -----------------
meteor/lib/collections/RundownLayouts.ts | 1 -
5 files changed, 67 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index ad211fcb6a..2263818762 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -1550,7 +1550,6 @@ interface IState {
shelfLayout: RundownLayoutShelfBase | undefined
rundownViewLayout: RundownLayoutBase | undefined
rundownHeaderLayout: RundownLayoutRundownHeader | undefined
- miniShelfLayout: RundownLayoutShelfBase | undefined
currentRundown: Rundown | undefined
/** Tracks whether the user has resized the shelf to prevent using default shelf settings */
wasShelfResizedByUser: boolean
@@ -1576,7 +1575,6 @@ interface ITrackedProps {
shelfLayoutId?: RundownLayoutId
rundownViewLayoutId?: RundownLayoutId
rundownHeaderLayoutId?: RundownLayoutId
- miniShelfLayoutId?: RundownLayoutId
shelfDisplayOptions: {
buckets: boolean
layout: boolean
@@ -1678,7 +1676,6 @@ export const RundownView = translateWithTracker((
shelfLayoutId: protectString((params['layout'] as string) || (params['shelfLayout'] as string) || ''), // 'layout' kept for backwards compatibility
rundownViewLayoutId: protectString((params['rundownViewLayout'] as string) || ''),
rundownHeaderLayoutId: protectString((params['rundownHeaderLayout'] as string) || ''),
- miniShelfLayoutId: protectString((params['miniShelfLayout'] as string) || ''),
shelfDisplayOptions: {
buckets: displayOptions.includes('buckets'),
layout: displayOptions.includes('layout') || displayOptions.includes('shelfLayout'),
@@ -1764,7 +1761,6 @@ export const RundownView = translateWithTracker((
shelfLayout: undefined,
rundownViewLayout: undefined,
rundownHeaderLayout: undefined,
- miniShelfLayout: undefined,
currentRundown: undefined,
wasShelfResizedByUser: false,
}
@@ -1774,7 +1770,6 @@ export const RundownView = translateWithTracker((
let selectedShelfLayout: RundownLayoutBase | undefined = undefined
let selectedViewLayout: RundownLayoutBase | undefined = undefined
let selectedHeaderLayout: RundownLayoutBase | undefined = undefined
- let selectedMiniShelfLayout: RundownLayoutBase | undefined = undefined
if (props.rundownLayouts) {
// first try to use the one selected by the user
@@ -1790,10 +1785,6 @@ export const RundownView = translateWithTracker((
selectedHeaderLayout = props.rundownLayouts.find((i) => i._id === props.rundownHeaderLayoutId)
}
- if (props.miniShelfLayoutId) {
- selectedMiniShelfLayout = props.rundownLayouts.find((i) => i._id === props.miniShelfLayoutId)
- }
-
// if couldn't find based on id, try matching part of the name
if (props.shelfLayoutId && !selectedShelfLayout) {
selectedShelfLayout = props.rundownLayouts.find(
@@ -1813,12 +1804,6 @@ export const RundownView = translateWithTracker((
)
}
- if (props.miniShelfLayoutId && !selectedMiniShelfLayout) {
- selectedMiniShelfLayout = props.rundownLayouts.find(
- (i) => i.name.indexOf(unprotectString(props.miniShelfLayoutId!)) >= 0
- )
- }
-
// Try to load defaults from rundown view layouts
if (selectedViewLayout && RundownLayoutsAPI.isLayoutForRundownView(selectedViewLayout)) {
const rundownLayout = selectedViewLayout as RundownViewLayout
@@ -1826,10 +1811,6 @@ export const RundownView = translateWithTracker((
selectedShelfLayout = props.rundownLayouts.find((i) => i._id === rundownLayout.shelfLayout)
}
- if (!selectedMiniShelfLayout && rundownLayout.miniShelfLayout) {
- selectedMiniShelfLayout = props.rundownLayouts.find((i) => i._id === rundownLayout.miniShelfLayout)
- }
-
if (!selectedHeaderLayout && rundownLayout.rundownHeaderLayout) {
selectedHeaderLayout = props.rundownLayouts.find((i) => i._id === rundownLayout.rundownHeaderLayout)
}
@@ -1852,10 +1833,6 @@ export const RundownView = translateWithTracker((
if (!selectedHeaderLayout) {
selectedHeaderLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.isLayoutForRundownHeader(i))
}
-
- if (!selectedMiniShelfLayout) {
- selectedMiniShelfLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.isLayoutForMiniShelf(i))
- }
}
let currentRundown: Rundown | undefined = undefined
@@ -1876,10 +1853,6 @@ export const RundownView = translateWithTracker((
selectedHeaderLayout && RundownLayoutsAPI.isLayoutForRundownHeader(selectedHeaderLayout)
? selectedHeaderLayout
: undefined,
- miniShelfLayout:
- selectedMiniShelfLayout && RundownLayoutsAPI.isLayoutForMiniShelf(selectedMiniShelfLayout)
- ? selectedMiniShelfLayout
- : undefined,
currentRundown,
}
}
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index 6a8296fd86..b0e171f0de 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -303,7 +303,6 @@ export default translateWithTracker((props: IProp
const isShelfLayout = RundownLayoutsAPI.isLayoutForShelf(item)
const isRundownViewLayout = RundownLayoutsAPI.isLayoutForRundownView(item)
const isRundownHeaderLayout = RundownLayoutsAPI.isLayoutForRundownHeader(item)
- const isMiniShelfLayout = RundownLayoutsAPI.isLayoutForMiniShelf(item)
return (
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index 80cd9b8896..5daab3e325 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -55,20 +55,6 @@ export default withTranslation()(
>
-
-
- {t('Mini Shelf Layout')}
-
-
-
{t('Rundown Header Layout')}
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index cb45aa1992..4e57a0b470 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -59,7 +59,6 @@ export interface CustomizableRegionLayout {
class RundownLayoutsRegistry {
private shelfLayouts: Map> = new Map()
private rundownViewLayouts: Map> = new Map()
- private miniShelfLayouts: Map> = new Map()
private rundownHeaderLayouts: Map> = new Map()
public registerShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) {
@@ -70,10 +69,6 @@ class RundownLayoutsRegistry {
this.rundownViewLayouts.set(id, description)
}
- public registerMiniShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) {
- this.miniShelfLayouts.set(id, description)
- }
-
public registerRundownHeaderLayouts(id: RundownLayoutType, description: LayoutDescriptor) {
this.rundownHeaderLayouts.set(id, description)
}
@@ -86,10 +81,6 @@ class RundownLayoutsRegistry {
return regionId === CustomizableRegions.RundownView
}
- public isMiniShelfLayout(regionId: string) {
- return regionId === CustomizableRegions.MiniShelf
- }
-
public isRundownHeaderLayout(regionId: string) {
return regionId === CustomizableRegions.RundownHeader
}
@@ -118,11 +109,6 @@ class RundownLayoutsRegistry {
title: t('Shelf Layouts'),
layouts: this.wrapToCustomizableRegionLayout(this.shelfLayouts),
},
- {
- _id: CustomizableRegions.MiniShelf,
- title: t('Mini Shelf Layouts'),
- layouts: this.wrapToCustomizableRegionLayout(this.miniShelfLayouts),
- },
{
_id: CustomizableRegions.RundownHeader,
title: t('Rundown Header Layouts'),
@@ -154,12 +140,6 @@ export namespace RundownLayoutsAPI {
RundownLayoutElementType.PIECE_COUNTDOWN,
],
})
- registry.registerMiniShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
- supportedElements: [],
- })
- registry.registerMiniShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
- supportedElements: [],
- })
registry.registerRundownViewLayout(RundownLayoutType.RUNDOWN_VIEW_LAYOUT, {
supportedElements: [],
})
@@ -179,10 +159,6 @@ export namespace RundownLayoutsAPI {
return registry.isRudownViewLayout(layout.regionId)
}
- export function isLayoutForMiniShelf(layout: RundownLayoutBase): layout is RundownLayoutShelfBase {
- return registry.isMiniShelfLayout(layout.regionId)
- }
-
export function isLayoutForRundownHeader(layout: RundownLayoutBase): layout is RundownLayoutRundownHeader {
return registry.isRundownHeaderLayout(layout.regionId)
}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 6f415b7cc3..e8eb751cb5 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -175,7 +175,6 @@ export interface RundownViewLayout extends RundownLayoutBase {
/** Expose as a layout that can be selected by the user in the lobby view */
exposeAsSelectableLayout: boolean
shelfLayout: RundownLayoutId
- miniShelfLayout: RundownLayoutId
rundownHeaderLayout: RundownLayoutId
}
From b26d2d6e1dcc6465d8ecf4a7d6b3d1e223e56cc7 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 29 Jun 2021 13:38:08 +0100
Subject: [PATCH 041/112] fix: Issues after rebase
---
meteor/client/ui/RundownView.tsx | 19 +++++++++++--------
.../ui/Settings/ShowStyleBaseSettings.tsx | 2 --
.../ui/Settings/components/FilterEditor.tsx | 2 +-
meteor/lib/rundown/rundownTiming.ts | 10 +++++-----
4 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 2263818762..97cbb9de1a 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -400,14 +400,17 @@ const TimingDisplay = withTranslation()(
) : null
) : (
-
- {this.props.layout?.expectedEndText ?? t('Expected End')}
-
-
+
+
+ {this.props.layout?.expectedEndText ?? t('Expected End')}
+
+
+
+ )
) : null}
{this.props.timingDurations && this.props.rundownCount < 2 ? ( // TEMPORARY: disable the diff counter for playlists longer than one rundown -- Jan Starzak, 2021-05-06
((props: IProps) => {
+export default translateWithTracker((_props: IProps) => {
return {}
})(
class FilterEditor extends MeteorReactComponent, IState> {
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index cfdf09b7f8..a63c555781 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -47,8 +47,8 @@ export class RundownTimingCalculator {
let startsAtAccumulator = 0
let displayStartsAtAccumulator = 0
- let rundownExpectedDurations: Record = {}
- let rundownAsPlayedDurations: Record = {}
+ const rundownExpectedDurations: Record = {}
+ const rundownAsPlayedDurations: Record = {}
let rundownsBeforeNextBreak: Rundown[] | undefined
let breakIsLastRundown: boolean | undefined
@@ -412,13 +412,13 @@ export class RundownTimingCalculator {
return undefined
}
- let currentRundownIndex = orderedRundowns.findIndex((r) => r._id === currentRundown._id)
+ const currentRundownIndex = orderedRundowns.findIndex((r) => r._id === currentRundown._id)
if (currentRundownIndex === -1) {
return undefined
}
- let nextBreakIndex = orderedRundowns.findIndex((rundown, index) => {
+ const nextBreakIndex = orderedRundowns.findIndex((rundown, index) => {
if (index < currentRundownIndex) {
return false
}
@@ -494,7 +494,7 @@ export function computeSegmentDuration(
partIds: PartId[],
display?: boolean
): number {
- let partDurations = timingDurations.partDurations
+ const partDurations = timingDurations.partDurations
if (partDurations === undefined) return 0
From 8fb7a3d781b8081e63a5139b2e14ef405a640a97 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 29 Jun 2021 13:40:44 +0100
Subject: [PATCH 042/112] chore: Remove unneeded member
---
meteor/lib/collections/Rundowns.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/meteor/lib/collections/Rundowns.ts b/meteor/lib/collections/Rundowns.ts
index 2103d32e43..195ba2ea38 100644
--- a/meteor/lib/collections/Rundowns.ts
+++ b/meteor/lib/collections/Rundowns.ts
@@ -67,8 +67,6 @@ export interface DBRundown
/** External id of the Rundown Playlist to put this rundown in */
playlistExternalId?: string
- /** Whether the end of the rundown marks a commercial break */
- endIsBreak?: boolean
/** Name (user-facing) of the external NCS this rundown came from */
externalNRCSName: string
/** The id of the Rundown Playlist this rundown is in */
From dfcc127d5951e8c526e42a6f151bdb7b7a464b31 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 29 Jun 2021 14:10:31 +0100
Subject: [PATCH 043/112] chore: rename endIsBreak and add detail to comments
---
meteor/lib/collections/Rundowns.ts | 2 +-
meteor/lib/rundown/rundownTiming.ts | 2 +-
packages/blueprints-integration/src/rundown.ts | 7 +++++--
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/meteor/lib/collections/Rundowns.ts b/meteor/lib/collections/Rundowns.ts
index 195ba2ea38..73b36879a0 100644
--- a/meteor/lib/collections/Rundowns.ts
+++ b/meteor/lib/collections/Rundowns.ts
@@ -105,7 +105,7 @@ export class Rundown implements DBRundown {
public notifiedCurrentPlayingPartExternalId?: string
public notes?: Array
public playlistExternalId?: string
- public endIsBreak?: boolean
+ public endOfRundownIsShowBreak?: boolean
public externalNRCSName: string
public playlistId: RundownPlaylistId
public playlistIdIsSetInSofie?: boolean
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index a63c555781..37d3195759 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -64,7 +64,7 @@ export class RundownTimingCalculator {
? this.getRundownsBeforeNextBreak(
rundowns,
currentRundown,
- rundowns.filter((r) => r.endIsBreak)
+ rundowns.filter((r) => r.endOfRundownIsShowBreak)
)
: undefined
diff --git a/packages/blueprints-integration/src/rundown.ts b/packages/blueprints-integration/src/rundown.ts
index c22d0b5322..a2a6b3d6dc 100644
--- a/packages/blueprints-integration/src/rundown.ts
+++ b/packages/blueprints-integration/src/rundown.ts
@@ -45,8 +45,11 @@ export interface IBlueprintRundown {
/** A hint to the Core that the Rundown should be a part of a playlist */
playlistExternalId?: string
- /** Whether the end of the rundown marks a commercial break */
- endIsBreak?: boolean
+ /**
+ * Whether the end of the rundown marks a break in the show.
+ * Allows the Next Break timer in the Rundown Header to time to the end of this rundown when looking for the next break.
+ */
+ endOfRundownIsShowBreak?: boolean
}
/** The Rundown sent from Core */
From 24393e8baae9cbf20cc2ce5d8b2420b15c6b1cc7 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 29 Jun 2021 14:20:01 +0100
Subject: [PATCH 044/112] fix: Clean up logic for finding next break
---
meteor/lib/rundown/rundownTiming.ts | 13 +++----------
1 file changed, 3 insertions(+), 10 deletions(-)
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index 37d3195759..3cb906bee8 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -60,13 +60,7 @@ export class RundownTimingCalculator {
let currentAIndex = -1
if (playlist) {
- const breakProps = currentRundown
- ? this.getRundownsBeforeNextBreak(
- rundowns,
- currentRundown,
- rundowns.filter((r) => r.endOfRundownIsShowBreak)
- )
- : undefined
+ const breakProps = currentRundown ? this.getRundownsBeforeNextBreak(rundowns, currentRundown) : undefined
if (breakProps) {
rundownsBeforeNextBreak = breakProps.rundownsBeforeNextBreak
@@ -405,8 +399,7 @@ export class RundownTimingCalculator {
private getRundownsBeforeNextBreak(
orderedRundowns: Rundown[],
- currentRundown: Rundown | undefined,
- breakRundowns: Rundown[]
+ currentRundown: Rundown | undefined
): { rundownsBeforeNextBreak: Rundown[]; breakIsLastRundown } | undefined {
if (!currentRundown) {
return undefined
@@ -423,7 +416,7 @@ export class RundownTimingCalculator {
return false
}
- return breakRundowns.some((r) => r._id == rundown._id)
+ return rundown.endOfRundownIsShowBreak === true
})
return {
From 1b83c81ebf313b01ff542ede19dabee562f01627 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 29 Jun 2021 16:14:05 +0100
Subject: [PATCH 045/112] feat: Move next break props calculation to tracker
---
meteor/lib/rundown/rundownTiming.ts | 69 +++++++++++++++++++++--------
1 file changed, 50 insertions(+), 19 deletions(-)
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index 3cb906bee8..4868a37786 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -1,4 +1,6 @@
+import { Tracker } from 'meteor/tracker'
import _ from 'underscore'
+import { memoizedIsolatedAutorun } from '../../client/lib/reactiveData/reactiveDataHelper'
import {
findPartInstanceInMapOrWrapToTemporary,
PartInstance,
@@ -13,6 +15,11 @@ import { Settings } from '../Settings'
// Minimum duration that a part can be assigned. Used by gap parts to allow them to "compress" to indicate time running out.
const MINIMAL_NONZERO_DURATION = 1
+interface BreakProps {
+ rundownsBeforeNextBreak: Rundown[]
+ breakIsLastRundown
+}
+
export class RundownTimingCalculator {
private temporaryPartInstances: Map = new Map()
@@ -26,6 +33,11 @@ export class RundownTimingCalculator {
private partDisplayDurations: Record = {}
private partDisplayDurationsNoPlayback: Record = {}
private displayDurationGroups: Record = {}
+ private breakProps: {
+ comp: Tracker.Computation | undefined
+ props: BreakProps | undefined
+ state: string | undefined
+ } = { comp: undefined, props: undefined, state: undefined }
updateDurations(
now: number,
@@ -400,29 +412,48 @@ export class RundownTimingCalculator {
private getRundownsBeforeNextBreak(
orderedRundowns: Rundown[],
currentRundown: Rundown | undefined
- ): { rundownsBeforeNextBreak: Rundown[]; breakIsLastRundown } | undefined {
- if (!currentRundown) {
- return undefined
+ ): BreakProps | undefined {
+ let currentState = orderedRundowns.map((r) => r.endOfRundownIsShowBreak ?? '_').join('')
+ if (this.breakProps.comp && this.breakProps.state !== currentState) {
+ this.breakProps.comp.invalidate()
}
- const currentRundownIndex = orderedRundowns.findIndex((r) => r._id === currentRundown._id)
-
- if (currentRundownIndex === -1) {
- return undefined
+ if (!this.breakProps.comp) {
+ this.breakProps.comp = Tracker.autorun(() => {
+ memoizedIsolatedAutorun(
+ (orderedRundowns, currentRundown) => {
+ if (!currentRundown) {
+ this.breakProps.props = undefined
+ }
+
+ const currentRundownIndex = orderedRundowns.findIndex((r) => r._id === currentRundown._id)
+
+ if (currentRundownIndex === -1) {
+ this.breakProps.props = undefined
+ }
+
+ const nextBreakIndex = orderedRundowns.findIndex((rundown, index) => {
+ if (index < currentRundownIndex) {
+ return false
+ }
+
+ return rundown.endOfRundownIsShowBreak === true
+ })
+
+ this.breakProps.props = {
+ rundownsBeforeNextBreak: orderedRundowns.slice(currentRundownIndex, nextBreakIndex + 1),
+ breakIsLastRundown: nextBreakIndex === orderedRundowns.length,
+ }
+ },
+ 'getRundownsBeforeNextBreak',
+ orderedRundowns,
+ currentRundown
+ )
+ })
}
- const nextBreakIndex = orderedRundowns.findIndex((rundown, index) => {
- if (index < currentRundownIndex) {
- return false
- }
-
- return rundown.endOfRundownIsShowBreak === true
- })
-
- return {
- rundownsBeforeNextBreak: orderedRundowns.slice(currentRundownIndex, nextBreakIndex + 1),
- breakIsLastRundown: nextBreakIndex === orderedRundowns.length,
- }
+ this.breakProps.state = currentState
+ return this.breakProps.props
}
}
From 24ce5ed34f227faf42f3f19e4474ba6ff94522c9 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 30 Jun 2021 14:36:18 +0100
Subject: [PATCH 046/112] feat: Segment count up and down panels
---
meteor/client/styles/shelf/endWordsPanel.scss | 14 +-
.../styles/shelf/segmentCountDownPanel.scss | 8 +
.../RundownTiming/PlaylistStartTiming.tsx | 42 ++---
.../RundownTiming/SegmentDuration.tsx | 9 +-
.../ui/Settings/components/FilterEditor.tsx | 152 +++++++++--------
meteor/client/ui/Shelf/EndWordsPanel.tsx | 8 +-
.../ui/Shelf/PlaylistStartTimerPanel.tsx | 7 +-
meteor/client/ui/Shelf/SegmentTimingPanel.tsx | 155 ++++++++++++++++++
meteor/client/ui/Shelf/Shelf.tsx | 2 +
.../client/ui/Shelf/ShelfDashboardLayout.tsx | 11 +-
meteor/lib/api/rundownLayouts.ts | 6 +
meteor/lib/collections/RundownLayouts.ts | 14 ++
meteor/lib/rundown/rundownTiming.ts | 19 +++
13 files changed, 332 insertions(+), 115 deletions(-)
create mode 100644 meteor/client/styles/shelf/segmentCountDownPanel.scss
create mode 100644 meteor/client/ui/Shelf/SegmentTimingPanel.tsx
diff --git a/meteor/client/styles/shelf/endWordsPanel.scss b/meteor/client/styles/shelf/endWordsPanel.scss
index a1c49da902..8c81d3161d 100644
--- a/meteor/client/styles/shelf/endWordsPanel.scss
+++ b/meteor/client/styles/shelf/endWordsPanel.scss
@@ -1,22 +1,18 @@
.end-words-panel {
- padding: 1vw 1vh;
overflow: hidden;
position: absolute;
- .row {
- display: inline-block;
+ .timing-clock {
width: 100%;
- overflow: hidden;
- white-space: nowrap;
- }
-
- .title {
- font-weight: bold;
}
.text {
text-overflow: ellipsis;
text-align: left;
direction: rtl;
+ overflow: hidden;
+ white-space: nowrap;
+ display: inline-block;
+ width: 100%;
}
}
diff --git a/meteor/client/styles/shelf/segmentCountDownPanel.scss b/meteor/client/styles/shelf/segmentCountDownPanel.scss
new file mode 100644
index 0000000000..718a78bcfe
--- /dev/null
+++ b/meteor/client/styles/shelf/segmentCountDownPanel.scss
@@ -0,0 +1,8 @@
+.segment-timing-panel {
+ position: absolute;
+
+ .negative {
+ color: var(--general-late-color);
+ font-weight: 500;
+ }
+}
diff --git a/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx
index dd61559890..24446da531 100644
--- a/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx
@@ -10,6 +10,7 @@ import ClassNames from 'classnames'
interface IEndTimingProps {
rundownPlaylist: RundownPlaylist
+ hideExpectedStart?: boolean
hideDiff?: boolean
}
@@ -26,26 +27,27 @@ export const PlaylistStartTiming = withTranslation()(
return (
- {rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
-
- {t('Started')}
-
-
- ) : rundownPlaylist.expectedStart ? (
-
- {t('Planned Start')}
-
-
- ) : rundownPlaylist.expectedEnd && rundownPlaylist.expectedDuration ? (
-
- {t('Expected Start')}
-
-
- ) : null}
+ {!this.props.hideExpectedStart &&
+ (rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
+
+ {t('Started')}
+
+
+ ) : rundownPlaylist.expectedStart ? (
+
+ {t('Planned Start')}
+
+
+ ) : rundownPlaylist.expectedEnd && rundownPlaylist.expectedDuration ? (
+
+ {t('Expected Start')}
+
+
+ ) : null)}
{!this.props.hideDiff && expectedStart && (
()(function
const duration = budget - playedOut
- return (
+ return props.countUp ? (
+ <>
+ {props.label}
+ {RundownUtils.formatDiffToTimecode(playedOut, false, false, true, false, true, '+')}
+ >
+ ) : (
<>
{props.label}
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index 2b732f47b2..7d6ff7144e 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -19,6 +19,7 @@ import {
RundownLayoutPlaylistEndTimer,
RundownLayoutPlaylistStartTimer,
RundownLayouts,
+ RundownLayoutSegmentTiming,
} from '../../../../lib/collections/RundownLayouts'
import { EditAttribute } from '../../../lib/EditAttribute'
import { MeteorReactComponent } from '../../../lib/MeteorReactComponent'
@@ -619,7 +620,7 @@ export default translateWithTracker((props: IProp
/>
- {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
{isDashboardLayout && (
@@ -637,19 +638,6 @@ export default translateWithTracker((props: IProp
)}
-
-
- {t('Scale')}
-
-
-
)
}
@@ -803,22 +791,7 @@ export default translateWithTracker((props: IProp
mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
/>
- {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
- {isDashboardLayout && (
-
-
- {t('Scale')}
-
-
-
- )}
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
)
}
@@ -859,22 +832,20 @@ export default translateWithTracker((props: IProp
/>
- {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
- {isDashboardLayout && (
-
-
- {t('Scale')}
-
-
-
- )}
+
+
+ {t('Hide Expected Start')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
)
}
@@ -953,22 +924,7 @@ export default translateWithTracker((props: IProp
/>
- {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
- {isDashboardLayout && (
-
-
- {t('Scale')}
-
-
-
- )}
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
)
}
@@ -1068,27 +1024,12 @@ export default translateWithTracker((props: IProp
- {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
- {isDashboardLayout && (
-
-
- {t('Scale')}
-
-
-
- )}
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
)
}
- renderDashboardLayoutSettings(item: RundownLayoutBase, index: number) {
+ renderDashboardLayoutSettings(item: RundownLayoutBase, index: number, scalable?: boolean) {
let { t } = this.props
return (
@@ -1145,6 +1086,51 @@ export default translateWithTracker((props: IProp
/>
+ {scalable && (
+
+
+ {t('Scale')}
+
+
+
+ )}
+
+ )
+ }
+
+ renderSegmentCountDown(
+ item: RundownLayoutBase,
+ tab: RundownLayoutSegmentTiming,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ let { t } = this.props
+
+ return (
+
+
+
+ {t('Type')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
)
}
@@ -1246,6 +1232,14 @@ export default translateWithTracker((props: IProp
isRundownLayout,
isDashboardLayout
)
+ : RundownLayoutsAPI.isSegmentTiming(this.props.filter)
+ ? this.renderSegmentCountDown(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
: undefined}
)
diff --git a/meteor/client/ui/Shelf/EndWordsPanel.tsx b/meteor/client/ui/Shelf/EndWordsPanel.tsx
index 9443785789..ecc4f42a7f 100644
--- a/meteor/client/ui/Shelf/EndWordsPanel.tsx
+++ b/meteor/client/ui/Shelf/EndWordsPanel.tsx
@@ -45,7 +45,7 @@ export class EndWordsPanelInner extends MeteorReactComponent<
return (
-
{t('End Words')}
-
{endOfScript}
+
+ {t('End Words')}
+ {endOfScript}
+
)
}
diff --git a/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx b/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
index 3874389c66..873401ca92 100644
--- a/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
+++ b/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
@@ -14,7 +14,6 @@ import { withTranslation } from 'react-i18next'
import { PlaylistStartTiming } from '../RundownView/RundownTiming/PlaylistStartTiming'
interface IPlaylistStartTimerPanelProps {
- visible?: boolean
layout: RundownLayoutBase
panel: RundownLayoutPlaylistStartTimer
playlist: RundownPlaylist
@@ -51,7 +50,11 @@ export class PlaylistStartTimerPanelInner extends MeteorReactComponent<
: {}
)}
>
-
+
)
}
diff --git a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
new file mode 100644
index 0000000000..74258cb9fc
--- /dev/null
+++ b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
@@ -0,0 +1,155 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutSegmentCountDown,
+ RundownLayoutBase,
+ RundownLayoutSegmentTiming,
+} from '../../../lib/collections/RundownLayouts'
+import { Translated, translateWithTracker, withTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownUtils } from '../../lib/rundown'
+import { RundownPlaylist, RundownPlaylistId } from '../../../lib/collections/RundownPlaylists'
+import { Segment } from '../../../lib/collections/Segments'
+import { WithTiming } from '../RundownView/RundownTiming/withTiming'
+import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration'
+import { PartExtended } from '../../../lib/Rundown'
+import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper'
+import { Part, PartId } from '../../../lib/collections/Parts'
+import { PartInstance } from '../../../lib/collections/PartInstances'
+import { ShowStyleBase } from '../../../lib/collections/ShowStyleBases'
+import { dashboardElementPosition } from './DashboardPanel'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+
+interface ISegmentTimingPanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutSegmentTiming
+ playlist: RundownPlaylist
+ showStyleBase: ShowStyleBase
+}
+
+interface ISegmentTimingPanelTrackedProps {
+ liveSegment?: Segment
+ parts?: PartExtended[]
+}
+
+interface IState {}
+
+class SegmentTimingPanelInner extends MeteorReactComponent<
+ WithTiming>,
+ IState
+> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ let { t, panel } = this.props
+
+ return (
+
+
+
+ {panel.timingType === 'count_down' ? t('Segment Count Down') : t('Segment Count Up')}
+
+ {this.props.parts && }
+
+
+ )
+ }
+}
+
+export const SegmentTimingPanel = translateWithTracker<
+ ISegmentTimingPanelProps,
+ IState,
+ ISegmentTimingPanelTrackedProps
+>(
+ (props: ISegmentTimingPanelProps & ISegmentTimingPanelTrackedProps) => {
+ if (props.playlist.currentPartInstanceId) {
+ let livePart = props.playlist.getActivePartInstances({ _id: props.playlist.currentPartInstanceId })[0]
+ let liveSegment = livePart ? props.playlist.getSegments({ _id: livePart.segmentId })[0] : undefined
+
+ if (!liveSegment) return {}
+
+ const [orderedAllPartIds, { currentPartInstance, nextPartInstance }] = slowDownReactivity(
+ () =>
+ [
+ memoizedIsolatedAutorun(
+ (_playlistId: RundownPlaylistId) =>
+ (
+ props.playlist.getAllOrderedParts(undefined, {
+ fields: {
+ segmentId: 1,
+ _rank: 1,
+ },
+ }) as Pick[]
+ ).map((part) => part._id),
+ 'playlist.getAllOrderedParts',
+ props.playlist._id
+ ),
+ memoizedIsolatedAutorun(
+ (_playlistId: RundownPlaylistId, _currentPartInstanceId, _nextPartInstanceId) =>
+ props.playlist.getSelectedPartInstances(),
+ 'playlist.getSelectedPartInstances',
+ props.playlist._id,
+ props.playlist.currentPartInstanceId,
+ props.playlist.nextPartInstanceId
+ ),
+ ] as [
+ PartId[],
+ { currentPartInstance: PartInstance | undefined; nextPartInstance: PartInstance | undefined }
+ ],
+ // if the rundown isn't active, run the changes ASAP, we don't care if there's going to be jank
+ // if this is the current or next segment (will have those two properties defined), run the changes ASAP,
+ // otherwise, trigger the updates in a window of 500-2500 ms from change
+ props.playlist.activationId === undefined ? 0 : Math.random() * 2000 + 500
+ )
+
+ const orderedSegmentsAndParts = props.playlist.getSegmentsAndPartsSync()
+ const rundownOrder = props.playlist.getRundownIDs()
+ const rundownIndex = rundownOrder.indexOf(liveSegment.rundownId)
+ const rundowns = props.playlist.getRundowns()
+ const rundown = rundowns.find((r) => r._id === liveSegment!.rundownId)
+ const segmentIndex = orderedSegmentsAndParts.segments.findIndex((s) => s._id === liveSegment!._id)
+
+ if (!rundown) return {}
+
+ const rundownsToShowstyles = new Map()
+ for (const rundown of rundowns) {
+ rundownsToShowstyles.set(rundown._id, rundown.showStyleBaseId)
+ }
+
+ const o = RundownUtils.getResolvedSegment(
+ props.showStyleBase,
+ props.playlist,
+ rundown,
+ liveSegment,
+ new Set(orderedSegmentsAndParts.segments.map((s) => s._id).slice(0, segmentIndex)),
+ rundownOrder.slice(0, rundownIndex),
+ rundownsToShowstyles,
+ orderedAllPartIds,
+ currentPartInstance,
+ nextPartInstance,
+ true,
+ true
+ )
+
+ return { liveSegment, parts: o.parts }
+ }
+ return {}
+ },
+ (_data, props: ISegmentTimingPanelProps, nextProps: ISegmentTimingPanelProps) => {
+ return !_.isEqual(props, nextProps)
+ }
+)(SegmentTimingPanelInner)
diff --git a/meteor/client/ui/Shelf/Shelf.tsx b/meteor/client/ui/Shelf/Shelf.tsx
index b5ab2f0e45..d055292d52 100644
--- a/meteor/client/ui/Shelf/Shelf.tsx
+++ b/meteor/client/ui/Shelf/Shelf.tsx
@@ -33,6 +33,7 @@ import RundownViewEventBus, {
} from '../RundownView/RundownViewEventBus'
import { IAdLibListItem } from './AdLibListItem'
import ShelfContextMenu from './ShelfContextMenu'
+import { Rundown } from '../../../lib/collections/Rundowns'
export enum ShelfTabs {
ADLIB = 'adlib',
@@ -44,6 +45,7 @@ export interface IShelfProps extends React.ComponentPropsWithRef {
isExpanded: boolean
buckets: Array
playlist: RundownPlaylist
+ currentRundown: Rundown
studio: Studio
showStyleBase: ShowStyleBase
studioMode: boolean
diff --git a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
index bac44b4e35..4c87bd3a51 100644
--- a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
+++ b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
@@ -17,6 +17,7 @@ import { AdLibPieceUi } from './AdLibPanel'
import { PlaylistStartTimerPanel } from './PlaylistStartTimerPanel'
import { EndWordsPanel } from './EndWordsPanel'
import { PlaylistEndTimerPanel } from './PlaylistEndTimerPanel'
+import { SegmentTimingPanel } from './SegmentTimingPanel'
export interface IShelfDashboardLayoutProps {
rundownLayout: DashboardLayout
@@ -112,7 +113,15 @@ export function ShelfDashboardLayout(props: IShelfDashboardLayoutProps) {
) : RundownLayoutsAPI.isEndWords(panel) ? (
- ) : undefined
+ ) : RundownLayoutsAPI.isSegmentTiming(panel) ? (
+
+ ) : null
)}
{rundownLayout.actionButtons && (
= {}
let rundownAsPlayedDurations: Record = {}
+ let segmentExpectedDurations: Record = {}
+ let segmentAsPlayedDurations: Record = {}
+
let rundownsBeforeNextBreak: Rundown[] | undefined
let breakIsLastRundown: boolean | undefined
@@ -216,6 +219,11 @@ export class RundownTimingCalculator {
rundownAsPlayedDurations[unprotectString(partInstance.part.rundownId)] +=
valToAddToAsPlayedDuration
}
+ if (!segmentAsPlayedDurations[unprotectString(partInstance.segmentId)]) {
+ segmentAsPlayedDurations[unprotectString(partInstance.segmentId)] = valToAddToAsPlayedDuration
+ } else {
+ segmentAsPlayedDurations[unprotectString(partInstance.segmentId)] += valToAddToAsPlayedDuration
+ }
}
// asDisplayed is the actual duration so far and expected durations in unplayed lines
@@ -295,6 +303,11 @@ export class RundownTimingCalculator {
} else {
rundownExpectedDurations[unprotectString(partInstance.part.rundownId)] += partExpectedDuration
}
+ if (!segmentExpectedDurations[unprotectString(partInstance.segmentId)]) {
+ segmentExpectedDurations[unprotectString(partInstance.segmentId)] = partExpectedDuration
+ } else {
+ segmentExpectedDurations[unprotectString(partInstance.segmentId)] += partExpectedDuration
+ }
})
// This is where the waitAccumulator-generated data in the linearSegLines is used to calculate the countdowns.
@@ -366,6 +379,8 @@ export class RundownTimingCalculator {
asPlayedPlaylistDuration: asPlayedRundownDuration,
rundownExpectedDurations,
rundownAsPlayedDurations,
+ segmentExpectedDurations,
+ segmentAsPlayedDurations,
partCountdown: _.object(this.linearParts),
partDurations: this.partDurations,
partPlayed: this.partPlayed,
@@ -446,6 +461,10 @@ export interface RundownTimingContext {
rundownExpectedDurations?: Record
/** This is the complete duration of each rundown: as planned for the unplayed content, and as-run for the played-out, but ignoring unplayed/unplayable parts in order */
rundownAsPlayedDurations?: Record
+ /** Expected duration of each segment in playlist (based on part expected durations) */
+ segmentExpectedDurations?: Record
+ /** Complete duration of each segment; as planned for unplayed content, and as-run for the played-out, but ignoring unplayed/unplayable parts in order */
+ segmentAsPlayedDurations?: Record
/** this is the countdown to each of the parts relative to the current on air part. */
partCountdown?: Record
/** The calculated durations of each of the Parts: as-planned/as-run depending on state. */
From a9860632334f5148ead99a3de4e98ce8d58adf45 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 30 Jun 2021 16:52:40 +0100
Subject: [PATCH 047/112] feat: Part count down panel
---
meteor/client/styles/partTimingPanel.scss | 3 +
.../RundownTiming/CurrentPartElapsed.tsx | 35 +++++++
.../ui/Settings/components/FilterEditor.tsx | 39 ++++++++
meteor/client/ui/Shelf/PartTimingPanel.tsx | 97 +++++++++++++++++++
meteor/client/ui/Shelf/SegmentTimingPanel.tsx | 3 +-
.../client/ui/Shelf/ShelfDashboardLayout.tsx | 3 +
meteor/lib/api/rundownLayouts.ts | 6 ++
meteor/lib/collections/RundownLayouts.ts | 14 +++
8 files changed, 198 insertions(+), 2 deletions(-)
create mode 100644 meteor/client/styles/partTimingPanel.scss
create mode 100644 meteor/client/ui/RundownView/RundownTiming/CurrentPartElapsed.tsx
create mode 100644 meteor/client/ui/Shelf/PartTimingPanel.tsx
diff --git a/meteor/client/styles/partTimingPanel.scss b/meteor/client/styles/partTimingPanel.scss
new file mode 100644
index 0000000000..c9741c51cc
--- /dev/null
+++ b/meteor/client/styles/partTimingPanel.scss
@@ -0,0 +1,3 @@
+.part-timing-panel {
+ position: absolute;
+}
diff --git a/meteor/client/ui/RundownView/RundownTiming/CurrentPartElapsed.tsx b/meteor/client/ui/RundownView/RundownTiming/CurrentPartElapsed.tsx
new file mode 100644
index 0000000000..1e6b32b255
--- /dev/null
+++ b/meteor/client/ui/RundownView/RundownTiming/CurrentPartElapsed.tsx
@@ -0,0 +1,35 @@
+import * as React from 'react'
+import ClassNames from 'classnames'
+import { withTiming, WithTiming } from './withTiming'
+import { RundownUtils } from '../../../lib/rundown'
+import { PartId } from '../../../../lib/collections/Parts'
+import { unprotectString } from '../../../../lib/lib'
+
+interface IPartElapsedProps {
+ currentPartId: PartId | undefined
+ className?: string
+}
+
+/**
+ * A presentational component that will render the elapsed duration of the current part
+ * @class CurrentPartElapsed
+ * @extends React.Component>
+ */
+export const CurrentPartElapsed = withTiming({
+ isHighResolution: true,
+})(
+ class CurrentPartElapsed extends React.Component> {
+ render() {
+ const displayTimecode =
+ this.props.currentPartId && this.props.timingDurations.partPlayed
+ ? this.props.timingDurations.partPlayed[unprotectString(this.props.currentPartId)] || 0
+ : 0
+
+ return (
+
+ {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)}
+
+ )
+ }
+ }
+)
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index 7d6ff7144e..db4cd3b4c2 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -15,6 +15,7 @@ import {
RundownLayoutEndWords,
RundownLayoutExternalFrame,
RundownLayoutFilterBase,
+ RundownLayoutPartTiming,
RundownLayoutPieceCountdown,
RundownLayoutPlaylistEndTimer,
RundownLayoutPlaylistStartTimer,
@@ -1135,6 +1136,36 @@ export default translateWithTracker((props: IProp
)
}
+ renderPartCountDown(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPartTiming,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ let { t } = this.props
+
+ return (
+
+
+
+ {t('Type')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
render() {
const { t } = this.props
@@ -1240,6 +1271,14 @@ export default translateWithTracker((props: IProp
isRundownLayout,
isDashboardLayout
)
+ : RundownLayoutsAPI.isPartTiming(this.props.filter)
+ ? this.renderPartCountDown(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
: undefined}
)
diff --git a/meteor/client/ui/Shelf/PartTimingPanel.tsx b/meteor/client/ui/Shelf/PartTimingPanel.tsx
new file mode 100644
index 0000000000..ac6daf4bc2
--- /dev/null
+++ b/meteor/client/ui/Shelf/PartTimingPanel.tsx
@@ -0,0 +1,97 @@
+import * as React from 'react'
+import ClassNames from 'classnames'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutPartCountDown,
+ DashboardLayoutSegmentCountDown,
+ RundownLayoutBase,
+ RundownLayoutPartTiming,
+ RundownLayoutSegmentTiming,
+} from '../../../lib/collections/RundownLayouts'
+import { Translated, translateWithTracker, withTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownUtils } from '../../lib/rundown'
+import { RundownPlaylist, RundownPlaylistId } from '../../../lib/collections/RundownPlaylists'
+import { Segment } from '../../../lib/collections/Segments'
+import { withTiming, WithTiming } from '../RundownView/RundownTiming/withTiming'
+import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration'
+import { PartExtended } from '../../../lib/Rundown'
+import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper'
+import { Part, PartId } from '../../../lib/collections/Parts'
+import { PartInstance } from '../../../lib/collections/PartInstances'
+import { ShowStyleBase } from '../../../lib/collections/ShowStyleBases'
+import { dashboardElementPosition } from './DashboardPanel'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { getAllowSpeaking } from '../../lib/localStorage'
+import { CurrentPartRemaining } from '../RundownView/RundownTiming/CurrentPartRemaining'
+import { CurrentPartElapsed } from '../RundownView/RundownTiming/CurrentPartElapsed'
+
+interface IPartTimingPanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutPartTiming
+ playlist: RundownPlaylist
+}
+
+interface IPartTimingPanelTrackedProps {
+ livePart?: PartInstance
+}
+
+interface IState {}
+
+class PartTimingPanelInner extends MeteorReactComponent<
+ Translated,
+ IState
+> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ let { t, panel } = this.props
+
+ return (
+
+
+
+ {panel.timingType === 'count_down' ? t('Part Count Down') : t('Part Count Up')}
+
+ {panel.timingType === 'count_down' ? (
+
+ ) : (
+
+ )}
+
+
+ )
+ }
+}
+
+export const PartTimingPanel = translateWithTracker(
+ (props: IPartTimingPanelProps & IPartTimingPanelTrackedProps) => {
+ if (props.playlist.currentPartInstanceId) {
+ let livePart = props.playlist.getActivePartInstances({ _id: props.playlist.currentPartInstanceId })[0]
+
+ return { livePart }
+ }
+ return {}
+ },
+ (_data, props: IPartTimingPanelProps, nextProps: IPartTimingPanelProps) => {
+ return !_.isEqual(props, nextProps)
+ }
+)(PartTimingPanelInner)
diff --git a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
index 74258cb9fc..e5dfe30516 100644
--- a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
+++ b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
@@ -10,7 +10,6 @@ import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import { RundownUtils } from '../../lib/rundown'
import { RundownPlaylist, RundownPlaylistId } from '../../../lib/collections/RundownPlaylists'
import { Segment } from '../../../lib/collections/Segments'
-import { WithTiming } from '../RundownView/RundownTiming/withTiming'
import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration'
import { PartExtended } from '../../../lib/Rundown'
import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper'
@@ -36,7 +35,7 @@ interface ISegmentTimingPanelTrackedProps {
interface IState {}
class SegmentTimingPanelInner extends MeteorReactComponent<
- WithTiming>,
+ Translated,
IState
> {
constructor(props) {
diff --git a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
index 4c87bd3a51..5b1dd54c67 100644
--- a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
+++ b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
@@ -18,6 +18,7 @@ import { PlaylistStartTimerPanel } from './PlaylistStartTimerPanel'
import { EndWordsPanel } from './EndWordsPanel'
import { PlaylistEndTimerPanel } from './PlaylistEndTimerPanel'
import { SegmentTimingPanel } from './SegmentTimingPanel'
+import { PartTimingPanel } from './PartTimingPanel'
export interface IShelfDashboardLayoutProps {
rundownLayout: DashboardLayout
@@ -121,6 +122,8 @@ export function ShelfDashboardLayout(props: IShelfDashboardLayoutProps) {
panel={panel}
showStyleBase={props.showStyleBase}
/>
+ ) : RundownLayoutsAPI.isPartTiming(panel) ? (
+
) : null
)}
{rundownLayout.actionButtons && (
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 2d6618a884..c8aec6fe97 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -19,6 +19,7 @@ import {
RundownLayoutPlaylistEndTimer,
RundownLayoutEndWords,
RundownLayoutSegmentTiming,
+ RundownLayoutPartTiming,
} from '../collections/RundownLayouts'
import { ShowStyleBaseId } from '../collections/ShowStyleBases'
import * as _ from 'underscore'
@@ -178,6 +179,7 @@ export namespace RundownLayoutsAPI {
RundownLayoutElementType.PLAYLIST_END_TIMER,
RundownLayoutElementType.END_WORDS,
RundownLayoutElementType.SEGMENT_TIMING,
+ RundownLayoutElementType.PART_TIMING,
],
})
@@ -251,6 +253,10 @@ export namespace RundownLayoutsAPI {
return element.type === RundownLayoutElementType.SEGMENT_TIMING
}
+ export function isPartTiming(element: RundownLayoutElementBase): element is RundownLayoutPartTiming {
+ return element.type === RundownLayoutElementType.PART_TIMING
+ }
+
export function adLibRegionToFilter(element: RundownLayoutAdLibRegion): RundownLayoutFilterBase {
return {
..._.pick(element, '_id', 'name', 'rank', 'tags'),
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 9306503828..c96e83d55c 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -49,6 +49,7 @@ export enum RundownLayoutElementType {
PLAYLIST_END_TIMER = 'playlist_end_timer',
END_WORDS = 'end_words',
SEGMENT_TIMING = 'segment_timing',
+ PART_TIMING = 'part_timing',
}
export interface RundownLayoutElementBase {
@@ -113,6 +114,12 @@ export interface RundownLayoutSegmentTiming extends RundownLayoutElementBase {
timingType: 'count_down' | 'count_up'
}
+export interface RundownLayoutPartTiming extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.PART_TIMING
+ timingType: 'count_down' | 'count_up'
+ speakCountDown: boolean
+}
+
/**
* A filter to be applied against the AdLib Pieces. If a member is undefined, the pool is not tested
* against that filter. A member must match all of the sub-filters to be included in a filter view
@@ -194,6 +201,13 @@ export interface DashboardLayoutSegmentCountDown extends RundownLayoutSegmentTim
scale: number
}
+export interface DashboardLayoutPartCountDown extends RundownLayoutPartTiming {
+ x: number
+ y: number
+ width: number
+ scale: number
+}
+
export interface DashboardLayoutFilter extends RundownLayoutFilterBase {
x: number
y: number
From 8ce015c8a837f406a850d4b264699d453e0cf3fd Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Thu, 1 Jul 2021 17:04:24 +0100
Subject: [PATCH 048/112] wip: Functionality-complete panels
- Show Style Panel
- System Status Panel
- Text Label Panel
- Time of Day Panel
- Playlist Name Panel
---
meteor/client/styles/partTimingPanel.scss | 5 +
meteor/client/ui/RundownView.tsx | 34 +-
.../ui/Settings/components/FilterEditor.tsx | 296 +++++++++++++-----
meteor/client/ui/Shelf/DashboardPanel.tsx | 39 ++-
meteor/client/ui/Shelf/EndWordsPanel.tsx | 33 +-
meteor/client/ui/Shelf/PartTimingPanel.tsx | 29 +-
meteor/client/ui/Shelf/PlaylistNamePanel.tsx | 76 +++++
meteor/client/ui/Shelf/SegmentTimingPanel.tsx | 19 +-
meteor/client/ui/Shelf/Shelf.tsx | 3 +
.../client/ui/Shelf/ShelfDashboardLayout.tsx | 30 ++
meteor/client/ui/Shelf/ShowStylePanel.tsx | 67 ++++
meteor/client/ui/Shelf/SystemStatusPanel.tsx | 79 +++++
meteor/client/ui/Shelf/TextLabelPanel.tsx | 47 +++
meteor/client/ui/Shelf/TimeOfDayPanel.tsx | 55 ++++
meteor/lib/api/rundownLayouts.ts | 28 ++
meteor/lib/collections/RundownLayouts.ts | 92 +++++-
16 files changed, 798 insertions(+), 134 deletions(-)
create mode 100644 meteor/client/ui/Shelf/PlaylistNamePanel.tsx
create mode 100644 meteor/client/ui/Shelf/ShowStylePanel.tsx
create mode 100644 meteor/client/ui/Shelf/SystemStatusPanel.tsx
create mode 100644 meteor/client/ui/Shelf/TextLabelPanel.tsx
create mode 100644 meteor/client/ui/Shelf/TimeOfDayPanel.tsx
diff --git a/meteor/client/styles/partTimingPanel.scss b/meteor/client/styles/partTimingPanel.scss
index c9741c51cc..c65278ee70 100644
--- a/meteor/client/styles/partTimingPanel.scss
+++ b/meteor/client/styles/partTimingPanel.scss
@@ -1,3 +1,8 @@
.part-timing-panel {
position: absolute;
+
+ .overtime {
+ color: var(--general-late-color);
+ font-weight: 500;
+ }
}
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 9296b1ae6b..df6042616d 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -110,6 +110,7 @@ import { NextBreakTiming } from './RundownView/RundownTiming/NextBreakTiming'
import { RundownName } from './RundownView/RundownTiming/RundownName'
import { TimeOfDay } from './RundownView/RundownTiming/TimeOfDay'
import { PlaylistStartTiming } from './RundownView/RundownTiming/PlaylistStartTiming'
+import { ShowStyleVariant, ShowStyleVariants } from '../../lib/collections/ShowStyleVariants'
export const MAGIC_TIME_SCALE_FACTOR = 0.03
@@ -310,6 +311,7 @@ interface HotkeyDefinition {
interface IRundownHeaderProps {
playlist: RundownPlaylist
showStyleBase: ShowStyleBase
+ showStyleVariant: ShowStyleVariant
currentRundown: Rundown | undefined
studio: Studio
rundownIds: RundownId[]
@@ -1217,6 +1219,7 @@ const RundownHeader = withTranslation()(
rundownLayout={this.props.layout}
playlist={this.props.playlist}
showStyleBase={this.props.showStyleBase}
+ showStyleVariant={this.props.showStyleVariant}
studio={this.props.studio}
studioMode={this.props.studioMode}
shouldQueue={this.state.shouldQueue}
@@ -1323,6 +1326,7 @@ interface ITrackedProps {
rundownsToShowstyles: Map
studio?: Studio
showStyleBase?: ShowStyleBase
+ showStyleVariant?: ShowStyleVariant
rundownLayouts?: Array
buckets: Bucket[]
casparCGPlayoutDevices?: PeripheralDevice[]
@@ -1399,6 +1403,7 @@ export const RundownView = translateWithTracker((
playlist,
studio: studio,
showStyleBase: rundowns.length > 0 ? ShowStyleBases.findOne(rundowns[0].showStyleBaseId) : undefined,
+ showStyleVariant: rundowns.length > 0 ? ShowStyleVariants.findOne(rundowns[0].showStyleVariantId) : undefined,
rundownLayouts:
rundowns.length > 0 ? RundownLayouts.find({ showStyleBaseId: rundowns[0].showStyleBaseId }).fetch() : undefined,
buckets:
@@ -1676,13 +1681,19 @@ export const RundownView = translateWithTracker((
fields: {
_id: 1,
showStyleBaseId: 1,
+ showStyleVariantId: 1,
},
- }) as Pick[]
+ }) as Pick[]
this.subscribe(PubSub.showStyleBases, {
_id: {
$in: rundowns.map((i) => i.showStyleBaseId),
},
})
+ this.subscribe(PubSub.showStyleVariants, {
+ _id: {
+ $in: rundowns.map((i) => i.showStyleVariantId),
+ },
+ })
this.subscribe(PubSub.rundownLayouts, {
showStyleBaseId: {
$in: rundowns.map((i) => i.showStyleBaseId),
@@ -2525,7 +2536,13 @@ export const RundownView = translateWithTracker((
const { t } = this.props
if (this.state.subsReady) {
- if (this.props.playlist && this.props.studio && this.props.showStyleBase && !this.props.onlyShelf) {
+ if (
+ this.props.playlist &&
+ this.props.studio &&
+ this.props.showStyleBase &&
+ this.props.showStyleVariant &&
+ !this.props.onlyShelf
+ ) {
const selectedPiece = this.state.selectedPiece
const selectedPieceRundown: Rundown | undefined =
(selectedPiece &&
@@ -2659,6 +2676,7 @@ export const RundownView = translateWithTracker((
currentRundown={this.state.currentRundown || this.props.rundowns[0]}
layout={this.state.rundownHeaderLayout}
showStyleBase={this.props.showStyleBase}
+ showStyleVariant={this.props.showStyleVariant}
/>
@@ -2721,6 +2739,7 @@ export const RundownView = translateWithTracker((
hotkeys={this.state.usedHotkeys}
playlist={this.props.playlist}
showStyleBase={this.props.showStyleBase}
+ showStyleVariant={this.props.showStyleVariant}
studioMode={this.state.studioMode}
onChangeBottomMargin={this.onChangeBottomMargin}
onRegisterHotkeys={this.onRegisterHotkeys}
@@ -2756,7 +2775,13 @@ export const RundownView = translateWithTracker((
)
- } else if (this.props.playlist && this.props.studio && this.props.showStyleBase && this.props.onlyShelf) {
+ } else if (
+ this.props.playlist &&
+ this.props.studio &&
+ this.props.showStyleBase &&
+ this.props.showStyleVariant &&
+ this.props.onlyShelf
+ ) {
return (
@@ -2770,6 +2795,7 @@ export const RundownView = translateWithTracker((
hotkeys={this.state.usedHotkeys}
playlist={this.props.playlist}
showStyleBase={this.props.showStyleBase}
+ showStyleVariant={this.props.showStyleVariant}
studioMode={this.state.studioMode}
onChangeBottomMargin={this.onChangeBottomMargin}
onRegisterHotkeys={this.onRegisterHotkeys}
@@ -2793,7 +2819,7 @@ export const RundownView = translateWithTracker((
? t('Error: The studio of this Rundown was not found.')
: !this.props.rundowns.length
? t('This playlist is empty')
- : !this.props.showStyleBase
+ : !this.props.showStyleBase || !this.props.showStyleVariant
? t('Error: The ShowStyle of this Rundown was not found.')
: t('Unknown error')}
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index db4cd3b4c2..712400ddf6 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -18,9 +18,12 @@ import {
RundownLayoutPartTiming,
RundownLayoutPieceCountdown,
RundownLayoutPlaylistEndTimer,
+ RundownLayoutPlaylistName,
RundownLayoutPlaylistStartTimer,
RundownLayouts,
RundownLayoutSegmentTiming,
+ RundownLayoutTextLabel,
+ RundownLayoutTimeOfDay,
} from '../../../../lib/collections/RundownLayouts'
import { EditAttribute } from '../../../lib/EditAttribute'
import { MeteorReactComponent } from '../../../lib/MeteorReactComponent'
@@ -936,6 +939,102 @@ export default translateWithTracker((props: IProp
index: number,
isRundownLayout: boolean,
isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+ {this.renderRequiresActiveLayerSettings(
+ item,
+ index,
+ t('Script Source Layers'),
+ t('Source layers containing script')
+ )}
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderSegmentCountDown(
+ item: RundownLayoutBase,
+ tab: RundownLayoutSegmentTiming,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ let { t } = this.props
+
+ return (
+
+
+
+ {t('Type')}
+
+
+
+ {this.renderRequiresActiveLayerSettings(item, index, t('Require Piece on Source Layer'), '')}
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderPartCountDown(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPartTiming,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ let { t } = this.props
+
+ return (
+
+
+
+ {t('Type')}
+
+
+
+ {this.renderRequiresActiveLayerSettings(item, index, t('Require Piece on Source Layer'), '')}
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderTextLabel(
+ item: RundownLayoutBase,
+ tab: RundownLayoutTextLabel,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
) {
const { t } = this.props
return (
@@ -954,10 +1053,106 @@ export default translateWithTracker((props: IProp
- {t('Script Source Layers')}
+
+ {t('Text')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderPlaylistName(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPlaylistName,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+
+
+ {t('Show Rundown Name')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderTimeOfDay(
+ item: RundownLayoutBase,
+ tab: RundownLayoutTimeOfDay,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderRequiresActiveLayerSettings(
+ item: RundownLayoutBase,
+ index: number,
+ activeLayerTitle: string,
+ activeLayersLabel
+ ) {
+ const { t } = this.props
+ return (
+
+
+ {activeLayerTitle}
((props: IProp
/>
{
return { name: l.name, value: l._id }
@@ -978,10 +1173,10 @@ export default translateWithTracker((props: IProp
className="input text-input input-l dropdown"
mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
/>
- {t('Source layers containing script')}
+ {activeLayersLabel}
- {t('Required Source Layers')}
+ {t('Also Require Source Layers')}
((props: IProp
mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
/>
- {t('Specify layers where at least one layer must have an active piece for end words to be shown')}
+ {t('Specify additional layers where at least one layer must have an active piece')}
@@ -1020,12 +1215,9 @@ export default translateWithTracker((props: IProp
collection={RundownLayouts}
className="mod mas"
/>
-
- {t('All required source layers must have active pieces to show end words')}
-
+ {t('All required source layers must have active pieces')}
- {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
)
}
@@ -1106,66 +1298,6 @@ export default translateWithTracker((props: IProp
)
}
- renderSegmentCountDown(
- item: RundownLayoutBase,
- tab: RundownLayoutSegmentTiming,
- index: number,
- isRundownLayout: boolean,
- isDashboardLayout: boolean
- ) {
- let { t } = this.props
-
- return (
-
-
-
- {t('Type')}
-
-
-
- {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
-
- )
- }
-
- renderPartCountDown(
- item: RundownLayoutBase,
- tab: RundownLayoutPartTiming,
- index: number,
- isRundownLayout: boolean,
- isDashboardLayout: boolean
- ) {
- let { t } = this.props
-
- return (
-
-
-
- {t('Type')}
-
-
-
- {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
-
- )
- }
-
render() {
const { t } = this.props
@@ -1279,6 +1411,30 @@ export default translateWithTracker((props: IProp
isRundownLayout,
isDashboardLayout
)
+ : RundownLayoutsAPI.isTextLabel(this.props.filter)
+ ? this.renderTextLabel(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
+ : RundownLayoutsAPI.isPlaylistName(this.props.filter)
+ ? this.renderPlaylistName(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
+ : RundownLayoutsAPI.isTimeOfDay(this.props.filter)
+ ? this.renderTimeOfDay(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
: undefined}
)
diff --git a/meteor/client/ui/Shelf/DashboardPanel.tsx b/meteor/client/ui/Shelf/DashboardPanel.tsx
index 9bbff1c841..a71a480000 100644
--- a/meteor/client/ui/Shelf/DashboardPanel.tsx
+++ b/meteor/client/ui/Shelf/DashboardPanel.tsx
@@ -12,7 +12,7 @@ import { IOutputLayer, ISourceLayer, IBlueprintActionTriggerMode } from '@sofie-
import { PubSub } from '../../../lib/api/pubsub'
import { doUserAction, UserAction } from '../../lib/userAction'
import { NotificationCenter, Notification, NoticeLevel } from '../../lib/notifications/notifications'
-import { DashboardLayoutFilter } from '../../../lib/collections/RundownLayouts'
+import { DashboardLayoutFilter, FilterRequiresActiveLayers } from '../../../lib/collections/RundownLayouts'
import { unprotectString, getCurrentTime } from '../../../lib/lib'
import {
IAdLibPanelProps,
@@ -38,6 +38,7 @@ import { PartInstanceId } from '../../../lib/collections/PartInstances'
import { ContextMenuTrigger } from '@jstarpl/react-contextmenu'
import { setShelfContextMenuContext, ContextType } from './ShelfContextMenu'
import { RundownUtils } from '../../lib/rundown'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
interface IState {
outputLayers: {
@@ -920,3 +921,39 @@ export const DashboardPanel = translateWithTracker<
return !_.isEqual(props, nextProps)
}
)(DashboardPanelInner)
+
+/**
+ * If the conditions of the filter are met, activePieceInstance will include the first piece instance found that matches the filter, otherwise it will be undefined.
+ */
+export function getIsFilterActive(
+ playlist: RundownPlaylist,
+ panel: FilterRequiresActiveLayers
+): { active: boolean; activePieceInstance: PieceInstance | undefined } {
+ const unfinishedPieces = getUnfinishedPieceInstancesReactive(playlist.currentPartInstanceId, true)
+ let activePieceInstance: PieceInstance | undefined
+ let activeLayers = unfinishedPieces.map((p) => p.piece.sourceLayerId)
+ let containsEveryRequiredLayer = panel.requireAllSourcelayers
+ ? panel.requiredLayers?.length && panel.requiredLayers.every((s) => activeLayers.includes(s))
+ : false
+ let containsRequiredLayer = containsEveryRequiredLayer
+ ? true
+ : panel.requiredLayers && panel.requiredLayers.length
+ ? panel.requiredLayers.some((s) => activeLayers.includes(s))
+ : false
+
+ if (
+ (!panel.requireAllSourcelayers || containsEveryRequiredLayer) &&
+ (!panel.requiredLayers?.length || containsRequiredLayer)
+ ) {
+ activePieceInstance =
+ panel.activeLayerIds && panel.activeLayerIds.length
+ ? _.flatten(Object.values(unfinishedPieces)).find((piece: PieceInstance) => {
+ return (
+ (panel.activeLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 &&
+ piece.partInstanceId === playlist.currentPartInstanceId
+ )
+ })
+ : undefined
+ }
+ return { active: activePieceInstance !== undefined || !panel.requiredLayers?.length, activePieceInstance }
+}
diff --git a/meteor/client/ui/Shelf/EndWordsPanel.tsx b/meteor/client/ui/Shelf/EndWordsPanel.tsx
index ecc4f42a7f..225737d36f 100644
--- a/meteor/client/ui/Shelf/EndWordsPanel.tsx
+++ b/meteor/client/ui/Shelf/EndWordsPanel.tsx
@@ -6,7 +6,7 @@ import {
RundownLayoutEndWords,
} from '../../../lib/collections/RundownLayouts'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
-import { dashboardElementPosition, getUnfinishedPieceInstancesReactive } from './DashboardPanel'
+import { dashboardElementPosition, getIsFilterActive, getUnfinishedPieceInstancesReactive } from './DashboardPanel'
import { Translated, translateWithTracker, withTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
@@ -65,34 +65,9 @@ export class EndWordsPanelInner extends MeteorReactComponent<
}
export const EndWordsPanel = translateWithTracker(
- (props: IEndsWordsPanelProps & IEndsWordsPanelTrackedProps) => {
- const unfinishedPieces = getUnfinishedPieceInstancesReactive(props.playlist.currentPartInstanceId, true)
- let livePieceInstance: PieceInstance | undefined
- let activeLayers = unfinishedPieces.map((p) => p.piece.sourceLayerId)
- let containsEveryRequiredLayer = props.panel.requireAllSourcelayers
- ? props.panel.requiredLayers?.every((s) => activeLayers.includes(s))
- : false
- let containsRequiredLayer = containsEveryRequiredLayer
- ? true
- : props.panel.requiredLayers && props.panel.requiredLayers.length
- ? props.panel.requiredLayers.some((s) => activeLayers.includes(s))
- : false
-
- if (
- (!props.panel.requireAllSourcelayers || containsEveryRequiredLayer) &&
- (!props.panel.requiredLayers?.length || containsRequiredLayer)
- ) {
- livePieceInstance =
- props.panel.scriptSourceLayerIds && props.panel.scriptSourceLayerIds.length
- ? _.flatten(Object.values(unfinishedPieces)).find((piece: PieceInstance) => {
- return (
- (props.panel.scriptSourceLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 &&
- piece.partInstanceId === props.playlist.currentPartInstanceId
- )
- })
- : undefined
- }
- return { livePieceInstance }
+ (props: IEndsWordsPanelProps) => {
+ let { activePieceInstance } = getIsFilterActive(props.playlist, props.panel)
+ return { livePieceInstance: activePieceInstance }
},
(_data, props: IEndsWordsPanelProps, nextProps: IEndsWordsPanelProps) => {
return !_.isEqual(props, nextProps)
diff --git a/meteor/client/ui/Shelf/PartTimingPanel.tsx b/meteor/client/ui/Shelf/PartTimingPanel.tsx
index ac6daf4bc2..1c728d0ed0 100644
--- a/meteor/client/ui/Shelf/PartTimingPanel.tsx
+++ b/meteor/client/ui/Shelf/PartTimingPanel.tsx
@@ -20,7 +20,7 @@ import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveD
import { Part, PartId } from '../../../lib/collections/Parts'
import { PartInstance } from '../../../lib/collections/PartInstances'
import { ShowStyleBase } from '../../../lib/collections/ShowStyleBases'
-import { dashboardElementPosition } from './DashboardPanel'
+import { dashboardElementPosition, getIsFilterActive } from './DashboardPanel'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
import { getAllowSpeaking } from '../../lib/localStorage'
import { CurrentPartRemaining } from '../RundownView/RundownTiming/CurrentPartRemaining'
@@ -35,6 +35,7 @@ interface IPartTimingPanelProps {
interface IPartTimingPanelTrackedProps {
livePart?: PartInstance
+ active: boolean
}
interface IState {}
@@ -67,15 +68,16 @@ class PartTimingPanelInner extends MeteorReactComponent<
{panel.timingType === 'count_down' ? t('Part Count Down') : t('Part Count Up')}
- {panel.timingType === 'count_down' ? (
-
- ) : (
-
- )}
+ {this.props.active &&
+ (panel.timingType === 'count_down' ? (
+
+ ) : (
+
+ ))}
)
@@ -83,13 +85,14 @@ class PartTimingPanelInner extends MeteorReactComponent<
}
export const PartTimingPanel = translateWithTracker(
- (props: IPartTimingPanelProps & IPartTimingPanelTrackedProps) => {
+ (props: IPartTimingPanelProps) => {
if (props.playlist.currentPartInstanceId) {
let livePart = props.playlist.getActivePartInstances({ _id: props.playlist.currentPartInstanceId })[0]
+ let { active } = getIsFilterActive(props.playlist, props.panel)
- return { livePart }
+ return { active, livePart }
}
- return {}
+ return { active: false }
},
(_data, props: IPartTimingPanelProps, nextProps: IPartTimingPanelProps) => {
return !_.isEqual(props, nextProps)
diff --git a/meteor/client/ui/Shelf/PlaylistNamePanel.tsx b/meteor/client/ui/Shelf/PlaylistNamePanel.tsx
new file mode 100644
index 0000000000..3dcd0a60ec
--- /dev/null
+++ b/meteor/client/ui/Shelf/PlaylistNamePanel.tsx
@@ -0,0 +1,76 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutPlaylistName,
+ DashboardLayoutTextLabel,
+ RundownLayoutBase,
+ RundownLayoutPlaylistName,
+ RundownLayoutTextLabel,
+} from '../../../lib/collections/RundownLayouts'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { dashboardElementPosition } from './DashboardPanel'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { Rundown } from '../../../lib/collections/Rundowns'
+
+interface IPlaylistNamePanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutPlaylistName
+ playlist: RundownPlaylist
+}
+
+interface IState {}
+
+interface IPlaylistNamePannelTrackedProps {
+ currentRundown?: Rundown
+}
+
+class PlaylistNamePanelInner extends MeteorReactComponent<
+ IPlaylistNamePanelProps & IPlaylistNamePannelTrackedProps,
+ IState
+> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ let { panel } = this.props
+
+ return (
+
+ {this.props.playlist.name}
+ {this.props.panel.showCurrentRundownName && this.props.currentRundown && (
+ {this.props.currentRundown.name}
+ )}
+
+ )
+ }
+}
+
+export const PlaylistNamePanel = withTracker(
+ (props: IPlaylistNamePanelProps) => {
+ if (props.playlist.currentPartInstanceId) {
+ let livePart = props.playlist.getActivePartInstances({ _id: props.playlist.currentPartInstanceId })[0]
+ let currentRundown = props.playlist.getRundowns({ _id: livePart.rundownId })[0]
+
+ return {
+ currentRundown,
+ }
+ }
+
+ return {}
+ }
+)(PlaylistNamePanelInner)
diff --git a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
index e5dfe30516..24512758f7 100644
--- a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
+++ b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
@@ -16,7 +16,7 @@ import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveD
import { Part, PartId } from '../../../lib/collections/Parts'
import { PartInstance } from '../../../lib/collections/PartInstances'
import { ShowStyleBase } from '../../../lib/collections/ShowStyleBases'
-import { dashboardElementPosition } from './DashboardPanel'
+import { dashboardElementPosition, getIsFilterActive } from './DashboardPanel'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
interface ISegmentTimingPanelProps {
@@ -30,6 +30,7 @@ interface ISegmentTimingPanelProps {
interface ISegmentTimingPanelTrackedProps {
liveSegment?: Segment
parts?: PartExtended[]
+ active: boolean
}
interface IState {}
@@ -62,7 +63,9 @@ class SegmentTimingPanelInner extends MeteorReactComponent<
{panel.timingType === 'count_down' ? t('Segment Count Down') : t('Segment Count Up')}
- {this.props.parts && }
+ {this.props.active && this.props.parts && (
+
+ )}
)
@@ -74,12 +77,14 @@ export const SegmentTimingPanel = translateWithTracker<
IState,
ISegmentTimingPanelTrackedProps
>(
- (props: ISegmentTimingPanelProps & ISegmentTimingPanelTrackedProps) => {
+ (props: ISegmentTimingPanelProps) => {
if (props.playlist.currentPartInstanceId) {
let livePart = props.playlist.getActivePartInstances({ _id: props.playlist.currentPartInstanceId })[0]
let liveSegment = livePart ? props.playlist.getSegments({ _id: livePart.segmentId })[0] : undefined
- if (!liveSegment) return {}
+ const { active } = getIsFilterActive(props.playlist, props.panel)
+
+ if (!liveSegment) return { active }
const [orderedAllPartIds, { currentPartInstance, nextPartInstance }] = slowDownReactivity(
() =>
@@ -122,7 +127,7 @@ export const SegmentTimingPanel = translateWithTracker<
const rundown = rundowns.find((r) => r._id === liveSegment!.rundownId)
const segmentIndex = orderedSegmentsAndParts.segments.findIndex((s) => s._id === liveSegment!._id)
- if (!rundown) return {}
+ if (!rundown) return { active }
const rundownsToShowstyles = new Map()
for (const rundown of rundowns) {
@@ -144,9 +149,9 @@ export const SegmentTimingPanel = translateWithTracker<
true
)
- return { liveSegment, parts: o.parts }
+ return { active, liveSegment, parts: o.parts }
}
- return {}
+ return { active: false }
},
(_data, props: ISegmentTimingPanelProps, nextProps: ISegmentTimingPanelProps) => {
return !_.isEqual(props, nextProps)
diff --git a/meteor/client/ui/Shelf/Shelf.tsx b/meteor/client/ui/Shelf/Shelf.tsx
index d055292d52..94a66ab554 100644
--- a/meteor/client/ui/Shelf/Shelf.tsx
+++ b/meteor/client/ui/Shelf/Shelf.tsx
@@ -34,6 +34,7 @@ import RundownViewEventBus, {
import { IAdLibListItem } from './AdLibListItem'
import ShelfContextMenu from './ShelfContextMenu'
import { Rundown } from '../../../lib/collections/Rundowns'
+import { ShowStyleVariant } from '../../../lib/collections/ShowStyleVariants'
export enum ShelfTabs {
ADLIB = 'adlib',
@@ -48,6 +49,7 @@ export interface IShelfProps extends React.ComponentPropsWithRef {
currentRundown: Rundown
studio: Studio
showStyleBase: ShowStyleBase
+ showStyleVariant: ShowStyleVariant
studioMode: boolean
hotkeys: Array<{
key: string
@@ -506,6 +508,7 @@ export class ShelfBase extends React.Component, IState>
) : RundownLayoutsAPI.isPartTiming(panel) ? (
+ ) : RundownLayoutsAPI.isTextLabel(panel) ? (
+
+ ) : RundownLayoutsAPI.isPlaylistName(panel) ? (
+
+ ) : RundownLayoutsAPI.isTimeOfDay(panel) ? (
+
+ ) : RundownLayoutsAPI.isSystemStatus(panel) ? (
+
+ ) : RundownLayoutsAPI.isShowStyleDisplay(panel) ? (
+
) : null
)}
{rundownLayout.actionButtons && (
diff --git a/meteor/client/ui/Shelf/ShowStylePanel.tsx b/meteor/client/ui/Shelf/ShowStylePanel.tsx
new file mode 100644
index 0000000000..312b7f2313
--- /dev/null
+++ b/meteor/client/ui/Shelf/ShowStylePanel.tsx
@@ -0,0 +1,67 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutPartCountDown,
+ DashboardLayoutShowStyleDisplay,
+ RundownLayoutBase,
+ RundownLayoutShowStyleDisplay,
+} from '../../../lib/collections/RundownLayouts'
+import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { PartInstance } from '../../../lib/collections/PartInstances'
+import { dashboardElementPosition, getIsFilterActive } from './DashboardPanel'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { getAllowSpeaking } from '../../lib/localStorage'
+import { CurrentPartRemaining } from '../RundownView/RundownTiming/CurrentPartRemaining'
+import { CurrentPartElapsed } from '../RundownView/RundownTiming/CurrentPartElapsed'
+import { ShowStyleBase } from '../../../lib/collections/ShowStyleBases'
+import { ShowStyleVariant } from '../../../lib/collections/ShowStyleVariants'
+import { withTranslation } from 'react-i18next'
+
+interface IShowStylePanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutShowStyleDisplay
+ playlist: RundownPlaylist
+ showStyleBase: ShowStyleBase
+ showStyleVariant: ShowStyleVariant
+}
+
+interface IState {}
+
+class ShowStylePanelInner extends MeteorReactComponent, IState> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ let { t, panel } = this.props
+
+ return (
+
+
+ {t('Show Style')}
+ {this.props.showStyleBase.name}
+
+
+ {t('Show Style Variant')}
+ {this.props.showStyleVariant.name}
+
+
+ )
+ }
+}
+
+export const ShowStylePanel = withTranslation()(ShowStylePanelInner)
diff --git a/meteor/client/ui/Shelf/SystemStatusPanel.tsx b/meteor/client/ui/Shelf/SystemStatusPanel.tsx
new file mode 100644
index 0000000000..9f6ff57904
--- /dev/null
+++ b/meteor/client/ui/Shelf/SystemStatusPanel.tsx
@@ -0,0 +1,79 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutSystemStatus,
+ RundownLayoutBase,
+ RundownLayoutSytemStatus,
+} from '../../../lib/collections/RundownLayouts'
+import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { dashboardElementPosition } from './DashboardPanel'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { RundownSystemStatus } from '../RundownView/RundownSystemStatus'
+import { DBStudio } from '../../../lib/collections/Studios'
+import { Rundown, RundownId } from '../../../lib/collections/Rundowns'
+
+interface ISystemStatusPanelProps {
+ studio: DBStudio
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutSytemStatus
+ playlist: RundownPlaylist
+}
+
+interface IState {}
+
+interface ISystemStatusPanelTrackedProps {
+ firstRundown: Rundown | undefined
+ rundownIds: RundownId[]
+}
+
+class SystemStatusPanelInner extends MeteorReactComponent<
+ Translated,
+ IState
+> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ let { t, panel } = this.props
+
+ return (
+
+
+ {t('System Status')}
+
+
+
+ )
+ }
+}
+
+export const SystemStatusPanel = translateWithTracker(
+ (props: ISystemStatusPanelProps) => {
+ let rundownIds = props.playlist.getRundownIDs() ?? []
+ let firstRundown = rundownIds.length ? props.playlist.getRundowns({ _id: rundownIds[0] })[0] : undefined
+ return {
+ rundownIds,
+ firstRundown,
+ }
+ }
+)(SystemStatusPanelInner)
diff --git a/meteor/client/ui/Shelf/TextLabelPanel.tsx b/meteor/client/ui/Shelf/TextLabelPanel.tsx
new file mode 100644
index 0000000000..c31051b8ee
--- /dev/null
+++ b/meteor/client/ui/Shelf/TextLabelPanel.tsx
@@ -0,0 +1,47 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutTextLabel,
+ RundownLayoutBase,
+ RundownLayoutTextLabel,
+} from '../../../lib/collections/RundownLayouts'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { dashboardElementPosition } from './DashboardPanel'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+
+interface ITextLabelPanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutTextLabel
+ playlist: RundownPlaylist
+}
+
+interface IState {}
+
+export class TextLabelPanel extends MeteorReactComponent {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ let { panel } = this.props
+
+ return (
+
+ {this.props.panel.text}
+
+ )
+ }
+}
diff --git a/meteor/client/ui/Shelf/TimeOfDayPanel.tsx b/meteor/client/ui/Shelf/TimeOfDayPanel.tsx
new file mode 100644
index 0000000000..6eb114e334
--- /dev/null
+++ b/meteor/client/ui/Shelf/TimeOfDayPanel.tsx
@@ -0,0 +1,55 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutTimeOfDay,
+ RundownLayoutBase,
+ RundownLayoutTimeOfDay,
+} from '../../../lib/collections/RundownLayouts'
+import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { dashboardElementPosition } from './DashboardPanel'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { withTranslation } from 'react-i18next'
+import { TimeOfDay } from '../RundownView/RundownTiming/TimeOfDay'
+
+interface ITimeOfDayPanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutTimeOfDay
+ playlist: RundownPlaylist
+}
+
+interface IState {}
+
+class TimeOfDayPanelInner extends MeteorReactComponent, IState> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ let { t, panel } = this.props
+
+ return (
+
+
+ {t('Local Time')}
+
+
+
+ )
+ }
+}
+
+export const TimeOfDayPanel = withTranslation()(TimeOfDayPanelInner)
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index c8aec6fe97..67c8d5f0df 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -20,6 +20,11 @@ import {
RundownLayoutEndWords,
RundownLayoutSegmentTiming,
RundownLayoutPartTiming,
+ RundownLayoutTextLabel,
+ RundownLayoutPlaylistName,
+ RundownLayoutTimeOfDay,
+ RundownLayoutSytemStatus,
+ RundownLayoutShowStyleDisplay,
} from '../collections/RundownLayouts'
import { ShowStyleBaseId } from '../collections/ShowStyleBases'
import * as _ from 'underscore'
@@ -180,6 +185,9 @@ export namespace RundownLayoutsAPI {
RundownLayoutElementType.END_WORDS,
RundownLayoutElementType.SEGMENT_TIMING,
RundownLayoutElementType.PART_TIMING,
+ RundownLayoutElementType.TEXT_LABEL,
+ RundownLayoutElementType.PLAYLIST_NAME,
+ RundownLayoutElementType.TIME_OF_DAY,
],
})
@@ -257,6 +265,26 @@ export namespace RundownLayoutsAPI {
return element.type === RundownLayoutElementType.PART_TIMING
}
+ export function isTextLabel(element: RundownLayoutElementBase): element is RundownLayoutTextLabel {
+ return element.type === RundownLayoutElementType.TEXT_LABEL
+ }
+
+ export function isPlaylistName(element: RundownLayoutElementBase): element is RundownLayoutPlaylistName {
+ return element.type === RundownLayoutElementType.PLAYLIST_NAME
+ }
+
+ export function isTimeOfDay(element: RundownLayoutElementBase): element is RundownLayoutTimeOfDay {
+ return element.type === RundownLayoutElementType.TIME_OF_DAY
+ }
+
+ export function isSystemStatus(element: RundownLayoutElementBase): element is RundownLayoutSytemStatus {
+ return element.type === RundownLayoutElementType.SYSTEM_STATUS
+ }
+
+ export function isShowStyleDisplay(element: RundownLayoutElementBase): element is RundownLayoutShowStyleDisplay {
+ return element.type === RundownLayoutElementType.SHOWSTYLE_DISPLAY
+ }
+
export function adLibRegionToFilter(element: RundownLayoutAdLibRegion): RundownLayoutFilterBase {
return {
..._.pick(element, '_id', 'name', 'rank', 'tags'),
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index c96e83d55c..57bdcc4175 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -50,6 +50,11 @@ export enum RundownLayoutElementType {
END_WORDS = 'end_words',
SEGMENT_TIMING = 'segment_timing',
PART_TIMING = 'part_timing',
+ TEXT_LABEL = 'text_label',
+ PLAYLIST_NAME = 'playlist_name',
+ TIME_OF_DAY = 'time_of_day',
+ SYSTEM_STATUS = 'system_status',
+ SHOWSTYLE_DISPLAY = 'showstyle_display',
}
export interface RundownLayoutElementBase {
@@ -59,6 +64,23 @@ export interface RundownLayoutElementBase {
type?: RundownLayoutElementType // if not set, the value is RundownLayoutElementType.FILTER
}
+/**
+ * An interface for filters that check for a piece to be present on a source layer to change their behaviour (or in order to perform any action at all).
+ * If `activeLayerIds` is empty / undefined, the filter should be treated as "always active".
+ * @param activeLayerIds Layers that the filter will check for some active ('live') piece. (Match any layer in array).
+ * @param requiredLayers Layers that must be active in addition to the active layers, i.e. "any of `activeLayerIds`, with at least one of `requiredLayers`".
+ * @param requireAllSourcelayers Require all layers in `requiredLayers` to contain an active piece.
+ */
+export interface FilterRequiresActiveLayers {
+ activeLayerIds?: string[]
+ requiredLayers?: string[]
+ /**
+ * Require that all required sourcelayers be active.
+ * This allows behaviour to be tied to a combination of e.g. script + VT.
+ */
+ requireAllSourcelayers: boolean
+}
+
export interface RundownLayoutExternalFrame extends RundownLayoutElementBase {
type: RundownLayoutElementType.EXTERNAL_FRAME
url: string
@@ -98,28 +120,43 @@ export interface RundownLayoutPlaylistEndTimer extends RundownLayoutElementBase
hidePlannedEnd: boolean
}
-export interface RundownLayoutEndWords extends RundownLayoutElementBase {
+export interface RundownLayoutEndWords extends RundownLayoutElementBase, FilterRequiresActiveLayers {
type: RundownLayoutElementType.PLAYLIST_END_TIMER
- scriptSourceLayerIds?: string[]
- requiredLayers?: string[]
- /**
- * Require that all required sourcelayers be active in order to show end words.
- * This allows end words to be tied to a combination of e.g. script + VT.
- */
- requireAllSourcelayers: boolean
}
-export interface RundownLayoutSegmentTiming extends RundownLayoutElementBase {
+export interface RundownLayoutSegmentTiming extends RundownLayoutElementBase, FilterRequiresActiveLayers {
type: RundownLayoutElementType.SEGMENT_TIMING
timingType: 'count_down' | 'count_up'
}
-export interface RundownLayoutPartTiming extends RundownLayoutElementBase {
+export interface RundownLayoutPartTiming extends RundownLayoutElementBase, FilterRequiresActiveLayers {
type: RundownLayoutElementType.PART_TIMING
timingType: 'count_down' | 'count_up'
speakCountDown: boolean
}
+export interface RundownLayoutTextLabel extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.TEXT_LABEL
+ text: string
+}
+
+export interface RundownLayoutPlaylistName extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.PLAYLIST_NAME
+ showCurrentRundownName: boolean
+}
+
+export interface RundownLayoutTimeOfDay extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.TIME_OF_DAY
+}
+
+export interface RundownLayoutSytemStatus extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.SYSTEM_STATUS
+}
+
+export interface RundownLayoutShowStyleDisplay extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.SHOWSTYLE_DISPLAY
+}
+
/**
* A filter to be applied against the AdLib Pieces. If a member is undefined, the pool is not tested
* against that filter. A member must match all of the sub-filters to be included in a filter view
@@ -208,6 +245,41 @@ export interface DashboardLayoutPartCountDown extends RundownLayoutPartTiming {
scale: number
}
+export interface DashboardLayoutTextLabel extends RundownLayoutTextLabel {
+ x: number
+ y: number
+ width: number
+ scale: number
+}
+
+export interface DashboardLayoutPlaylistName extends RundownLayoutPlaylistName {
+ x: number
+ y: number
+ width: number
+ scale: number
+}
+
+export interface DashboardLayoutTimeOfDay extends RundownLayoutTimeOfDay {
+ x: number
+ y: number
+ width: number
+ scale: number
+}
+
+export interface DashboardLayoutSystemStatus extends RundownLayoutSytemStatus {
+ x: number
+ y: number
+ width: number
+ scale: number
+}
+
+export interface DashboardLayoutShowStyleDisplay extends RundownLayoutShowStyleDisplay {
+ x: number
+ y: number
+ width: number
+ scale: number
+}
+
export interface DashboardLayoutFilter extends RundownLayoutFilterBase {
x: number
y: number
From 9e716cc6466762afd6d0d8c3888200336862ce09 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Fri, 2 Jul 2021 13:38:29 +0100
Subject: [PATCH 049/112] feat: Styling of header components
---
.../styles/{ => shelf}/partTimingPanel.scss | 0
.../styles/shelf/playlistNamePanel.scss | 49 ++++++++++++
.../client/styles/shelf/showStylePanel.scss | 11 +++
.../styles/shelf/systemStatusPanel.scss | 14 ++++
.../client/styles/shelf/textLabelPanel.scss | 28 +++++++
.../client/styles/shelf/timeOfDayPanel.scss | 17 +++++
.../ui/Settings/components/FilterEditor.tsx | 74 +++++++++++++++++++
meteor/client/ui/Shelf/DashboardPanel.tsx | 5 +-
meteor/client/ui/Shelf/PlaylistNamePanel.tsx | 10 ++-
meteor/client/ui/Shelf/ShowStylePanel.tsx | 15 ++--
meteor/client/ui/Shelf/SystemStatusPanel.tsx | 2 +-
meteor/client/ui/Shelf/TextLabelPanel.tsx | 4 +-
meteor/client/ui/Shelf/TimeOfDayPanel.tsx | 2 +-
meteor/lib/api/rundownLayouts.ts | 2 +
14 files changed, 215 insertions(+), 18 deletions(-)
rename meteor/client/styles/{ => shelf}/partTimingPanel.scss (100%)
create mode 100644 meteor/client/styles/shelf/playlistNamePanel.scss
create mode 100644 meteor/client/styles/shelf/showStylePanel.scss
create mode 100644 meteor/client/styles/shelf/systemStatusPanel.scss
create mode 100644 meteor/client/styles/shelf/textLabelPanel.scss
create mode 100644 meteor/client/styles/shelf/timeOfDayPanel.scss
diff --git a/meteor/client/styles/partTimingPanel.scss b/meteor/client/styles/shelf/partTimingPanel.scss
similarity index 100%
rename from meteor/client/styles/partTimingPanel.scss
rename to meteor/client/styles/shelf/partTimingPanel.scss
diff --git a/meteor/client/styles/shelf/playlistNamePanel.scss b/meteor/client/styles/shelf/playlistNamePanel.scss
new file mode 100644
index 0000000000..bc931ca5e1
--- /dev/null
+++ b/meteor/client/styles/shelf/playlistNamePanel.scss
@@ -0,0 +1,49 @@
+.playlist-name-panel {
+ position: absolute;
+ margin: 0 0;
+ min-width: auto;
+ text-align: center;
+ isolation: isolate;
+
+ .wrapper {
+ position: relative;
+ margin-right: 1em;
+ font-family: 'Roboto', Helvetica Neue, Arial, sans-serif;
+ font-weight: 100;
+ margin-top: 0.8em;
+ word-break: keep-all;
+ white-space: nowrap;
+ text-align: left;
+ font-size: 1.5em;
+ width: 100%;
+
+ .playlist-name {
+ position: absolute;
+ top: -1em;
+ color: #b8b8b8;
+ text-transform: uppercase;
+ white-space: nowrap;
+ font-weight: 600;
+ font-size: 0.5em;
+
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .rundown-name {
+ position: absolute;
+ top: 0em;
+ color: #b8b8b8;
+ text-transform: uppercase;
+ white-space: nowrap;
+ font-weight: 300;
+ font-size: 0.5em;
+ width: 100%;
+
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+}
diff --git a/meteor/client/styles/shelf/showStylePanel.scss b/meteor/client/styles/shelf/showStylePanel.scss
new file mode 100644
index 0000000000..3f18584ea4
--- /dev/null
+++ b/meteor/client/styles/shelf/showStylePanel.scss
@@ -0,0 +1,11 @@
+.show-style-panel {
+ position: absolute;
+
+ .timing-clock {
+ margin-right: 1.2em;
+ }
+
+ .name {
+ font-weight: 400;
+ }
+}
diff --git a/meteor/client/styles/shelf/systemStatusPanel.scss b/meteor/client/styles/shelf/systemStatusPanel.scss
new file mode 100644
index 0000000000..1d40a91de6
--- /dev/null
+++ b/meteor/client/styles/shelf/systemStatusPanel.scss
@@ -0,0 +1,14 @@
+.system-status-panel {
+ position: absolute;
+ isolation: isolate;
+
+ .rundown-system-status {
+ top: 0.2em !important;
+ transform: unset !important;
+ left: unset !important;
+
+ .rundown-system-status__indicators {
+ justify-content: unset !important;
+ }
+ }
+}
diff --git a/meteor/client/styles/shelf/textLabelPanel.scss b/meteor/client/styles/shelf/textLabelPanel.scss
new file mode 100644
index 0000000000..e05eb4f8c9
--- /dev/null
+++ b/meteor/client/styles/shelf/textLabelPanel.scss
@@ -0,0 +1,28 @@
+.text-label-panel {
+ position: absolute;
+ margin: 0 0;
+ min-width: auto;
+ text-align: center;
+ isolation: isolate;
+
+ .wrapper {
+ position: relative;
+ margin-right: 1em;
+ font-family: 'Roboto', Helvetica Neue, Arial, sans-serif;
+ font-weight: 100;
+ margin-top: 0.8em;
+ word-break: keep-all;
+ white-space: nowrap;
+ text-align: left;
+ font-size: 1.5em;
+
+ .text {
+ position: absolute;
+ top: -1em;
+ color: #b8b8b8;
+ white-space: nowrap;
+ font-weight: 300;
+ font-size: 0.5em;
+ }
+ }
+}
diff --git a/meteor/client/styles/shelf/timeOfDayPanel.scss b/meteor/client/styles/shelf/timeOfDayPanel.scss
new file mode 100644
index 0000000000..41774d998c
--- /dev/null
+++ b/meteor/client/styles/shelf/timeOfDayPanel.scss
@@ -0,0 +1,17 @@
+.time-of-day-panel {
+ position: absolute;
+ isolation: isolate;
+
+ &.timing {
+ .timing-clock {
+ .time-now {
+ transform: unset !important;
+ font-size: 1em !important;
+ }
+ }
+ }
+
+ time {
+ font-size: 1em;
+ }
+}
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index 712400ddf6..cc4361d29e 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -22,6 +22,8 @@ import {
RundownLayoutPlaylistStartTimer,
RundownLayouts,
RundownLayoutSegmentTiming,
+ RundownLayoutShowStyleDisplay,
+ RundownLayoutSytemStatus,
RundownLayoutTextLabel,
RundownLayoutTimeOfDay,
} from '../../../../lib/collections/RundownLayouts'
@@ -1111,6 +1113,62 @@ export default translateWithTracker((props: IProp
)
}
+ renderShowStyleDisplay(
+ item: RundownLayoutBase,
+ tab: RundownLayoutShowStyleDisplay,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderSystemStatus(
+ item: RundownLayoutBase,
+ tab: RundownLayoutSytemStatus,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
renderTimeOfDay(
item: RundownLayoutBase,
tab: RundownLayoutTimeOfDay,
@@ -1435,6 +1493,22 @@ export default translateWithTracker((props: IProp
isRundownLayout,
isDashboardLayout
)
+ : RundownLayoutsAPI.isShowStyleDisplay(this.props.filter)
+ ? this.renderShowStyleDisplay(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
+ : RundownLayoutsAPI.isSystemStatus(this.props.filter)
+ ? this.renderSystemStatus(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
: undefined}
)
diff --git a/meteor/client/ui/Shelf/DashboardPanel.tsx b/meteor/client/ui/Shelf/DashboardPanel.tsx
index a71a480000..3bb909283c 100644
--- a/meteor/client/ui/Shelf/DashboardPanel.tsx
+++ b/meteor/client/ui/Shelf/DashboardPanel.tsx
@@ -955,5 +955,8 @@ export function getIsFilterActive(
})
: undefined
}
- return { active: activePieceInstance !== undefined || !panel.requiredLayers?.length, activePieceInstance }
+ return {
+ active: activePieceInstance !== undefined || (!panel.activeLayerIds?.length && !panel.requiredLayers?.length),
+ activePieceInstance,
+ }
}
diff --git a/meteor/client/ui/Shelf/PlaylistNamePanel.tsx b/meteor/client/ui/Shelf/PlaylistNamePanel.tsx
index 3dcd0a60ec..d8f4ad6734 100644
--- a/meteor/client/ui/Shelf/PlaylistNamePanel.tsx
+++ b/meteor/client/ui/Shelf/PlaylistNamePanel.tsx
@@ -51,10 +51,12 @@ class PlaylistNamePanelInner extends MeteorReactComponent<
: {}
)}
>
- {this.props.playlist.name}
- {this.props.panel.showCurrentRundownName && this.props.currentRundown && (
- {this.props.currentRundown.name}
- )}
+
+ {this.props.playlist.name}
+ {this.props.panel.showCurrentRundownName && this.props.currentRundown && (
+ {this.props.currentRundown.name}
+ )}
+
)
}
diff --git a/meteor/client/ui/Shelf/ShowStylePanel.tsx b/meteor/client/ui/Shelf/ShowStylePanel.tsx
index 312b7f2313..c7809814cb 100644
--- a/meteor/client/ui/Shelf/ShowStylePanel.tsx
+++ b/meteor/client/ui/Shelf/ShowStylePanel.tsx
@@ -1,20 +1,15 @@
import * as React from 'react'
import * as _ from 'underscore'
import {
- DashboardLayoutPartCountDown,
DashboardLayoutShowStyleDisplay,
RundownLayoutBase,
RundownLayoutShowStyleDisplay,
} from '../../../lib/collections/RundownLayouts'
-import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
-import { PartInstance } from '../../../lib/collections/PartInstances'
-import { dashboardElementPosition, getIsFilterActive } from './DashboardPanel'
+import { dashboardElementPosition } from './DashboardPanel'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
-import { getAllowSpeaking } from '../../lib/localStorage'
-import { CurrentPartRemaining } from '../RundownView/RundownTiming/CurrentPartRemaining'
-import { CurrentPartElapsed } from '../RundownView/RundownTiming/CurrentPartElapsed'
import { ShowStyleBase } from '../../../lib/collections/ShowStyleBases'
import { ShowStyleVariant } from '../../../lib/collections/ShowStyleVariants'
import { withTranslation } from 'react-i18next'
@@ -41,7 +36,7 @@ class ShowStylePanelInner extends MeteorReactComponent
{t('Show Style')}
- {this.props.showStyleBase.name}
+ {this.props.showStyleBase.name}
{t('Show Style Variant')}
- {this.props.showStyleVariant.name}
+ {this.props.showStyleVariant.name}
)
diff --git a/meteor/client/ui/Shelf/SystemStatusPanel.tsx b/meteor/client/ui/Shelf/SystemStatusPanel.tsx
index 9f6ff57904..7ad56adc9d 100644
--- a/meteor/client/ui/Shelf/SystemStatusPanel.tsx
+++ b/meteor/client/ui/Shelf/SystemStatusPanel.tsx
@@ -43,7 +43,7 @@ class SystemStatusPanelInner extends MeteorReactComponent<
return (
-
{this.props.panel.text}
+
+ {this.props.panel.text}
+
)
}
diff --git a/meteor/client/ui/Shelf/TimeOfDayPanel.tsx b/meteor/client/ui/Shelf/TimeOfDayPanel.tsx
index 6eb114e334..cd63b6c0d2 100644
--- a/meteor/client/ui/Shelf/TimeOfDayPanel.tsx
+++ b/meteor/client/ui/Shelf/TimeOfDayPanel.tsx
@@ -33,7 +33,7 @@ class TimeOfDayPanelInner extends MeteorReactComponent
Date: Tue, 6 Jul 2021 13:17:22 +0100
Subject: [PATCH 050/112] chore: Clean up inheritance structure
---
.../ui/Settings/RundownLayoutEditor.tsx | 27 ++++++++++---------
.../ui/Settings/components/FilterEditor.tsx | 6 ++---
meteor/lib/api/rundownLayouts.ts | 21 ++++++++-------
meteor/lib/collections/RundownLayouts.ts | 7 +++--
4 files changed, 33 insertions(+), 28 deletions(-)
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index b0e171f0de..6597cb668c 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -116,7 +116,7 @@ export default translateWithTracker((props: IProp
const layout = this.props.customRegion.layouts.find((l) => l.type === item.type)
const filtersTitle = layout?.filtersTitle ? t(layout.filtersTitle) : t('New Filter')
- if (!layout?.supportedElements.length) {
+ if (!layout?.supportedFilters?.length) {
return
}
@@ -336,7 +336,7 @@ export default translateWithTracker((props: IProp
{isShelfLayout && }
{isRundownHeaderLayout && }
{isRundownViewLayout && }
- {layout?.supportsFilters ? (
+ {layout?.supportedFilters?.length && RundownLayoutsAPI.isLayoutWithFilters(item) ? (
{layout?.filtersTitle ? t(`${layout?.filtersTitle}`) : t('Filters')}
{item.filters.length === 0 ? (
@@ -344,16 +344,17 @@ export default translateWithTracker((props: IProp
) : null}
) : null}
- {item.filters.map((tab, index) => (
-
- ))}
+ {RundownLayoutsAPI.isLayoutWithFilters(item) &&
+ item.filters.map((tab, index) => (
+
+ ))}
)
}
@@ -431,7 +432,7 @@ export default translateWithTracker((props: IProp
{this.renderElements(item, layout)}
- {layout?.supportedElements.length ? (
+ {layout?.supportedFilters?.length ? (
this.onAddElement(item)}>
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index 5b4268a13b..c935363513 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -28,7 +28,7 @@ interface IProps {
filter: RundownLayoutElementBase
index: number
showStyleBase: ShowStyleBase
- supportedElements: RundownLayoutElementType[]
+ supportedFilters: RundownLayoutElementType[]
}
interface ITrackedProps {}
@@ -1030,9 +1030,9 @@ export default translateWithTracker((_props: IPro
modifiedClassName="bghl"
attribute={`filters.${this.props.index}.type`}
obj={this.props.item}
- options={this.props.supportedElements}
+ options={this.props.supportedFilters}
type="dropdown"
- mutateDisplayValue={(v) => (v === undefined ? this.props.supportedElements[0] : v)}
+ mutateDisplayValue={(v) => (v === undefined ? this.props.supportedFilters[0] : v)}
collection={RundownLayouts}
className="input text-input input-l"
>
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 4e57a0b470..56888d0c83 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -15,6 +15,7 @@ import {
RundownLayoutRundownHeader,
RundownLayoutShelfBase,
CustomizableRegions,
+ RundownLayoutWithFilters,
} from '../collections/RundownLayouts'
import { ShowStyleBaseId } from '../collections/ShowStyleBases'
import * as _ from 'underscore'
@@ -37,9 +38,8 @@ export enum RundownLayoutsAPIMethods {
}
export interface LayoutDescriptor {
- supportedElements: RundownLayoutElementType[]
+ supportedFilters?: RundownLayoutElementType[]
filtersTitle?: string // e.g. tabs/panels
- supportsFilters?: boolean
}
export interface CustomizableRegionSettingsManifest {
@@ -52,8 +52,7 @@ export interface CustomizableRegionLayout {
_id: string
type: RundownLayoutType
filtersTitle?: string
- supportsFilters?: boolean
- supportedElements: RundownLayoutElementType[]
+ supportedFilters?: RundownLayoutElementType[]
}
class RundownLayoutsRegistry {
@@ -122,8 +121,7 @@ export namespace RundownLayoutsAPI {
const registry = new RundownLayoutsRegistry()
registry.registerShelfLayout(RundownLayoutType.RUNDOWN_LAYOUT, {
filtersTitle: 'Panels',
- supportsFilters: true,
- supportedElements: [
+ supportedFilters: [
RundownLayoutElementType.ADLIB_REGION,
RundownLayoutElementType.EXTERNAL_FRAME,
RundownLayoutElementType.FILTER,
@@ -132,8 +130,7 @@ export namespace RundownLayoutsAPI {
})
registry.registerShelfLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
filtersTitle: 'Tabs',
- supportsFilters: true,
- supportedElements: [
+ supportedFilters: [
RundownLayoutElementType.ADLIB_REGION,
RundownLayoutElementType.EXTERNAL_FRAME,
RundownLayoutElementType.FILTER,
@@ -141,16 +138,20 @@ export namespace RundownLayoutsAPI {
],
})
registry.registerRundownViewLayout(RundownLayoutType.RUNDOWN_VIEW_LAYOUT, {
- supportedElements: [],
+ supportedFilters: [],
})
registry.registerRundownHeaderLayouts(RundownLayoutType.RUNDOWN_HEADER_LAYOUT, {
- supportedElements: [],
+ supportedFilters: [],
})
export function getSettingsManifest(t: TFunction): CustomizableRegionSettingsManifest[] {
return registry.getSettingsManifest(t)
}
+ export function isLayoutWithFilters(layout: RundownLayoutBase): layout is RundownLayoutWithFilters {
+ return Object.keys(layout).includes('filters')
+ }
+
export function isLayoutForShelf(layout: RundownLayoutBase): layout is RundownLayoutShelfBase {
return registry.isShelfLayout(layout.regionId)
}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index e8eb751cb5..ef63361520 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -162,13 +162,16 @@ export interface RundownLayoutBase {
userId?: UserId
name: string
type: RundownLayoutType
- filters: RundownLayoutElementBase[]
icon: string
iconColor: string
/* Customizable region that the layout modifies. */
regionId: string
}
+export interface RundownLayoutWithFilters extends RundownLayoutBase {
+ filters: RundownLayoutElementBase[]
+}
+
export interface RundownViewLayout extends RundownLayoutBase {
type: RundownLayoutType.RUNDOWN_VIEW_LAYOUT
expectedEndText: string
@@ -178,7 +181,7 @@ export interface RundownViewLayout extends RundownLayoutBase {
rundownHeaderLayout: RundownLayoutId
}
-export interface RundownLayoutShelfBase extends RundownLayoutBase {
+export interface RundownLayoutShelfBase extends RundownLayoutWithFilters {
exposeAsStandalone: boolean
openByDefault: boolean
startingHeight?: number
From 07659bf5e093e89bc469ac854f6d442bbaf7cd31 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 6 Jul 2021 15:25:57 +0100
Subject: [PATCH 051/112] fix: Over/under timer with only expected end
---
meteor/client/ui/Prompter/OverUnderTimer.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/meteor/client/ui/Prompter/OverUnderTimer.tsx b/meteor/client/ui/Prompter/OverUnderTimer.tsx
index e38fbe67d1..2e1ed0b346 100644
--- a/meteor/client/ui/Prompter/OverUnderTimer.tsx
+++ b/meteor/client/ui/Prompter/OverUnderTimer.tsx
@@ -3,6 +3,7 @@ import { withTiming, WithTiming } from '../RundownView/RundownTiming/withTiming'
import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
import { RundownUtils } from '../../lib/rundown'
import ClassNames from 'classnames'
+import { getCurrentTime } from '../../../lib/lib'
interface IProps {
rundownPlaylist: RundownPlaylist
@@ -16,7 +17,10 @@ export const OverUnderTimer = withTiming()(
class OverUnderTimer extends React.Component> {
render() {
const target =
- this.props.rundownPlaylist.expectedDuration || this.props.timingDurations.totalPlaylistDuration || 0
+ this.props.rundownPlaylist.expectedDuration ||
+ (this.props.rundownPlaylist.expectedEnd ? this.props.rundownPlaylist.expectedEnd - getCurrentTime() : null) ||
+ this.props.timingDurations.totalPlaylistDuration ||
+ 0
return target ? (
Date: Tue, 6 Jul 2021 15:46:51 +0100
Subject: [PATCH 052/112] fix: Expected end where start is defined
---
meteor/client/ui/RundownView.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 97cbb9de1a..c6d473b611 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -407,7 +407,10 @@ const TimingDisplay = withTranslation()(
)
From 69effd6f973c84d288d0a02fa8e1ca0eb3d81f51 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 7 Jul 2021 15:38:09 +0100
Subject: [PATCH 053/112] fix: More explicit relationship between timing props
---
meteor/client/ui/RundownView.tsx | 99 +++++++++----------
meteor/lib/collections/RundownPlaylists.ts | 14 +--
meteor/lib/rundown/rundownTiming.ts | 48 +++++++++
.../blueprints-integration/src/rundown.ts | 55 +++++++++--
4 files changed, 146 insertions(+), 70 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index c6d473b611..32d04c9c80 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -78,7 +78,6 @@ import {
RundownLayoutBase,
RundownLayoutId,
RundownViewLayout,
- DashboardLayout,
RundownLayoutShelfBase,
RundownLayoutRundownHeader,
} from '../../lib/collections/RundownLayouts'
@@ -102,6 +101,7 @@ import RundownViewEventBus, { RundownViewEvents } from './RundownView/RundownVie
import { LoopingIcon } from '../lib/ui/icons/looping'
import StudioPackageContainersContext from './RundownView/StudioPackageContainersContext'
import { RundownLayoutsAPI } from '../../lib/api/rundownLayouts'
+import { PlaylistTiming } from '../../lib/rundown/rundownTiming'
export const MAGIC_TIME_SCALE_FACTOR = 0.03
@@ -140,15 +140,17 @@ const WarningDisplay = withTranslation()(
})
}
+ const expectedStart = PlaylistTiming.getExpectedStart(this.props.playlist.timing)
+ const expectedDuration = PlaylistTiming.getExpectedDuration(this.props.playlist.timing)
+
if (
this.props.playlist.activationId &&
this.props.playlist.rehearsal &&
- this.props.playlist.expectedStart &&
+ expectedStart &&
// the expectedStart is near
- getCurrentTime() + REHEARSAL_MARGIN > this.props.playlist.expectedStart &&
+ getCurrentTime() + REHEARSAL_MARGIN > expectedStart &&
// but it's not horribly in the past
- getCurrentTime() <
- this.props.playlist.expectedStart + (this.props.playlist.expectedDuration || 60 * 60 * 1000) &&
+ getCurrentTime() < expectedStart + (expectedDuration || 60 * 60 * 1000) &&
!this.props.inActiveRundownView &&
!this.state.plannedStartCloseShown
) {
@@ -275,10 +277,14 @@ const TimingDisplay = withTranslation()(
)
}
render() {
- const { t, rundownPlaylist, currentRundown } = this.props
+ const { t, rundownPlaylist } = this.props
if (!rundownPlaylist) return null
+ const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing)
+ const expectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing)
+ const expectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing)
+
return (
{rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
@@ -286,27 +292,28 @@ const TimingDisplay = withTranslation()(
{t('Started')}
- ) : rundownPlaylist.expectedStart ? (
+ ) : PlaylistTiming.isPlaylistTimingForwardTime(rundownPlaylist.timing) ? (
{t('Planned Start')}
-
+
- ) : rundownPlaylist.expectedEnd && rundownPlaylist.expectedDuration ? (
+ ) : PlaylistTiming.isPlaylistTimingBackTime(rundownPlaylist.timing) &&
+ rundownPlaylist.timing.expectedDuration ? (
{t('Expected Start')}
) : null}
{rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
- rundownPlaylist.expectedStart ? (
+ expectedStart ? (
{this.renderRundownName()}
{RundownUtils.formatDiffToTimecode(
- rundownPlaylist.startedPlayback - rundownPlaylist.expectedStart,
+ rundownPlaylist.startedPlayback - expectedStart!,
true,
false,
true,
@@ -318,21 +325,14 @@ const TimingDisplay = withTranslation()(
{this.renderRundownName()}
)
) : (
- (rundownPlaylist.expectedStart ? (
+ (expectedStart ? (
rundownPlaylist.expectedStart,
+ heavy: getCurrentTime() > expectedStart,
})}
>
{this.renderRundownName()}
- {RundownUtils.formatDiffToTimecode(
- getCurrentTime() - rundownPlaylist.expectedStart,
- true,
- false,
- true,
- true,
- true
- )}
+ {RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)}
) : (
{this.renderRundownName()}
@@ -353,7 +353,7 @@ const TimingDisplay = withTranslation()(
) : null}
)}
- {rundownPlaylist.expectedDuration ? (
+ {expectedEnd ? (
{!rundownPlaylist.startedPlayback ||
this.props.timingDurations.breakIsLastRundown ||
@@ -362,9 +362,8 @@ const TimingDisplay = withTranslation()(
) ? (
) : null}
@@ -408,7 +407,7 @@ const TimingDisplay = withTranslation()(
interval={0}
format="HH:mm:ss"
date={
- (rundownPlaylist.expectedStart || getCurrentTime()) +
+ (expectedStart || getCurrentTime()) +
(this.props.timingDurations.remainingPlaylistDuration || 0)
}
/>
@@ -451,9 +450,8 @@ const TimingDisplay = withTranslation()(
interface IEndTimingProps {
loop?: boolean
- expectedStart?: number
- expectedDuration: number
- expectedEnd?: number
+ expectedDuration?: number
+ expectedEnd: number
endLabel?: string
}
@@ -461,35 +459,21 @@ const PlaylistEndTiming = withTranslation()(
withTiming()(
class PlaylistEndTiming extends React.Component>> {
render() {
- let { t } = this.props
+ const { t } = this.props
return (
- {!this.props.loop && this.props.expectedStart ? (
-
- {t(this.props.endLabel || 'Planned End')}
-
-
- ) : !this.props.loop && this.props.expectedEnd ? (
+ {!this.props.loop && (
{t(this.props.endLabel || 'Planned End')}
- ) : null}
- {!this.props.loop && this.props.expectedStart && this.props.expectedDuration ? (
-
- {RundownUtils.formatDiffToTimecode(
- getCurrentTime() - (this.props.expectedStart + this.props.expectedDuration),
- true,
- true,
- true
- )}
-
- ) : !this.props.loop && this.props.expectedEnd ? (
+ )}
+ {!this.props.loop && (
{RundownUtils.formatDiffToTimecode(getCurrentTime() - this.props.expectedEnd, true, true, true)}
- ) : null}
+ )}
{this.props.expectedDuration ? (
()(
class PlaylistEndTiming extends React.Component>> {
render() {
- let { t, rundownsBeforeBreak } = this.props
- let breakRundown = rundownsBeforeBreak.length ? rundownsBeforeBreak[rundownsBeforeBreak.length - 1] : undefined
+ const { t, rundownsBeforeBreak } = this.props
+ const breakRundown = rundownsBeforeBreak.length
+ ? rundownsBeforeBreak[rundownsBeforeBreak.length - 1]
+ : undefined
const rundownAsPlayedDuration = this.props.timingDurations.rundownAsPlayedDurations
? rundownsBeforeBreak.reduce(
@@ -1010,15 +996,20 @@ const RundownHeader = withTranslation()(
}
rundownShouldHaveStarted() {
- return getCurrentTime() > (this.props.playlist.expectedStart || 0)
+ return getCurrentTime() > (PlaylistTiming.getExpectedStart(this.props.playlist.timing) || 0)
}
rundownWillShortlyStart() {
return (
- !this.rundownShouldHaveEnded() && getCurrentTime() > (this.props.playlist.expectedStart || 0) - REHEARSAL_MARGIN
+ !this.rundownShouldHaveEnded() &&
+ getCurrentTime() > (PlaylistTiming.getExpectedStart(this.props.playlist.timing) || 0) - REHEARSAL_MARGIN
)
}
rundownShouldHaveEnded() {
- return getCurrentTime() > (this.props.playlist.expectedStart || 0) + (this.props.playlist.expectedDuration || 0)
+ return (
+ getCurrentTime() >
+ (PlaylistTiming.getExpectedStart(this.props.playlist.timing) || 0) +
+ (PlaylistTiming.getExpectedDuration(this.props.playlist.timing) || 0)
+ )
}
handleAnotherPlaylistActive = (
diff --git a/meteor/lib/collections/RundownPlaylists.ts b/meteor/lib/collections/RundownPlaylists.ts
index 87c888f5ad..8542214cb2 100644
--- a/meteor/lib/collections/RundownPlaylists.ts
+++ b/meteor/lib/collections/RundownPlaylists.ts
@@ -15,7 +15,7 @@ import { RundownHoldState, Rundowns, Rundown, DBRundown, RundownId } from './Run
import { Studio, Studios, StudioId } from './Studios'
import { Segments, Segment, DBSegment, SegmentId } from './Segments'
import { Parts, Part, DBPart, PartId } from './Parts'
-import { TimelinePersistentState } from '@sofie-automation/blueprints-integration'
+import { RundownPlaylistTiming, TimelinePersistentState } from '@sofie-automation/blueprints-integration'
import { PartInstance, PartInstances, PartInstanceId } from './PartInstances'
import { createMongoCollection } from './lib'
import { OrganizationId } from './Organization'
@@ -61,12 +61,8 @@ export interface DBRundownPlaylist {
created: Time
/** Last modified timestamp */
modified: Time
- /** When the playlist is expected to start */
- expectedStart?: Time
- /** How long the playlist is expected to take ON AIR */
- expectedDuration?: number
- /** When the playlist is expected to end */
- expectedEnd?: Time
+ /** Rundown timing information */
+ timing: RundownPlaylistTiming
/** Is the playlist in rehearsal mode (can be used, when active: true) */
rehearsal?: boolean
/** Playout hold state */
@@ -122,9 +118,7 @@ export class RundownPlaylist implements DBRundownPlaylist {
public startedPlayback?: Time
public lastIncorrectPartPlaybackReported?: Time
public rundownsStartedPlayback?: Record
- public expectedStart?: Time
- public expectedDuration?: number
- public expectedEnd?: Time
+ public timing: RundownPlaylistTiming
public rehearsal?: boolean
public holdState?: RundownHoldState
public activationId?: RundownPlaylistActivationId
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index 4868a37786..4fa785b7eb 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -1,3 +1,10 @@
+import {
+ PlaylistTimingBackTime,
+ PlaylistTimingForwardTime,
+ PlaylistTimingNone,
+ PlaylistTimingType,
+ RundownPlaylistTiming,
+} from '@sofie-automation/blueprints-integration'
import { Tracker } from 'meteor/tracker'
import _ from 'underscore'
import { memoizedIsolatedAutorun } from '../../client/lib/reactiveData/reactiveDataHelper'
@@ -530,3 +537,44 @@ export function computeSegmentDuration(
return memo + partDuration
}, 0)
}
+
+export namespace PlaylistTiming {
+ export function isPlaylistTimingNone(timing: RundownPlaylistTiming): timing is PlaylistTimingNone {
+ return timing.type === PlaylistTimingType.None
+ }
+
+ export function isPlaylistTimingForwardTime(timing: RundownPlaylistTiming): timing is PlaylistTimingForwardTime {
+ return timing.type === PlaylistTimingType.ForwardTime
+ }
+
+ export function isPlaylistTimingBackTime(timing: RundownPlaylistTiming): timing is PlaylistTimingBackTime {
+ return timing.type === PlaylistTimingType.BackTime
+ }
+
+ export function getExpectedStart(timing: RundownPlaylistTiming): number | undefined {
+ return PlaylistTiming.isPlaylistTimingForwardTime(timing)
+ ? timing.expectedStart
+ : PlaylistTiming.isPlaylistTimingBackTime(timing)
+ ? // Use expectedStart if present, otherwise try to calculate from expectedEnd - expectedDuration
+ timing.expectedStart ||
+ (timing.expectedDuration ? timing.expectedEnd - timing.expectedDuration : undefined)
+ : undefined
+ }
+
+ export function getExpectedEnd(timing: RundownPlaylistTiming): number | undefined {
+ return PlaylistTiming.isPlaylistTimingBackTime(timing)
+ ? timing.expectedEnd
+ : PlaylistTiming.isPlaylistTimingForwardTime(timing)
+ ? timing.expectedEnd ||
+ (timing.expectedDuration ? timing.expectedStart + timing.expectedDuration : undefined)
+ : undefined
+ }
+
+ export function getExpectedDuration(timing: RundownPlaylistTiming): number | undefined {
+ return PlaylistTiming.isPlaylistTimingForwardTime(timing)
+ ? timing.expectedDuration
+ : PlaylistTiming.isPlaylistTimingBackTime(timing)
+ ? timing.expectedDuration
+ : undefined
+ }
+}
diff --git a/packages/blueprints-integration/src/rundown.ts b/packages/blueprints-integration/src/rundown.ts
index a2a6b3d6dc..fe334fe465 100644
--- a/packages/blueprints-integration/src/rundown.ts
+++ b/packages/blueprints-integration/src/rundown.ts
@@ -9,12 +9,8 @@ export interface IBlueprintRundownPlaylistInfo {
/** Rundown playlist slug - user-presentable name */
name: string
- /** Expected start should be set to the expected time this rundown playlist should run on air */
- expectedStart?: Time
- /** Expected duration of the rundown playlist */
- expectedDuration?: number
- /** Expected end time of the rundown playlist */
- expectedEnd?: Time
+ /** Playlist timing information */
+ timing: RundownPlaylistTiming
/** Should the rundown playlist use out-of-order timing mode (unplayed content will be played eventually) as opposed to normal timing mode (unplayed content behind the OnAir line has been skipped) */
outOfOrderTiming?: boolean
/** Should the rundown playlist loop at the end */
@@ -23,6 +19,53 @@ export interface IBlueprintRundownPlaylistInfo {
timeOfDayCountdowns?: boolean
}
+export enum PlaylistTimingType {
+ None = 'none',
+ ForwardTime = 'forward_time',
+ BackTime = 'back_time',
+}
+
+export interface PlaylistTimingBase {
+ type: PlaylistTimingType
+}
+
+export interface PlaylistTimingNone {
+ type: PlaylistTimingType.None
+}
+
+export interface PlaylistTimingForwardTime extends PlaylistTimingBase {
+ type: PlaylistTimingType.ForwardTime
+ /** Expected start should be set to the expected time this rundown playlist should run on air */
+ expectedStart: Time
+ /** Expected duration of the rundown playlist
+ * If set, the over/under diff will be calculated based on this value. Otherwise it will be planned content duration - played out duration.
+ */
+ expectedDuration?: number
+ /** Expected end time of the rundown playlist
+ * In this timing mode this is only for display before the show starts as an "expected" end time,
+ * during the show this display value will be calculated from expected start + remaining playlist duration.
+ * If this is not set, `expectedDuration` will be used (if set) in addition to expectedStart.
+ */
+ expectedEnd?: Time
+}
+
+export interface PlaylistTimingBackTime extends PlaylistTimingBase {
+ type: PlaylistTimingType.BackTime
+ /** Expected start should be set to the expected time this rundown playlist should run on air
+ * In this timing mode this is only for display before the show starts as an "expected" start time,
+ * during the show this display will be set to when the show actually started.
+ */
+ expectedStart?: Time
+ /** Expected duration of the rundown playlist
+ * If set, the over/under diff will be calculated based on this value. Otherwise it will be planned content duration - played out duration.
+ */
+ expectedDuration?: number
+ /** Expected end time of the rundown playlist */
+ expectedEnd: Time
+}
+
+export type RundownPlaylistTiming = PlaylistTimingNone | PlaylistTimingForwardTime | PlaylistTimingBackTime
+
/** The Rundown generated from Blueprint */
export interface IBlueprintRundown {
externalId: string
From c63a52d073ddc8e2b51dc1cc5c136538b50e3e64 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 7 Jul 2021 15:48:30 +0100
Subject: [PATCH 054/112] chore: Some lint fixes
---
.../ui/RundownView/RundownTiming/RundownTimingProvider.tsx | 1 -
.../components/rundownLayouts/RundownViewLayoutSettings.tsx | 2 +-
meteor/lib/api/rundownLayouts.ts | 2 +-
meteor/lib/rundown/rundownTiming.ts | 2 +-
4 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
index c3cd181778..961844265c 100644
--- a/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/RundownTimingProvider.tsx
@@ -1,7 +1,6 @@
import { Meteor } from 'meteor/meteor'
import * as React from 'react'
import * as PropTypes from 'prop-types'
-import * as _ from 'underscore'
import { withTracker } from '../../../lib/ReactMeteorData/react-meteor-data'
import { Part, PartId } from '../../../../lib/collections/Parts'
import { getCurrentTime } from '../../../../lib/lib'
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index 5daab3e325..2d3317d4a8 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -1,7 +1,7 @@
import React from 'react'
import { withTranslation } from 'react-i18next'
import { RundownLayoutsAPI } from '../../../../../lib/api/rundownLayouts'
-import { RundownLayoutBase, RundownLayoutId, RundownLayouts } from '../../../../../lib/collections/RundownLayouts'
+import { RundownLayoutBase, RundownLayouts } from '../../../../../lib/collections/RundownLayouts'
import { unprotectString } from '../../../../../lib/lib'
import { EditAttribute } from '../../../../lib/EditAttribute'
import { MeteorReactComponent } from '../../../../lib/MeteorReactComponent'
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 56888d0c83..9c5969288f 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -37,7 +37,7 @@ export enum RundownLayoutsAPIMethods {
'createRundownLayout' = 'rundownLayout.createRundownLayout',
}
-export interface LayoutDescriptor {
+export interface LayoutDescriptor<_T extends RundownLayoutBase> {
supportedFilters?: RundownLayoutElementType[]
filtersTitle?: string // e.g. tabs/panels
}
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index 4fa785b7eb..6d339c9e75 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -420,7 +420,7 @@ export class RundownTimingCalculator {
orderedRundowns: Rundown[],
currentRundown: Rundown | undefined
): BreakProps | undefined {
- let currentState = orderedRundowns.map((r) => r.endOfRundownIsShowBreak ?? '_').join('')
+ const currentState = orderedRundowns.map((r) => r.endOfRundownIsShowBreak ?? '_').join('')
if (this.breakProps.comp && this.breakProps.state !== currentState) {
this.breakProps.comp.invalidate()
}
From 9a11f18a2ccf5a27fad65da20dc0abbd3e492dc5 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 7 Jul 2021 16:37:15 +0100
Subject: [PATCH 055/112] fix: Rundown timing prop
---
meteor/client/ui/RundownView.tsx | 8 +++--
meteor/lib/collections/RundownPlaylists.ts | 4 +--
meteor/lib/collections/Rundowns.ts | 6 ++--
meteor/server/api/rundownPlaylist.ts | 29 ++++++++++++++-----
.../blueprints-integration/src/rundown.ts | 8 ++---
5 files changed, 31 insertions(+), 24 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 32d04c9c80..78f256fc64 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -536,15 +536,17 @@ const NextBreakTiming = withTranslation()(
return null
}
+ const expectedEnd = PlaylistTiming.getExpectedEnd(breakRundown.timing)
+
return (
{this.props.breakText ?? t('Next Break')}
-
+
- {!this.props.loop && breakRundown.expectedEnd ? (
+ {!this.props.loop && expectedEnd ? (
- {RundownUtils.formatDiffToTimecode(getCurrentTime() - breakRundown.expectedEnd, true, true, true)}
+ {RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedEnd, true, true, true)}
) : null}
{accumulatedExpectedDurations ? (
diff --git a/meteor/lib/collections/RundownPlaylists.ts b/meteor/lib/collections/RundownPlaylists.ts
index 8542214cb2..8a284c8fcd 100644
--- a/meteor/lib/collections/RundownPlaylists.ts
+++ b/meteor/lib/collections/RundownPlaylists.ts
@@ -208,9 +208,7 @@ export class RundownPlaylist implements DBRundownPlaylist {
name: 1,
_rank: 1,
playlistId: 1,
- expectedStart: 1,
- expectedDuration: 1,
- expectedEnd: 1,
+ timing: 1,
showStyleBaseId: 1,
},
})
diff --git a/meteor/lib/collections/Rundowns.ts b/meteor/lib/collections/Rundowns.ts
index 73b36879a0..8a3e66f0e9 100644
--- a/meteor/lib/collections/Rundowns.ts
+++ b/meteor/lib/collections/Rundowns.ts
@@ -5,7 +5,7 @@ import { Parts, Part, DBPart } from './Parts'
import { FindOptions, MongoQuery } from '../typings/meteor'
import { StudioId } from './Studios'
import { Meteor } from 'meteor/meteor'
-import { IBlueprintRundownDB } from '@sofie-automation/blueprints-integration'
+import { IBlueprintRundownDB, RundownPlaylistTiming } from '@sofie-automation/blueprints-integration'
import { ShowStyleVariantId, ShowStyleVariant, ShowStyleVariants } from './ShowStyleVariants'
import { ShowStyleBase, ShowStyleBases, ShowStyleBaseId } from './ShowStyleBases'
import { RundownNote } from '../api/notes'
@@ -84,9 +84,7 @@ export class Rundown implements DBRundown {
public organizationId: OrganizationId
public name: string
public description?: string
- public expectedStart?: Time
- public expectedDuration?: number
- public expectedEnd?: Time
+ public timing: RundownPlaylistTiming
public metaData?: unknown
// From IBlueprintRundownDB:
public _id: RundownId
diff --git a/meteor/server/api/rundownPlaylist.ts b/meteor/server/api/rundownPlaylist.ts
index ff88d46c31..989a40934c 100644
--- a/meteor/server/api/rundownPlaylist.ts
+++ b/meteor/server/api/rundownPlaylist.ts
@@ -54,6 +54,7 @@ import { DbCacheWriteCollection } from '../cache/CacheCollection'
import { Random } from 'meteor/random'
import { ExpectedPackages } from '../../lib/collections/ExpectedPackages'
import { checkAccessToPlaylist } from './lib'
+import { PlaylistTiming } from '../../lib/rundown/rundownTiming'
export function removeEmptyPlaylists(studioId: StudioId) {
runStudioOperationWithCache('removeEmptyPlaylists', studioId, StudioLockFunctionPriority.MISC, async (cache) => {
@@ -169,9 +170,7 @@ export function produceRundownPlaylistInfoFromRundown(
organizationId: studio.organizationId,
studioId: studio._id,
name: playlistInfo.playlist.name,
- expectedStart: playlistInfo.playlist.expectedStart,
- expectedDuration: playlistInfo.playlist.expectedDuration,
- expectedEnd: playlistInfo.playlist.expectedEnd,
+ timing: playlistInfo.playlist.timing,
loop: playlistInfo.playlist.loop,
@@ -212,9 +211,7 @@ function defaultPlaylistForRundown(
organizationId: studio.organizationId,
studioId: studio._id,
name: rundown.name,
- expectedStart: rundown.expectedStart,
- expectedDuration: rundown.expectedDuration,
- expectedEnd: rundown.expectedEnd,
+ timing: rundown.timing,
modified: getCurrentTime(),
}
@@ -488,10 +485,26 @@ export function restoreRundownsInPlaylistToDefaultOrder(context: MethodContext,
function sortDefaultRundownInPlaylistOrder(rundowns: ReadonlyDeep>): ReadonlyDeep> {
return mongoFindOptions, ReadonlyDeep>(rundowns, {
sort: {
- expectedStart: 1,
- expectedEnd: 1,
name: 1,
_id: 1,
},
+ }).sort((a, b) => {
+ // Compare start times, then allow rundowns with start time to be first
+ if (
+ PlaylistTiming.isPlaylistTimingForwardTime(a.timing) &&
+ PlaylistTiming.isPlaylistTimingForwardTime(b.timing)
+ )
+ return a.timing.expectedStart - b.timing.expectedStart
+ if (PlaylistTiming.isPlaylistTimingForwardTime(a.timing)) return -1
+ if (PlaylistTiming.isPlaylistTimingForwardTime(b.timing)) return 1
+
+ // Compare end times, then allow rundowns with end time to be first
+ if (PlaylistTiming.isPlaylistTimingBackTime(a.timing) && PlaylistTiming.isPlaylistTimingBackTime(b.timing))
+ return a.timing.expectedEnd - b.timing.expectedEnd
+ if (PlaylistTiming.isPlaylistTimingBackTime(a.timing)) return -1
+ if (PlaylistTiming.isPlaylistTimingBackTime(b.timing)) return 1
+
+ // No timing
+ return 0
})
}
diff --git a/packages/blueprints-integration/src/rundown.ts b/packages/blueprints-integration/src/rundown.ts
index fe334fe465..9aa1993ca8 100644
--- a/packages/blueprints-integration/src/rundown.ts
+++ b/packages/blueprints-integration/src/rundown.ts
@@ -75,12 +75,8 @@ export interface IBlueprintRundown {
/** Rundown description: Longer user-presentable description of the rundown */
description?: string
- /** Expected start should be set to the expected time this rundown should run on air */
- expectedStart?: Time
- /** Expected duration of the rundown */
- expectedDuration?: number
- /** Expected end time of the rundown */
- expectedEnd?: Time
+ /** Rundown timing information */
+ timing: RundownPlaylistTiming
/** Arbitrary data storage for plugins */
metaData?: TMetadata
From ead54b1b0608f35c298be1ca8317fd247283ddca Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Thu, 8 Jul 2021 12:57:27 +0100
Subject: [PATCH 056/112] fix: Rundown timing crash
---
meteor/lib/rundown/rundownTiming.ts | 70 +++++++++++++----------------
1 file changed, 30 insertions(+), 40 deletions(-)
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index 6d339c9e75..7c6c53dc02 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -5,9 +5,7 @@ import {
PlaylistTimingType,
RundownPlaylistTiming,
} from '@sofie-automation/blueprints-integration'
-import { Tracker } from 'meteor/tracker'
import _ from 'underscore'
-import { memoizedIsolatedAutorun } from '../../client/lib/reactiveData/reactiveDataHelper'
import {
findPartInstanceInMapOrWrapToTemporary,
PartInstance,
@@ -41,10 +39,9 @@ export class RundownTimingCalculator {
private partDisplayDurationsNoPlayback: Record = {}
private displayDurationGroups: Record = {}
private breakProps: {
- comp: Tracker.Computation | undefined
props: BreakProps | undefined
state: string | undefined
- } = { comp: undefined, props: undefined, state: undefined }
+ } = { props: undefined, state: undefined }
updateDurations(
now: number,
@@ -421,47 +418,40 @@ export class RundownTimingCalculator {
currentRundown: Rundown | undefined
): BreakProps | undefined {
const currentState = orderedRundowns.map((r) => r.endOfRundownIsShowBreak ?? '_').join('')
- if (this.breakProps.comp && this.breakProps.state !== currentState) {
- this.breakProps.comp.invalidate()
- }
-
- if (!this.breakProps.comp) {
- this.breakProps.comp = Tracker.autorun(() => {
- memoizedIsolatedAutorun(
- (orderedRundowns, currentRundown) => {
- if (!currentRundown) {
- this.breakProps.props = undefined
- }
-
- const currentRundownIndex = orderedRundowns.findIndex((r) => r._id === currentRundown._id)
-
- if (currentRundownIndex === -1) {
- this.breakProps.props = undefined
- }
-
- const nextBreakIndex = orderedRundowns.findIndex((rundown, index) => {
- if (index < currentRundownIndex) {
- return false
- }
-
- return rundown.endOfRundownIsShowBreak === true
- })
-
- this.breakProps.props = {
- rundownsBeforeNextBreak: orderedRundowns.slice(currentRundownIndex, nextBreakIndex + 1),
- breakIsLastRundown: nextBreakIndex === orderedRundowns.length,
- }
- },
- 'getRundownsBeforeNextBreak',
- orderedRundowns,
- currentRundown
- )
- })
+ if (this.breakProps.state !== currentState) {
+ this.recalculateBreaks(orderedRundowns, currentRundown)
}
this.breakProps.state = currentState
return this.breakProps.props
}
+
+ private recalculateBreaks(orderedRundowns: Rundown[], currentRundown: Rundown | undefined) {
+ if (!currentRundown) {
+ this.breakProps.props = undefined
+ return
+ }
+
+ const currentRundownIndex = orderedRundowns.findIndex((r) => r._id === currentRundown._id)
+
+ if (currentRundownIndex === -1) {
+ this.breakProps.props = undefined
+ return
+ }
+
+ const nextBreakIndex = orderedRundowns.findIndex((rundown, index) => {
+ if (index < currentRundownIndex) {
+ return false
+ }
+
+ return rundown.endOfRundownIsShowBreak === true
+ })
+
+ this.breakProps.props = {
+ rundownsBeforeNextBreak: orderedRundowns.slice(currentRundownIndex, nextBreakIndex + 1),
+ breakIsLastRundown: nextBreakIndex === orderedRundowns.length,
+ }
+ }
}
export interface RundownTimingContext {
From 305d0d223ef66b7ccb77d3775daea2d74fb471c0 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Thu, 8 Jul 2021 13:24:14 +0100
Subject: [PATCH 057/112] fix: Various lint errors
---
.../ui/Settings/RundownLayoutEditor.tsx | 17 ++++------
.../ui/Settings/components/FilterEditor.tsx | 32 +++++++++----------
2 files changed, 23 insertions(+), 26 deletions(-)
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index 6597cb668c..5c74cb20e1 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -1,4 +1,3 @@
-import * as _ from 'underscore'
import * as React from 'react'
import ClassNames from 'classnames'
import { EditAttribute } from '../../lib/EditAttribute'
@@ -363,7 +362,7 @@ export default translateWithTracker((props: IProp
const { t } = this.props
return (this.props.rundownLayouts || [])
.filter((l) => l.regionId === this.props.customRegion._id && this.props.layoutTypes.includes(l.type))
- .map((item, index) => {
+ .map((item) => {
const layout = this.props.customRegion.layouts.find((l) => l.type === item.type)
return (
@@ -388,10 +387,10 @@ export default translateWithTracker((props: IProp
))}
- this.downloadItem(item)}>
+ this.downloadItem(item)}>
- this.editItem(item)}>
+ this.editItem(item)}>
this.onDeleteLayout(e, item)}>
@@ -434,7 +433,7 @@ export default translateWithTracker((props: IProp
{this.renderElements(item, layout)}
{layout?.supportedFilters?.length ? (
-
this.onAddElement(item)}>
+ this.onAddElement(item)}>
{layout?.filtersTitle ?? t(`Add filter`)}
@@ -445,10 +444,10 @@ export default translateWithTracker((props: IProp
<>
{RundownLayoutsAPI.isDashboardLayout(item) ? this.renderActionButtons(item) : null}
-
this.finishEditItem(item)}>
+ this.finishEditItem(item)}>
- this.onAddButton(item)}>
+ this.onAddButton(item)}>
{t('Add button')}
@@ -458,7 +457,7 @@ export default translateWithTracker((props: IProp
) : (
<>
- this.finishEditItem(item)}>
+ this.finishEditItem(item)}>
@@ -546,8 +545,6 @@ export default translateWithTracker((props: IProp
}
render() {
- const { t } = this.props
-
return (
{this.props.customRegion.title}
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index c935363513..3f69fa66d4 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -60,7 +60,7 @@ export default translateWithTracker
((_props: IPro
item: RundownLayoutBase,
tab: RundownLayoutFilterBase,
index: number,
- isRundownLayout: boolean,
+ _isRundownLayout: boolean,
isDashboardLayout: boolean
) {
const { t } = this.props
@@ -276,7 +276,7 @@ export default translateWithTracker((_props: IPro
collection={RundownLayouts}
className="mod mas"
mutateDisplayValue={(v) => (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={(v) => undefined}
+ mutateUpdateValue={(_v) => undefined}
/>
((_props: IPro
collection={RundownLayouts}
className="mod mas"
mutateDisplayValue={(v) => (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={(v) => undefined}
+ mutateUpdateValue={(_v) => undefined}
/>
((_props: IPro
collection={RundownLayouts}
className="mod mas"
mutateDisplayValue={(v) => (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={(v) => undefined}
+ mutateUpdateValue={(_v) => undefined}
/>
((_props: IPro
collection={RundownLayouts}
className="mod mas"
mutateDisplayValue={(v) => (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={(v) => undefined}
+ mutateUpdateValue={(_v) => undefined}
/>
((_props: IPro
collection={RundownLayouts}
className="mod mas"
mutateDisplayValue={(v) => (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={(v) => undefined}
+ mutateUpdateValue={(_v) => undefined}
/>
((_props: IPro
renderFrame(
item: RundownLayoutBase,
- tab: RundownLayoutExternalFrame,
+ _tab: RundownLayoutExternalFrame,
index: number,
- isRundownLayout: boolean,
+ _isRundownLayout: boolean,
isDashboardLayout: boolean
) {
const { t } = this.props
@@ -717,9 +717,9 @@ export default translateWithTracker((_props: IPro
renderAdLibRegion(
item: RundownLayoutBase,
- tab: RundownLayoutAdLibRegion,
+ _tab: RundownLayoutAdLibRegion,
index: number,
- isRundownLayout: boolean,
+ _isRundownLayout: boolean,
isDashboardLayout: boolean
) {
const { t } = this.props
@@ -776,7 +776,7 @@ export default translateWithTracker((_props: IPro
collection={RundownLayouts}
className="mod mas"
mutateDisplayValue={(v) => (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={(v) => undefined}
+ mutateUpdateValue={(_v) => undefined}
/>
((_props: IPro
renderPieceCountdown(
item: RundownLayoutBase,
- tab: RundownLayoutPieceCountdown,
+ _tab: RundownLayoutPieceCountdown,
index: number,
- isRundownLayout: boolean,
+ _isRundownLayout: boolean,
isDashboardLayout: boolean
) {
const { t } = this.props
@@ -916,7 +916,7 @@ export default translateWithTracker((_props: IPro
collection={RundownLayouts}
className="mod mas"
mutateDisplayValue={(v) => (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={(v) => undefined}
+ mutateUpdateValue={(_v) => undefined}
/>
((_props: IPro
this.onRemoveElement(this.props.item, this.props.filter)}
+ onClick={(_e) => this.onRemoveElement(this.props.item, this.props.filter)}
>
@@ -1011,7 +1011,7 @@ export default translateWithTracker
((_props: IPro
className={ClassNames('action-btn right mod man pas', {
star: (this.props.filter as any).default,
})}
- onClick={(e) =>
+ onClick={(_e) =>
this.onToggleDefault(
this.props.item as RundownLayout,
this.props.index,
From 8ce6d928cc919a972e6d10b306358b59310757d5 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Thu, 8 Jul 2021 14:24:12 +0100
Subject: [PATCH 058/112] fix: Failing tests
---
meteor/__mocks__/defaultCollectionObjects.ts | 8 +-
meteor/__mocks__/helpers/database.ts | 7 +
.../lib/rundown/__tests__/infinites.test.ts | 5 +-
.../rundown/__tests__/rundownTiming.test.ts | 137 ++++++++++++++++--
meteor/server/__tests__/cronjobs.test.ts | 5 +-
.../__tests__/externalMessageQueue.test.ts | 13 ++
.../api/__tests__/peripheralDevice.test.ts | 8 +-
.../api/blueprints/__tests__/cache.test.ts | 4 +
.../blueprints/__tests__/postProcess.test.ts | 4 +
.../api/ingest/__tests__/updateNext.test.ts | 7 +
.../lookahead/__tests__/lookahead.test.ts | 5 +-
.../playout/lookahead/__tests__/util.test.ts | 5 +-
meteor/server/api/rundownLayouts.ts | 2 -
.../migration/__tests__/migrations.test.ts | 5 +
.../migration/deprecatedDataTypes/1_0_1.ts | 11 +-
15 files changed, 200 insertions(+), 26 deletions(-)
diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts
index c6777e04c7..91e76cfe82 100644
--- a/meteor/__mocks__/defaultCollectionObjects.ts
+++ b/meteor/__mocks__/defaultCollectionObjects.ts
@@ -8,7 +8,7 @@ import { DBRundown, RundownId } from '../lib/collections/Rundowns'
import { DBSegment, SegmentId } from '../lib/collections/Segments'
import { PartId, DBPart } from '../lib/collections/Parts'
import { RundownAPI } from '../lib/api/rundown'
-import { PieceLifespan } from '@sofie-automation/blueprints-integration'
+import { PieceLifespan, PlaylistTimingType } from '@sofie-automation/blueprints-integration'
import { PieceId, Piece } from '../lib/collections/Pieces'
import { AdLibPiece } from '../lib/collections/AdLibPieces'
import { getRundownId } from '../server/api/ingest/lib'
@@ -30,6 +30,9 @@ export function defaultRundownPlaylist(_id: RundownPlaylistId, studioId: StudioI
currentPartInstanceId: null,
nextPartInstanceId: null,
previousPartInstanceId: null,
+ timing: {
+ type: PlaylistTimingType.None,
+ },
}
}
export function defaultRundown(
@@ -66,6 +69,9 @@ export function defaultRundown(
},
externalNRCSName: 'mock',
+ timing: {
+ type: PlaylistTimingType.None,
+ },
}
}
diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts
index 892cfe9f20..11ce0d427e 100644
--- a/meteor/__mocks__/helpers/database.ts
+++ b/meteor/__mocks__/helpers/database.ts
@@ -22,6 +22,7 @@ import {
BlueprintResultPart,
IBlueprintPart,
IBlueprintPiece,
+ PlaylistTimingType,
} from '@sofie-automation/blueprints-integration'
import { ShowStyleBase, ShowStyleBases, DBShowStyleBase, ShowStyleBaseId } from '../../lib/collections/ShowStyleBases'
import {
@@ -311,6 +312,9 @@ export async function setupMockShowStyleBlueprint(
// expectedStart?:
// expectedDuration?: number;
metaData: ingestRundown.payload,
+ timing: {
+ type: PlaylistTimingType.None,
+ },
}
// Allow the rundown to specify a playlistExternalId that should be used
@@ -468,6 +472,9 @@ export function setupDefaultRundown(
studioId: env.studio._id,
showStyleBaseId: env.showStyleBase._id,
showStyleVariantId: env.showStyleVariant._id,
+ timing: {
+ type: PlaylistTimingType.None,
+ },
playlistId: playlistId,
_rank: 0,
diff --git a/meteor/lib/rundown/__tests__/infinites.test.ts b/meteor/lib/rundown/__tests__/infinites.test.ts
index f9b08cae38..b903beb2b1 100644
--- a/meteor/lib/rundown/__tests__/infinites.test.ts
+++ b/meteor/lib/rundown/__tests__/infinites.test.ts
@@ -3,7 +3,7 @@ import { testInFiber } from '../../../__mocks__/helpers/jest'
import { setupDefaultStudioEnvironment, DefaultEnvironment } from '../../../__mocks__/helpers/database'
import { PieceInstance, PieceInstancePiece } from '../../../lib/collections/PieceInstances'
import { literal, protectString, getCurrentTime } from '../../../lib/lib'
-import { PieceLifespan } from '@sofie-automation/blueprints-integration'
+import { PieceLifespan, PlaylistTimingType } from '@sofie-automation/blueprints-integration'
import { getPlayheadTrackingInfinitesForPart, processAndPrunePieceInstanceTimings } from '../infinites'
import { Piece } from '../../../lib/collections/Pieces'
import { PartInstance, PartInstanceId } from '../../collections/PartInstances'
@@ -491,6 +491,9 @@ describe('Infinites', () => {
},
externalNRCSName: 'test',
playlistId,
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
)
}
diff --git a/meteor/lib/rundown/__tests__/rundownTiming.test.ts b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
index 434b529382..a70400d5d9 100644
--- a/meteor/lib/rundown/__tests__/rundownTiming.test.ts
+++ b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
@@ -1,7 +1,9 @@
+import { PlaylistTimingType } from '@sofie-automation/blueprints-integration'
import { PartInstance } from '../../collections/PartInstances'
import { DBPart, Part, PartId } from '../../collections/Parts'
import { DBRundownPlaylist, RundownPlaylist } from '../../collections/RundownPlaylists'
-import { literal, protectString } from '../../lib'
+import { DBRundown, Rundown } from '../../collections/Rundowns'
+import { literal, protectString, unprotectString } from '../../lib'
import { RundownTimingCalculator, RundownTimingContext } from '../rundownTiming'
const DEFAULT_DURATION = 4000
@@ -19,6 +21,9 @@ function makeMockPlaylist(): RundownPlaylist {
currentPartInstanceId: null,
nextPartInstanceId: null,
previousPartInstanceId: null,
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
)
}
@@ -43,13 +48,46 @@ function makeMockPart(
)
}
+function makeMockRundown(id: string, playlistId: string, rank: number) {
+ return new Rundown(
+ literal({
+ _id: protectString(id),
+ externalId: id,
+ timing: {
+ type: PlaylistTimingType.None,
+ },
+ studioId: protectString('studio0'),
+ showStyleBaseId: protectString(''),
+ showStyleVariantId: protectString('variant0'),
+ peripheralDeviceId: protectString(''),
+ created: 0,
+ modified: 0,
+ importVersions: {} as any,
+ name: 'test',
+ externalNRCSName: 'mockNRCS',
+ organizationId: protectString(''),
+ playlistId: protectString(playlistId),
+ _rank: rank,
+ })
+ )
+}
+
describe('rundown Timing Calculator', () => {
it('Provides output for empty playlist', () => {
const timing = new RundownTimingCalculator()
const playlist: RundownPlaylist = makeMockPlaylist()
const parts: Part[] = []
const partInstancesMap: Map = new Map()
- const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ const result = timing.updateDurations(
+ 0,
+ false,
+ playlist,
+ [],
+ undefined,
+ parts,
+ partInstancesMap,
+ DEFAULT_DURATION
+ )
expect(result).toEqual(
literal({
isLowResolution: false,
@@ -58,6 +96,7 @@ describe('rundown Timing Calculator', () => {
currentPartWillAutoNext: false,
currentTime: 0,
rundownExpectedDurations: {},
+ rundownAsPlayedDurations: {},
partCountdown: {},
partDisplayDurations: {},
partDisplayStartsAt: {},
@@ -74,8 +113,11 @@ describe('rundown Timing Calculator', () => {
it('Calculates time for unplayed playlist with start time and duration', () => {
const timing = new RundownTimingCalculator()
const playlist: RundownPlaylist = makeMockPlaylist()
- playlist.expectedStart = 0
- playlist.expectedDuration = 4000
+ playlist.timing = {
+ type: PlaylistTimingType.ForwardTime,
+ expectedStart: 0,
+ expectedDuration: 40000,
+ }
const rundownId = 'rundown1'
const segmentId1 = 'segment1'
const segmentId2 = 'segment2'
@@ -85,7 +127,18 @@ describe('rundown Timing Calculator', () => {
parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
parts.push(makeMockPart('part4', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
const partInstancesMap: Map = new Map()
- const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ const rundown = makeMockRundown(rundownId, unprotectString(playlist._id), 0)
+ const rundowns = [rundown]
+ const result = timing.updateDurations(
+ 0,
+ false,
+ playlist,
+ rundowns,
+ undefined,
+ parts,
+ partInstancesMap,
+ DEFAULT_DURATION
+ )
expect(result).toEqual(
literal({
isLowResolution: false,
@@ -96,6 +149,9 @@ describe('rundown Timing Calculator', () => {
rundownExpectedDurations: {
[rundownId]: 4000,
},
+ rundownAsPlayedDurations: {
+ [rundownId]: 4000,
+ },
partCountdown: {
part1: 0,
part2: 1000,
@@ -147,8 +203,11 @@ describe('rundown Timing Calculator', () => {
it('Calculates time for unplayed playlist with end time and duration', () => {
const timing = new RundownTimingCalculator()
const playlist: RundownPlaylist = makeMockPlaylist()
- playlist.expectedDuration = 4000
- playlist.expectedEnd = 4000
+ playlist.timing = {
+ type: PlaylistTimingType.ForwardTime,
+ expectedStart: 0,
+ expectedDuration: 40000,
+ }
const rundownId = 'rundown1'
const segmentId1 = 'segment1'
const segmentId2 = 'segment2'
@@ -158,7 +217,18 @@ describe('rundown Timing Calculator', () => {
parts.push(makeMockPart('part3', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
parts.push(makeMockPart('part4', 0, rundownId, segmentId2, { expectedDuration: 1000 }))
const partInstancesMap: Map = new Map()
- const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ const rundown = makeMockRundown(rundownId, unprotectString(playlist._id), 0)
+ const rundowns = [rundown]
+ const result = timing.updateDurations(
+ 0,
+ false,
+ playlist,
+ rundowns,
+ undefined,
+ parts,
+ partInstancesMap,
+ DEFAULT_DURATION
+ )
expect(result).toEqual(
literal({
isLowResolution: false,
@@ -169,6 +239,9 @@ describe('rundown Timing Calculator', () => {
rundownExpectedDurations: {
[rundownId]: 4000,
},
+ rundownAsPlayedDurations: {
+ [rundownId]: 4000,
+ },
partCountdown: {
part1: 0,
part2: 1000,
@@ -220,8 +293,11 @@ describe('rundown Timing Calculator', () => {
it('Produces timing per rundown with start time and duration', () => {
const timing = new RundownTimingCalculator()
const playlist: RundownPlaylist = makeMockPlaylist()
- playlist.expectedStart = 0
- playlist.expectedDuration = 4000
+ playlist.timing = {
+ type: PlaylistTimingType.ForwardTime,
+ expectedStart: 0,
+ expectedDuration: 40000,
+ }
const rundownId1 = 'rundown1'
const rundownId2 = 'rundown2'
const segmentId1 = 'segment1'
@@ -232,7 +308,19 @@ describe('rundown Timing Calculator', () => {
parts.push(makeMockPart('part3', 0, rundownId2, segmentId2, { expectedDuration: 1000 }))
parts.push(makeMockPart('part4', 0, rundownId2, segmentId2, { expectedDuration: 1000 }))
const partInstancesMap: Map = new Map()
- const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ const rundown1 = makeMockRundown(rundownId1, unprotectString(playlist._id), 0)
+ const rundown2 = makeMockRundown(rundownId1, unprotectString(playlist._id), 0)
+ const rundowns = [rundown1, rundown2]
+ const result = timing.updateDurations(
+ 0,
+ false,
+ playlist,
+ rundowns,
+ undefined,
+ parts,
+ partInstancesMap,
+ DEFAULT_DURATION
+ )
expect(result).toEqual(
literal({
isLowResolution: false,
@@ -244,6 +332,10 @@ describe('rundown Timing Calculator', () => {
[rundownId1]: 2000,
[rundownId2]: 2000,
},
+ rundownAsPlayedDurations: {
+ [rundownId1]: 2000,
+ [rundownId2]: 2000,
+ },
partCountdown: {
part1: 0,
part2: 1000,
@@ -295,8 +387,11 @@ describe('rundown Timing Calculator', () => {
it('Handles display duration groups', () => {
const timing = new RundownTimingCalculator()
const playlist: RundownPlaylist = makeMockPlaylist()
- playlist.expectedStart = 0
- playlist.expectedDuration = 4000
+ playlist.timing = {
+ type: PlaylistTimingType.ForwardTime,
+ expectedStart: 0,
+ expectedDuration: 40000,
+ }
const rundownId1 = 'rundown1'
const segmentId1 = 'segment1'
const segmentId2 = 'segment2'
@@ -330,7 +425,18 @@ describe('rundown Timing Calculator', () => {
})
)
const partInstancesMap: Map = new Map()
- const result = timing.updateDurations(0, false, playlist, parts, partInstancesMap, DEFAULT_DURATION)
+ const rundown = makeMockRundown(rundownId1, unprotectString(playlist._id), 0)
+ const rundowns = [rundown]
+ const result = timing.updateDurations(
+ 0,
+ false,
+ playlist,
+ rundowns,
+ undefined,
+ parts,
+ partInstancesMap,
+ DEFAULT_DURATION
+ )
expect(result).toEqual(
literal({
isLowResolution: false,
@@ -341,6 +447,9 @@ describe('rundown Timing Calculator', () => {
rundownExpectedDurations: {
[rundownId1]: 4000,
},
+ rundownAsPlayedDurations: {
+ [rundownId1]: 4000,
+ },
partCountdown: {
part1: 0,
part2: 2000,
diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts
index 8aeb54a10e..933b1476bf 100644
--- a/meteor/server/__tests__/cronjobs.test.ts
+++ b/meteor/server/__tests__/cronjobs.test.ts
@@ -8,7 +8,7 @@ import { getRandomId, protectString } from '../../lib/lib'
import { Rundowns, RundownId } from '../../lib/collections/Rundowns'
import { UserActionsLog, UserActionsLogItemId } from '../../lib/collections/UserActionsLog'
import { Snapshots, SnapshotId, SnapshotType } from '../../lib/collections/Snapshots'
-import { TSR } from '@sofie-automation/blueprints-integration'
+import { PlaylistTimingType, TSR } from '@sofie-automation/blueprints-integration'
import { PeripheralDeviceCommands } from '../../lib/collections/PeripheralDeviceCommands'
import { PeripheralDevices, PeripheralDeviceId } from '../../lib/collections/PeripheralDevices'
import { PeripheralDeviceAPI } from '../../lib/api/peripheralDevice'
@@ -134,6 +134,9 @@ describe('cronjobs', () => {
showStyleVariantId: protectString(''),
studioId: protectString(''),
externalNRCSName: 'mock',
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
// Detached IngestDataCache object 0
const dataCache0Id = protectString(Random.id())
diff --git a/meteor/server/api/__tests__/externalMessageQueue.test.ts b/meteor/server/api/__tests__/externalMessageQueue.test.ts
index 7d966b13ff..a1361df375 100644
--- a/meteor/server/api/__tests__/externalMessageQueue.test.ts
+++ b/meteor/server/api/__tests__/externalMessageQueue.test.ts
@@ -7,6 +7,7 @@ import { Rundown, Rundowns } from '../../../lib/collections/Rundowns'
import {
IBlueprintExternalMessageQueueType,
ExternalMessageQueueObjSlack,
+ PlaylistTimingType,
} from '@sofie-automation/blueprints-integration'
import { testInFiber, runAllTimers, beforeAllInFiber } from '../../../__mocks__/helpers/jest'
import { DefaultEnvironment, setupDefaultStudioEnvironment } from '../../../__mocks__/helpers/database'
@@ -32,6 +33,9 @@ describe('Test external message queue static methods', () => {
nextPartInstanceId: protectString('partNext'),
previousPartInstanceId: null,
activationId: protectString('active'),
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
Rundowns.insert({
_id: protectString('rundown_1'),
@@ -55,6 +59,9 @@ describe('Test external message queue static methods', () => {
},
externalNRCSName: 'mockNRCS',
organizationId: protectString(''),
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
rundown = Rundowns.findOne() as Rundown
})
@@ -172,6 +179,9 @@ describe('Test sending messages to mocked endpoints', () => {
nextPartInstanceId: protectString('partNext'),
previousPartInstanceId: null,
activationId: protectString('active'),
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
Rundowns.insert({
_id: protectString('rundown_1'),
@@ -195,6 +205,9 @@ describe('Test sending messages to mocked endpoints', () => {
},
externalNRCSName: 'mockNRCS',
organizationId: protectString(''),
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
rundown = Rundowns.findOne() as Rundown
diff --git a/meteor/server/api/__tests__/peripheralDevice.test.ts b/meteor/server/api/__tests__/peripheralDevice.test.ts
index 8de1499bbe..2d5f4372bc 100644
--- a/meteor/server/api/__tests__/peripheralDevice.test.ts
+++ b/meteor/server/api/__tests__/peripheralDevice.test.ts
@@ -34,7 +34,7 @@ import { MediaWorkFlows } from '../../../lib/collections/MediaWorkFlows'
import { MediaWorkFlowSteps } from '../../../lib/collections/MediaWorkFlowSteps'
import { MediaManagerAPI } from '../../../lib/api/mediaManager'
import { MediaObjects } from '../../../lib/collections/MediaObjects'
-import { PieceLifespan } from '@sofie-automation/blueprints-integration'
+import { PieceLifespan, PlaylistTimingType } from '@sofie-automation/blueprints-integration'
import { VerifiedRundownPlaylistContentAccess } from '../lib'
import { PartInstance } from '../../../lib/collections/PartInstances'
@@ -70,6 +70,9 @@ describe('test peripheralDevice general API methods', () => {
nextPartInstanceId: null,
previousPartInstanceId: null,
activationId: protectString('active'),
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
Rundowns.insert({
_id: rundownID,
@@ -92,6 +95,9 @@ describe('test peripheralDevice general API methods', () => {
},
externalNRCSName: 'mockNRCS',
organizationId: protectString(''),
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
const segmentID: SegmentId = protectString('segment0')
const segmentExternalID = 'segment0'
diff --git a/meteor/server/api/blueprints/__tests__/cache.test.ts b/meteor/server/api/blueprints/__tests__/cache.test.ts
index 44f35529e6..93d32e9bac 100644
--- a/meteor/server/api/blueprints/__tests__/cache.test.ts
+++ b/meteor/server/api/blueprints/__tests__/cache.test.ts
@@ -15,6 +15,7 @@ import {
BlueprintManifestType,
BlueprintResultRundown,
BlueprintResultSegment,
+ PlaylistTimingType,
} from '@sofie-automation/blueprints-integration'
import { Studios, Studio } from '../../../../lib/collections/Studios'
import { ShowStyleBase, ShowStyleBases } from '../../../../lib/collections/ShowStyleBases'
@@ -362,6 +363,9 @@ describe('Test blueprint cache', () => {
name: 'test',
externalNRCSName: 'mockNRCS',
organizationId: protectString(''),
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
)
}
diff --git a/meteor/server/api/blueprints/__tests__/postProcess.test.ts b/meteor/server/api/blueprints/__tests__/postProcess.test.ts
index 6f42e27c31..540e7b0812 100644
--- a/meteor/server/api/blueprints/__tests__/postProcess.test.ts
+++ b/meteor/server/api/blueprints/__tests__/postProcess.test.ts
@@ -18,6 +18,7 @@ import {
TSR,
PieceLifespan,
IUserNotesContext,
+ PlaylistTimingType,
} from '@sofie-automation/blueprints-integration'
import { Piece } from '../../../../lib/collections/Pieces'
import { TimelineObjGeneric, TimelineObjType } from '../../../../lib/collections/Timeline'
@@ -57,6 +58,9 @@ describe('Test blueprint post-process', () => {
externalNRCSName: 'mockNRCS',
playlistId: protectString(''),
_rank: 0,
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
// const playlist = new RundownPlaylist({
// _id: protectString(''),
diff --git a/meteor/server/api/ingest/__tests__/updateNext.test.ts b/meteor/server/api/ingest/__tests__/updateNext.test.ts
index a6b83a3900..c533ef34fa 100644
--- a/meteor/server/api/ingest/__tests__/updateNext.test.ts
+++ b/meteor/server/api/ingest/__tests__/updateNext.test.ts
@@ -12,6 +12,7 @@ import { defaultStudio } from '../../../../__mocks__/defaultCollectionObjects'
import { removeRundownsFromDb } from '../../rundownPlaylist'
import { PlayoutLockFunctionPriority, runPlayoutOperationWithCache } from '../../playout/lockFunction'
import { saveIntoDb } from '../../../lib/database'
+import { PlaylistTimingType } from '@sofie-automation/blueprints-integration'
jest.mock('../../playout/playout')
require('../../peripheralDevice.ts') // include in order to create the Meteor methods needed
@@ -38,6 +39,9 @@ async function createMockRO(): Promise {
nextPartInstanceId: null,
previousPartInstanceId: null,
activationId: protectString('active'),
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
Rundowns.insert({
@@ -55,6 +59,9 @@ async function createMockRO(): Promise {
_rank: 0,
externalNRCSName: 'mockNRCS',
organizationId: protectString(''),
+ timing: {
+ type: PlaylistTimingType.None,
+ },
})
await saveIntoDb(
diff --git a/meteor/server/api/playout/lookahead/__tests__/lookahead.test.ts b/meteor/server/api/playout/lookahead/__tests__/lookahead.test.ts
index 992a9c9f78..d9d569180f 100644
--- a/meteor/server/api/playout/lookahead/__tests__/lookahead.test.ts
+++ b/meteor/server/api/playout/lookahead/__tests__/lookahead.test.ts
@@ -9,7 +9,7 @@ import { RundownPlaylist, RundownPlaylistId, RundownPlaylists } from '../../../.
import { getCurrentTime, getRandomId, protectString } from '../../../../../lib/lib'
import { SegmentId } from '../../../../../lib/collections/Segments'
import { DBPart, Part, PartId, Parts } from '../../../../../lib/collections/Parts'
-import { LookaheadMode, TSR } from '@sofie-automation/blueprints-integration'
+import { LookaheadMode, PlaylistTimingType, TSR } from '@sofie-automation/blueprints-integration'
import { MappingsExt, Studios } from '../../../../../lib/collections/Studios'
import { OnGenerateTimelineObjExt, TimelineObjRundown } from '../../../../../lib/collections/Timeline'
import { PartAndPieces, PartInstanceAndPieceInstances } from '../util'
@@ -80,6 +80,9 @@ describe('Lookahead', () => {
core: '',
},
externalNRCSName: 'mock',
+ timing: {
+ type: PlaylistTimingType.None,
+ },
}
Rundowns.insert(rundown)
diff --git a/meteor/server/api/playout/lookahead/__tests__/util.test.ts b/meteor/server/api/playout/lookahead/__tests__/util.test.ts
index a09dde2ece..cf28166886 100644
--- a/meteor/server/api/playout/lookahead/__tests__/util.test.ts
+++ b/meteor/server/api/playout/lookahead/__tests__/util.test.ts
@@ -9,7 +9,7 @@ import { RundownPlaylist, RundownPlaylistId, RundownPlaylists } from '../../../.
import { getCurrentTime, protectString } from '../../../../../lib/lib'
import { SegmentId, Segments } from '../../../../../lib/collections/Segments'
import { DBPart, Part, PartId, Parts } from '../../../../../lib/collections/Parts'
-import { LookaheadMode, TSR } from '@sofie-automation/blueprints-integration'
+import { LookaheadMode, PlaylistTimingType, TSR } from '@sofie-automation/blueprints-integration'
import { MappingsExt, Studios } from '../../../../../lib/collections/Studios'
import { PartInstances, wrapPartToTemporaryInstance } from '../../../../../lib/collections/PartInstances'
import _ from 'underscore'
@@ -69,6 +69,9 @@ describe('getOrderedPartsAfterPlayhead', () => {
},
externalNRCSName: 'mock',
+ timing: {
+ type: PlaylistTimingType.None,
+ },
}
Rundowns.insert(rundown)
RundownPlaylists.update(playlistId, { $set: { activationId: protectString('active') } })
diff --git a/meteor/server/api/rundownLayouts.ts b/meteor/server/api/rundownLayouts.ts
index 75d166d143..25c6ba767a 100644
--- a/meteor/server/api/rundownLayouts.ts
+++ b/meteor/server/api/rundownLayouts.ts
@@ -33,7 +33,6 @@ export function createRundownLayout(
name,
showStyleBaseId,
blueprintId,
- filters: [],
type,
userId,
icon: '',
@@ -70,7 +69,6 @@ PickerPOST.route('/shelfLayouts/upload/:showStyleBaseId', (params, req: Incoming
const layout = JSON.parse(body) as RundownLayoutBase
check(layout._id, Match.Optional(String))
check(layout.name, String)
- check(layout.filters, Array)
check(layout.type, String)
layout.showStyleBaseId = showStyleBase._id
diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts
index ed8aab18fc..8d2246a3f4 100644
--- a/meteor/server/migration/__tests__/migrations.test.ts
+++ b/meteor/server/migration/__tests__/migrations.test.ts
@@ -12,6 +12,8 @@ import {
MigrationStep,
MigrationContextStudio,
MigrationContextShowStyle,
+ PlaylistTimingType,
+ PlaylistTimingNone,
} from '@sofie-automation/blueprints-integration'
import { PeripheralDeviceAPI } from '../../../lib/api/peripheralDevice'
import { Studios, Studio } from '../../../lib/collections/Studios'
@@ -355,6 +357,9 @@ describe('Migrations', () => {
rundown: {
externalId: '',
name: '',
+ timing: literal({
+ type: PlaylistTimingType.None,
+ }),
},
globalAdLibPieces: [],
baseline: { timelineObjects: [] },
diff --git a/meteor/server/migration/deprecatedDataTypes/1_0_1.ts b/meteor/server/migration/deprecatedDataTypes/1_0_1.ts
index 8c386680ad..46451d7754 100644
--- a/meteor/server/migration/deprecatedDataTypes/1_0_1.ts
+++ b/meteor/server/migration/deprecatedDataTypes/1_0_1.ts
@@ -1,7 +1,7 @@
import { Time, literal, protectString, getRandomId } from '../../../lib/lib'
import { RundownImportVersions, RundownHoldState, DBRundown } from '../../../lib/collections/Rundowns'
import { RundownNote } from '../../../lib/api/notes'
-import { TimelinePersistentState } from '@sofie-automation/blueprints-integration'
+import { PlaylistTimingType, TimelinePersistentState } from '@sofie-automation/blueprints-integration'
import { DBRundownPlaylist, RundownPlaylistId } from '../../../lib/collections/RundownPlaylists'
import { ShowStyleVariantId } from '../../../lib/collections/ShowStyleVariants'
import { StudioId } from '../../../lib/collections/Studios'
@@ -60,9 +60,12 @@ export function makePlaylistFromRundown_1_0_0(
created: rundown.created,
currentPartInstanceId: null,
nextPartInstanceId: null,
- expectedDuration: rundown.expectedDuration,
- expectedStart: rundown.expectedStart,
- expectedEnd: rundown.expectedEnd,
+ timing: {
+ type: PlaylistTimingType.ForwardTime,
+ expectedDuration: rundown.expectedDuration,
+ expectedStart: rundown.expectedStart || 0,
+ expectedEnd: rundown.expectedEnd,
+ },
holdState: rundown.holdState,
name: rundown.name,
nextPartManual: rundown.nextPartManual,
From 1d558465eb6114517952ac80290705fd79104a58 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Thu, 8 Jul 2021 14:33:29 +0100
Subject: [PATCH 059/112] fix: Playout test snapshots
---
.../playout/__tests__/__snapshots__/playout.test.ts.snap | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/meteor/server/api/playout/__tests__/__snapshots__/playout.test.ts.snap b/meteor/server/api/playout/__tests__/__snapshots__/playout.test.ts.snap
index 9332f94dc4..84a426a2e7 100644
--- a/meteor/server/api/playout/__tests__/__snapshots__/playout.test.ts.snap
+++ b/meteor/server/api/playout/__tests__/__snapshots__/playout.test.ts.snap
@@ -130,6 +130,9 @@ Object {
"showStyleBaseId": "randomId9000",
"showStyleVariantId": "randomId9001",
"studioId": "mockStudio4",
+ "timing": {
+ "type": "none"
+ }
}
`;
@@ -180,6 +183,9 @@ Object {
"previousPartInstanceId": null,
"rehearsal": false,
"studioId": "mockStudio4",
+ "timing": {
+ "type": "none"
+ }
}
`;
@@ -205,5 +211,8 @@ Object {
"showStyleBaseId": "randomId9000",
"showStyleVariantId": "randomId9001",
"studioId": "mockStudio4",
+ "timing": {
+ "type": "none"
+ }
}
`;
From 7a35261a7aee162abd1b8ac1a5e2ceade0f44b00 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Thu, 8 Jul 2021 14:43:48 +0100
Subject: [PATCH 060/112] fix: Circular import
---
meteor/client/lib/rundownLayouts.ts | 132 ++++++++++++++++++
meteor/client/ui/Shelf/DashboardPanel.tsx | 129 +----------------
meteor/client/ui/Shelf/EndWordsPanel.tsx | 5 +-
meteor/client/ui/Shelf/PartTimingPanel.tsx | 18 +--
meteor/client/ui/Shelf/SegmentTimingPanel.tsx | 3 +-
meteor/lib/collections/RundownLayouts.ts | 8 +-
6 files changed, 147 insertions(+), 148 deletions(-)
create mode 100644 meteor/client/lib/rundownLayouts.ts
diff --git a/meteor/client/lib/rundownLayouts.ts b/meteor/client/lib/rundownLayouts.ts
new file mode 100644
index 0000000000..6318177622
--- /dev/null
+++ b/meteor/client/lib/rundownLayouts.ts
@@ -0,0 +1,132 @@
+import _ from 'underscore'
+import { PartInstanceId } from '../../lib/collections/PartInstances'
+import { PieceInstance, PieceInstances } from '../../lib/collections/PieceInstances'
+import { RequiresActiveLayers } from '../../lib/collections/RundownLayouts'
+import { RundownPlaylist } from '../../lib/collections/RundownPlaylists'
+import { getCurrentTime } from '../../lib/lib'
+import { invalidateAt } from './invalidatingTime'
+
+/**
+ * If the conditions of the filter are met, activePieceInstance will include the first piece instance found that matches the filter, otherwise it will be undefined.
+ */
+export function getIsFilterActive(
+ playlist: RundownPlaylist,
+ panel: RequiresActiveLayers
+): { active: boolean; activePieceInstance: PieceInstance | undefined } {
+ const unfinishedPieces = getUnfinishedPieceInstancesReactive(playlist.currentPartInstanceId, true)
+ let activePieceInstance: PieceInstance | undefined
+ let activeLayers = unfinishedPieces.map((p) => p.piece.sourceLayerId)
+ let containsEveryRequiredLayer = panel.requireAllSourcelayers
+ ? panel.requiredLayers?.length && panel.requiredLayers.every((s) => activeLayers.includes(s))
+ : false
+ let containsRequiredLayer = containsEveryRequiredLayer
+ ? true
+ : panel.requiredLayers && panel.requiredLayers.length
+ ? panel.requiredLayers.some((s) => activeLayers.includes(s))
+ : false
+
+ if (
+ (!panel.requireAllSourcelayers || containsEveryRequiredLayer) &&
+ (!panel.requiredLayers?.length || containsRequiredLayer)
+ ) {
+ activePieceInstance =
+ panel.activeLayerIds && panel.activeLayerIds.length
+ ? _.flatten(Object.values(unfinishedPieces)).find((piece: PieceInstance) => {
+ return (
+ (panel.activeLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 &&
+ piece.partInstanceId === playlist.currentPartInstanceId
+ )
+ })
+ : undefined
+ }
+ return {
+ active: activePieceInstance !== undefined || (!panel.activeLayerIds?.length && !panel.requiredLayers?.length),
+ activePieceInstance,
+ }
+}
+
+export function getUnfinishedPieceInstancesReactive(
+ currentPartInstanceId: PartInstanceId | null,
+ includeNonAdLibPieces?: boolean
+) {
+ let prospectivePieces: PieceInstance[] = []
+ const now = getCurrentTime()
+ if (currentPartInstanceId) {
+ prospectivePieces = PieceInstances.find({
+ startedPlayback: {
+ $exists: true,
+ },
+ $and: [
+ {
+ $or: [
+ {
+ stoppedPlayback: {
+ $eq: 0,
+ },
+ },
+ {
+ stoppedPlayback: {
+ $exists: false,
+ },
+ },
+ ],
+ },
+ !includeNonAdLibPieces
+ ? {
+ $or: [
+ {
+ adLibSourceId: {
+ $exists: true,
+ },
+ },
+ {
+ 'piece.tags': {
+ $exists: true,
+ },
+ },
+ ],
+ }
+ : {},
+ {
+ $or: [
+ {
+ userDuration: {
+ $exists: false,
+ },
+ },
+ {
+ 'userDuration.end': {
+ $exists: false,
+ },
+ },
+ ],
+ },
+ ],
+ }).fetch()
+
+ let nearestEnd = Number.POSITIVE_INFINITY
+ prospectivePieces = prospectivePieces.filter((pieceInstance) => {
+ const piece = pieceInstance.piece
+ const end: number | undefined =
+ pieceInstance.userDuration && typeof pieceInstance.userDuration.end === 'number'
+ ? pieceInstance.userDuration.end
+ : typeof piece.enable.duration === 'number'
+ ? piece.enable.duration + pieceInstance.startedPlayback!
+ : undefined
+
+ if (end !== undefined) {
+ if (end > now) {
+ nearestEnd = nearestEnd > end ? end : nearestEnd
+ return true
+ } else {
+ return false
+ }
+ }
+ return true
+ })
+
+ if (Number.isFinite(nearestEnd)) invalidateAt(nearestEnd)
+ }
+
+ return prospectivePieces
+}
diff --git a/meteor/client/ui/Shelf/DashboardPanel.tsx b/meteor/client/ui/Shelf/DashboardPanel.tsx
index 3bb909283c..2fb027709f 100644
--- a/meteor/client/ui/Shelf/DashboardPanel.tsx
+++ b/meteor/client/ui/Shelf/DashboardPanel.tsx
@@ -12,7 +12,7 @@ import { IOutputLayer, ISourceLayer, IBlueprintActionTriggerMode } from '@sofie-
import { PubSub } from '../../../lib/api/pubsub'
import { doUserAction, UserAction } from '../../lib/userAction'
import { NotificationCenter, Notification, NoticeLevel } from '../../lib/notifications/notifications'
-import { DashboardLayoutFilter, FilterRequiresActiveLayers } from '../../../lib/collections/RundownLayouts'
+import { DashboardLayoutFilter } from '../../../lib/collections/RundownLayouts'
import { unprotectString, getCurrentTime } from '../../../lib/lib'
import {
IAdLibPanelProps,
@@ -38,7 +38,7 @@ import { PartInstanceId } from '../../../lib/collections/PartInstances'
import { ContextMenuTrigger } from '@jstarpl/react-contextmenu'
import { setShelfContextMenuContext, ContextType } from './ShelfContextMenu'
import { RundownUtils } from '../../lib/rundown'
-import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { getUnfinishedPieceInstancesReactive } from '../../lib/rundownLayouts'
interface IState {
outputLayers: {
@@ -677,92 +677,6 @@ export class DashboardPanelInner extends MeteorReactComponent<
}
}
-export function getUnfinishedPieceInstancesReactive(
- currentPartInstanceId: PartInstanceId | null,
- includeNonAdLibPieces?: boolean
-) {
- let prospectivePieces: PieceInstance[] = []
- const now = getCurrentTime()
- if (currentPartInstanceId) {
- prospectivePieces = PieceInstances.find({
- startedPlayback: {
- $exists: true,
- },
- $and: [
- {
- $or: [
- {
- stoppedPlayback: {
- $eq: 0,
- },
- },
- {
- stoppedPlayback: {
- $exists: false,
- },
- },
- ],
- },
- !includeNonAdLibPieces
- ? {
- $or: [
- {
- adLibSourceId: {
- $exists: true,
- },
- },
- {
- 'piece.tags': {
- $exists: true,
- },
- },
- ],
- }
- : {},
- {
- $or: [
- {
- userDuration: {
- $exists: false,
- },
- },
- {
- 'userDuration.end': {
- $exists: false,
- },
- },
- ],
- },
- ],
- }).fetch()
-
- let nearestEnd = Number.POSITIVE_INFINITY
- prospectivePieces = prospectivePieces.filter((pieceInstance) => {
- const piece = pieceInstance.piece
- const end: number | undefined =
- pieceInstance.userDuration && typeof pieceInstance.userDuration.end === 'number'
- ? pieceInstance.userDuration.end
- : typeof piece.enable.duration === 'number'
- ? piece.enable.duration + pieceInstance.startedPlayback!
- : undefined
-
- if (end !== undefined) {
- if (end > now) {
- nearestEnd = nearestEnd > end ? end : nearestEnd
- return true
- } else {
- return false
- }
- }
- return true
- })
-
- if (Number.isFinite(nearestEnd)) invalidateAt(nearestEnd)
- }
-
- return prospectivePieces
-}
-
export function getNextPiecesReactive(nextPartInstanceId: PartInstanceId | null): PieceInstance[] {
let prospectivePieceInstances: PieceInstance[] = []
if (nextPartInstanceId) {
@@ -921,42 +835,3 @@ export const DashboardPanel = translateWithTracker<
return !_.isEqual(props, nextProps)
}
)(DashboardPanelInner)
-
-/**
- * If the conditions of the filter are met, activePieceInstance will include the first piece instance found that matches the filter, otherwise it will be undefined.
- */
-export function getIsFilterActive(
- playlist: RundownPlaylist,
- panel: FilterRequiresActiveLayers
-): { active: boolean; activePieceInstance: PieceInstance | undefined } {
- const unfinishedPieces = getUnfinishedPieceInstancesReactive(playlist.currentPartInstanceId, true)
- let activePieceInstance: PieceInstance | undefined
- let activeLayers = unfinishedPieces.map((p) => p.piece.sourceLayerId)
- let containsEveryRequiredLayer = panel.requireAllSourcelayers
- ? panel.requiredLayers?.length && panel.requiredLayers.every((s) => activeLayers.includes(s))
- : false
- let containsRequiredLayer = containsEveryRequiredLayer
- ? true
- : panel.requiredLayers && panel.requiredLayers.length
- ? panel.requiredLayers.some((s) => activeLayers.includes(s))
- : false
-
- if (
- (!panel.requireAllSourcelayers || containsEveryRequiredLayer) &&
- (!panel.requiredLayers?.length || containsRequiredLayer)
- ) {
- activePieceInstance =
- panel.activeLayerIds && panel.activeLayerIds.length
- ? _.flatten(Object.values(unfinishedPieces)).find((piece: PieceInstance) => {
- return (
- (panel.activeLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 &&
- piece.partInstanceId === playlist.currentPartInstanceId
- )
- })
- : undefined
- }
- return {
- active: activePieceInstance !== undefined || (!panel.activeLayerIds?.length && !panel.requiredLayers?.length),
- activePieceInstance,
- }
-}
diff --git a/meteor/client/ui/Shelf/EndWordsPanel.tsx b/meteor/client/ui/Shelf/EndWordsPanel.tsx
index 225737d36f..abaa00035f 100644
--- a/meteor/client/ui/Shelf/EndWordsPanel.tsx
+++ b/meteor/client/ui/Shelf/EndWordsPanel.tsx
@@ -6,13 +6,14 @@ import {
RundownLayoutEndWords,
} from '../../../lib/collections/RundownLayouts'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
-import { dashboardElementPosition, getIsFilterActive, getUnfinishedPieceInstancesReactive } from './DashboardPanel'
-import { Translated, translateWithTracker, withTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { dashboardElementPosition } from './DashboardPanel'
+import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
import { PieceInstance } from '../../../lib/collections/PieceInstances'
import { ScriptContent } from '@sofie-automation/blueprints-integration'
import { GetScriptPreview } from '../scriptPreview'
+import { getIsFilterActive } from '../../lib/rundownLayouts'
interface IEndsWordsPanelProps {
visible?: boolean
diff --git a/meteor/client/ui/Shelf/PartTimingPanel.tsx b/meteor/client/ui/Shelf/PartTimingPanel.tsx
index 1c728d0ed0..765bbc193f 100644
--- a/meteor/client/ui/Shelf/PartTimingPanel.tsx
+++ b/meteor/client/ui/Shelf/PartTimingPanel.tsx
@@ -1,30 +1,20 @@
import * as React from 'react'
-import ClassNames from 'classnames'
import * as _ from 'underscore'
import {
DashboardLayoutPartCountDown,
- DashboardLayoutSegmentCountDown,
RundownLayoutBase,
RundownLayoutPartTiming,
- RundownLayoutSegmentTiming,
} from '../../../lib/collections/RundownLayouts'
-import { Translated, translateWithTracker, withTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
-import { RundownUtils } from '../../lib/rundown'
-import { RundownPlaylist, RundownPlaylistId } from '../../../lib/collections/RundownPlaylists'
-import { Segment } from '../../../lib/collections/Segments'
-import { withTiming, WithTiming } from '../RundownView/RundownTiming/withTiming'
-import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration'
-import { PartExtended } from '../../../lib/Rundown'
-import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper'
-import { Part, PartId } from '../../../lib/collections/Parts'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
import { PartInstance } from '../../../lib/collections/PartInstances'
-import { ShowStyleBase } from '../../../lib/collections/ShowStyleBases'
-import { dashboardElementPosition, getIsFilterActive } from './DashboardPanel'
+import { dashboardElementPosition } from './DashboardPanel'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
import { getAllowSpeaking } from '../../lib/localStorage'
import { CurrentPartRemaining } from '../RundownView/RundownTiming/CurrentPartRemaining'
import { CurrentPartElapsed } from '../RundownView/RundownTiming/CurrentPartElapsed'
+import { getIsFilterActive } from '../../lib/rundownLayouts'
interface IPartTimingPanelProps {
visible?: boolean
diff --git a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
index 24512758f7..e5a4f7da79 100644
--- a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
+++ b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
@@ -16,8 +16,9 @@ import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveD
import { Part, PartId } from '../../../lib/collections/Parts'
import { PartInstance } from '../../../lib/collections/PartInstances'
import { ShowStyleBase } from '../../../lib/collections/ShowStyleBases'
-import { dashboardElementPosition, getIsFilterActive } from './DashboardPanel'
+import { dashboardElementPosition } from './DashboardPanel'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { getIsFilterActive } from '../../lib/rundownLayouts'
interface ISegmentTimingPanelProps {
visible?: boolean
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 57bdcc4175..f246199f4b 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -71,7 +71,7 @@ export interface RundownLayoutElementBase {
* @param requiredLayers Layers that must be active in addition to the active layers, i.e. "any of `activeLayerIds`, with at least one of `requiredLayers`".
* @param requireAllSourcelayers Require all layers in `requiredLayers` to contain an active piece.
*/
-export interface FilterRequiresActiveLayers {
+export interface RequiresActiveLayers {
activeLayerIds?: string[]
requiredLayers?: string[]
/**
@@ -120,16 +120,16 @@ export interface RundownLayoutPlaylistEndTimer extends RundownLayoutElementBase
hidePlannedEnd: boolean
}
-export interface RundownLayoutEndWords extends RundownLayoutElementBase, FilterRequiresActiveLayers {
+export interface RundownLayoutEndWords extends RundownLayoutElementBase, RequiresActiveLayers {
type: RundownLayoutElementType.PLAYLIST_END_TIMER
}
-export interface RundownLayoutSegmentTiming extends RundownLayoutElementBase, FilterRequiresActiveLayers {
+export interface RundownLayoutSegmentTiming extends RundownLayoutElementBase, RequiresActiveLayers {
type: RundownLayoutElementType.SEGMENT_TIMING
timingType: 'count_down' | 'count_up'
}
-export interface RundownLayoutPartTiming extends RundownLayoutElementBase, FilterRequiresActiveLayers {
+export interface RundownLayoutPartTiming extends RundownLayoutElementBase, RequiresActiveLayers {
type: RundownLayoutElementType.PART_TIMING
timingType: 'count_down' | 'count_up'
speakCountDown: boolean
From bd762b50e549af766fe50722f94d159973f2d168 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Fri, 9 Jul 2021 14:16:35 +0100
Subject: [PATCH 061/112] fix: Import
---
meteor/client/ui/Shelf/PieceCountdownPanel.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/meteor/client/ui/Shelf/PieceCountdownPanel.tsx b/meteor/client/ui/Shelf/PieceCountdownPanel.tsx
index adf816745b..4eda2147d9 100644
--- a/meteor/client/ui/Shelf/PieceCountdownPanel.tsx
+++ b/meteor/client/ui/Shelf/PieceCountdownPanel.tsx
@@ -7,7 +7,7 @@ import {
DashboardLayoutPieceCountdown,
} from '../../../lib/collections/RundownLayouts'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
-import { dashboardElementPosition, getUnfinishedPieceInstancesReactive } from './DashboardPanel'
+import { dashboardElementPosition } from './DashboardPanel'
import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import { RundownUtils } from '../../lib/rundown'
@@ -15,6 +15,7 @@ import { RundownTiming, TimingEvent } from '../RundownView/RundownTiming/Rundown
import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
import { PieceInstance } from '../../../lib/collections/PieceInstances'
import { VTContent } from '@sofie-automation/blueprints-integration'
+import { getUnfinishedPieceInstancesReactive } from '../../lib/rundownLayouts'
interface IPieceCountdownPanelProps {
visible?: boolean
layout: RundownLayoutBase
From 2656bf69e2c4136a4751a36d585b298c0f9f1696 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 26 Jul 2021 16:55:56 +0100
Subject: [PATCH 062/112] fix: Lint and failing tests
---
.../client/ui/ClockView/PresenterScreen.tsx | 15 +++----
meteor/client/ui/Prompter/OverUnderTimer.tsx | 7 +++-
meteor/client/ui/RundownList.tsx | 5 ++-
.../ui/RundownList/ActiveProgressBar.tsx | 4 +-
.../ui/RundownList/RundownListItemView.tsx | 29 +++++++------
.../ui/RundownList/RundownPlaylistUi.tsx | 42 ++++++++++---------
.../ui/RundownView/RundownDividerHeader.tsx | 20 +++++----
.../client/ui/RundownView/RundownOverview.tsx | 3 +-
.../RundownTiming/PartCountdown.tsx | 7 +++-
.../ui/SegmentTimeline/SegmentTimeline.tsx | 2 -
.../SegmentTimelineContainer.tsx | 5 ++-
.../StudioScreenSaver/StudioScreenSaver.tsx | 31 +++++++-------
meteor/lib/rundown/rundownTiming.ts | 20 +++++++++
.../__snapshots__/playout.test.ts.snap | 18 ++++----
meteor/server/api/rundownPlaylist.ts | 20 +--------
15 files changed, 125 insertions(+), 103 deletions(-)
diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx
index 2a70fcc35d..3623de608e 100644
--- a/meteor/client/ui/ClockView/PresenterScreen.tsx
+++ b/meteor/client/ui/ClockView/PresenterScreen.tsx
@@ -20,6 +20,7 @@ import { PieceInstances } from '../../../lib/collections/PieceInstances'
import { PieceLifespan } from '@sofie-automation/blueprints-integration'
import { Part } from '../../../lib/collections/Parts'
import { PieceCountdownContainer } from '../PieceIcons/PieceCountdown'
+import { PlaylistTiming } from '../../../lib/rundown/rundownTiming'
interface SegmentUi extends DBSegment {
items: Array
@@ -72,9 +73,7 @@ function getShowStyleBaseIdSegmentPartUi(
_rank: 1,
showStyleBaseId: 1,
name: 1,
- expectedStart: 1,
- expectedDuration: 1,
- expectedEnd: 1,
+ timing: 1,
},
})
showStyleBaseId = currentRundown?.showStyleBaseId
@@ -317,8 +316,10 @@ export class PresenterScreenBase extends MeteorReactComponent<
const nextPart = this.props.nextPartInstance
const nextSegment = this.props.nextSegment
- const overUnderClock = playlist.expectedDuration
- ? (this.props.timingDurations.asDisplayedPlaylistDuration || 0) - playlist.expectedDuration
+ const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing)
+ const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing)
+ const overUnderClock = expectedDuration
+ ? (this.props.timingDurations.asDisplayedPlaylistDuration || 0) - expectedDuration
: (this.props.timingDurations.asDisplayedPlaylistDuration || 0) -
(this.props.timingDurations.totalPlaylistDuration || 0)
@@ -363,9 +364,9 @@ export class PresenterScreenBase extends MeteorReactComponent<
>
- ) : playlist.expectedStart ? (
+ ) : expectedStart ? (
-
+
) : null}
diff --git a/meteor/client/ui/Prompter/OverUnderTimer.tsx b/meteor/client/ui/Prompter/OverUnderTimer.tsx
index 2e1ed0b346..9dc86bc312 100644
--- a/meteor/client/ui/Prompter/OverUnderTimer.tsx
+++ b/meteor/client/ui/Prompter/OverUnderTimer.tsx
@@ -4,6 +4,7 @@ import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
import { RundownUtils } from '../../lib/rundown'
import ClassNames from 'classnames'
import { getCurrentTime } from '../../../lib/lib'
+import { PlaylistTiming } from '../../../lib/rundown/rundownTiming'
interface IProps {
rundownPlaylist: RundownPlaylist
@@ -16,9 +17,11 @@ interface IProps {
export const OverUnderTimer = withTiming()(
class OverUnderTimer extends React.Component> {
render() {
+ const expectedDuration = PlaylistTiming.getExpectedDuration(this.props.rundownPlaylist.timing)
+ const expectedEnd = PlaylistTiming.getExpectedEnd(this.props.rundownPlaylist.timing)
const target =
- this.props.rundownPlaylist.expectedDuration ||
- (this.props.rundownPlaylist.expectedEnd ? this.props.rundownPlaylist.expectedEnd - getCurrentTime() : null) ||
+ expectedDuration ||
+ (expectedEnd ? expectedEnd - getCurrentTime() : null) ||
this.props.timingDurations.totalPlaylistDuration ||
0
return target ? (
diff --git a/meteor/client/ui/RundownList.tsx b/meteor/client/ui/RundownList.tsx
index 7909d96cb0..b8c757ffcc 100644
--- a/meteor/client/ui/RundownList.tsx
+++ b/meteor/client/ui/RundownList.tsx
@@ -33,6 +33,7 @@ import RundownPlaylistDragLayer from './RundownList/RundownPlaylistDragLayer'
import { RundownPlaylistUi } from './RundownList/RundownPlaylistUi'
import { doUserAction, UserAction } from '../lib/userAction'
import { RundownLayoutsAPI } from '../../lib/api/rundownLayouts'
+import { PlaylistTiming } from '../../lib/rundown/rundownTiming'
export enum ToolTipStep {
TOOLTIP_START_HERE = 'TOOLTIP_START_HERE',
@@ -305,7 +306,9 @@ export const RundownList = translateWithTracker((): IRundownsListProps => {
{t('On Air Start Time')}
{t('Duration')}
{this.props.rundownPlaylists.some(
- (p) => !!p.expectedEnd || p.rundowns.some((r) => r.expectedEnd)
+ (p) =>
+ !!PlaylistTiming.getExpectedEnd(p.timing) ||
+ p.rundowns.some((r) => PlaylistTiming.getExpectedEnd(r.timing))
) && {t('Expected End Time')} }
{t('Last updated')}
{this.props.rundownLayouts.some(
diff --git a/meteor/client/ui/RundownList/ActiveProgressBar.tsx b/meteor/client/ui/RundownList/ActiveProgressBar.tsx
index e54f412f67..15ead8dc1f 100644
--- a/meteor/client/ui/RundownList/ActiveProgressBar.tsx
+++ b/meteor/client/ui/RundownList/ActiveProgressBar.tsx
@@ -2,6 +2,7 @@ import React from 'react'
import timer from 'react-timer-hoc'
import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
import { getCurrentTime } from '../../../lib/lib'
+import { PlaylistTiming } from '../../../lib/rundown/rundownTiming'
export interface IActiveProgressBarProps {
rundownPlaylist: RundownPlaylist
@@ -10,7 +11,8 @@ export interface IActiveProgressBarProps {
export const ActiveProgressBar = timer(1000)(
class ActiveProgressBar extends React.Component {
render() {
- const { startedPlayback, expectedDuration } = this.props.rundownPlaylist
+ const { startedPlayback, timing } = this.props.rundownPlaylist
+ const expectedDuration = PlaylistTiming.getExpectedDuration(timing)
if (startedPlayback && expectedDuration) {
const progress = Math.min(((getCurrentTime() - startedPlayback) / expectedDuration) * 100, 100)
diff --git a/meteor/client/ui/RundownList/RundownListItemView.tsx b/meteor/client/ui/RundownList/RundownListItemView.tsx
index 5bd22e9230..348422f59c 100644
--- a/meteor/client/ui/RundownList/RundownListItemView.tsx
+++ b/meteor/client/ui/RundownList/RundownListItemView.tsx
@@ -13,6 +13,7 @@ import { LoopingIcon } from '../../lib/ui/icons/looping'
import { RundownViewLayoutSelection } from './RundownViewLayoutSelection'
import { RundownLayoutBase } from '../../../lib/collections/RundownLayouts'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { PlaylistTiming } from '../../../lib/rundown/rundownTiming'
interface IRundownListItemViewProps {
isActive: boolean
@@ -66,6 +67,10 @@ export default withTranslation()(function RundownListItemView(props: Translated<
props.rundown.name
)
+ const expectedStart = PlaylistTiming.getExpectedStart(rundown.timing)
+ const expectedDuration = PlaylistTiming.getExpectedDuration(rundown.timing)
+ const expectedEnd = PlaylistTiming.getExpectedEnd(rundown.timing)
+
return connectDropTarget(
@@ -107,28 +112,28 @@ export default withTranslation()(function RundownListItemView(props: Translated<
{showStyleBaseURL ? {showStyleName} : showStyleName || ''}
- {rundown.expectedStart ? (
-
- ) : rundown.expectedEnd && rundown.expectedDuration ? (
-
+ {expectedStart ? (
+
+ ) : expectedEnd && expectedDuration ? (
+
) : (
{t('Not set')}
)}
- {rundown.expectedDuration ? (
+ {expectedDuration ? (
isOnlyRundownInPlaylist && playlist.loop ? (
{t('({{timecode}})', {
- timecode: RundownUtils.formatDiffToTimecode(rundown.expectedDuration, false, true, true, false, true),
+ timecode: RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, false, true),
})}
) : (
- RundownUtils.formatDiffToTimecode(rundown.expectedDuration, false, true, true, false, true)
+ RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, false, true)
)
) : isOnlyRundownInPlaylist && playlist.loop ? (
@@ -141,16 +146,16 @@ export default withTranslation()(function RundownListItemView(props: Translated<
)}
- {rundown.expectedEnd ? (
-
- ) : rundown.expectedStart && rundown.expectedDuration ? (
-
+ {expectedEnd ? (
+
+ ) : expectedStart && expectedDuration ? (
+
) : (
{t('Not set')}
)}
-
+
{rundownLayouts.some(
(l) =>
diff --git a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
index f8e58b86e2..e250e9b57a 100644
--- a/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
+++ b/meteor/client/ui/RundownList/RundownPlaylistUi.tsx
@@ -40,6 +40,7 @@ import { getAllowStudio } from '../../lib/localStorage'
import { doUserAction, UserAction } from '../../lib/userAction'
import { RundownViewLayoutSelection } from './RundownViewLayoutSelection'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { PlaylistTiming } from '../../../lib/rundown/rundownTiming'
export interface RundownPlaylistUi extends RundownPlaylist {
rundowns: Rundown[]
@@ -292,27 +293,24 @@ export const RundownPlaylistUi = DropTarget(
) : null
})
+ const playlistExpectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing)
+ const playlistExpectedStart = PlaylistTiming.getExpectedStart(playlist.timing)
+ const playlistExpectedEnd = PlaylistTiming.getExpectedEnd(playlist.timing)
+
const expectedDuration =
- playlist.expectedDuration !== undefined &&
+ playlistExpectedDuration !== undefined &&
(playlist.loop ? (
{t('({{timecode}})', {
- timecode: RundownUtils.formatDiffToTimecode(
- playlist.expectedDuration,
- false,
- true,
- true,
- false,
- true
- ),
+ timecode: RundownUtils.formatDiffToTimecode(playlistExpectedDuration, false, true, true, false, true),
})}
) : (
- RundownUtils.formatDiffToTimecode(playlist.expectedDuration, false, true, true, false, true)
+ RundownUtils.formatDiffToTimecode(playlistExpectedDuration, false, true, true, false, true)
))
const classNames = ClassNames(['rundown-playlist', { droptarget: isActiveDropZone }])
@@ -340,10 +338,10 @@ export const RundownPlaylistUi = DropTarget(
) : null}
- {playlist.expectedStart ? (
-
- ) : playlist.expectedEnd && playlist.expectedDuration ? (
-
+ {playlistExpectedStart ? (
+
+ ) : playlistExpectedEnd && playlistExpectedDuration ? (
+
) : (
{t('Not set')}
)}
@@ -360,16 +358,16 @@ export const RundownPlaylistUi = DropTarget(
)}
- {playlist.expectedEnd ? (
-
- ) : playlist.expectedStart && playlist.expectedDuration ? (
-
+ {playlistExpectedEnd ? (
+
+ ) : playlistExpectedStart && playlistExpectedDuration ? (
+
) : (
{t('Not set')}
)}
-
+
{rundownLayouts.some(
(l) =>
@@ -396,7 +394,11 @@ export const RundownPlaylistUi = DropTarget(
)
function createProgressBarRow(playlist: RundownPlaylistUi): React.ReactElement | null {
- if (playlist.activationId && playlist.expectedDuration !== undefined && playlist.startedPlayback) {
+ if (
+ playlist.activationId &&
+ PlaylistTiming.getExpectedDuration(playlist.timing) !== undefined &&
+ playlist.startedPlayback
+ ) {
return
}
diff --git a/meteor/client/ui/RundownView/RundownDividerHeader.tsx b/meteor/client/ui/RundownView/RundownDividerHeader.tsx
index 41e0a5527f..46db34c6ad 100644
--- a/meteor/client/ui/RundownView/RundownDividerHeader.tsx
+++ b/meteor/client/ui/RundownView/RundownDividerHeader.tsx
@@ -6,6 +6,7 @@ import { withTiming, WithTiming } from './RundownTiming/withTiming'
import { RundownUtils } from '../../lib/rundown'
import { withTranslation } from 'react-i18next'
import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { PlaylistTiming } from '../../../lib/rundown/rundownTiming'
interface IProps {
rundown: Rundown
@@ -67,11 +68,14 @@ const MarkerCountdownText = withTranslation()(
*/
export const RundownDividerHeader = withTranslation()(function RundownDividerHeader(props: Translated) {
const { t, rundown, playlist } = props
+ const expectedStart = PlaylistTiming.getExpectedStart(rundown.timing)
+ const expectedDuration = PlaylistTiming.getExpectedDuration(rundown.timing)
+ const expectedEnd = PlaylistTiming.getExpectedEnd(rundown.timing)
return (
{rundown.name}
{rundown.name !== playlist.name &&
{playlist.name} }
- {rundown.expectedStart ? (
+ {expectedStart ? (
{t('Planned Start')}
- {rundown.expectedStart}
+ {expectedStart}
) : null}
- {rundown.expectedDuration ? (
+ {expectedDuration ? (
{t('Planned Duration')}
- {RundownUtils.formatDiffToTimecode(rundown.expectedDuration, false, true, true, false, true)}
+ {RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, false, true)}
) : null}
- {rundown.expectedEnd ? (
+ {expectedEnd ? (
{t('Planned End')}
- {rundown.expectedEnd}
+ {expectedEnd}
) : null}
diff --git a/meteor/client/ui/RundownView/RundownOverview.tsx b/meteor/client/ui/RundownView/RundownOverview.tsx
index ee560cd3a7..231be524c0 100644
--- a/meteor/client/ui/RundownView/RundownOverview.tsx
+++ b/meteor/client/ui/RundownView/RundownOverview.tsx
@@ -12,6 +12,7 @@ import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import { RundownUtils } from '../../lib/rundown'
import { RundownPlaylists, RundownPlaylist, RundownPlaylistId } from '../../../lib/collections/RundownPlaylists'
import { findPartInstanceOrWrapToTemporary } from '../../../lib/collections/PartInstances'
+import { PlaylistTiming } from '../../../lib/rundown/rundownTiming'
interface SegmentUi extends DBSegment {
items: Array
@@ -222,7 +223,7 @@ export const RundownOverview = withTracker()(function Part
? // if show is activated, use currentTime as base
props.timingDurations.currentTime ?? 0
: // if show is not activated, use expectedStart or currentTime, whichever is later
- Math.max(props.playlist.expectedStart ?? 0, props.timingDurations.currentTime ?? 0)) +
- (thisPartCountdown || 0)
+ Math.max(
+ PlaylistTiming.getExpectedStart(props.playlist.timing) ?? 0,
+ props.timingDurations.currentTime ?? 0
+ )) + (thisPartCountdown || 0)
}
/>
) : (
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
index 0f45f6cba2..b471ac6033 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
@@ -39,8 +39,6 @@ import { PartInstanceId } from '../../../lib/collections/PartInstances'
import { SegmentTimelineSmallPartFlag } from './SmallParts/SegmentTimelineSmallPartFlag'
import { UIStateStorage } from '../../lib/UIStateStorage'
import { RundownTimingContext } from '../../../lib/rundown/rundownTiming'
-import { PartInstanceId } from '../../../lib/collections/PartInstances'
-import { SegmentTimelineSmallPartFlag } from './SmallParts/SegmentTimelineSmallPartFlag'
interface IProps {
id: string
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
index 6216b9b274..0d9e61c436 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
@@ -43,7 +43,7 @@ import RundownViewEventBus, {
import { memoizedIsolatedAutorun, slowDownReactivity } from '../../lib/reactiveData/reactiveDataHelper'
import { checkPieceContentStatus, getNoteTypeForPieceStatus, ScanInfoForPackages } from '../../../lib/mediaObjects'
import { getBasicNotesForSegment } from '../../../lib/rundownNotifications'
-import { computeSegmentDuration, RundownTimingContext } from '../../../lib/rundown/rundownTiming'
+import { computeSegmentDuration, PlaylistTiming, RundownTimingContext } from '../../../lib/rundown/rundownTiming'
import { SegmentTimelinePartClass } from './SegmentTimelinePart'
import { Piece, Pieces } from '../../../lib/collections/Pieces'
import { RundownAPI } from '../../../lib/api/rundown'
@@ -302,7 +302,8 @@ export const SegmentTimelineContainer = translateWithTracker {
studioId: props.studioId,
},
{
- sort: {
- expectedStart: 1,
- },
fields: {
name: 1,
- expectedStart: 1,
- expectedDuration: 1,
- expectedEnd: 1,
+ timing: 1,
studioId: 1,
},
}
)
.fetch()
+ .sort(PlaylistTiming.sortTiminings)
.find((rundownPlaylist) => {
- if (rundownPlaylist.expectedStart && rundownPlaylist.expectedStart > now) {
+ const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing)
+ const expectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing)
+ if (expectedStart && expectedStart > now) {
// is expected to start next
return true
} else if (
- rundownPlaylist.expectedStart &&
- rundownPlaylist.expectedDuration &&
- rundownPlaylist.expectedStart <= now &&
- rundownPlaylist.expectedStart + rundownPlaylist.expectedDuration > now
+ expectedStart &&
+ expectedDuration &&
+ expectedStart <= now &&
+ expectedStart + expectedDuration > now
) {
// should be live right now
return true
@@ -321,7 +320,8 @@ export const StudioScreenSaver = translateWithTracker(findNextPlaylist)(
render() {
const { t, rundownPlaylist } = this.props
- const hasRundown = rundownPlaylist && rundownPlaylist.expectedStart
+ const expectedStart = rundownPlaylist && PlaylistTiming.getExpectedStart(rundownPlaylist.timing)
+ const hasRundown = !!expectedStart
return (
- {hasRundown && rundownPlaylist && rundownPlaylist.expectedStart ? (
+ {hasRundown && rundownPlaylist && expectedStart ? (
<>
{t('Next scheduled show')}
{rundownPlaylist.name}
-
+
>
) : (
this.props.studio?.name && (
diff --git a/meteor/lib/rundown/rundownTiming.ts b/meteor/lib/rundown/rundownTiming.ts
index 2b9ae4d842..256329e9a3 100644
--- a/meteor/lib/rundown/rundownTiming.ts
+++ b/meteor/lib/rundown/rundownTiming.ts
@@ -588,4 +588,24 @@ export namespace PlaylistTiming {
? timing.expectedDuration
: undefined
}
+
+ export function sortTiminings(a, b): number {
+ // Compare start times, then allow rundowns with start time to be first
+ if (
+ PlaylistTiming.isPlaylistTimingForwardTime(a.timing) &&
+ PlaylistTiming.isPlaylistTimingForwardTime(b.timing)
+ )
+ return a.timing.expectedStart - b.timing.expectedStart
+ if (PlaylistTiming.isPlaylistTimingForwardTime(a.timing)) return -1
+ if (PlaylistTiming.isPlaylistTimingForwardTime(b.timing)) return 1
+
+ // Compare end times, then allow rundowns with end time to be first
+ if (PlaylistTiming.isPlaylistTimingBackTime(a.timing) && PlaylistTiming.isPlaylistTimingBackTime(b.timing))
+ return a.timing.expectedEnd - b.timing.expectedEnd
+ if (PlaylistTiming.isPlaylistTimingBackTime(a.timing)) return -1
+ if (PlaylistTiming.isPlaylistTimingBackTime(b.timing)) return 1
+
+ // No timing
+ return 0
+ }
}
diff --git a/meteor/server/api/playout/__tests__/__snapshots__/playout.test.ts.snap b/meteor/server/api/playout/__tests__/__snapshots__/playout.test.ts.snap
index 84a426a2e7..8a09978efd 100644
--- a/meteor/server/api/playout/__tests__/__snapshots__/playout.test.ts.snap
+++ b/meteor/server/api/playout/__tests__/__snapshots__/playout.test.ts.snap
@@ -130,9 +130,9 @@ Object {
"showStyleBaseId": "randomId9000",
"showStyleVariantId": "randomId9001",
"studioId": "mockStudio4",
- "timing": {
- "type": "none"
- }
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -183,9 +183,9 @@ Object {
"previousPartInstanceId": null,
"rehearsal": false,
"studioId": "mockStudio4",
- "timing": {
- "type": "none"
- }
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -211,8 +211,8 @@ Object {
"showStyleBaseId": "randomId9000",
"showStyleVariantId": "randomId9001",
"studioId": "mockStudio4",
- "timing": {
- "type": "none"
- }
+ "timing": Object {
+ "type": "none",
+ },
}
`;
diff --git a/meteor/server/api/rundownPlaylist.ts b/meteor/server/api/rundownPlaylist.ts
index 989a40934c..2042c4659c 100644
--- a/meteor/server/api/rundownPlaylist.ts
+++ b/meteor/server/api/rundownPlaylist.ts
@@ -488,23 +488,5 @@ function sortDefaultRundownInPlaylistOrder(rundowns: ReadonlyDeep
{
- // Compare start times, then allow rundowns with start time to be first
- if (
- PlaylistTiming.isPlaylistTimingForwardTime(a.timing) &&
- PlaylistTiming.isPlaylistTimingForwardTime(b.timing)
- )
- return a.timing.expectedStart - b.timing.expectedStart
- if (PlaylistTiming.isPlaylistTimingForwardTime(a.timing)) return -1
- if (PlaylistTiming.isPlaylistTimingForwardTime(b.timing)) return 1
-
- // Compare end times, then allow rundowns with end time to be first
- if (PlaylistTiming.isPlaylistTimingBackTime(a.timing) && PlaylistTiming.isPlaylistTimingBackTime(b.timing))
- return a.timing.expectedEnd - b.timing.expectedEnd
- if (PlaylistTiming.isPlaylistTimingBackTime(a.timing)) return -1
- if (PlaylistTiming.isPlaylistTimingBackTime(b.timing)) return 1
-
- // No timing
- return 0
- })
+ }).sort(PlaylistTiming.sortTiminings)
}
From 918e72ab57a7b34ea3a254f97e0e0377be4204e3 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 27 Jul 2021 13:29:08 +0100
Subject: [PATCH 063/112] fix: Import issues in tests
---
meteor/__mocks__/defaultCollectionObjects.ts | 6 +++---
meteor/__mocks__/helpers/database.ts | 5 ++---
meteor/lib/rundown/__tests__/rundownTiming.test.ts | 13 ++++++-------
.../server/api/blueprints/__tests__/cache.test.ts | 3 +--
.../server/api/ingest/__tests__/updateNext.test.ts | 5 ++---
.../playout/lookahead/__tests__/lookahead.test.ts | 4 ++--
.../api/playout/lookahead/__tests__/util.test.ts | 4 ++--
packages/blueprints-integration/src/rundown.ts | 4 ++--
8 files changed, 20 insertions(+), 24 deletions(-)
diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts
index 91e76cfe82..590b1aec67 100644
--- a/meteor/__mocks__/defaultCollectionObjects.ts
+++ b/meteor/__mocks__/defaultCollectionObjects.ts
@@ -8,7 +8,7 @@ import { DBRundown, RundownId } from '../lib/collections/Rundowns'
import { DBSegment, SegmentId } from '../lib/collections/Segments'
import { PartId, DBPart } from '../lib/collections/Parts'
import { RundownAPI } from '../lib/api/rundown'
-import { PieceLifespan, PlaylistTimingType } from '@sofie-automation/blueprints-integration'
+import { PieceLifespan } from '@sofie-automation/blueprints-integration'
import { PieceId, Piece } from '../lib/collections/Pieces'
import { AdLibPiece } from '../lib/collections/AdLibPieces'
import { getRundownId } from '../server/api/ingest/lib'
@@ -31,7 +31,7 @@ export function defaultRundownPlaylist(_id: RundownPlaylistId, studioId: StudioI
nextPartInstanceId: null,
previousPartInstanceId: null,
timing: {
- type: PlaylistTimingType.None,
+ type: 'none' as any,
},
}
}
@@ -70,7 +70,7 @@ export function defaultRundown(
externalNRCSName: 'mock',
timing: {
- type: PlaylistTimingType.None,
+ type: 'none' as any,
},
}
}
diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts
index 11ce0d427e..ebbedbad2f 100644
--- a/meteor/__mocks__/helpers/database.ts
+++ b/meteor/__mocks__/helpers/database.ts
@@ -22,7 +22,6 @@ import {
BlueprintResultPart,
IBlueprintPart,
IBlueprintPiece,
- PlaylistTimingType,
} from '@sofie-automation/blueprints-integration'
import { ShowStyleBase, ShowStyleBases, DBShowStyleBase, ShowStyleBaseId } from '../../lib/collections/ShowStyleBases'
import {
@@ -313,7 +312,7 @@ export async function setupMockShowStyleBlueprint(
// expectedDuration?: number;
metaData: ingestRundown.payload,
timing: {
- type: PlaylistTimingType.None,
+ type: 'none' as any,
},
}
@@ -473,7 +472,7 @@ export function setupDefaultRundown(
showStyleBaseId: env.showStyleBase._id,
showStyleVariantId: env.showStyleVariant._id,
timing: {
- type: PlaylistTimingType.None,
+ type: 'none' as any,
},
playlistId: playlistId,
diff --git a/meteor/lib/rundown/__tests__/rundownTiming.test.ts b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
index a70400d5d9..c078d6f3a1 100644
--- a/meteor/lib/rundown/__tests__/rundownTiming.test.ts
+++ b/meteor/lib/rundown/__tests__/rundownTiming.test.ts
@@ -1,4 +1,3 @@
-import { PlaylistTimingType } from '@sofie-automation/blueprints-integration'
import { PartInstance } from '../../collections/PartInstances'
import { DBPart, Part, PartId } from '../../collections/Parts'
import { DBRundownPlaylist, RundownPlaylist } from '../../collections/RundownPlaylists'
@@ -22,7 +21,7 @@ function makeMockPlaylist(): RundownPlaylist {
nextPartInstanceId: null,
previousPartInstanceId: null,
timing: {
- type: PlaylistTimingType.None,
+ type: 'none' as any,
},
})
)
@@ -54,7 +53,7 @@ function makeMockRundown(id: string, playlistId: string, rank: number) {
_id: protectString(id),
externalId: id,
timing: {
- type: PlaylistTimingType.None,
+ type: 'none' as any,
},
studioId: protectString('studio0'),
showStyleBaseId: protectString(''),
@@ -114,7 +113,7 @@ describe('rundown Timing Calculator', () => {
const timing = new RundownTimingCalculator()
const playlist: RundownPlaylist = makeMockPlaylist()
playlist.timing = {
- type: PlaylistTimingType.ForwardTime,
+ type: 'forward-time' as any,
expectedStart: 0,
expectedDuration: 40000,
}
@@ -204,7 +203,7 @@ describe('rundown Timing Calculator', () => {
const timing = new RundownTimingCalculator()
const playlist: RundownPlaylist = makeMockPlaylist()
playlist.timing = {
- type: PlaylistTimingType.ForwardTime,
+ type: 'forward-time' as any,
expectedStart: 0,
expectedDuration: 40000,
}
@@ -294,7 +293,7 @@ describe('rundown Timing Calculator', () => {
const timing = new RundownTimingCalculator()
const playlist: RundownPlaylist = makeMockPlaylist()
playlist.timing = {
- type: PlaylistTimingType.ForwardTime,
+ type: 'forward-time' as any,
expectedStart: 0,
expectedDuration: 40000,
}
@@ -388,7 +387,7 @@ describe('rundown Timing Calculator', () => {
const timing = new RundownTimingCalculator()
const playlist: RundownPlaylist = makeMockPlaylist()
playlist.timing = {
- type: PlaylistTimingType.ForwardTime,
+ type: 'forward-time' as any,
expectedStart: 0,
expectedDuration: 40000,
}
diff --git a/meteor/server/api/blueprints/__tests__/cache.test.ts b/meteor/server/api/blueprints/__tests__/cache.test.ts
index 93d32e9bac..b2a9deceda 100644
--- a/meteor/server/api/blueprints/__tests__/cache.test.ts
+++ b/meteor/server/api/blueprints/__tests__/cache.test.ts
@@ -15,7 +15,6 @@ import {
BlueprintManifestType,
BlueprintResultRundown,
BlueprintResultSegment,
- PlaylistTimingType,
} from '@sofie-automation/blueprints-integration'
import { Studios, Studio } from '../../../../lib/collections/Studios'
import { ShowStyleBase, ShowStyleBases } from '../../../../lib/collections/ShowStyleBases'
@@ -364,7 +363,7 @@ describe('Test blueprint cache', () => {
externalNRCSName: 'mockNRCS',
organizationId: protectString(''),
timing: {
- type: PlaylistTimingType.None,
+ type: 'none' as any,
},
})
)
diff --git a/meteor/server/api/ingest/__tests__/updateNext.test.ts b/meteor/server/api/ingest/__tests__/updateNext.test.ts
index c533ef34fa..d7aecb2188 100644
--- a/meteor/server/api/ingest/__tests__/updateNext.test.ts
+++ b/meteor/server/api/ingest/__tests__/updateNext.test.ts
@@ -12,7 +12,6 @@ import { defaultStudio } from '../../../../__mocks__/defaultCollectionObjects'
import { removeRundownsFromDb } from '../../rundownPlaylist'
import { PlayoutLockFunctionPriority, runPlayoutOperationWithCache } from '../../playout/lockFunction'
import { saveIntoDb } from '../../../lib/database'
-import { PlaylistTimingType } from '@sofie-automation/blueprints-integration'
jest.mock('../../playout/playout')
require('../../peripheralDevice.ts') // include in order to create the Meteor methods needed
@@ -40,7 +39,7 @@ async function createMockRO(): Promise {
previousPartInstanceId: null,
activationId: protectString('active'),
timing: {
- type: PlaylistTimingType.None,
+ type: 'none' as any,
},
})
@@ -60,7 +59,7 @@ async function createMockRO(): Promise {
externalNRCSName: 'mockNRCS',
organizationId: protectString(''),
timing: {
- type: PlaylistTimingType.None,
+ type: 'none' as any,
},
})
diff --git a/meteor/server/api/playout/lookahead/__tests__/lookahead.test.ts b/meteor/server/api/playout/lookahead/__tests__/lookahead.test.ts
index d9d569180f..2f5dca3815 100644
--- a/meteor/server/api/playout/lookahead/__tests__/lookahead.test.ts
+++ b/meteor/server/api/playout/lookahead/__tests__/lookahead.test.ts
@@ -9,7 +9,7 @@ import { RundownPlaylist, RundownPlaylistId, RundownPlaylists } from '../../../.
import { getCurrentTime, getRandomId, protectString } from '../../../../../lib/lib'
import { SegmentId } from '../../../../../lib/collections/Segments'
import { DBPart, Part, PartId, Parts } from '../../../../../lib/collections/Parts'
-import { LookaheadMode, PlaylistTimingType, TSR } from '@sofie-automation/blueprints-integration'
+import { LookaheadMode, TSR } from '@sofie-automation/blueprints-integration'
import { MappingsExt, Studios } from '../../../../../lib/collections/Studios'
import { OnGenerateTimelineObjExt, TimelineObjRundown } from '../../../../../lib/collections/Timeline'
import { PartAndPieces, PartInstanceAndPieceInstances } from '../util'
@@ -81,7 +81,7 @@ describe('Lookahead', () => {
},
externalNRCSName: 'mock',
timing: {
- type: PlaylistTimingType.None,
+ type: 'none' as any,
},
}
Rundowns.insert(rundown)
diff --git a/meteor/server/api/playout/lookahead/__tests__/util.test.ts b/meteor/server/api/playout/lookahead/__tests__/util.test.ts
index cf28166886..0d54400ec5 100644
--- a/meteor/server/api/playout/lookahead/__tests__/util.test.ts
+++ b/meteor/server/api/playout/lookahead/__tests__/util.test.ts
@@ -9,7 +9,7 @@ import { RundownPlaylist, RundownPlaylistId, RundownPlaylists } from '../../../.
import { getCurrentTime, protectString } from '../../../../../lib/lib'
import { SegmentId, Segments } from '../../../../../lib/collections/Segments'
import { DBPart, Part, PartId, Parts } from '../../../../../lib/collections/Parts'
-import { LookaheadMode, PlaylistTimingType, TSR } from '@sofie-automation/blueprints-integration'
+import { LookaheadMode, TSR } from '@sofie-automation/blueprints-integration'
import { MappingsExt, Studios } from '../../../../../lib/collections/Studios'
import { PartInstances, wrapPartToTemporaryInstance } from '../../../../../lib/collections/PartInstances'
import _ from 'underscore'
@@ -70,7 +70,7 @@ describe('getOrderedPartsAfterPlayhead', () => {
externalNRCSName: 'mock',
timing: {
- type: PlaylistTimingType.None,
+ type: 'none' as any,
},
}
Rundowns.insert(rundown)
diff --git a/packages/blueprints-integration/src/rundown.ts b/packages/blueprints-integration/src/rundown.ts
index bf4344bca9..4b7a845bd6 100644
--- a/packages/blueprints-integration/src/rundown.ts
+++ b/packages/blueprints-integration/src/rundown.ts
@@ -21,8 +21,8 @@ export interface IBlueprintRundownPlaylistInfo {
export enum PlaylistTimingType {
None = 'none',
- ForwardTime = 'forward_time',
- BackTime = 'back_time',
+ ForwardTime = 'forward-time',
+ BackTime = 'back-time',
}
export interface PlaylistTimingBase {
From 8840e9f7497b68c3f8fdda9e3dbea67c165a5248 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 27 Jul 2021 13:59:08 +0100
Subject: [PATCH 064/112] fix: More test snapshots
---
.../__snapshots__/mosIngest.test.ts.snap | 93 +++++++++++++++++++
1 file changed, 93 insertions(+)
diff --git a/meteor/server/api/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap b/meteor/server/api/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap
index bedba0ef80..785dc7f7a3 100644
--- a/meteor/server/api/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap
+++ b/meteor/server/api/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap
@@ -12,6 +12,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -157,6 +160,9 @@ Object {
"showStyleBaseId": "randomId9000",
"showStyleVariantId": "randomId9001",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -298,6 +304,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -443,6 +452,9 @@ Object {
"showStyleBaseId": "randomId9000",
"showStyleVariantId": "randomId9001",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -583,6 +595,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -730,6 +745,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -882,6 +900,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -1029,6 +1050,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -1170,6 +1194,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -1316,6 +1343,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -1457,6 +1487,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -1604,6 +1637,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -1710,6 +1746,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -1857,6 +1896,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -2010,6 +2052,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -2157,6 +2202,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -2319,6 +2367,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -2466,6 +2517,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -2610,6 +2664,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -2757,6 +2814,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -2901,6 +2961,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -3048,6 +3111,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -3323,6 +3389,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -3467,6 +3536,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -3614,6 +3686,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -3749,6 +3824,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -3896,6 +3974,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -4067,6 +4148,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -4214,6 +4298,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -4358,6 +4445,9 @@ Object {
"organizationId": null,
"previousPartInstanceId": null,
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
@@ -4505,6 +4595,9 @@ Object {
"showStyleVariantId": "randomId9001",
"status": "BUSY",
"studioId": "mockStudio4",
+ "timing": Object {
+ "type": "none",
+ },
}
`;
From 738ef269368f7c917e4822537a736b9798766873 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Thu, 8 Jul 2021 15:35:53 +0100
Subject: [PATCH 065/112] feat: Require layers to have active pieces for live
line counter
---
meteor/client/ui/RundownView.tsx | 19 +++--
.../ui/SegmentTimeline/SegmentTimeline.tsx | 13 ++--
.../SegmentTimelineContainer.tsx | 13 ++++
.../ui/Settings/RundownLayoutEditor.tsx | 8 +-
.../RundownViewLayoutSettings.tsx | 74 +++++++++++++++++++
meteor/lib/collections/RundownLayouts.ts | 1 +
6 files changed, 116 insertions(+), 12 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index ab89569e05..2d8cc477e5 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -1326,7 +1326,7 @@ interface IState {
isClipTrimmerOpen: boolean
selectedPiece: AdLibPieceUi | PieceUi | undefined
shelfLayout: RundownLayoutShelfBase | undefined
- rundownViewLayout: RundownLayoutBase | undefined
+ rundownViewLayout: RundownViewLayout | undefined
rundownHeaderLayout: RundownLayoutRundownHeader | undefined
miniShelfLayout: RundownLayoutShelfBase | undefined
currentRundown: Rundown | undefined
@@ -1552,7 +1552,7 @@ export const RundownView = translateWithTracker((
static getDerivedStateFromProps(props: Translated): Partial {
let selectedShelfLayout: RundownLayoutBase | undefined = undefined
- let selectedViewLayout: RundownLayoutBase | undefined = undefined
+ let selectedViewLayout: RundownViewLayout | undefined = undefined
let selectedHeaderLayout: RundownLayoutBase | undefined = undefined
let selectedMiniShelfLayout: RundownLayoutBase | undefined = undefined
@@ -1563,7 +1563,9 @@ export const RundownView = translateWithTracker((
}
if (props.rundownViewLayoutId) {
- selectedViewLayout = props.rundownLayouts.find((i) => i._id === props.rundownViewLayoutId)
+ selectedViewLayout = props.rundownLayouts.find(
+ (i) => i._id === props.rundownViewLayoutId && RundownLayoutsAPI.isRundownViewLayout(i)
+ ) as RundownViewLayout
}
if (props.rundownHeaderLayoutId) {
@@ -1583,8 +1585,10 @@ export const RundownView = translateWithTracker((
if (props.rundownViewLayoutId && !selectedViewLayout) {
selectedViewLayout = props.rundownLayouts.find(
- (i) => i.name.indexOf(unprotectString(props.rundownViewLayoutId!)) >= 0
- )
+ (i) =>
+ i.name.indexOf(unprotectString(props.rundownViewLayoutId!)) >= 0 &&
+ RundownLayoutsAPI.isRundownViewLayout(i)
+ ) as RundownViewLayout
}
if (props.rundownHeaderLayoutId && !selectedHeaderLayout) {
@@ -1626,7 +1630,9 @@ export const RundownView = translateWithTracker((
}
if (!selectedViewLayout) {
- selectedViewLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.isLayoutForRundownView(i))
+ selectedViewLayout = props.rundownLayouts.find((i) =>
+ RundownLayoutsAPI.IsLayoutForRundownView(i)
+ ) as RundownViewLayout
}
if (!selectedHeaderLayout) {
@@ -2296,6 +2302,7 @@ export const RundownView = translateWithTracker((
studio={this.props.studio}
showStyleBase={this.props.showStyleBase}
followLiveSegments={this.state.followLiveSegments}
+ rundownViewLayout={this.state.rundownViewLayout}
rundownId={rundownAndSegments.rundown._id}
segmentId={segment._id}
playlist={this.props.playlist}
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
index ea790dd29b..8770b0b688 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
@@ -68,6 +68,7 @@ interface IProps {
followLiveLine: boolean
liveLineHistorySize: number
livePosition: number
+ displayLiveLineCounter: boolean
autoNextPart: boolean
onScroll: (scrollLeft: number, event: any) => void
onZoomChange: (newScale: number, event: any) => void
@@ -701,11 +702,13 @@ export class SegmentTimelineClass extends React.Component, IS
{t('On Air')}
-
+ {this.props.displayLiveLineCounter && (
+
+ )}
{this.props.autoNextPart ? (
) : (
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
index 0d9e61c436..ab07b60874 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
@@ -47,6 +47,8 @@ import { computeSegmentDuration, PlaylistTiming, RundownTimingContext } from '..
import { SegmentTimelinePartClass } from './SegmentTimelinePart'
import { Piece, Pieces } from '../../../lib/collections/Pieces'
import { RundownAPI } from '../../../lib/api/rundown'
+import { RundownViewLayout } from '../../../lib/collections/RundownLayouts'
+import { getIsFilterActive } from '../../lib/rundownLayouts'
export const SIMULATED_PLAYBACK_SOFT_MARGIN = 0
export const SIMULATED_PLAYBACK_HARD_MARGIN = 3500
@@ -111,6 +113,7 @@ interface IProps {
isLastSegment: boolean
ownCurrentPartInstance: PartInstance | undefined
ownNextPartInstance: PartInstance | undefined
+ rundownViewLayout: RundownViewLayout | undefined
}
interface IState {
scrollLeft: number
@@ -137,6 +140,7 @@ interface ITrackedProps {
hasGuestItems: boolean
hasAlreadyPlayed: boolean
lastValidPartIndex: number | undefined
+ displayLiveLineCounter: boolean
}
export const SegmentTimelineContainer = translateWithTracker
(
(props: IProps) => {
@@ -152,6 +156,7 @@ export const SegmentTimelineContainer = translateWithTracker {
@@ -927,6 +939,7 @@ export const SegmentTimelineContainer = translateWithTracker((props: IProp
{isShelfLayout && }
{isRundownHeaderLayout && }
- {isRundownViewLayout && }
+ {isRundownViewLayout && (
+
+ )}
{RundownLayoutsAPI.isLayoutWithFilters(item) && layout?.supportedFilters.length ? (
{layout?.filtersTitle ?? t('Filters')}
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index c176c65d90..3705566b13 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -2,6 +2,7 @@ import React from 'react'
import { withTranslation } from 'react-i18next'
import { RundownLayoutsAPI } from '../../../../../lib/api/rundownLayouts'
import { RundownLayoutBase, RundownLayouts } from '../../../../../lib/collections/RundownLayouts'
+import { ShowStyleBase } from '../../../../../lib/collections/ShowStyleBases'
import { unprotectString } from '../../../../../lib/lib'
import { EditAttribute } from '../../../../lib/EditAttribute'
import { MeteorReactComponent } from '../../../../lib/MeteorReactComponent'
@@ -17,6 +18,7 @@ function filterLayouts(
interface IProps {
item: RundownLayoutBase
layouts: RundownLayoutBase[]
+ showStyleBase: ShowStyleBase
}
interface IState {}
@@ -83,6 +85,78 @@ export default withTranslation()(
>
+
+ {t('Live line countdown requires sourcelayer')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+ {
+ return { name: l.name, value: l._id }
+ })}
+ type="multiselect"
+ label={t('Disabled')}
+ collection={RundownLayouts}
+ className="input text-input input-l dropdown"
+ mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
+ />
+
+ {t('One of these sourcelayers must have an active piece for the live line countdown to be show')}
+
+
+
+ {t('Also Require Source Layers')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+ {
+ return { name: l.name, value: l._id }
+ })}
+ type="multiselect"
+ label={t('Disabled')}
+ collection={RundownLayouts}
+ className="input text-input input-l dropdown"
+ mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
+ />
+
+ {t('Specify additional layers where at least one layer must have an active piece')}
+
+
+
+
+ {t('Require All Sourcelayers')}
+
+ {t('All required source layers must have active pieces')}
+
+
)
}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index f7b68e2915..4c4600b18d 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -328,6 +328,7 @@ export interface RundownViewLayout extends RundownLayoutBase {
shelfLayout: RundownLayoutId
miniShelfLayout: RundownLayoutId
rundownHeaderLayout: RundownLayoutId
+ liveLineProps?: RequiresActiveLayers
}
export interface RundownLayoutShelfBase extends RundownLayoutWithFilters {
From b1d3d062ba3c4557be67501be4391ea89e665fe4 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Fri, 9 Jul 2021 14:13:15 +0100
Subject: [PATCH 066/112] feat: Break marker segment + hide rundown divider
---
meteor/client/styles/rundownView.scss | 12 +++
meteor/client/ui/RundownView.tsx | 8 +-
.../ui/SegmentTimeline/BreakSegment.tsx | 74 +++++++++++++++++++
.../RundownViewLayoutSettings.tsx | 27 +++++++
meteor/lib/collections/RundownLayouts.ts | 2 +
meteor/lib/collections/RundownPlaylists.ts | 1 +
6 files changed, 123 insertions(+), 1 deletion(-)
create mode 100644 meteor/client/ui/SegmentTimeline/BreakSegment.tsx
diff --git a/meteor/client/styles/rundownView.scss b/meteor/client/styles/rundownView.scss
index 324676d6f4..bbc67c8a39 100644
--- a/meteor/client/styles/rundownView.scss
+++ b/meteor/client/styles/rundownView.scss
@@ -668,6 +668,18 @@ svg.icon {
}
}
+ &.has-break {
+ width: 12.5rem;
+
+ .segment-timeline__title {
+ background: $rundown-divider-background-color;
+
+ h2 {
+ color: $rundown-divider-color;
+ }
+ }
+ }
+
&.live {
.segment-timeline__title {
color: $segment-title-text-color-live;
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 2d8cc477e5..137aa205a4 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -110,6 +110,7 @@ import { TimeOfDay } from './RundownView/RundownTiming/TimeOfDay'
import { PlaylistStartTiming } from './RundownView/RundownTiming/PlaylistStartTiming'
import { ShowStyleVariant, ShowStyleVariants } from '../../lib/collections/ShowStyleVariants'
import { PlaylistTiming } from '../../lib/rundown/rundownTiming'
+import { BreakSegment } from './SegmentTimeline/BreakSegment'
export const MAGIC_TIME_SCALE_FACTOR = 0.03
@@ -2271,6 +2272,8 @@ export const RundownView = translateWithTracker((
}
renderSegments() {
+ const { t } = this.props
+
if (this.props.matchedSegments) {
let globalIndex = 0
const rundowns = this.props.matchedSegments.map((m) => m.rundown._id)
@@ -2278,7 +2281,7 @@ export const RundownView = translateWithTracker((
const rundownIdsBefore = rundowns.slice(0, rundownIndex)
return (
- {this.props.matchedSegments.length > 1 && (
+ {this.props.matchedSegments.length > 1 && !this.state.rundownViewLayout?.hideRundownDivider && (
((
)
}
})}
+ {this.state.rundownViewLayout?.showBreaksAsSegments && rundownAndSegments.rundown.endIsBreak && (
+
+ )}
)
})
diff --git a/meteor/client/ui/SegmentTimeline/BreakSegment.tsx b/meteor/client/ui/SegmentTimeline/BreakSegment.tsx
new file mode 100644
index 0000000000..665aec1dba
--- /dev/null
+++ b/meteor/client/ui/SegmentTimeline/BreakSegment.tsx
@@ -0,0 +1,74 @@
+import React from 'react'
+import { withTranslation } from 'react-i18next'
+import Moment from 'react-moment'
+import { getCurrentTime } from '../../../lib/lib'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { RundownUtils } from '../../lib/rundown'
+import { RundownTiming } from '../RundownView/RundownTiming/RundownTiming'
+
+interface IProps {
+ breakTime: number | undefined
+}
+
+interface IState {
+ displayTimecode: number | undefined
+}
+
+class BreakSegmentInner extends MeteorReactComponent, IState> {
+ constructor(props: IProps) {
+ super(props)
+
+ this.state = {
+ displayTimecode: undefined,
+ }
+
+ this.updateTimecode = this.updateTimecode.bind(this)
+ }
+
+ componentDidMount() {
+ window.addEventListener(RundownTiming.Events.timeupdate, this.updateTimecode)
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener(RundownTiming.Events.timeupdate, this.updateTimecode)
+ }
+
+ updateTimecode() {
+ this.setState({
+ displayTimecode: this.props.breakTime ? this.props.breakTime - getCurrentTime() : undefined,
+ })
+ }
+
+ render() {
+ const { t } = this.props
+
+ return (
+
+
+
+ {this.props.breakTime && }
+ {t('BREAK')}
+
+
+ {this.state.displayTimecode && (
+
+ {t('Break In')}
+
+ {RundownUtils.formatDiffToTimecode(
+ this.state.displayTimecode,
+ false,
+ undefined,
+ undefined,
+ undefined,
+ true
+ )}
+
+
+ )}
+
+ )
+ }
+}
+
+export const BreakSegment = withTranslation()(BreakSegmentInner)
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index 3705566b13..5f26a7d37c 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -157,6 +157,33 @@ export default withTranslation()(
{t('All required source layers must have active pieces')}
+
+
+ {t('Hide Rundown Divider')}
+
+ {t('Hide rundown divider between rundowns in a playlist')}
+
+
+
+
+ {t('Show Breaks as Segments')}
+
+
+
)
}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 4c4600b18d..4c53df38f7 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -329,6 +329,8 @@ export interface RundownViewLayout extends RundownLayoutBase {
miniShelfLayout: RundownLayoutId
rundownHeaderLayout: RundownLayoutId
liveLineProps?: RequiresActiveLayers
+ hideRundownDivider: boolean
+ showBreaksAsSegments: boolean
}
export interface RundownLayoutShelfBase extends RundownLayoutWithFilters {
diff --git a/meteor/lib/collections/RundownPlaylists.ts b/meteor/lib/collections/RundownPlaylists.ts
index 8a284c8fcd..a71db4b5c1 100644
--- a/meteor/lib/collections/RundownPlaylists.ts
+++ b/meteor/lib/collections/RundownPlaylists.ts
@@ -210,6 +210,7 @@ export class RundownPlaylist implements DBRundownPlaylist {
playlistId: 1,
timing: 1,
showStyleBaseId: 1,
+ endIsBreak: 1,
},
})
const segments = Segments.find(
From e13a14cbbb14f89b416f7f2b29ed2e220b288771 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Fri, 9 Jul 2021 15:54:52 +0100
Subject: [PATCH 067/112] feat: Only countdown to segment if certain
sourcelayers are present
---
meteor/client/ui/RundownView.tsx | 3 ++
.../ui/SegmentTimeline/SegmentTimeline.tsx | 3 +-
.../SegmentTimelineContainer.tsx | 17 ++++++++++-
.../RundownViewLayoutSettings.tsx | 29 +++++++++++++++++++
meteor/lib/collections/RundownLayouts.ts | 5 +++-
5 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 137aa205a4..fb24f07f44 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -2340,6 +2340,9 @@ export const RundownView = translateWithTracker((
? this.props.nextPartInstance
: undefined
}
+ countdownToSegmentRequireLayers={
+ this.state.rundownViewLayout?.countdownToSegmentRequireLayers
+ }
/>
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
index 8770b0b688..90264b5980 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
@@ -81,6 +81,7 @@ interface IProps {
segmentRef?: (el: SegmentTimelineClass, segmentId: SegmentId) => void
isLastSegment: boolean
lastValidPartIndex: number | undefined
+ showCountdownToSegment: boolean
}
interface IStateHeader {
timelineWidth: number
@@ -1029,7 +1030,7 @@ export class SegmentTimelineClass extends React.Component, IS
)}
- {this.props.playlist && this.props.parts && this.props.parts.length > 0 && (
+ {this.props.playlist && this.props.parts && this.props.parts.length > 0 && this.props.showCountdownToSegment && (
(
(props: IProps) => {
@@ -157,6 +159,7 @@ export const SegmentTimelineContainer = translateWithTracker pa.pieces.map((pi) => pi.sourceLayer?._id))
+ .flat()
+ .filter((s) => !!s) as string[]
+ showCountdownToSegment = props.countdownToSegmentRequireLayers.some((s) => sourcelayersInSegment.includes(s))
+ }
+
return {
segmentui: o.segmentExtended,
parts: o.parts,
@@ -277,6 +289,7 @@ export const SegmentTimelineContainer = translateWithTracker {
@@ -289,7 +302,8 @@ export const SegmentTimelineContainer = translateWithTracker
)) ||
null
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index 5f26a7d37c..279a39a0ed 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -184,6 +184,35 @@ export default withTranslation()(
>
+
+ {t('Segment countdown requires sourcelayer')}
+ (v === undefined || v.length === 0 ? false : true)}
+ mutateUpdateValue={(v) => undefined}
+ />
+ {
+ return { name: l.name, value: l._id }
+ })}
+ type="multiselect"
+ label={t('Disabled')}
+ collection={RundownLayouts}
+ className="input text-input input-l dropdown"
+ mutateUpdateValue={(v) => (v && v.length > 0 ? v : undefined)}
+ />
+
+ {t('One of these sourcelayers must have a piece for the countdown to segment on-air to be show')}
+
+
)
}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 4c53df38f7..5a61eacbdd 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -322,15 +322,18 @@ export interface RundownLayoutWithFilters extends RundownLayoutBase {
export interface RundownViewLayout extends RundownLayoutBase {
type: RundownLayoutType.RUNDOWN_VIEW_LAYOUT
- expectedEndText: string
/** Expose as a layout that can be selected by the user in the lobby view */
exposeAsSelectableLayout: boolean
shelfLayout: RundownLayoutId
miniShelfLayout: RundownLayoutId
rundownHeaderLayout: RundownLayoutId
liveLineProps?: RequiresActiveLayers
+ /** Hide the rundown divider header in playlists */
hideRundownDivider: boolean
+ /** Show breaks in segment timeline list */
showBreaksAsSegments: boolean
+ /** Only count down to the segment if it contains pieces on these layers */
+ countdownToSegmentRequireLayers: string[]
}
export interface RundownLayoutShelfBase extends RundownLayoutWithFilters {
From 074f07a1cff2016fb398d5940b6f1b4b88a39f41 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 28 Jul 2021 15:37:27 +0100
Subject: [PATCH 068/112] fix: Rebase fixes
---
meteor/client/styles/rundownView.scss | 6 ++++
meteor/client/ui/RundownView.tsx | 18 +++-------
.../ui/SegmentTimeline/SegmentTimeline.tsx | 34 ++++++++++---------
.../SegmentTimelineContainer.tsx | 4 +--
.../RundownViewLayoutSettings.tsx | 6 ++--
meteor/lib/collections/RundownPlaylists.ts | 2 +-
meteor/lib/collections/Rundowns.ts | 2 +-
7 files changed, 36 insertions(+), 36 deletions(-)
diff --git a/meteor/client/styles/rundownView.scss b/meteor/client/styles/rundownView.scss
index bbc67c8a39..a00d9294b1 100644
--- a/meteor/client/styles/rundownView.scss
+++ b/meteor/client/styles/rundownView.scss
@@ -678,6 +678,12 @@ svg.icon {
color: $rundown-divider-color;
}
}
+
+ // Add a tiny bit of padding so the break text doesn't look squashed against the side of the segment container
+ .segment-timeline__timeUntil,
+ .segment-timeline__timeUntil__label {
+ padding-right: 0.3vw;
+ }
}
&.live {
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index fb24f07f44..161a2c03a0 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -12,7 +12,6 @@ import ClassNames from 'classnames'
import * as _ from 'underscore'
import Escape from 'react-escape'
import * as i18next from 'i18next'
-import Moment from 'react-moment'
import Tooltip from 'rc-tooltip'
import { NavLink, Route, Prompt } from 'react-router-dom'
import { RundownPlaylist, RundownPlaylists, RundownPlaylistId } from '../../lib/collections/RundownPlaylists'
@@ -315,12 +314,6 @@ const TimingDisplay = withTranslation()(
)
)
-interface INextBreakTimingProps {
- loop?: boolean
- rundownsBeforeBreak: Rundown[]
- breakText?: string
-}
-
interface HotkeyDefinition {
key: string
label: string
@@ -1632,7 +1625,7 @@ export const RundownView = translateWithTracker((
if (!selectedViewLayout) {
selectedViewLayout = props.rundownLayouts.find((i) =>
- RundownLayoutsAPI.IsLayoutForRundownView(i)
+ RundownLayoutsAPI.isLayoutForRundownView(i)
) as RundownViewLayout
}
@@ -2272,8 +2265,6 @@ export const RundownView = translateWithTracker((
}
renderSegments() {
- const { t } = this.props
-
if (this.props.matchedSegments) {
let globalIndex = 0
const rundowns = this.props.matchedSegments.map((m) => m.rundown._id)
@@ -2349,9 +2340,10 @@ export const RundownView = translateWithTracker((
)
}
})}
- {this.state.rundownViewLayout?.showBreaksAsSegments && rundownAndSegments.rundown.endIsBreak && (
-
- )}
+ {this.state.rundownViewLayout?.showBreaksAsSegments &&
+ rundownAndSegments.rundown.endOfRundownIsShowBreak && (
+
+ )}
)
})
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
index 90264b5980..241b5badb2 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
@@ -39,7 +39,6 @@ import { RundownTimingContext } from '../../../lib/rundown/rundownTiming'
import { PartInstanceId } from '../../../lib/collections/PartInstances'
import { SegmentTimelineSmallPartFlag } from './SmallParts/SegmentTimelineSmallPartFlag'
import { UIStateStorage } from '../../lib/UIStateStorage'
-import { RundownTimingContext } from '../../../lib/rundown/rundownTiming'
interface IProps {
id: string
@@ -1030,21 +1029,24 @@ export class SegmentTimelineClass extends React.Component, IS
)}
- {this.props.playlist && this.props.parts && this.props.parts.length > 0 && this.props.showCountdownToSegment && (
-
{t('On Air At')}
- ) : (
- {t('On Air In')}
- )
- }
- />
- )}
+ {this.props.playlist &&
+ this.props.parts &&
+ this.props.parts.length > 0 &&
+ this.props.showCountdownToSegment && (
+ {t('On Air At')}
+ ) : (
+ {t('On Air In')}
+ )
+ }
+ />
+ )}
{Settings.preserveUnsyncedPlayingSegmentContents && this.props.segment.orphaned && (
{t('Unsynced')}
)}
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
index 98b610e96d..b236104a95 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
@@ -267,13 +267,13 @@ export const SegmentTimelineContainer = translateWithTracker pa.pieces.map((pi) => pi.sourceLayer?._id))
.flat()
.filter((s) => !!s) as string[]
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index 279a39a0ed..4eaecc83d4 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -95,7 +95,7 @@ export default withTranslation()(
collection={RundownLayouts}
className="mod mas"
mutateDisplayValue={(v) => (v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={(v) => undefined}
+ mutateUpdateValue={() => undefined}
/>
(v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={(v) => undefined}
+ mutateUpdateValue={() => undefined}
/>
(v === undefined || v.length === 0 ? false : true)}
- mutateUpdateValue={(v) => undefined}
+ mutateUpdateValue={() => undefined}
/>
Date: Fri, 9 Jul 2021 14:13:15 +0100
Subject: [PATCH 069/112] feat: Break marker segment + hide rundown divider
---
meteor/client/ui/RundownView.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 161a2c03a0..5f96fdcb12 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -2265,6 +2265,8 @@ export const RundownView = translateWithTracker((
}
renderSegments() {
+ const { t } = this.props
+
if (this.props.matchedSegments) {
let globalIndex = 0
const rundowns = this.props.matchedSegments.map((m) => m.rundown._id)
From ad44fd274625dc42e2ee5ae6e9b31c3b1736369e Mon Sep 17 00:00:00 2001
From: ianshade
Date: Tue, 27 Jul 2021 17:45:51 +0200
Subject: [PATCH 070/112] feat: add static segment duration option
makes the segment duration in the segment header always show the planned duration instead of acting as a counter
---
meteor/client/ui/RundownView.tsx | 3 +--
.../RundownTiming/SegmentDuration.tsx | 21 +++++++++++--------
.../ui/SegmentTimeline/SegmentTimeline.tsx | 2 ++
.../SegmentTimelineContainer.tsx | 2 ++
.../RundownViewLayoutSettings.tsx | 18 ++++++++++++++++
meteor/lib/collections/RundownLayouts.ts | 2 ++
6 files changed, 37 insertions(+), 11 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 5f96fdcb12..ef77b0716b 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -2265,8 +2265,6 @@ export const RundownView = translateWithTracker((
}
renderSegments() {
- const { t } = this.props
-
if (this.props.matchedSegments) {
let globalIndex = 0
const rundowns = this.props.matchedSegments.map((m) => m.rundown._id)
@@ -2336,6 +2334,7 @@ export const RundownView = translateWithTracker((
countdownToSegmentRequireLayers={
this.state.rundownViewLayout?.countdownToSegmentRequireLayers
}
+ staticSegmentDuration={this.state.rundownViewLayout?.staticSegmentDuration}
/>
diff --git a/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx b/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx
index 46aa37c1bd..74ad1c9e0e 100644
--- a/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx
@@ -9,6 +9,8 @@ interface ISegmentDurationProps {
label?: ReactNode
/** If set, the timer will display just the played out duration */
countUp?: boolean
+ /** Always show planned segment duration instead of counting up/down */
+ static?: boolean
}
/**
@@ -32,17 +34,18 @@ export const SegmentDuration = withTiming()(function
const duration = budget - playedOut
- return props.countUp ? (
+ return (
<>
{props.label}
- {RundownUtils.formatDiffToTimecode(playedOut, false, false, true, false, true, '+')}
- >
- ) : (
- <>
- {props.label}
-
- {RundownUtils.formatDiffToTimecode(duration, false, false, true, false, true, '+')}
-
+ {props.static ? (
+ {RundownUtils.formatDiffToTimecode(budget, false, false, true, false, true, '+')}
+ ) : props.countUp ? (
+ {RundownUtils.formatDiffToTimecode(playedOut, false, false, true, false, true, '+')}
+ ) : (
+
+ {RundownUtils.formatDiffToTimecode(duration, false, false, true, false, true, '+')}
+
+ )}
>
)
}
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
index 241b5badb2..b5b5992fbd 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
@@ -81,6 +81,7 @@ interface IProps {
isLastSegment: boolean
lastValidPartIndex: number | undefined
showCountdownToSegment: boolean
+ staticSegmentDuration: boolean | undefined
}
interface IStateHeader {
timelineWidth: number
@@ -1025,6 +1026,7 @@ export class SegmentTimelineClass extends React.Component, IS
{t('Duration')}}
+ static={this.props.staticSegmentDuration}
/>
)}
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
index b236104a95..103340f2cd 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
@@ -115,6 +115,7 @@ interface IProps {
ownNextPartInstance: PartInstance | undefined
rundownViewLayout: RundownViewLayout | undefined
countdownToSegmentRequireLayers: string[] | undefined
+ staticSegmentDuration: boolean | undefined
}
interface IState {
scrollLeft: number
@@ -963,6 +964,7 @@ export const SegmentTimelineContainer = translateWithTracker
)) ||
null
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index 4eaecc83d4..a3f2ab4738 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -213,6 +213,24 @@ export default withTranslation()(
{t('One of these sourcelayers must have a piece for the countdown to segment on-air to be show')}
+
+
+ {t('Static duration in Segment header')}
+
+
+ {t(
+ 'The segment duration in the segment header always displays the planned duration instead of acting as a counter'
+ )}
+
+
+
)
}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 5a61eacbdd..3c9e82d851 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -334,6 +334,8 @@ export interface RundownViewLayout extends RundownLayoutBase {
showBreaksAsSegments: boolean
/** Only count down to the segment if it contains pieces on these layers */
countdownToSegmentRequireLayers: string[]
+ /** Always show planned segment duration instead of counting up/down when the segment is live */
+ staticSegmentDuration: boolean
}
export interface RundownLayoutShelfBase extends RundownLayoutWithFilters {
From f65cb0cb6715c341ff9d3a602616af39cf99b076 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Thu, 29 Jul 2021 17:05:56 +0100
Subject: [PATCH 071/112] feat: Dashboard in clock view
---
meteor/client/ui/ClockView/ClockView.tsx | 2 +-
.../client/ui/ClockView/PresenterScreen.tsx | 158 +++++++++++++-
.../ui/Shelf/DashboardActionButtonGroup.tsx | 2 +-
.../client/ui/Shelf/ShelfDashboardLayout.tsx | 201 +++++++++---------
meteor/lib/api/rundownLayouts.ts | 26 +++
meteor/lib/collections/RundownLayouts.ts | 6 +
6 files changed, 288 insertions(+), 107 deletions(-)
diff --git a/meteor/client/ui/ClockView/ClockView.tsx b/meteor/client/ui/ClockView/ClockView.tsx
index 3408de16b3..3590126e23 100644
--- a/meteor/client/ui/ClockView/ClockView.tsx
+++ b/meteor/client/ui/ClockView/ClockView.tsx
@@ -53,7 +53,7 @@ export const ClockView = withTracker(function (props: IPropsHeader) {
{this.props.playlist ? (
-
+
) : (
diff --git a/meteor/client/ui/ClockView/PresenterScreen.tsx b/meteor/client/ui/ClockView/PresenterScreen.tsx
index 3623de608e..263c8a4900 100644
--- a/meteor/client/ui/ClockView/PresenterScreen.tsx
+++ b/meteor/client/ui/ClockView/PresenterScreen.tsx
@@ -3,12 +3,12 @@ import ClassNames from 'classnames'
import { DBSegment, Segment } from '../../../lib/collections/Segments'
import { PartUi } from '../SegmentTimeline/SegmentTimelineContainer'
import { RundownPlaylistId, RundownPlaylist, RundownPlaylists } from '../../../lib/collections/RundownPlaylists'
-import { ShowStyleBaseId, ShowStyleBases } from '../../../lib/collections/ShowStyleBases'
+import { ShowStyleBase, ShowStyleBaseId, ShowStyleBases } from '../../../lib/collections/ShowStyleBases'
import { Rundown, RundownId, Rundowns } from '../../../lib/collections/Rundowns'
import { withTranslation, WithTranslation } from 'react-i18next'
import { withTiming, WithTiming } from '../RundownView/RundownTiming/withTiming'
-import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
-import { extendMandadory, getCurrentTime } from '../../../lib/lib'
+import { Translated, withTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { extendMandadory, getCurrentTime, protectString, unprotectString } from '../../../lib/lib'
import { PartInstance } from '../../../lib/collections/PartInstances'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import { PubSub } from '../../../lib/api/pubsub'
@@ -21,6 +21,18 @@ import { PieceLifespan } from '@sofie-automation/blueprints-integration'
import { Part } from '../../../lib/collections/Parts'
import { PieceCountdownContainer } from '../PieceIcons/PieceCountdown'
import { PlaylistTiming } from '../../../lib/rundown/rundownTiming'
+import { parse as queryStringParse } from 'query-string'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import {
+ DashboardLayout,
+ RundownLayoutBase,
+ RundownLayoutId,
+ RundownLayoutPresenterView,
+ RundownLayouts,
+} from '../../../lib/collections/RundownLayouts'
+import { ShelfDashboardLayout } from '../Shelf/ShelfDashboardLayout'
+import { ShowStyleVariant, ShowStyleVariantId, ShowStyleVariants } from '../../../lib/collections/ShowStyleVariants'
+import { Studio, StudioId, Studios } from '../../../lib/collections/Studios'
interface SegmentUi extends DBSegment {
items: Array
@@ -31,21 +43,31 @@ interface TimeMap {
}
interface RundownOverviewProps {
+ studioId: StudioId
playlistId: RundownPlaylistId
segmentLiveDurations?: TimeMap
}
-interface RundownOverviewState {}
+interface RundownOverviewState {
+ presenterLayout: RundownLayoutPresenterView | undefined
+}
export interface RundownOverviewTrackedProps {
+ studio: Studio | undefined
playlist?: RundownPlaylist
+ rundowns: Rundown[]
segments: Array
currentSegment: SegmentUi | undefined
currentPartInstance: PartUi | undefined
nextSegment: SegmentUi | undefined
nextPartInstance: PartUi | undefined
currentShowStyleBaseId: ShowStyleBaseId | undefined
+ currentShowStyleBase: ShowStyleBase | undefined
+ currentShowStyleVariantId: ShowStyleVariantId | undefined
+ currentShowStyleVariant: ShowStyleVariant | undefined
nextShowStyleBaseId: ShowStyleBaseId | undefined
showStyleBaseIds: ShowStyleBaseId[]
rundownIds: RundownId[]
+ rundownLayouts?: Array
+ presenterLayoutId: RundownLayoutId | undefined
}
function getShowStyleBaseIdSegmentPartUi(
@@ -60,10 +82,16 @@ function getShowStyleBaseIdSegmentPartUi(
nextPartInstance: PartInstance | undefined
): {
showStyleBaseId: ShowStyleBaseId | undefined
+ showStyleBase: ShowStyleBase | undefined
+ showStyleVariantId: ShowStyleVariantId | undefined
+ showStyleVariant: ShowStyleVariant | undefined
segment: SegmentUi | undefined
partInstance: PartUi | undefined
} {
let showStyleBaseId: ShowStyleBaseId | undefined = undefined
+ let showStyleBase: ShowStyleBase | undefined = undefined
+ let showStyleVariantId: ShowStyleVariantId | undefined = undefined
+ let showStyleVariant: ShowStyleVariant | undefined = undefined
let segment: SegmentUi | undefined = undefined
let partInstanceUi: PartUi | undefined = undefined
@@ -72,17 +100,20 @@ function getShowStyleBaseIdSegmentPartUi(
_id: 1,
_rank: 1,
showStyleBaseId: 1,
+ showStyleVariantId: 1,
name: 1,
timing: 1,
},
})
showStyleBaseId = currentRundown?.showStyleBaseId
+ showStyleVariantId = currentRundown?.showStyleVariantId
const segmentIndex = orderedSegmentsAndParts.segments.findIndex((s) => s._id === partInstance.segmentId)
if (currentRundown && segmentIndex >= 0) {
const rundownOrder = playlist.getRundownIDs()
const rundownIndex = rundownOrder.indexOf(partInstance.rundownId)
- const showStyleBase = ShowStyleBases.findOne(showStyleBaseId)
+ showStyleBase = ShowStyleBases.findOne(showStyleBaseId)
+ showStyleVariant = ShowStyleVariants.findOne(showStyleVariantId)
if (showStyleBase) {
// This registers a reactive dependency on infinites-capping pieces, so that the segment can be
@@ -113,13 +144,18 @@ function getShowStyleBaseIdSegmentPartUi(
return {
showStyleBaseId: showStyleBaseId,
+ showStyleBase,
+ showStyleVariantId,
+ showStyleVariant,
segment: segment,
partInstance: partInstanceUi,
}
}
export const getPresenterScreenReactive = (props: RundownOverviewProps): RundownOverviewTrackedProps => {
+ const studio = Studios.findOne(props.studioId)
let playlist: RundownPlaylist | undefined
+
if (props.playlistId)
playlist = RundownPlaylists.findOne(props.playlistId, {
fields: {
@@ -140,11 +176,17 @@ export const getPresenterScreenReactive = (props: RundownOverviewProps): Rundown
let currentSegment: SegmentUi | undefined = undefined
let currentPartInstanceUi: PartUi | undefined = undefined
let currentShowStyleBaseId: ShowStyleBaseId | undefined = undefined
+ let currentShowStyleBase: ShowStyleBase | undefined = undefined
+ let currentShowStyleVariantId: ShowStyleVariantId | undefined = undefined
+ let currentShowStyleVariant: ShowStyleVariant | undefined = undefined
let nextSegment: SegmentUi | undefined = undefined
let nextPartInstanceUi: PartUi | undefined = undefined
let nextShowStyleBaseId: ShowStyleBaseId | undefined = undefined
+ const params = queryStringParse(location.search)
+ const presenterLayoutId = protectString((params['presenterLayout'] as string) || '')
+
if (playlist) {
rundowns = playlist.getRundowns()
const orderedSegmentsAndParts = playlist.getSegmentsAndPartsSync()
@@ -186,6 +228,9 @@ export const getPresenterScreenReactive = (props: RundownOverviewProps): Rundown
currentSegment = current.segment
currentPartInstanceUi = current.partInstance
currentShowStyleBaseId = current.showStyleBaseId
+ currentShowStyleBase = current.showStyleBase
+ currentShowStyleVariantId = current.showStyleVariantId
+ currentShowStyleVariant = current.showStyleVariant
}
if (nextPartInstance) {
@@ -204,16 +249,24 @@ export const getPresenterScreenReactive = (props: RundownOverviewProps): Rundown
}
}
return {
+ studio,
segments,
playlist,
+ rundowns,
showStyleBaseIds,
rundownIds,
currentSegment,
currentPartInstance: currentPartInstanceUi,
currentShowStyleBaseId,
+ currentShowStyleBase,
+ currentShowStyleVariantId,
+ currentShowStyleVariant,
nextSegment,
nextPartInstance: nextPartInstanceUi,
nextShowStyleBaseId,
+ rundownLayouts:
+ rundowns.length > 0 ? RundownLayouts.find({ showStyleBaseId: rundowns[0].showStyleBaseId }).fetch() : undefined,
+ presenterLayoutId,
}
}
@@ -223,6 +276,13 @@ export class PresenterScreenBase extends MeteorReactComponent<
> {
protected bodyClassList: string[] = ['dark', 'xdark']
+ constructor(props) {
+ super(props)
+ this.state = {
+ presenterLayout: undefined,
+ }
+ }
+
componentDidMount() {
document.body.classList.add(...this.bodyClassList)
this.subscribeToData()
@@ -230,6 +290,9 @@ export class PresenterScreenBase extends MeteorReactComponent<
protected subscribeToData() {
this.autorun(() => {
+ this.subscribe(PubSub.studios, {
+ _id: this.props.studioId,
+ })
const playlist = RundownPlaylists.findOne(this.props.playlistId, {
fields: {
_id: 1,
@@ -239,6 +302,13 @@ export class PresenterScreenBase extends MeteorReactComponent<
this.subscribe(PubSub.rundowns, {
playlistId: playlist._id,
})
+ const rundowns = playlist.getRundowns(undefined, {
+ fields: {
+ _id: 1,
+ showStyleBaseId: 1,
+ showStyleVariantId: 1,
+ },
+ }) as Pick[]
this.autorun(() => {
const rundownIds = playlist!.getRundownIDs()
@@ -249,6 +319,13 @@ export class PresenterScreenBase extends MeteorReactComponent<
},
}) as Pick[]
).map((r) => r.showStyleBaseId)
+ const showStyleVariantIds = (
+ playlist!.getRundowns(undefined, {
+ fields: {
+ showStyleVariantId: 1,
+ },
+ }) as Pick[]
+ ).map((r) => r.showStyleVariantId)
this.subscribe(PubSub.segments, {
rundownId: { $in: rundownIds },
@@ -265,6 +342,16 @@ export class PresenterScreenBase extends MeteorReactComponent<
$in: showStyleBaseIds,
},
})
+ this.subscribe(PubSub.showStyleVariants, {
+ _id: {
+ $in: showStyleVariantIds,
+ },
+ })
+ this.subscribe(PubSub.rundownLayouts, {
+ showStyleBaseId: {
+ $in: rundowns.map((i) => i.showStyleBaseId),
+ },
+ })
this.autorun(() => {
const playlistR = RundownPlaylists.findOne(this.props.playlistId, {
@@ -296,12 +383,51 @@ export class PresenterScreenBase extends MeteorReactComponent<
})
}
+ static getDerivedStateFromProps(
+ props: Translated
+ ): Partial {
+ let selectedPresenterLayout: RundownLayoutBase | undefined = undefined
+
+ if (props.rundownLayouts) {
+ // first try to use the one selected by the user
+ if (props.presenterLayoutId) {
+ selectedPresenterLayout = props.rundownLayouts.find((i) => i._id === props.presenterLayoutId)
+ }
+
+ // if couldn't find based on id, try matching part of the name
+ if (props.presenterLayoutId && !selectedPresenterLayout) {
+ selectedPresenterLayout = props.rundownLayouts.find(
+ (i) => i.name.indexOf(unprotectString(props.presenterLayoutId!)) >= 0
+ )
+ }
+
+ // if still not found, use the first one
+ if (!selectedPresenterLayout) {
+ selectedPresenterLayout = props.rundownLayouts.find((i) => RundownLayoutsAPI.isLayoutForPresenterView(i))
+ }
+ }
+
+ return {
+ presenterLayout:
+ selectedPresenterLayout && RundownLayoutsAPI.isLayoutForPresenterView(selectedPresenterLayout)
+ ? selectedPresenterLayout
+ : undefined,
+ }
+ }
+
componentWillUnmount() {
super.componentWillUnmount()
document.body.classList.remove(...this.bodyClassList)
}
render() {
+ if (this.state.presenterLayout && RundownLayoutsAPI.isDashboardLayout(this.state.presenterLayout)) {
+ return this.renderDashboardLayout(this.state.presenterLayout)
+ }
+ return this.renderDefaultLayout()
+ }
+
+ renderDefaultLayout() {
const { playlist, segments, currentShowStyleBaseId, nextShowStyleBaseId, playlistId } = this.props
if (playlist && playlistId && segments) {
@@ -422,6 +548,28 @@ export class PresenterScreenBase extends MeteorReactComponent<
}
return null
}
+
+ renderDashboardLayout(layout: DashboardLayout) {
+ const { studio, playlist, currentShowStyleBase, currentShowStyleVariant } = this.props
+
+ if (studio && playlist && currentShowStyleBase && currentShowStyleVariant) {
+ return (
+
+
+
+ )
+ }
+ return null
+ }
}
/**
diff --git a/meteor/client/ui/Shelf/DashboardActionButtonGroup.tsx b/meteor/client/ui/Shelf/DashboardActionButtonGroup.tsx
index 639ae7259b..36a5bcd6bf 100644
--- a/meteor/client/ui/Shelf/DashboardActionButtonGroup.tsx
+++ b/meteor/client/ui/Shelf/DashboardActionButtonGroup.tsx
@@ -14,7 +14,7 @@ export interface IDashboardButtonGroupProps {
studioMode: boolean
playlist: RundownPlaylist
- onChangeQueueAdLib: (isQueue: boolean, e: any) => void
+ onChangeQueueAdLib?: (isQueue: boolean, e: any) => void
}
export const DashboardActionButtonGroup = withTranslation()(
diff --git a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
index 22cfd2cde1..16e5558e9b 100644
--- a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
+++ b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
@@ -35,127 +35,128 @@ export interface IShelfDashboardLayoutProps {
studioMode: boolean
shouldQueue: boolean
studio: Studio
- onChangeQueueAdLib: (isQueue: boolean, e: any) => void
+ onChangeQueueAdLib?: (isQueue: boolean, e: any) => void
selectedPiece: BucketAdLibItem | IAdLibListItem | PieceUi | undefined
- onSelectPiece: (piece: AdLibPieceUi | PieceUi) => void
+ onSelectPiece?: (piece: AdLibPieceUi | PieceUi) => void
}
export function ShelfDashboardLayout(props: IShelfDashboardLayoutProps) {
const { rundownLayout } = props
return (
- {rundownLayout.filters
- .sort((a, b) => a.rank - b.rank)
- .map((panel) =>
- RundownLayoutsAPI.isFilter(panel) ? (
- (panel as DashboardLayoutFilter).showAsTimeline ? (
-
a.rank - b.rank)
+ .map((panel) =>
+ RundownLayoutsAPI.isFilter(panel) ? (
+ (panel as DashboardLayoutFilter).showAsTimeline ? (
+
+ ) : (
+
+ )
+ ) : RundownLayoutsAPI.isExternalFrame(panel) ? (
+
+ ) : RundownLayoutsAPI.isAdLibRegion(panel) ? (
+
- ) : (
-
+ ) : RundownLayoutsAPI.isPlaylistStartTimer(panel) ? (
+
+ ) : RundownLayoutsAPI.isPlaylistEndTimer(panel) ? (
+
+ ) : RundownLayoutsAPI.isEndWords(panel) ? (
+
+ ) : RundownLayoutsAPI.isSegmentTiming(panel) ? (
+
+ ) : RundownLayoutsAPI.isPartTiming(panel) ? (
+
+ ) : RundownLayoutsAPI.isTextLabel(panel) ? (
+
+ ) : RundownLayoutsAPI.isPlaylistName(panel) ? (
+
+ ) : RundownLayoutsAPI.isTimeOfDay(panel) ? (
+
+ ) : RundownLayoutsAPI.isSystemStatus(panel) ? (
+
- )
- ) : RundownLayoutsAPI.isExternalFrame(panel) ? (
-
- ) : RundownLayoutsAPI.isAdLibRegion(panel) ? (
-
- ) : RundownLayoutsAPI.isPieceCountdown(panel) ? (
-
- ) : RundownLayoutsAPI.isPlaylistStartTimer(panel) ? (
-
- ) : RundownLayoutsAPI.isPlaylistEndTimer(panel) ? (
-
- ) : RundownLayoutsAPI.isEndWords(panel) ? (
-
- ) : RundownLayoutsAPI.isSegmentTiming(panel) ? (
-
- ) : RundownLayoutsAPI.isPartTiming(panel) ? (
-
- ) : RundownLayoutsAPI.isTextLabel(panel) ? (
-
- ) : RundownLayoutsAPI.isPlaylistName(panel) ? (
-
- ) : RundownLayoutsAPI.isTimeOfDay(panel) ? (
-
- ) : RundownLayoutsAPI.isSystemStatus(panel) ? (
-
- ) : RundownLayoutsAPI.isShowStyleDisplay(panel) ? (
-
- ) : null
- )}
+ ) : RundownLayoutsAPI.isShowStyleDisplay(panel) ? (
+
+ ) : null
+ )}
{rundownLayout.actionButtons && (
= new Map()
private miniShelfLayouts: Map = new Map()
private rundownHeaderLayouts: Map = new Map()
+ private presenterViewLayouts: Map = new Map()
public registerShelfLayout(id: RundownLayoutType, description: LayoutDescriptor) {
this.shelfLayouts.set(id, description)
@@ -87,6 +89,10 @@ class RundownLayoutsRegistry {
this.rundownHeaderLayouts.set(id, description)
}
+ public registerPresenterViewLayout(id: RundownLayoutType, description: LayoutDescriptor) {
+ this.presenterViewLayouts.set(id, description)
+ }
+
public isShelfLayout(regionId: CustomizableRegions) {
return regionId === CustomizableRegions.Shelf
}
@@ -103,6 +109,10 @@ class RundownLayoutsRegistry {
return regionId === CustomizableRegions.RundownHeader
}
+ public isPresenterViewLayout(regionId: CustomizableRegions) {
+ return regionId === CustomizableRegions.PresenterView
+ }
+
private wrapToCustomizableRegionLayout(
layouts: Map
): CustomizableRegionLayout[] {
@@ -137,6 +147,11 @@ class RundownLayoutsRegistry {
title: t('Rundown Header Layouts'),
layouts: this.wrapToCustomizableRegionLayout(this.rundownHeaderLayouts),
},
+ {
+ _id: CustomizableRegions.PresenterView,
+ title: t('Presenter View Layouts'),
+ layouts: this.wrapToCustomizableRegionLayout(this.presenterViewLayouts),
+ },
]
}
}
@@ -189,6 +204,13 @@ export namespace RundownLayoutsAPI {
RundownLayoutElementType.SYSTEM_STATUS,
],
})
+ registry.registerPresenterViewLayout(RundownLayoutType.CLOCK_PRESENTER_VIEW_LAYOUT, {
+ supportedFilters: [],
+ })
+ registry.registerPresenterViewLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
+ filtersTitle: 'Layout Elements',
+ supportedFilters: [RundownLayoutElementType.PIECE_COUNTDOWN],
+ })
export function getSettingsManifest(t: TFunction): CustomizableRegionSettingsManifest[] {
return registry.GetSettingsManifest(t)
@@ -202,6 +224,10 @@ export namespace RundownLayoutsAPI {
return registry.isShelfLayout(layout.regionId)
}
+ export function isLayoutForPresenterView(layout: RundownLayoutBase): layout is RundownLayoutPresenterView {
+ return registry.isPresenterViewLayout(layout.regionId)
+ }
+
export function isLayoutForRundownView(layout: RundownLayoutBase): layout is RundownViewLayout {
return registry.isRudownViewLayout(layout.regionId)
}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 5a61eacbdd..1cf0e30768 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -20,6 +20,7 @@ export enum RundownLayoutType {
RUNDOWN_LAYOUT = 'rundown_layout',
DASHBOARD_LAYOUT = 'dashboard_layout',
RUNDOWN_HEADER_LAYOUT = 'rundown_header_layout',
+ CLOCK_PRESENTER_VIEW_LAYOUT = 'clock_presenter_view_layout',
}
export enum CustomizableRegions {
@@ -27,6 +28,7 @@ export enum CustomizableRegions {
Shelf = 'shelf_layouts',
MiniShelf = 'mini_shelf_layouts',
RundownHeader = 'rundown_header_layouts',
+ PresenterView = 'presenter_view_layouts',
}
/**
@@ -358,6 +360,10 @@ export interface RundownLayoutRundownHeader extends RundownLayoutBase {
lastRundownIsNotBreak: boolean
}
+export interface RundownLayoutPresenterView extends RundownLayoutBase {
+ type: RundownLayoutType.CLOCK_PRESENTER_VIEW_LAYOUT
+}
+
export enum ActionButtonType {
TAKE = 'take',
HOLD = 'hold',
From 33bea59c534edb3483460fd360b512e578e722ae Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Fri, 30 Jul 2021 16:55:10 +0100
Subject: [PATCH 072/112] feat: Clock panels
---
meteor/client/styles/countdown/presenter.scss | 146 ++++++++++++++++++
.../client/styles/shelf/studioNamePanel.scss | 34 ++++
.../RundownTiming/PlaylistEndTiming.tsx | 20 ++-
.../ui/Settings/components/FilterEditor.tsx | 106 ++++++++++++-
meteor/client/ui/Shelf/EndWordsPanel.tsx | 2 +-
meteor/client/ui/Shelf/PartTimingPanel.tsx | 10 +-
.../client/ui/Shelf/PieceCountdownPanel.tsx | 4 +-
.../client/ui/Shelf/PlaylistEndTimerPanel.tsx | 4 +-
meteor/client/ui/Shelf/PlaylistNamePanel.tsx | 8 +-
.../ui/Shelf/PlaylistStartTimerPanel.tsx | 2 +-
meteor/client/ui/Shelf/SegmentTimingPanel.tsx | 10 +-
.../client/ui/Shelf/ShelfDashboardLayout.tsx | 9 ++
meteor/client/ui/Shelf/ShowStylePanel.tsx | 2 +-
meteor/client/ui/Shelf/StudioNamePanel.tsx | 56 +++++++
meteor/client/ui/Shelf/SystemStatusPanel.tsx | 2 +-
meteor/client/ui/Shelf/TextLabelPanel.tsx | 2 +-
meteor/client/ui/Shelf/TimeOfDayPanel.tsx | 4 +-
meteor/lib/api/rundownLayouts.ts | 15 +-
meteor/lib/collections/RundownLayouts.ts | 29 ++++
19 files changed, 434 insertions(+), 31 deletions(-)
create mode 100644 meteor/client/styles/shelf/studioNamePanel.scss
create mode 100644 meteor/client/ui/Shelf/StudioNamePanel.tsx
diff --git a/meteor/client/styles/countdown/presenter.scss b/meteor/client/styles/countdown/presenter.scss
index 8461d4f28f..af30501ba8 100644
--- a/meteor/client/styles/countdown/presenter.scss
+++ b/meteor/client/styles/countdown/presenter.scss
@@ -1,4 +1,6 @@
@import '../colorScheme';
+$liveline-timecode-color: $general-countdown-to-next-color; //$general-live-color;
+$hold-status-color: $liveline-timecode-color;
.presenter-screen {
position: fixed;
@@ -181,4 +183,148 @@
.clocks-counter-heavy {
font-weight: 600;
}
+
+ .dashboard {
+ .timing {
+ margin: 0 0;
+ min-width: auto;
+ width: 100%;
+ text-align: center;
+
+ .timing-clock {
+ position: relative;
+ margin-right: 1em;
+ font-family: 'Roboto', Helvetica Neue, Arial, sans-serif;
+ font-weight: 100;
+ color: $general-clock;
+ font-size: 1.5em;
+ margin-top: 0.8em;
+ word-break: keep-all;
+ white-space: nowrap;
+
+ &.visual-last-child {
+ margin-right: 0;
+ }
+
+ &.countdown {
+ font-weight: 400;
+ }
+
+ &.playback-started {
+ display: inline-block;
+ width: 25%;
+ }
+
+ &.left {
+ text-align: left;
+ }
+
+ &.time-now {
+ position: absolute;
+ top: 0.05em;
+ left: 50%;
+ transform: translateX(-50%);
+ margin-top: 0px;
+ margin-right: 0;
+ font-size: 2.3em;
+ font-weight: 100;
+ text-align: center;
+ }
+
+ &.current-remaining {
+ position: absolute;
+ left: calc(50% + 3.5em);
+ text-align: left;
+ color: $liveline-timecode-color;
+ font-weight: 500;
+
+ .overtime {
+ color: $general-fast-color;
+ text-shadow: 0px 0px 6px $general-fast-color--shadow;
+ }
+ }
+
+ .timing-clock-label {
+ position: absolute;
+ top: -1em;
+ color: #b8b8b8;
+ text-transform: uppercase;
+ white-space: nowrap;
+ font-weight: 300;
+ font-size: 0.5em;
+
+ &.left {
+ left: 0;
+ right: auto;
+ text-align: left;
+ }
+
+ &.right {
+ right: 0;
+ left: auto;
+ text-align: right;
+ }
+
+ &.hide-overflow {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+ }
+
+ &.rundown-name {
+ width: auto;
+ max-width: calc(40vw - 138px);
+ min-width: 100%;
+
+ > strong {
+ margin-right: 0.4em;
+ }
+
+ > svg.icon.looping {
+ width: 1.4em;
+ height: 1.4em;
+ }
+ }
+ }
+
+ &.heavy-light {
+ font-weight: 600;
+
+ &.heavy {
+ // color: $general-late-color;
+ color: #ffe900;
+ background: none;
+ }
+
+ &.light {
+ color: $general-fast-color;
+ text-shadow: 0px 0px 6px $general-fast-color--shadow;
+ background: none;
+ }
+ }
+ }
+
+ .rundown__header-status {
+ position: absolute;
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ background: #fff;
+ border-radius: 1rem;
+ line-height: 1em;
+ font-weight: 700;
+ color: #000;
+ top: 2.4em;
+ left: 0;
+ padding: 2px 5px 1px;
+
+ &.rundown__header-status--hold {
+ background: $hold-status-color;
+ }
+ }
+
+ .timing-clock-header-label {
+ font-weight: 100px;
+ }
+ }
+ }
}
diff --git a/meteor/client/styles/shelf/studioNamePanel.scss b/meteor/client/styles/shelf/studioNamePanel.scss
new file mode 100644
index 0000000000..96db7fbc31
--- /dev/null
+++ b/meteor/client/styles/shelf/studioNamePanel.scss
@@ -0,0 +1,34 @@
+.studio-name-panel {
+ position: absolute;
+ margin: 0 0;
+ min-width: auto;
+ text-align: center;
+ isolation: isolate;
+
+ .wrapper {
+ position: relative;
+ margin-right: 1em;
+ font-family: 'Roboto', Helvetica Neue, Arial, sans-serif;
+ font-weight: 100;
+ margin-top: 0.8em;
+ word-break: keep-all;
+ white-space: nowrap;
+ text-align: left;
+ font-size: 1.5em;
+ width: 100%;
+
+ .studio-name {
+ position: absolute;
+ top: -1em;
+ color: #b8b8b8;
+ text-transform: uppercase;
+ white-space: nowrap;
+ font-weight: 600;
+ font-size: 0.5em;
+
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+}
diff --git a/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx
index 372438206c..f19f665441 100644
--- a/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx
@@ -15,6 +15,8 @@ interface IEndTimingProps {
expectedDuration?: number
expectedEnd?: number
endLabel?: string
+ hidePlannedEndLabel?: boolean
+ hideDiffLabel?: boolean
hidePlannedEnd?: boolean
hideCountdown?: boolean
hideDiff?: boolean
@@ -34,12 +36,16 @@ export const PlaylistEndTiming = withTranslation()(
this.props.expectedEnd ? (
!rundownPlaylist.startedPlayback ? (
- {this.props.endLabel ?? t('Planned End')}
+ {!this.props.hidePlannedEndLabel && (
+ {this.props.endLabel ?? t('Planned End')}
+ )}
) : (
- {this.props.endLabel ?? t('Expected End')}
+ {!this.props.hidePlannedEndLabel && (
+ {this.props.endLabel ?? t('Expected End')}
+ )}
)
@@ -49,7 +55,9 @@ export const PlaylistEndTiming = withTranslation()(
rundownPlaylist.activationId &&
rundownPlaylist.currentPartInstanceId ? (
- {t('Next Loop at')}
+ {!this.props.hidePlannedEndLabel && (
+ {t('Next Loop at')}
+ )}
- {this.props.endLabel ?? t('Expected End')}
+ {!this.props.hidePlannedEndLabel && (
+ {this.props.endLabel ?? t('Expected End')}
+ )}
- {t('Diff')}
+ {!this.props.hideDiffLabel && {t('Diff')} }
{RundownUtils.formatDiffToTimecode(
(this.props.timingDurations.asPlayedPlaylistDuration || 0) -
(expectedDuration ?? this.props.timingDurations.totalPlaylistDuration ?? 0),
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index 2f7c9c8640..448972b43e 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -23,6 +23,7 @@ import {
RundownLayouts,
RundownLayoutSegmentTiming,
RundownLayoutShowStyleDisplay,
+ RundownLayoutStudioName,
RundownLayoutSytemStatus,
RundownLayoutTextLabel,
RundownLayoutTimeOfDay,
@@ -885,6 +886,32 @@ export default withTranslation()(
>
{t('Text to show above countdown to end of show')}
+
+
+ {t('Hide Planned End Label')}
+
+
+
+
+
+ {t('Hide Diff Label')}
+
+
+
{t('Hide Diff')}
@@ -988,13 +1015,26 @@ export default withTranslation()(
>
+
+
+ {t('Hide Label')}
+
+
+
{this.renderRequiresActiveLayerSettings(item, index, t('Require Piece on Source Layer'), '')}
{isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
)
}
- renderPartCountDown(
+ renderPartTiming(
item: RundownLayoutBase,
tab: RundownLayoutPartTiming,
index: number,
@@ -1019,6 +1059,19 @@ export default withTranslation()(
>
+
+
+ {t('Hide Label')}
+
+
+
{this.renderRequiresActiveLayerSettings(item, index, t('Require Piece on Source Layer'), '')}
{isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
@@ -1107,6 +1160,34 @@ export default withTranslation()(
)
}
+ renderStudioName(
+ item: RundownLayoutBase,
+ tab: RundownLayoutStudioName,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
renderShowStyleDisplay(
item: RundownLayoutBase,
tab: RundownLayoutShowStyleDisplay,
@@ -1186,6 +1267,19 @@ export default withTranslation()(
/>
+
+
+ {t('Hide Label')}
+
+
+
{isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
)
@@ -1456,7 +1550,7 @@ export default withTranslation()(
isDashboardLayout
)
: RundownLayoutsAPI.isPartTiming(this.props.filter)
- ? this.renderPartCountDown(
+ ? this.renderPartTiming(
this.props.item,
this.props.filter,
this.props.index,
@@ -1479,6 +1573,14 @@ export default withTranslation()(
isRundownLayout,
isDashboardLayout
)
+ : RundownLayoutsAPI.isStudioName(this.props.filter)
+ ? this.renderStudioName(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
: RundownLayoutsAPI.isTimeOfDay(this.props.filter)
? this.renderTimeOfDay(
this.props.item,
diff --git a/meteor/client/ui/Shelf/EndWordsPanel.tsx b/meteor/client/ui/Shelf/EndWordsPanel.tsx
index d9beb26228..f0b1b707b1 100644
--- a/meteor/client/ui/Shelf/EndWordsPanel.tsx
+++ b/meteor/client/ui/Shelf/EndWordsPanel.tsx
@@ -50,7 +50,7 @@ export class EndWordsPanelInner extends MeteorReactComponent<
style={_.extend(
isDashboardLayout
? {
- ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutEndsWords), height: 1 }),
+ ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutEndsWords) }),
fontSize: ((panel as DashboardLayoutEndsWords).scale || 1) * 1.5 + 'em',
}
: {}
diff --git a/meteor/client/ui/Shelf/PartTimingPanel.tsx b/meteor/client/ui/Shelf/PartTimingPanel.tsx
index 6146a3f81d..cf3a299a9f 100644
--- a/meteor/client/ui/Shelf/PartTimingPanel.tsx
+++ b/meteor/client/ui/Shelf/PartTimingPanel.tsx
@@ -48,16 +48,18 @@ class PartTimingPanelInner extends MeteorReactComponent<
style={_.extend(
isDashboardLayout
? {
- ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutPartCountDown), height: 1 }),
+ ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutPartCountDown) }),
fontSize: ((panel as DashboardLayoutPartCountDown).scale || 1) * 1.5 + 'em',
}
: {}
)}
>
-
- {panel.timingType === 'count_down' ? t('Part Count Down') : t('Part Count Up')}
-
+ {!panel.hideLabel && (
+
+ {panel.timingType === 'count_down' ? t('Part Count Down') : t('Part Count Up')}
+
+ )}
{this.props.active &&
(panel.timingType === 'count_down' ? (
{
constructor(props) {
@@ -43,7 +43,7 @@ class PlaylistNamePanelInner extends MeteorReactComponent<
style={_.extend(
isDashboardLayout
? {
- ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutPlaylistName), height: 1 }),
+ ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutPlaylistName) }),
fontSize: ((panel as DashboardLayoutPlaylistName).scale || 1) * 1.5 + 'em',
}
: {}
@@ -60,7 +60,7 @@ class PlaylistNamePanelInner extends MeteorReactComponent<
}
}
-export const PlaylistNamePanel = withTracker(
+export const PlaylistNamePanel = withTracker(
(props: IPlaylistNamePanelProps) => {
if (props.playlist.currentPartInstanceId) {
const livePart = props.playlist.getActivePartInstances({ _id: props.playlist.currentPartInstanceId })[0]
diff --git a/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx b/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
index 2bb96d7689..e473bf15e6 100644
--- a/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
+++ b/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
@@ -45,7 +45,7 @@ export class PlaylistStartTimerPanelInner extends MeteorReactComponent<
style={_.extend(
isDashboardLayout
? {
- ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutPlaylistStartTimer), height: 1 }),
+ ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutPlaylistStartTimer) }),
fontSize: ((this.props.panel as DashboardLayoutPlaylistStartTimer).scale || 1) * 1.5 + 'em',
}
: {}
diff --git a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
index 17880f2c5c..f406eab9aa 100644
--- a/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
+++ b/meteor/client/ui/Shelf/SegmentTimingPanel.tsx
@@ -54,16 +54,18 @@ class SegmentTimingPanelInner extends MeteorReactComponent<
style={_.extend(
isDashboardLayout
? {
- ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutSegmentCountDown), height: 1 }),
+ ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutSegmentCountDown) }),
fontSize: ((panel as DashboardLayoutSegmentCountDown).scale || 1) * 1.5 + 'em',
}
: {}
)}
>
-
- {panel.timingType === 'count_down' ? t('Segment Count Down') : t('Segment Count Up')}
-
+ {!panel.hideLabel && (
+
+ {panel.timingType === 'count_down' ? t('Segment Count Down') : t('Segment Count Up')}
+
+ )}
{this.props.active && this.props.parts && (
)}
diff --git a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
index 16e5558e9b..1be3cab391 100644
--- a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
+++ b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
@@ -25,6 +25,7 @@ import { TimeOfDayPanel } from './TimeOfDayPanel'
import { SystemStatusPanel } from './SystemStatusPanel'
import { ShowStylePanel } from './ShowStylePanel'
import { ShowStyleVariant } from '../../../lib/collections/ShowStyleVariants'
+import { StudioNamePanel } from './StudioNamePanel'
export interface IShelfDashboardLayoutProps {
rundownLayout: DashboardLayout
@@ -136,6 +137,14 @@ export function ShelfDashboardLayout(props: IShelfDashboardLayoutProps) {
) : RundownLayoutsAPI.isPlaylistName(panel) ? (
+ ) : RundownLayoutsAPI.isStudioName(panel) ? (
+
) : RundownLayoutsAPI.isTimeOfDay(panel) ? (
) : RundownLayoutsAPI.isSystemStatus(panel) ? (
diff --git a/meteor/client/ui/Shelf/ShowStylePanel.tsx b/meteor/client/ui/Shelf/ShowStylePanel.tsx
index c688661c68..16d4078038 100644
--- a/meteor/client/ui/Shelf/ShowStylePanel.tsx
+++ b/meteor/client/ui/Shelf/ShowStylePanel.tsx
@@ -40,7 +40,7 @@ class ShowStylePanelInner extends MeteorReactComponent {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ const { panel } = this.props
+
+ return (
+
+
+ {this.props.studio.name}
+
+
+ )
+ }
+}
diff --git a/meteor/client/ui/Shelf/SystemStatusPanel.tsx b/meteor/client/ui/Shelf/SystemStatusPanel.tsx
index ea7121bc47..ce157023e1 100644
--- a/meteor/client/ui/Shelf/SystemStatusPanel.tsx
+++ b/meteor/client/ui/Shelf/SystemStatusPanel.tsx
@@ -47,7 +47,7 @@ class SystemStatusPanelInner extends MeteorReactComponent<
style={_.extend(
isDashboardLayout
? {
- ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutSystemStatus), height: 1 }),
+ ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutSystemStatus) }),
fontSize: ((panel as DashboardLayoutSystemStatus).scale || 1) * 1.5 + 'em',
}
: {}
diff --git a/meteor/client/ui/Shelf/TextLabelPanel.tsx b/meteor/client/ui/Shelf/TextLabelPanel.tsx
index 1231326e93..9fcfa0acfc 100644
--- a/meteor/client/ui/Shelf/TextLabelPanel.tsx
+++ b/meteor/client/ui/Shelf/TextLabelPanel.tsx
@@ -34,7 +34,7 @@ export class TextLabelPanel extends MeteorReactComponent
- {t('Local Time')}
+ {!panel.hideLabel && {t('Local Time')} }
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index f91610e4e1..15638447c3 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -27,6 +27,7 @@ import {
RundownLayoutShowStyleDisplay,
RundownLayoutWithFilters,
RundownLayoutPresenterView,
+ RundownLayoutStudioName,
} from '../collections/RundownLayouts'
import { ShowStyleBaseId } from '../collections/ShowStyleBases'
import * as _ from 'underscore'
@@ -209,7 +210,15 @@ export namespace RundownLayoutsAPI {
})
registry.registerPresenterViewLayout(RundownLayoutType.DASHBOARD_LAYOUT, {
filtersTitle: 'Layout Elements',
- supportedFilters: [RundownLayoutElementType.PIECE_COUNTDOWN],
+ supportedFilters: [
+ RundownLayoutElementType.PART_TIMING,
+ RundownLayoutElementType.TEXT_LABEL,
+ RundownLayoutElementType.SEGMENT_TIMING,
+ RundownLayoutElementType.PLAYLIST_END_TIMER,
+ RundownLayoutElementType.TIME_OF_DAY,
+ RundownLayoutElementType.PLAYLIST_NAME,
+ RundownLayoutElementType.STUDIO_NAME,
+ ],
})
export function getSettingsManifest(t: TFunction): CustomizableRegionSettingsManifest[] {
@@ -302,6 +311,10 @@ export namespace RundownLayoutsAPI {
return element.type === RundownLayoutElementType.PLAYLIST_NAME
}
+ export function isStudioName(element: RundownLayoutElementBase): element is RundownLayoutStudioName {
+ return element.type === RundownLayoutElementType.STUDIO_NAME
+ }
+
export function isTimeOfDay(element: RundownLayoutElementBase): element is RundownLayoutTimeOfDay {
return element.type === RundownLayoutElementType.TIME_OF_DAY
}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 1cf0e30768..0ea1444771 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -54,6 +54,7 @@ export enum RundownLayoutElementType {
PART_TIMING = 'part_timing',
TEXT_LABEL = 'text_label',
PLAYLIST_NAME = 'playlist_name',
+ STUDIO_NAME = 'studio_name',
TIME_OF_DAY = 'time_of_day',
SYSTEM_STATUS = 'system_status',
SHOWSTYLE_DISPLAY = 'showstyle_display',
@@ -117,6 +118,8 @@ export interface RundownLayoutPlaylistStartTimer extends RundownLayoutElementBas
export interface RundownLayoutPlaylistEndTimer extends RundownLayoutElementBase {
type: RundownLayoutElementType.PLAYLIST_END_TIMER
expectedEndText: string
+ hidePlannedEndLabel: boolean
+ hideDiffLabel: boolean
hideCountdown: boolean
hideDiff: boolean
hidePlannedEnd: boolean
@@ -129,12 +132,14 @@ export interface RundownLayoutEndWords extends RundownLayoutElementBase, Require
export interface RundownLayoutSegmentTiming extends RundownLayoutElementBase, RequiresActiveLayers {
type: RundownLayoutElementType.SEGMENT_TIMING
timingType: 'count_down' | 'count_up'
+ hideLabel: boolean
}
export interface RundownLayoutPartTiming extends RundownLayoutElementBase, RequiresActiveLayers {
type: RundownLayoutElementType.PART_TIMING
timingType: 'count_down' | 'count_up'
speakCountDown: boolean
+ hideLabel: boolean
}
export interface RundownLayoutTextLabel extends RundownLayoutElementBase {
@@ -147,8 +152,13 @@ export interface RundownLayoutPlaylistName extends RundownLayoutElementBase {
showCurrentRundownName: boolean
}
+export interface RundownLayoutStudioName extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.STUDIO_NAME
+}
+
export interface RundownLayoutTimeOfDay extends RundownLayoutElementBase {
type: RundownLayoutElementType.TIME_OF_DAY
+ hideLabel: boolean
}
export interface RundownLayoutSytemStatus extends RundownLayoutElementBase {
@@ -208,6 +218,7 @@ export interface DashboardLayoutAdLibRegion extends RundownLayoutAdLibRegion {
export interface DashboardLayoutPieceCountdown extends RundownLayoutPieceCountdown {
x: number
y: number
+ height: number
width: number
scale: number
}
@@ -215,6 +226,7 @@ export interface DashboardLayoutPieceCountdown extends RundownLayoutPieceCountdo
export interface DashboardLayoutPlaylistStartTimer extends RundownLayoutPlaylistStartTimer {
x: number
y: number
+ height: number
width: number
scale: number
}
@@ -222,6 +234,7 @@ export interface DashboardLayoutPlaylistStartTimer extends RundownLayoutPlaylist
export interface DashboardLayoutPlaylistEndTimer extends RundownLayoutPlaylistEndTimer {
x: number
y: number
+ height: number
width: number
scale: number
}
@@ -229,6 +242,7 @@ export interface DashboardLayoutPlaylistEndTimer extends RundownLayoutPlaylistEn
export interface DashboardLayoutEndsWords extends RundownLayoutEndWords {
x: number
y: number
+ height: number
width: number
scale: number
}
@@ -236,6 +250,7 @@ export interface DashboardLayoutEndsWords extends RundownLayoutEndWords {
export interface DashboardLayoutSegmentCountDown extends RundownLayoutSegmentTiming {
x: number
y: number
+ height: number
width: number
scale: number
}
@@ -243,6 +258,7 @@ export interface DashboardLayoutSegmentCountDown extends RundownLayoutSegmentTim
export interface DashboardLayoutPartCountDown extends RundownLayoutPartTiming {
x: number
y: number
+ height: number
width: number
scale: number
}
@@ -250,6 +266,7 @@ export interface DashboardLayoutPartCountDown extends RundownLayoutPartTiming {
export interface DashboardLayoutTextLabel extends RundownLayoutTextLabel {
x: number
y: number
+ height: number
width: number
scale: number
}
@@ -257,6 +274,15 @@ export interface DashboardLayoutTextLabel extends RundownLayoutTextLabel {
export interface DashboardLayoutPlaylistName extends RundownLayoutPlaylistName {
x: number
y: number
+ height: number
+ width: number
+ scale: number
+}
+
+export interface DashboardLayoutStudioName extends RundownLayoutStudioName {
+ x: number
+ y: number
+ height: number
width: number
scale: number
}
@@ -264,6 +290,7 @@ export interface DashboardLayoutPlaylistName extends RundownLayoutPlaylistName {
export interface DashboardLayoutTimeOfDay extends RundownLayoutTimeOfDay {
x: number
y: number
+ height: number
width: number
scale: number
}
@@ -271,6 +298,7 @@ export interface DashboardLayoutTimeOfDay extends RundownLayoutTimeOfDay {
export interface DashboardLayoutSystemStatus extends RundownLayoutSytemStatus {
x: number
y: number
+ height: number
width: number
scale: number
}
@@ -278,6 +306,7 @@ export interface DashboardLayoutSystemStatus extends RundownLayoutSytemStatus {
export interface DashboardLayoutShowStyleDisplay extends RundownLayoutShowStyleDisplay {
x: number
y: number
+ height: number
width: number
scale: number
}
From f8b5443699fd13f2b31f28aa80c4a93eca5edaff Mon Sep 17 00:00:00 2001
From: ianshade
Date: Mon, 2 Aug 2021 12:14:52 +0200
Subject: [PATCH 073/112] chore: rename static- to fixed-
---
meteor/client/ui/RundownView.tsx | 2 +-
.../client/ui/RundownView/RundownTiming/SegmentDuration.tsx | 4 ++--
meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx | 4 ++--
meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx | 4 ++--
.../components/rundownLayouts/RundownViewLayoutSettings.tsx | 4 ++--
meteor/lib/collections/RundownLayouts.ts | 2 +-
6 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index ef77b0716b..cf2e5a1719 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -2334,7 +2334,7 @@ export const RundownView = translateWithTracker((
countdownToSegmentRequireLayers={
this.state.rundownViewLayout?.countdownToSegmentRequireLayers
}
- staticSegmentDuration={this.state.rundownViewLayout?.staticSegmentDuration}
+ fixedSegmentDuration={this.state.rundownViewLayout?.fixedSegmentDuration}
/>
diff --git a/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx b/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx
index 74ad1c9e0e..4deb9e4c03 100644
--- a/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx
@@ -10,7 +10,7 @@ interface ISegmentDurationProps {
/** If set, the timer will display just the played out duration */
countUp?: boolean
/** Always show planned segment duration instead of counting up/down */
- static?: boolean
+ fixed?: boolean
}
/**
@@ -37,7 +37,7 @@ export const SegmentDuration = withTiming()(function
return (
<>
{props.label}
- {props.static ? (
+ {props.fixed ? (
{RundownUtils.formatDiffToTimecode(budget, false, false, true, false, true, '+')}
) : props.countUp ? (
{RundownUtils.formatDiffToTimecode(playedOut, false, false, true, false, true, '+')}
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
index b5b5992fbd..b33b856877 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
@@ -81,7 +81,7 @@ interface IProps {
isLastSegment: boolean
lastValidPartIndex: number | undefined
showCountdownToSegment: boolean
- staticSegmentDuration: boolean | undefined
+ fixedSegmentDuration: boolean | undefined
}
interface IStateHeader {
timelineWidth: number
@@ -1026,7 +1026,7 @@ export class SegmentTimelineClass extends React.Component, IS
{t('Duration')} }
- static={this.props.staticSegmentDuration}
+ fixed={this.props.fixedSegmentDuration}
/>
)}
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
index 103340f2cd..4fd135b4e6 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
@@ -115,7 +115,7 @@ interface IProps {
ownNextPartInstance: PartInstance | undefined
rundownViewLayout: RundownViewLayout | undefined
countdownToSegmentRequireLayers: string[] | undefined
- staticSegmentDuration: boolean | undefined
+ fixedSegmentDuration: boolean | undefined
}
interface IState {
scrollLeft: number
@@ -964,7 +964,7 @@ export const SegmentTimelineContainer = translateWithTracker
)) ||
null
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index a3f2ab4738..10bf1a63f0 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -215,10 +215,10 @@ export default withTranslation()(
- {t('Static duration in Segment header')}
+ {t('Fixed duration in Segment header')}
Date: Mon, 2 Aug 2021 16:07:30 +0100
Subject: [PATCH 074/112] feat: Part name panel, colored box elements
---
.../styles/shelf/colored-box-panel.scss | 22 +++
meteor/client/styles/shelf/partNamePanel.scss | 39 ++++
.../client/styles/shelf/segmentNamePanel.scss | 34 ++++
meteor/client/ui/PieceIcons/PieceIcon.tsx | 6 +-
meteor/client/ui/PieceIcons/utils.ts | 2 +-
.../ui/Settings/components/FilterEditor.tsx | 168 ++++++++++++++++++
meteor/client/ui/Shelf/ColoredBoxPanel.tsx | 61 +++++++
meteor/client/ui/Shelf/PartNamePanel.tsx | 135 ++++++++++++++
meteor/client/ui/Shelf/SegmentNamePanel.tsx | 81 +++++++++
.../client/ui/Shelf/ShelfDashboardLayout.tsx | 15 ++
meteor/lib/api/rundownLayouts.ts | 18 ++
meteor/lib/collections/RundownLayouts.ts | 43 +++++
12 files changed, 620 insertions(+), 4 deletions(-)
create mode 100644 meteor/client/styles/shelf/colored-box-panel.scss
create mode 100644 meteor/client/styles/shelf/partNamePanel.scss
create mode 100644 meteor/client/styles/shelf/segmentNamePanel.scss
create mode 100644 meteor/client/ui/Shelf/ColoredBoxPanel.tsx
create mode 100644 meteor/client/ui/Shelf/PartNamePanel.tsx
create mode 100644 meteor/client/ui/Shelf/SegmentNamePanel.tsx
diff --git a/meteor/client/styles/shelf/colored-box-panel.scss b/meteor/client/styles/shelf/colored-box-panel.scss
new file mode 100644
index 0000000000..556a0ccf22
--- /dev/null
+++ b/meteor/client/styles/shelf/colored-box-panel.scss
@@ -0,0 +1,22 @@
+.colored-box-panel {
+ position: absolute;
+ margin: 0 0;
+ min-width: auto;
+ text-align: center;
+ isolation: isolate;
+ padding: 1vw 1vh;
+
+ .wrapper {
+ content: '';
+ position: relative;
+ margin-right: 1em;
+ font-family: 'Roboto', Helvetica Neue, Arial, sans-serif;
+ font-weight: 100;
+ margin-top: 0.8em;
+ word-break: keep-all;
+ white-space: nowrap;
+ text-align: left;
+ font-size: 1.5em;
+ width: 100%;
+ }
+}
diff --git a/meteor/client/styles/shelf/partNamePanel.scss b/meteor/client/styles/shelf/partNamePanel.scss
new file mode 100644
index 0000000000..13e0739c46
--- /dev/null
+++ b/meteor/client/styles/shelf/partNamePanel.scss
@@ -0,0 +1,39 @@
+@import '../itemTypeColors';
+
+.part-name-panel {
+ position: absolute;
+ margin: 0 0;
+ min-width: auto;
+ text-align: center;
+ isolation: isolate;
+ padding: 1vw 1vh;
+
+ @include item-type-colors();
+
+ .wrapper {
+ position: relative;
+ margin-right: 1em;
+ font-family: 'Roboto', Helvetica Neue, Arial, sans-serif;
+ font-weight: 100;
+ margin-top: 0.8em;
+ word-break: keep-all;
+ white-space: nowrap;
+ text-align: left;
+ font-size: 1.5em;
+ width: 100%;
+
+ .part-name-title {
+ position: absolute;
+ top: -1em;
+ color: #f0f0f0;
+ text-transform: uppercase;
+ white-space: nowrap;
+ font-weight: 600;
+ font-size: 0.5em;
+
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+}
diff --git a/meteor/client/styles/shelf/segmentNamePanel.scss b/meteor/client/styles/shelf/segmentNamePanel.scss
new file mode 100644
index 0000000000..98deab2310
--- /dev/null
+++ b/meteor/client/styles/shelf/segmentNamePanel.scss
@@ -0,0 +1,34 @@
+.segment-name-panel {
+ position: absolute;
+ margin: 0 0;
+ min-width: auto;
+ text-align: center;
+ isolation: isolate;
+
+ .wrapper {
+ position: relative;
+ margin-right: 1em;
+ font-family: 'Roboto', Helvetica Neue, Arial, sans-serif;
+ font-weight: 100;
+ margin-top: 0.8em;
+ word-break: keep-all;
+ white-space: nowrap;
+ text-align: left;
+ font-size: 1.5em;
+ width: 100%;
+
+ .segment-name-title {
+ position: absolute;
+ top: -1em;
+ color: #b8b8b8;
+ text-transform: uppercase;
+ white-space: nowrap;
+ font-weight: 600;
+ font-size: 0.5em;
+
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+}
diff --git a/meteor/client/ui/PieceIcons/PieceIcon.tsx b/meteor/client/ui/PieceIcons/PieceIcon.tsx
index e23b0e1e03..90c043e72a 100644
--- a/meteor/client/ui/PieceIcons/PieceIcon.tsx
+++ b/meteor/client/ui/PieceIcons/PieceIcon.tsx
@@ -66,7 +66,7 @@ export const PieceIcon = (props: {
return null
}
-const supportedLayers = new Set([
+export const pieceIconSupportedLayers = new Set([
SourceLayerType.GRAPHICS,
SourceLayerType.LIVE_SPEAK,
SourceLayerType.REMOTE,
@@ -83,7 +83,7 @@ export const PieceIconContainerNoSub = withTracker(
}
renderUnknown?: boolean
}) => {
- return findPieceInstanceToShowFromInstances(props.pieceInstances, props.sourceLayers, supportedLayers)
+ return findPieceInstanceToShowFromInstances(props.pieceInstances, props.sourceLayers, pieceIconSupportedLayers)
}
)(
({
@@ -98,7 +98,7 @@ export const PieceIconContainerNoSub = withTracker(
)
export const PieceIconContainer = withTracker((props: IPropsHeader) => {
- return findPieceInstanceToShow(props, supportedLayers)
+ return findPieceInstanceToShow(props, pieceIconSupportedLayers)
})(
class PieceIconContainer extends MeteorReactComponent<
IPropsHeader & { sourceLayer: ISourceLayer; pieceInstance: PieceInstance }
diff --git a/meteor/client/ui/PieceIcons/utils.ts b/meteor/client/ui/PieceIcons/utils.ts
index a389b0abac..bcfd410f62 100644
--- a/meteor/client/ui/PieceIcons/utils.ts
+++ b/meteor/client/ui/PieceIcons/utils.ts
@@ -4,7 +4,7 @@ import { ShowStyleBases } from '../../../lib/collections/ShowStyleBases'
import { PieceInstances, PieceInstance } from '../../../lib/collections/PieceInstances'
import { IPropsHeader } from './PieceIcon'
-interface IFoundPieceInstance {
+export interface IFoundPieceInstance {
sourceLayer: ISourceLayer | undefined
pieceInstance: PieceInstance | undefined
}
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index 448972b43e..5d110b7f58 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -10,17 +10,20 @@ import {
RundownLayoutAdLibRegion,
RundownLayoutAdLibRegionRole,
RundownLayoutBase,
+ RundownLayoutColoredBox,
RundownLayoutElementBase,
RundownLayoutElementType,
RundownLayoutEndWords,
RundownLayoutExternalFrame,
RundownLayoutFilterBase,
+ RundownLayoutPartName,
RundownLayoutPartTiming,
RundownLayoutPieceCountdown,
RundownLayoutPlaylistEndTimer,
RundownLayoutPlaylistName,
RundownLayoutPlaylistStartTimer,
RundownLayouts,
+ RundownLayoutSegmentName,
RundownLayoutSegmentTiming,
RundownLayoutShowStyleDisplay,
RundownLayoutStudioName,
@@ -33,6 +36,7 @@ import { Translated } from '../../../lib/ReactMeteorData/react-meteor-data'
import { ShowStyleBase } from '../../../../lib/collections/ShowStyleBases'
import { SourceLayerType } from '@sofie-automation/blueprints-integration'
import { withTranslation } from 'react-i18next'
+import { defaultColorPickerPalette } from '../../../lib/colorPicker'
interface IProps {
item: RundownLayoutBase
@@ -1188,6 +1192,146 @@ export default withTranslation()(
)
}
+ renderSegmentName(
+ item: RundownLayoutBase,
+ tab: RundownLayoutSegmentName,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+
+
+ {t('Segment')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderPartName(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPartName,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+
+
+ {t('Part')}
+
+
+
+
+
+ {t('Show Piece Icon Color')}
+
+ {t('Use color of primary piece as background of panel')}
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderColoredBox(
+ item: RundownLayoutBase,
+ tab: RundownLayoutColoredBox,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ {t('Name')}
+
+
+
+
+
+ {t('Box color')}
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
renderShowStyleDisplay(
item: RundownLayoutBase,
tab: RundownLayoutShowStyleDisplay,
@@ -1581,6 +1725,30 @@ export default withTranslation()(
isRundownLayout,
isDashboardLayout
)
+ : RundownLayoutsAPI.isSegmentName(this.props.filter)
+ ? this.renderSegmentName(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
+ : RundownLayoutsAPI.isPartName(this.props.filter)
+ ? this.renderPartName(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
+ : RundownLayoutsAPI.isColoredBox(this.props.filter)
+ ? this.renderColoredBox(
+ this.props.item,
+ this.props.filter,
+ this.props.index,
+ isRundownLayout,
+ isDashboardLayout
+ )
: RundownLayoutsAPI.isTimeOfDay(this.props.filter)
? this.renderTimeOfDay(
this.props.item,
diff --git a/meteor/client/ui/Shelf/ColoredBoxPanel.tsx b/meteor/client/ui/Shelf/ColoredBoxPanel.tsx
new file mode 100644
index 0000000000..1e5850dd34
--- /dev/null
+++ b/meteor/client/ui/Shelf/ColoredBoxPanel.tsx
@@ -0,0 +1,61 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutColoredBox,
+ RundownLayoutBase,
+ RundownLayoutColoredBox,
+} from '../../../lib/collections/RundownLayouts'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { dashboardElementPosition } from './DashboardPanel'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { withTranslation } from 'react-i18next'
+
+interface IColoredBoxPanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutColoredBox
+ playlist: RundownPlaylist
+}
+
+interface IState {}
+
+interface IColoredBoxPanelTrackedProps {
+ name?: string
+}
+
+export class ColoredBoxPanelInner extends MeteorReactComponent<
+ Translated,
+ IState
+> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ const { panel } = this.props
+
+ return (
+
+ )
+ }
+}
+
+export const ColoredBoxPanel = withTranslation()(ColoredBoxPanelInner)
diff --git a/meteor/client/ui/Shelf/PartNamePanel.tsx b/meteor/client/ui/Shelf/PartNamePanel.tsx
new file mode 100644
index 0000000000..29e9fb26e2
--- /dev/null
+++ b/meteor/client/ui/Shelf/PartNamePanel.tsx
@@ -0,0 +1,135 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import ClassNames from 'classnames'
+import {
+ DashboardLayoutPartName,
+ RundownLayoutBase,
+ RundownLayoutPartName,
+} from '../../../lib/collections/RundownLayouts'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { dashboardElementPosition } from './DashboardPanel'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { PieceInstances } from '../../../lib/collections/PieceInstances'
+import { ShowStyleBase } from '../../../lib/collections/ShowStyleBases'
+import { findPieceInstanceToShowFromInstances, IFoundPieceInstance } from '../PieceIcons/utils'
+import { pieceIconSupportedLayers } from '../PieceIcons/PieceIcon'
+import { SourceLayerType } from '@sofie-automation/blueprints-integration'
+
+interface IPartNamePanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutPartName
+ playlist: RundownPlaylist
+ showStyleBase: ShowStyleBase
+}
+
+interface IState {}
+
+interface IPartNamePanelTrackedProps {
+ name?: string
+ instanceToShow?: IFoundPieceInstance
+}
+
+class PartNamePanelInner extends MeteorReactComponent<
+ Translated,
+ IState
+> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ const { t, panel } = this.props
+
+ const sourceLayerType = this.props.instanceToShow?.sourceLayer?.type
+ let backgroundSourceLayer = sourceLayerType ? sourceLayerTypeToString(sourceLayerType) : undefined
+
+ if (!backgroundSourceLayer) {
+ backgroundSourceLayer = ''
+ }
+
+ return (
+
+
+
+ {this.props.panel.part === 'current' ? t('Current Part') : t('Next Part')}
+
+ {this.props.name}
+
+
+ )
+ }
+}
+
+function sourceLayerTypeToString(sourceLayerType: SourceLayerType) {
+ if (!sourceLayerType) return
+
+ switch (sourceLayerType) {
+ case SourceLayerType.GRAPHICS:
+ return 'graphics'
+ case SourceLayerType.LIVE_SPEAK:
+ return 'live-speak'
+ case SourceLayerType.REMOTE:
+ return 'remote'
+ case SourceLayerType.SPLITS:
+ return 'splits'
+ case SourceLayerType.VT:
+ return 'vt'
+ case SourceLayerType.CAMERA:
+ return 'camera'
+ }
+}
+
+export const PartNamePanel = translateWithTracker(
+ (props) => {
+ const selectedPartInstanceId =
+ props.panel.part === 'current' ? props.playlist.currentPartInstanceId : props.playlist.nextPartInstanceId
+ let name: string | undefined
+ let instanceToShow: IFoundPieceInstance | undefined
+
+ if (selectedPartInstanceId) {
+ const selectedPartInstance = props.playlist.getActivePartInstances({ _id: selectedPartInstanceId })[0]
+ name = selectedPartInstance.part?.title
+
+ if (selectedPartInstance && props.panel.showPieceIconColor) {
+ const pieceInstances = PieceInstances.find({ partInstanceId: selectedPartInstance._id }).fetch()
+ instanceToShow = findPieceInstanceToShowFromInstances(
+ pieceInstances,
+ props.showStyleBase.sourceLayers.reduce((prev, curr) => {
+ prev[curr._id] = curr
+ return prev
+ }, {}),
+ pieceIconSupportedLayers
+ )
+ }
+ }
+
+ return {
+ ...props,
+ name,
+ instanceToShow,
+ }
+ },
+ (data, props, nextProps) => {
+ return (
+ !_.isEqual(props.panel, nextProps.panel) ||
+ props.playlist.currentPartInstanceId !== nextProps.playlist.currentPartInstanceId ||
+ props.playlist.nextPartInstanceId !== nextProps.playlist.nextPartInstanceId
+ )
+ }
+)(PartNamePanelInner)
diff --git a/meteor/client/ui/Shelf/SegmentNamePanel.tsx b/meteor/client/ui/Shelf/SegmentNamePanel.tsx
new file mode 100644
index 0000000000..4e4abf71ee
--- /dev/null
+++ b/meteor/client/ui/Shelf/SegmentNamePanel.tsx
@@ -0,0 +1,81 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import {
+ DashboardLayoutSegmentName,
+ RundownLayoutBase,
+ RundownLayoutSegmentName,
+} from '../../../lib/collections/RundownLayouts'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
+import { dashboardElementPosition } from './DashboardPanel'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+
+interface ISegmentNamePanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutSegmentName
+ playlist: RundownPlaylist
+}
+
+interface IState {}
+
+interface ISegmentNamePanelTrackedProps {
+ name?: string
+}
+
+class SegmentNamePanelInner extends MeteorReactComponent<
+ Translated,
+ IState
+> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ const { t, panel } = this.props
+
+ return (
+
+
+
+ {this.props.panel.segment === 'current' ? t('Current Segment') : t('Next Segment')}
+
+ {this.props.name}
+
+
+ )
+ }
+}
+
+export const SegmentNamePanel = translateWithTracker(
+ (props) => {
+ const selectedPartInstanceId =
+ props.panel.segment === 'current' ? props.playlist.currentPartInstanceId : props.playlist.nextPartInstanceId
+ let name: string | undefined
+
+ if (selectedPartInstanceId) {
+ const selectedPartInstance = props.playlist.getActivePartInstances({ _id: selectedPartInstanceId })[0]
+ const segment = selectedPartInstance._id
+ ? props.playlist.getSegments({ _id: selectedPartInstance.segmentId })[0]
+ : undefined
+ name = segment?.name
+ }
+
+ return {
+ ...props,
+ name,
+ }
+ }
+)(SegmentNamePanelInner)
diff --git a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
index 1be3cab391..de234c9212 100644
--- a/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
+++ b/meteor/client/ui/Shelf/ShelfDashboardLayout.tsx
@@ -26,6 +26,9 @@ import { SystemStatusPanel } from './SystemStatusPanel'
import { ShowStylePanel } from './ShowStylePanel'
import { ShowStyleVariant } from '../../../lib/collections/ShowStyleVariants'
import { StudioNamePanel } from './StudioNamePanel'
+import { SegmentNamePanel } from './SegmentNamePanel'
+import { PartNamePanel } from './PartNamePanel'
+import { ColoredBoxPanel } from './ColoredBoxPanel'
export interface IShelfDashboardLayoutProps {
rundownLayout: DashboardLayout
@@ -145,6 +148,16 @@ export function ShelfDashboardLayout(props: IShelfDashboardLayoutProps) {
layout={rundownLayout}
panel={panel}
/>
+ ) : RundownLayoutsAPI.isSegmentName(panel) ? (
+
+ ) : RundownLayoutsAPI.isPartName(panel) ? (
+
) : RundownLayoutsAPI.isTimeOfDay(panel) ? (
) : RundownLayoutsAPI.isSystemStatus(panel) ? (
@@ -164,6 +177,8 @@ export function ShelfDashboardLayout(props: IShelfDashboardLayoutProps) {
showStyleBase={props.showStyleBase}
showStyleVariant={props.showStyleVariant}
/>
+ ) : RundownLayoutsAPI.isColoredBox(panel) ? (
+
) : null
)}
{rundownLayout.actionButtons && (
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 15638447c3..30d05fc30b 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -28,6 +28,9 @@ import {
RundownLayoutWithFilters,
RundownLayoutPresenterView,
RundownLayoutStudioName,
+ RundownLayoutSegmentName,
+ RundownLayoutPartName,
+ RundownLayoutColoredBox,
} from '../collections/RundownLayouts'
import { ShowStyleBaseId } from '../collections/ShowStyleBases'
import * as _ from 'underscore'
@@ -218,6 +221,9 @@ export namespace RundownLayoutsAPI {
RundownLayoutElementType.TIME_OF_DAY,
RundownLayoutElementType.PLAYLIST_NAME,
RundownLayoutElementType.STUDIO_NAME,
+ RundownLayoutElementType.SEGMENT_NAME,
+ RundownLayoutElementType.PART_NAME,
+ RundownLayoutElementType.COLORED_BOX,
],
})
@@ -327,6 +333,18 @@ export namespace RundownLayoutsAPI {
return element.type === RundownLayoutElementType.SHOWSTYLE_DISPLAY
}
+ export function isSegmentName(element: RundownLayoutElementBase): element is RundownLayoutSegmentName {
+ return element.type === RundownLayoutElementType.SEGMENT_NAME
+ }
+
+ export function isPartName(element: RundownLayoutElementBase): element is RundownLayoutPartName {
+ return element.type === RundownLayoutElementType.PART_NAME
+ }
+
+ export function isColoredBox(element: RundownLayoutElementBase): element is RundownLayoutColoredBox {
+ return element.type === RundownLayoutElementType.COLORED_BOX
+ }
+
export function adLibRegionToFilter(element: RundownLayoutAdLibRegion): RundownLayoutFilterBase {
return {
..._.pick(element, '_id', 'name', 'rank', 'tags'),
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 0ea1444771..0ac7b8ef87 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -58,6 +58,9 @@ export enum RundownLayoutElementType {
TIME_OF_DAY = 'time_of_day',
SYSTEM_STATUS = 'system_status',
SHOWSTYLE_DISPLAY = 'showstyle_display',
+ SEGMENT_NAME = 'segment_name',
+ PART_NAME = 'part_name',
+ COLORED_BOX = 'colored_box',
}
export interface RundownLayoutElementBase {
@@ -169,6 +172,22 @@ export interface RundownLayoutShowStyleDisplay extends RundownLayoutElementBase
type: RundownLayoutElementType.SHOWSTYLE_DISPLAY
}
+export interface RundownLayoutSegmentName extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.SEGMENT_NAME
+ segment: 'current' | 'next'
+}
+
+export interface RundownLayoutPartName extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.PART_NAME
+ part: 'current' | 'next'
+ showPieceIconColor: boolean
+}
+
+export interface RundownLayoutColoredBox extends RundownLayoutElementBase {
+ type: RundownLayoutElementType.COLORED_BOX
+ iconColor: string
+}
+
/**
* A filter to be applied against the AdLib Pieces. If a member is undefined, the pool is not tested
* against that filter. A member must match all of the sub-filters to be included in a filter view
@@ -311,6 +330,30 @@ export interface DashboardLayoutShowStyleDisplay extends RundownLayoutShowStyleD
scale: number
}
+export interface DashboardLayoutSegmentName extends RundownLayoutSegmentName {
+ x: number
+ y: number
+ height: number
+ width: number
+ scale: number
+}
+
+export interface DashboardLayoutPartName extends RundownLayoutPartName {
+ x: number
+ y: number
+ height: number
+ width: number
+ scale: number
+}
+
+export interface DashboardLayoutColoredBox extends RundownLayoutColoredBox {
+ x: number
+ y: number
+ height: number
+ width: number
+ scale: number
+}
+
export interface DashboardLayoutFilter extends RundownLayoutFilterBase {
x: number
y: number
From 8f1093cdc68f40c978c2ad234e03d8d968da30b7 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 3 Aug 2021 11:15:44 +0100
Subject: [PATCH 075/112] fix: Segment name panel next segment behaviour
---
meteor/client/ui/Shelf/SegmentNamePanel.tsx | 48 ++++++++++++++++-----
1 file changed, 37 insertions(+), 11 deletions(-)
diff --git a/meteor/client/ui/Shelf/SegmentNamePanel.tsx b/meteor/client/ui/Shelf/SegmentNamePanel.tsx
index 4e4abf71ee..4888a0c5fc 100644
--- a/meteor/client/ui/Shelf/SegmentNamePanel.tsx
+++ b/meteor/client/ui/Shelf/SegmentNamePanel.tsx
@@ -10,6 +10,8 @@ import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
import { dashboardElementPosition } from './DashboardPanel'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { Segment } from '../../../lib/collections/Segments'
+import { PartInstance } from '../../../lib/collections/PartInstances'
interface ISegmentNamePanelProps {
visible?: boolean
@@ -59,19 +61,43 @@ class SegmentNamePanelInner extends MeteorReactComponent<
}
}
-export const SegmentNamePanel = translateWithTracker(
- (props) => {
- const selectedPartInstanceId =
- props.panel.segment === 'current' ? props.playlist.currentPartInstanceId : props.playlist.nextPartInstanceId
- let name: string | undefined
+function getSegmentName(selectedSegment: 'current' | 'next', playlist: RundownPlaylist): string | undefined {
+ const currentPartInstance = playlist.currentPartInstanceId
+ ? (playlist.getActivePartInstances({ _id: playlist.currentPartInstanceId })[0] as PartInstance | undefined)
+ : undefined
+
+ if (!currentPartInstance) return
- if (selectedPartInstanceId) {
- const selectedPartInstance = props.playlist.getActivePartInstances({ _id: selectedPartInstanceId })[0]
- const segment = selectedPartInstance._id
- ? props.playlist.getSegments({ _id: selectedPartInstance.segmentId })[0]
- : undefined
- name = segment?.name
+ if (selectedSegment === 'current') {
+ if (currentPartInstance) {
+ const segment = playlist.getSegments({ _id: currentPartInstance.segmentId })[0] as Segment | undefined
+ return segment?.name
}
+ } else {
+ if (playlist.nextPartInstanceId) {
+ const nextPartInstance = playlist.getActivePartInstances({
+ _id: playlist.nextPartInstanceId,
+ })[0] as PartInstance | undefined
+ if (nextPartInstance && nextPartInstance.segmentId !== currentPartInstance.segmentId) {
+ const segment = playlist.getSegments({ _id: nextPartInstance.segmentId })[0] as Segment | undefined
+ return segment?.name
+ }
+ }
+
+ // Current and next part are same segment, or next is not set
+ // Find next segment in order
+ const orderedSegmentsAndParts = playlist.getSegmentsAndPartsSync()
+ const segmentIndex = orderedSegmentsAndParts.segments.findIndex((s) => s._id === currentPartInstance.segmentId)
+ if (segmentIndex === -1) return
+
+ const nextSegment = orderedSegmentsAndParts.segments.slice(segmentIndex + 1)[0] as Segment | undefined
+ return nextSegment?.name
+ }
+}
+
+export const SegmentNamePanel = translateWithTracker(
+ (props) => {
+ const name: string | undefined = getSegmentName(props.panel.segment, props.playlist)
return {
...props,
From 2d266944062984f762356e82094f2fb5491841b3 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 3 Aug 2021 13:09:25 +0100
Subject: [PATCH 076/112] fix: Studio name panel title
---
meteor/client/styles/shelf/studioNamePanel.scss | 2 +-
meteor/client/ui/Shelf/StudioNamePanel.tsx | 11 ++++++++---
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/meteor/client/styles/shelf/studioNamePanel.scss b/meteor/client/styles/shelf/studioNamePanel.scss
index 96db7fbc31..047aa0dc10 100644
--- a/meteor/client/styles/shelf/studioNamePanel.scss
+++ b/meteor/client/styles/shelf/studioNamePanel.scss
@@ -17,7 +17,7 @@
font-size: 1.5em;
width: 100%;
- .studio-name {
+ .studio-name-title {
position: absolute;
top: -1em;
color: #b8b8b8;
diff --git a/meteor/client/ui/Shelf/StudioNamePanel.tsx b/meteor/client/ui/Shelf/StudioNamePanel.tsx
index 0956b19993..cabc7a35ea 100644
--- a/meteor/client/ui/Shelf/StudioNamePanel.tsx
+++ b/meteor/client/ui/Shelf/StudioNamePanel.tsx
@@ -10,6 +10,8 @@ import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
import { dashboardElementPosition } from './DashboardPanel'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
import { Studio } from '../../../lib/collections/Studios'
+import { withTranslation } from 'react-i18next'
+import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData'
interface IStudioNamePanelProps {
visible?: boolean
@@ -23,8 +25,8 @@ interface IState {}
interface IStudioNamePanelTrackedProps {}
-export class StudioNamePanel extends MeteorReactComponent<
- IStudioNamePanelProps & IStudioNamePanelTrackedProps,
+export class StudioNamePanelInner extends MeteorReactComponent<
+ Translated,
IState
> {
constructor(props) {
@@ -33,7 +35,7 @@ export class StudioNamePanel extends MeteorReactComponent<
render() {
const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
- const { panel } = this.props
+ const { t, panel } = this.props
return (
+ {t('Studio Name')}
{this.props.studio.name}
)
}
}
+
+export const StudioNamePanel = withTranslation()(StudioNamePanelInner)
From bd6b1cf13a4705a4eb14165a35a855a8cda96221 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 3 Aug 2021 13:24:51 +0100
Subject: [PATCH 077/112] fix: Better names for the required layer options
---
meteor/client/lib/rundownLayouts.ts | 19 ++++++++++---------
.../SegmentTimelineContainer.tsx | 2 +-
.../ui/Settings/components/FilterEditor.tsx | 14 +++++++-------
.../RundownViewLayoutSettings.tsx | 14 +++++++-------
meteor/lib/collections/RundownLayouts.ts | 16 ++++++++--------
5 files changed, 33 insertions(+), 32 deletions(-)
diff --git a/meteor/client/lib/rundownLayouts.ts b/meteor/client/lib/rundownLayouts.ts
index eb41e38327..be177ed769 100644
--- a/meteor/client/lib/rundownLayouts.ts
+++ b/meteor/client/lib/rundownLayouts.ts
@@ -16,31 +16,32 @@ export function getIsFilterActive(
const unfinishedPieces = getUnfinishedPieceInstancesReactive(playlist.currentPartInstanceId, true)
let activePieceInstance: PieceInstance | undefined
const activeLayers = unfinishedPieces.map((p) => p.piece.sourceLayerId)
- const containsEveryRequiredLayer = panel.requireAllSourcelayers
- ? panel.requiredLayers?.length && panel.requiredLayers.every((s) => activeLayers.includes(s))
+ const containsEveryRequiredLayer = panel.requireAllAdditionalSourcelayers
+ ? panel.additionalLayers?.length && panel.additionalLayers.every((s) => activeLayers.includes(s))
: false
const containsRequiredLayer = containsEveryRequiredLayer
? true
- : panel.requiredLayers && panel.requiredLayers.length
- ? panel.requiredLayers.some((s) => activeLayers.includes(s))
+ : panel.additionalLayers && panel.additionalLayers.length
+ ? panel.additionalLayers.some((s) => activeLayers.includes(s))
: false
if (
- (!panel.requireAllSourcelayers || containsEveryRequiredLayer) &&
- (!panel.requiredLayers?.length || containsRequiredLayer)
+ (!panel.requireAllAdditionalSourcelayers || containsEveryRequiredLayer) &&
+ (!panel.additionalLayers?.length || containsRequiredLayer)
) {
activePieceInstance =
- panel.activeLayerIds && panel.activeLayerIds.length
+ panel.requiredLayerIds && panel.requiredLayerIds.length
? _.flatten(Object.values(unfinishedPieces)).find((piece: PieceInstance) => {
return (
- (panel.activeLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 &&
+ (panel.requiredLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 &&
piece.partInstanceId === playlist.currentPartInstanceId
)
})
: undefined
}
return {
- active: activePieceInstance !== undefined || (!panel.activeLayerIds?.length && !panel.requiredLayers?.length),
+ active:
+ activePieceInstance !== undefined || (!panel.requiredLayerIds?.length && !panel.additionalLayers?.length),
activePieceInstance,
}
}
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
index 4fd135b4e6..4e25efe6ed 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
@@ -267,7 +267,7 @@ export const SegmentTimelineContainer = translateWithTracker{activeLayerTitle}
{
return { name: l.name, value: l._id }
@@ -1469,7 +1469,7 @@ export default withTranslation()(
{t('Also Require Source Layers')}
{
return { name: l.name, value: l._id }
@@ -1496,16 +1496,16 @@ export default withTranslation()(
- {t('Require All Sourcelayers')}
+ {t('Require All Additional Source Layers')}
- {t('All required source layers must have active pieces')}
+ {t('All additional source layers must have active pieces')}
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index 10bf1a63f0..349af965c1 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
@@ -89,7 +89,7 @@ export default withTranslation()(
{t('Live line countdown requires sourcelayer')}
{
return { name: l.name, value: l._id }
@@ -118,7 +118,7 @@ export default withTranslation()(
{t('Also Require Source Layers')}
{
return { name: l.name, value: l._id }
@@ -145,16 +145,16 @@ export default withTranslation()(
- {t('Require All Sourcelayers')}
+ {t('Require All Additional Source Layers')}
- {t('All required source layers must have active pieces')}
+ {t('All additional source layers must have active pieces')}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index acae82b713..a8933b5127 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -72,19 +72,19 @@ export interface RundownLayoutElementBase {
/**
* An interface for filters that check for a piece to be present on a source layer to change their behaviour (or in order to perform any action at all).
- * If `activeLayerIds` is empty / undefined, the filter should be treated as "always active".
- * @param activeLayerIds Layers that the filter will check for some active ('live') piece. (Match any layer in array).
- * @param requiredLayers Layers that must be active in addition to the active layers, i.e. "any of `activeLayerIds`, with at least one of `requiredLayers`".
- * @param requireAllSourcelayers Require all layers in `requiredLayers` to contain an active piece.
+ * If `requiredLayerIds` is empty / undefined, the filter should be treated as "always active".
+ * @param requiredLayerIds Layers that the filter will check for some active ('live') piece. (Match any layer in array).
+ * @param additionalLayers Layers that must be active in addition to the active layers, i.e. "any of `requiredLayerIds`, with at least one of `additionalLayers`".
+ * @param requireAllAdditionalSourcelayers Require all layers in `additionalLayers` to contain an active piece.
*/
export interface RequiresActiveLayers {
- activeLayerIds?: string[]
- requiredLayers?: string[]
+ requiredLayerIds?: string[]
+ additionalLayers?: string[]
/**
- * Require that all required sourcelayers be active.
+ * Require that all additional sourcelayers be active.
* This allows behaviour to be tied to a combination of e.g. script + VT.
*/
- requireAllSourcelayers: boolean
+ requireAllAdditionalSourcelayers: boolean
}
export interface RundownLayoutExternalFrame extends RundownLayoutElementBase {
From c0c4720e7e11366c91825e100a790b11a1cb08d8 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 3 Aug 2021 17:08:00 +0100
Subject: [PATCH 078/112] fix: Consistent label behavior between planned
start/end
---
meteor/client/ui/RundownView.tsx | 2 +-
.../RundownTiming/PlaylistStartTiming.tsx | 9 ++--
.../ui/Settings/components/FilterEditor.tsx | 44 +++++++++++++------
.../RundownHeaderLayoutSettings.tsx | 2 +-
.../RundownViewLayoutSettings.tsx | 8 ++--
.../client/ui/Shelf/PlaylistEndTimerPanel.tsx | 2 +-
.../ui/Shelf/PlaylistStartTimerPanel.tsx | 3 +-
meteor/lib/collections/RundownLayouts.ts | 8 ++--
8 files changed, 49 insertions(+), 29 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index cf2e5a1719..22e1d6ab1c 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -296,7 +296,7 @@ const TimingDisplay = withTranslation()(
expectedStart={expectedStart}
expectedEnd={expectedEnd}
expectedDuration={expectedDuration}
- endLabel={this.props.layout?.expectedEndText}
+ endLabel={this.props.layout?.plannedEndText}
rundownCount={this.props.rundownCount}
/>
) : null}
diff --git a/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx
index 30e0add8a2..6e64c8583f 100644
--- a/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx
@@ -11,8 +11,9 @@ import { PlaylistTiming } from '../../../../lib/rundown/rundownTiming'
interface IEndTimingProps {
rundownPlaylist: RundownPlaylist
- hideExpectedStart?: boolean
+ hidePlannedStart?: boolean
hideDiff?: boolean
+ plannedStartText?: string
}
export const PlaylistStartTiming = withTranslation()(
@@ -31,7 +32,7 @@ export const PlaylistStartTiming = withTranslation()(
return (
- {!this.props.hideExpectedStart &&
+ {!this.props.hidePlannedStart &&
(rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
{t('Started')}
@@ -39,12 +40,12 @@ export const PlaylistStartTiming = withTranslation()(
) : playlistExpectedStart ? (
- {t('Planned Start')}
+ {this.props.plannedStartText || t('Planned Start')}
) : playlistExpectedEnd && playlistExpectedDuration ? (
- {t('Expected Start')}
+ {this.props.plannedStartText || t('Expected Start')}
) : null)}
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index c759bb7c77..e75e195424 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -824,6 +824,20 @@ export default withTranslation()(
/>
+
+
+ {t('Planned Start Text')}
+
+ {t('Text to show above show start time')}
+
+
{t('Hide Diff')}
@@ -839,10 +853,10 @@ export default withTranslation()(
- {t('Hide Expected Start')}
+ {t('Hide Planned Start')}
-
- {t('Expected End text')}
-
- {t('Text to show above countdown to end of show')}
-
+
+
+ {t('Planned End text')}
+
+ {t('Text to show above show end time')}
+
+
{t('Hide Planned End Label')}
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
index 5b9688b9fa..5b1edcb081 100644
--- a/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
+++ b/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
@@ -23,7 +23,7 @@ export default withTranslation()(
{t('Expected End text')}
- {t('Live line countdown requires sourcelayer')}
+ {t('Live line countdown requires Source Layer')}
(v && v.length > 0 ? v : undefined)}
/>
- {t('One of these sourcelayers must have an active piece for the live line countdown to be show')}
+ {t('One of these source layers must have an active piece for the live line countdown to be show')}
@@ -185,7 +185,7 @@ export default withTranslation()(
- {t('Segment countdown requires sourcelayer')}
+ {t('Segment countdown requires source layer')}
(v && v.length > 0 ? v : undefined)}
/>
- {t('One of these sourcelayers must have a piece for the countdown to segment on-air to be show')}
+ {t('One of these source layers must have a piece for the countdown to segment on-air to be show')}
diff --git a/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx b/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
index 7df8bcb9b4..a1820f8a4e 100644
--- a/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
+++ b/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
@@ -58,7 +58,7 @@ export class PlaylistEndTimerPanelInner extends MeteorReactComponent<
expectedStart={PlaylistTiming.getExpectedStart(playlist.timing)}
expectedEnd={PlaylistTiming.getExpectedEnd(playlist.timing)}
expectedDuration={PlaylistTiming.getExpectedDuration(playlist.timing)}
- endLabel={panel.expectedEndText}
+ endLabel={panel.plannedEndText}
hidePlannedEndLabel={panel.hidePlannedEndLabel}
hideDiffLabel={panel.hideDiffLabel}
hideCountdown={panel.hideCountdown}
diff --git a/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx b/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
index e473bf15e6..ba24ab13ab 100644
--- a/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
+++ b/meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx
@@ -54,7 +54,8 @@ export class PlaylistStartTimerPanelInner extends MeteorReactComponent<
)
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index a8933b5127..7af8ac5843 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -114,13 +114,15 @@ export interface RundownLayoutPieceCountdown extends RundownLayoutElementBase {
export interface RundownLayoutPlaylistStartTimer extends RundownLayoutElementBase {
type: RundownLayoutElementType.PLAYLIST_START_TIMER
+ plannedStartText: string
hideDiff: boolean
- hideExpectedStart: boolean
+ hidePlannedStart: boolean
}
export interface RundownLayoutPlaylistEndTimer extends RundownLayoutElementBase {
type: RundownLayoutElementType.PLAYLIST_END_TIMER
- expectedEndText: string
+ headerHeight: string
+ plannedEndText: string
hidePlannedEndLabel: boolean
hideDiffLabel: boolean
hideCountdown: boolean
@@ -424,7 +426,7 @@ export interface RundownLayout extends RundownLayoutShelfBase {
export interface RundownLayoutRundownHeader extends RundownLayoutBase {
type: RundownLayoutType.RUNDOWN_HEADER_LAYOUT
- expectedEndText: string
+ plannedEndText: string
nextBreakText: string
/** When true, hide the Planned End timer when there is a rundown marked as a break in the future */
hideExpectedEndBeforeBreak: boolean
From 1436cd0200cd047b22cdac8f93ac3e1512ba9747 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Tue, 31 Aug 2021 15:28:44 +0100
Subject: [PATCH 079/112] feat: Custom classes for dashboard panels
---
meteor/client/styles/shelf/endTimerPanel.scss | 4 ----
meteor/client/styles/shelf/partNamePanel.scss | 1 +
...tDownPanel.scss => segmentTimingPanel.scss} | 1 +
.../RundownTiming/SegmentDuration.tsx | 12 +++++++++---
.../ui/Settings/components/FilterEditor.tsx | 18 ++++++++++++++++++
meteor/client/ui/Shelf/ColoredBoxPanel.tsx | 4 ++--
meteor/client/ui/Shelf/EndWordsPanel.tsx | 6 +++++-
meteor/client/ui/Shelf/ExternalFramePanel.tsx | 8 +++++++-
meteor/client/ui/Shelf/GlobalAdLibPanel.tsx | 2 ++
meteor/client/ui/Shelf/PartNamePanel.tsx | 2 +-
meteor/client/ui/Shelf/PartTimingPanel.tsx | 3 ++-
.../client/ui/Shelf/PlaylistEndTimerPanel.tsx | 10 +++++++---
meteor/client/ui/Shelf/PlaylistNamePanel.tsx | 6 +++++-
.../ui/Shelf/PlaylistStartTimerPanel.tsx | 6 +++++-
meteor/client/ui/Shelf/SegmentNamePanel.tsx | 6 +++++-
meteor/client/ui/Shelf/SegmentTimingPanel.tsx | 13 +++++++++++--
meteor/lib/collections/RundownLayouts.ts | 18 ++++++++++++++++++
17 files changed, 99 insertions(+), 21 deletions(-)
rename meteor/client/styles/shelf/{segmentCountDownPanel.scss => segmentTimingPanel.scss} (86%)
diff --git a/meteor/client/styles/shelf/endTimerPanel.scss b/meteor/client/styles/shelf/endTimerPanel.scss
index 7ceda059b8..af6110088c 100644
--- a/meteor/client/styles/shelf/endTimerPanel.scss
+++ b/meteor/client/styles/shelf/endTimerPanel.scss
@@ -1,7 +1,3 @@
.playlist-end-time-panel {
position: absolute;
-
- &.timing {
- width: unset !important;
- }
}
diff --git a/meteor/client/styles/shelf/partNamePanel.scss b/meteor/client/styles/shelf/partNamePanel.scss
index 13e0739c46..7b315f9a26 100644
--- a/meteor/client/styles/shelf/partNamePanel.scss
+++ b/meteor/client/styles/shelf/partNamePanel.scss
@@ -7,6 +7,7 @@
text-align: center;
isolation: isolate;
padding: 1vw 1vh;
+ overflow: hidden;
@include item-type-colors();
diff --git a/meteor/client/styles/shelf/segmentCountDownPanel.scss b/meteor/client/styles/shelf/segmentTimingPanel.scss
similarity index 86%
rename from meteor/client/styles/shelf/segmentCountDownPanel.scss
rename to meteor/client/styles/shelf/segmentTimingPanel.scss
index 718a78bcfe..afb651aed5 100644
--- a/meteor/client/styles/shelf/segmentCountDownPanel.scss
+++ b/meteor/client/styles/shelf/segmentTimingPanel.scss
@@ -1,5 +1,6 @@
.segment-timing-panel {
position: absolute;
+ overflow: hidden;
.negative {
color: var(--general-late-color);
diff --git a/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx b/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx
index 4deb9e4c03..66726c8cef 100644
--- a/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx
+++ b/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx
@@ -1,3 +1,4 @@
+import ClassNames from 'classnames'
import React, { ReactNode } from 'react'
import { withTiming, WithTiming } from './withTiming'
import { unprotectString } from '../../../../lib/lib'
@@ -7,6 +8,7 @@ import { PartUi } from '../../SegmentTimeline/SegmentTimelineContainer'
interface ISegmentDurationProps {
parts: PartUi[]
label?: ReactNode
+ className?: string
/** If set, the timer will display just the played out duration */
countUp?: boolean
/** Always show planned segment duration instead of counting up/down */
@@ -38,11 +40,15 @@ export const SegmentDuration = withTiming()(function
<>
{props.label}
{props.fixed ? (
- {RundownUtils.formatDiffToTimecode(budget, false, false, true, false, true, '+')}
+
+ {RundownUtils.formatDiffToTimecode(budget, false, false, true, false, true, '+')}
+
) : props.countUp ? (
- {RundownUtils.formatDiffToTimecode(playedOut, false, false, true, false, true, '+')}
+
+ {RundownUtils.formatDiffToTimecode(playedOut, false, false, true, false, true, '+')}
+
) : (
-
+
{RundownUtils.formatDiffToTimecode(duration, false, false, true, false, true, '+')}
)}
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index e75e195424..eed5c60b65 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -1600,6 +1600,24 @@ export default withTranslation()(
)}
+
+
+ {t('Custom Classes')}
+ v?.join(',')}
+ mutateUpdateValue={(v: string | undefined) => v?.split(',')}
+ />
+
+ Add custom css classes for customization. Separate classes with a ','
+
+
+
)
}
diff --git a/meteor/client/ui/Shelf/ColoredBoxPanel.tsx b/meteor/client/ui/Shelf/ColoredBoxPanel.tsx
index 1e5850dd34..9687a0af42 100644
--- a/meteor/client/ui/Shelf/ColoredBoxPanel.tsx
+++ b/meteor/client/ui/Shelf/ColoredBoxPanel.tsx
@@ -45,10 +45,10 @@ export class ColoredBoxPanelInner extends MeteorReactComponent<
? {
...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutColoredBox) }),
fontSize: ((panel as DashboardLayoutColoredBox).scale || 1) * 1.5 + 'em',
- 'background-color': this.props.panel.iconColor ?? 'transparent',
+ backgroundColor: this.props.panel.iconColor ?? 'transparent',
}
: {
- 'background-color': this.props.panel.iconColor ?? 'transparent',
+ backgroundColor: this.props.panel.iconColor ?? 'transparent',
}
)}
>
diff --git a/meteor/client/ui/Shelf/EndWordsPanel.tsx b/meteor/client/ui/Shelf/EndWordsPanel.tsx
index f0b1b707b1..466a320290 100644
--- a/meteor/client/ui/Shelf/EndWordsPanel.tsx
+++ b/meteor/client/ui/Shelf/EndWordsPanel.tsx
@@ -1,5 +1,6 @@
import * as React from 'react'
import * as _ from 'underscore'
+import ClassNames from 'classnames'
import {
DashboardLayoutEndsWords,
RundownLayoutBase,
@@ -46,7 +47,10 @@ export class EndWordsPanelInner extends MeteorReactComponent<
return (
{
e.preventDefault()
}
diff --git a/meteor/client/ui/Shelf/PartNamePanel.tsx b/meteor/client/ui/Shelf/PartNamePanel.tsx
index 29e9fb26e2..afefb8ddab 100644
--- a/meteor/client/ui/Shelf/PartNamePanel.tsx
+++ b/meteor/client/ui/Shelf/PartNamePanel.tsx
@@ -54,7 +54,7 @@ class PartNamePanelInner extends MeteorReactComponent<
return (
) : (
-
+
))}
diff --git a/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx b/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
index a1820f8a4e..596449d7e0 100644
--- a/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
+++ b/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
@@ -1,5 +1,6 @@
import * as React from 'react'
import * as _ from 'underscore'
+import ClassNames from 'classnames'
import {
DashboardLayoutPlaylistEndTimer,
RundownLayoutBase,
@@ -32,9 +33,9 @@ export class PlaylistEndTimerPanelInner extends MeteorReactComponent<
}
render() {
- const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ const { playlist, panel, layout } = this.props
- const { playlist, panel } = this.props
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout)
if (!PlaylistTiming.getExpectedDuration(playlist.timing)) {
return null
@@ -42,7 +43,10 @@ export class PlaylistEndTimerPanelInner extends MeteorReactComponent<
return (
)}
{this.props.active && this.props.parts && (
-
+
)}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 7af8ac5843..2018177c3a 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -227,6 +227,7 @@ export interface DashboardLayoutExternalFrame extends RundownLayoutExternalFrame
y: number
width: number
height: number
+ customClasses?: string[]
}
export interface DashboardLayoutAdLibRegion extends RundownLayoutAdLibRegion {
@@ -234,6 +235,7 @@ export interface DashboardLayoutAdLibRegion extends RundownLayoutAdLibRegion {
y: number
width: number
height: number
+ customClasses?: string[]
}
export interface DashboardLayoutPieceCountdown extends RundownLayoutPieceCountdown {
@@ -242,6 +244,7 @@ export interface DashboardLayoutPieceCountdown extends RundownLayoutPieceCountdo
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutPlaylistStartTimer extends RundownLayoutPlaylistStartTimer {
@@ -250,6 +253,7 @@ export interface DashboardLayoutPlaylistStartTimer extends RundownLayoutPlaylist
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutPlaylistEndTimer extends RundownLayoutPlaylistEndTimer {
@@ -258,6 +262,7 @@ export interface DashboardLayoutPlaylistEndTimer extends RundownLayoutPlaylistEn
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutEndsWords extends RundownLayoutEndWords {
@@ -266,6 +271,7 @@ export interface DashboardLayoutEndsWords extends RundownLayoutEndWords {
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutSegmentCountDown extends RundownLayoutSegmentTiming {
@@ -274,6 +280,7 @@ export interface DashboardLayoutSegmentCountDown extends RundownLayoutSegmentTim
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutPartCountDown extends RundownLayoutPartTiming {
@@ -282,6 +289,7 @@ export interface DashboardLayoutPartCountDown extends RundownLayoutPartTiming {
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutTextLabel extends RundownLayoutTextLabel {
@@ -290,6 +298,7 @@ export interface DashboardLayoutTextLabel extends RundownLayoutTextLabel {
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutPlaylistName extends RundownLayoutPlaylistName {
@@ -298,6 +307,7 @@ export interface DashboardLayoutPlaylistName extends RundownLayoutPlaylistName {
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutStudioName extends RundownLayoutStudioName {
@@ -306,6 +316,7 @@ export interface DashboardLayoutStudioName extends RundownLayoutStudioName {
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutTimeOfDay extends RundownLayoutTimeOfDay {
@@ -314,6 +325,7 @@ export interface DashboardLayoutTimeOfDay extends RundownLayoutTimeOfDay {
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutSystemStatus extends RundownLayoutSytemStatus {
@@ -322,6 +334,7 @@ export interface DashboardLayoutSystemStatus extends RundownLayoutSytemStatus {
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutShowStyleDisplay extends RundownLayoutShowStyleDisplay {
@@ -330,6 +343,7 @@ export interface DashboardLayoutShowStyleDisplay extends RundownLayoutShowStyleD
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutSegmentName extends RundownLayoutSegmentName {
@@ -338,6 +352,7 @@ export interface DashboardLayoutSegmentName extends RundownLayoutSegmentName {
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutPartName extends RundownLayoutPartName {
@@ -346,6 +361,7 @@ export interface DashboardLayoutPartName extends RundownLayoutPartName {
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutColoredBox extends RundownLayoutColoredBox {
@@ -354,6 +370,7 @@ export interface DashboardLayoutColoredBox extends RundownLayoutColoredBox {
height: number
width: number
scale: number
+ customClasses?: string[]
}
export interface DashboardLayoutFilter extends RundownLayoutFilterBase {
@@ -361,6 +378,7 @@ export interface DashboardLayoutFilter extends RundownLayoutFilterBase {
y: number
width: number
height: number
+ customClasses?: string[]
enableSearch: boolean
buttonWidthScale: number
From 255a71e3e20cb523e571a3bd115e97cd4b175a13 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 1 Sep 2021 16:08:27 +0100
Subject: [PATCH 080/112] fix: Missing custom classes on panels
---
meteor/client/ui/Shelf/StudioNamePanel.tsx | 6 +++++-
meteor/client/ui/Shelf/SystemStatusPanel.tsx | 6 +++++-
meteor/client/ui/Shelf/TextLabelPanel.tsx | 6 +++++-
3 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/meteor/client/ui/Shelf/StudioNamePanel.tsx b/meteor/client/ui/Shelf/StudioNamePanel.tsx
index cabc7a35ea..95413ca8f4 100644
--- a/meteor/client/ui/Shelf/StudioNamePanel.tsx
+++ b/meteor/client/ui/Shelf/StudioNamePanel.tsx
@@ -1,5 +1,6 @@
import * as React from 'react'
import * as _ from 'underscore'
+import ClassNames from 'classnames'
import {
DashboardLayoutStudioName,
RundownLayoutBase,
@@ -39,7 +40,10 @@ export class StudioNamePanelInner extends MeteorReactComponent<
return (
Date: Wed, 1 Sep 2021 16:48:34 +0100
Subject: [PATCH 081/112] chore: Lint
---
meteor/client/ui/Settings/components/FilterEditor.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index eed5c60b65..e1846d5029 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -1614,7 +1614,7 @@ export default withTranslation()(
mutateUpdateValue={(v: string | undefined) => v?.split(',')}
/>
- Add custom css classes for customization. Separate classes with a ','
+ Add custom css classes for customization. Separate classes with a ‘,’
From fda8a544246c9198d2aa0b95a29a86ccca2e2f38 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 6 Sep 2021 15:47:56 +0100
Subject: [PATCH 082/112] fix: Show expected end timer
---
meteor/client/ui/RundownView.tsx | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/meteor/client/ui/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 22e1d6ab1c..2fb2e612f2 100644
--- a/meteor/client/ui/RundownView.tsx
+++ b/meteor/client/ui/RundownView.tsx
@@ -259,9 +259,11 @@ const TimingDisplay = withTranslation()(
const expectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing)
const expectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing)
const showEndTiming =
- this.props.timingDurations.rundownsBeforeNextBreak?.length &&
- (!this.props.layout?.hideExpectedEndBeforeBreak ||
- (this.props.timingDurations.breakIsLastRundown && this.props.layout?.lastRundownIsNotBreak))
+ !this.props.timingDurations.rundownsBeforeNextBreak ||
+ !this.props.layout?.showNextBreakTiming ||
+ (this.props.timingDurations.rundownsBeforeNextBreak.length > 0 &&
+ (!this.props.layout?.hideExpectedEndBeforeBreak ||
+ (this.props.timingDurations.breakIsLastRundown && this.props.layout?.lastRundownIsNotBreak)))
const showNextBreakTiming =
rundownPlaylist.startedPlayback &&
this.props.timingDurations.rundownsBeforeNextBreak?.length &&
From b4afbfe05585213dae9054b320652cbb0642b79f Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 6 Sep 2021 17:26:47 +0100
Subject: [PATCH 083/112] feat: Allow hiding end words label
---
.../client/ui/Settings/components/FilterEditor.tsx | 13 +++++++++++++
meteor/client/ui/Shelf/EndWordsPanel.tsx | 2 +-
meteor/lib/collections/RundownLayouts.ts | 1 +
3 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/meteor/client/ui/Settings/components/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index e1846d5029..fc2ce17813 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -998,6 +998,19 @@ export default withTranslation()(
className="input text-input input-l"
/>
+
+
+ {t('Hide Label')}
+
+
+
{this.renderRequiresActiveLayerSettings(
item,
diff --git a/meteor/client/ui/Shelf/EndWordsPanel.tsx b/meteor/client/ui/Shelf/EndWordsPanel.tsx
index 466a320290..f0b8d8c387 100644
--- a/meteor/client/ui/Shelf/EndWordsPanel.tsx
+++ b/meteor/client/ui/Shelf/EndWordsPanel.tsx
@@ -61,7 +61,7 @@ export class EndWordsPanelInner extends MeteorReactComponent<
)}
>
- {t('End Words')}
+ {!this.props.panel.hideLabel && {t('End Words')} }
{endOfScript}
diff --git a/meteor/lib/collections/RundownLayouts.ts b/meteor/lib/collections/RundownLayouts.ts
index 2018177c3a..1e10781da2 100644
--- a/meteor/lib/collections/RundownLayouts.ts
+++ b/meteor/lib/collections/RundownLayouts.ts
@@ -132,6 +132,7 @@ export interface RundownLayoutPlaylistEndTimer extends RundownLayoutElementBase
export interface RundownLayoutEndWords extends RundownLayoutElementBase, RequiresActiveLayers {
type: RundownLayoutElementType.PLAYLIST_END_TIMER
+ hideLabel: boolean
}
export interface RundownLayoutSegmentTiming extends RundownLayoutElementBase, RequiresActiveLayers {
From d724be2b8dded59bc1b10b680e40898b7e0cdffd Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Mon, 6 Sep 2021 17:27:02 +0100
Subject: [PATCH 084/112] fix: Timing displays
---
meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx | 4 ----
meteor/client/ui/Shelf/PlaylistStartTimerPanel.tsx | 6 ------
2 files changed, 10 deletions(-)
diff --git a/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx b/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
index 596449d7e0..d6c5a2e0e6 100644
--- a/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
+++ b/meteor/client/ui/Shelf/PlaylistEndTimerPanel.tsx
@@ -37,10 +37,6 @@ export class PlaylistEndTimerPanelInner extends MeteorReactComponent<
const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout)
- if (!PlaylistTiming.getExpectedDuration(playlist.timing)) {
- return null
- }
-
return (
Date: Mon, 6 Sep 2021 17:27:24 +0100
Subject: [PATCH 085/112] feat: Add colored box dashboard component to header
---
meteor/lib/api/rundownLayouts.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/meteor/lib/api/rundownLayouts.ts b/meteor/lib/api/rundownLayouts.ts
index 30d05fc30b..f0ffaec452 100644
--- a/meteor/lib/api/rundownLayouts.ts
+++ b/meteor/lib/api/rundownLayouts.ts
@@ -206,6 +206,7 @@ export namespace RundownLayoutsAPI {
RundownLayoutElementType.TIME_OF_DAY,
RundownLayoutElementType.SHOWSTYLE_DISPLAY,
RundownLayoutElementType.SYSTEM_STATUS,
+ RundownLayoutElementType.COLORED_BOX,
],
})
registry.registerPresenterViewLayout(RundownLayoutType.CLOCK_PRESENTER_VIEW_LAYOUT, {
From 493f1d8d2fdcec687d64c44b23f3cad760a77ccb Mon Sep 17 00:00:00 2001
From: Johan Nyman
Date: Thu, 16 Sep 2021 07:08:56 +0200
Subject: [PATCH 086/112] chore: lint
---
.../src/lib/corePeripherals.ts | 35 +++++--------------
1 file changed, 9 insertions(+), 26 deletions(-)
diff --git a/packages/server-core-integration/src/lib/corePeripherals.ts b/packages/server-core-integration/src/lib/corePeripherals.ts
index 0180689c46..6e6db7e8eb 100644
--- a/packages/server-core-integration/src/lib/corePeripherals.ts
+++ b/packages/server-core-integration/src/lib/corePeripherals.ts
@@ -17,7 +17,7 @@ export namespace PeripheralDeviceAPI {
/** Not good. Operation is affected. Will be able to recover on it's own when the situation changes. */
BAD = 4,
/** Not good. Operation is affected. Will NOT be able to to recover from this, manual intervention will be required. */
- FATAL = 5
+ FATAL = 5,
}
export interface StatusObject {
@@ -28,7 +28,7 @@ export namespace PeripheralDeviceAPI {
export enum DeviceCategory {
INGEST = 'ingest',
PLAYOUT = 'playout',
- MEDIA_MANAGER = 'media_manager'
+ MEDIA_MANAGER = 'media_manager',
}
/**
* Deprecated and should not be used in new integrations.
@@ -41,14 +41,9 @@ export namespace PeripheralDeviceAPI {
// Playout devices:
PLAYOUT = 'playout',
// Media-manager devices:
- MEDIA_MANAGER = 'media_manager'
+ MEDIA_MANAGER = 'media_manager',
}
- export type DeviceSubType =
- | SUBTYPE_PROCESS
- | TSR_DeviceType
- | MOS_DeviceType
- | Spreadsheet_DeviceType
- | string
+ export type DeviceSubType = SUBTYPE_PROCESS | TSR_DeviceType | MOS_DeviceType | Spreadsheet_DeviceType | string
/** SUBTYPE_PROCESS means that the device is NOT a sub-device, but a (parent) process. */
export type SUBTYPE_PROCESS = '_process'
@@ -66,7 +61,7 @@ export namespace PeripheralDeviceAPI {
connectionId: string
parentDeviceId?: string
versions?: {
- [libraryName: string]: string;
+ [libraryName: string]: string
}
configManifest?: DeviceConfigManifest
@@ -167,24 +162,12 @@ export namespace PeripheralDeviceAPI {
'removePackageInfo' = 'peripheralDevice.packageManager.removePackageInfo',
'requestUserAuthToken' = 'peripheralDevice.spreadsheet.requestUserAuthToken',
- 'storeAccessToken' = 'peripheralDevice.spreadsheet.storeAccessToken'
+ 'storeAccessToken' = 'peripheralDevice.spreadsheet.storeAccessToken',
}
- export type initialize = (
- id: string,
- token: string,
- options: InitOptions
- ) => Promise
- export type unInitialize = (
- id: string,
- token: string,
- status: StatusObject
- ) => Promise
- export type setStatus = (
- id: string,
- token: string,
- status: StatusObject
- ) => Promise
+ export type initialize = (id: string, token: string, options: InitOptions) => Promise
+ export type unInitialize = (id: string, token: string, status: StatusObject) => Promise
+ export type setStatus = (id: string, token: string, status: StatusObject) => Promise
export type executeFunction = (
deviceId: string,
cb: (err: any, result: any) => void,
From 817e7cfdf60de7c6fadf23d185de5c8a3eb2338e Mon Sep 17 00:00:00 2001
From: Johan Nyman
Date: Thu, 16 Sep 2021 07:11:07 +0200
Subject: [PATCH 087/112] chore: move StatusCode to blueprints-integration, so
that it can be used from the outside.
---
.../ui/RundownView/RundownSystemStatus.tsx | 7 ++++---
meteor/client/ui/Settings.tsx | 3 ++-
meteor/client/ui/Status/SystemStatus.tsx | 2 +-
meteor/lib/api/peripheralDevice.ts | 18 ++++++++----------
packages/blueprints-integration/src/index.ts | 1 +
.../blueprints-integration/src/package.ts | 19 +++++++++++++++++++
packages/blueprints-integration/src/status.ts | 14 ++++++++++++++
7 files changed, 49 insertions(+), 15 deletions(-)
create mode 100644 packages/blueprints-integration/src/status.ts
diff --git a/meteor/client/ui/RundownView/RundownSystemStatus.tsx b/meteor/client/ui/RundownView/RundownSystemStatus.tsx
index d5985e4401..a20238227d 100644
--- a/meteor/client/ui/RundownView/RundownSystemStatus.tsx
+++ b/meteor/client/ui/RundownView/RundownSystemStatus.tsx
@@ -12,6 +12,7 @@ import { withTranslation, WithTranslation } from 'react-i18next'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
import { PubSub } from '../../../lib/api/pubsub'
+import { StatusCode } from '@sofie-automation/blueprints-integration'
interface IMOSStatusProps {
lastUpdate: Time
@@ -73,10 +74,10 @@ interface OnLineOffLineList {
}
interface ITrackedProps {
- mosStatus: PeripheralDeviceAPI.StatusCode
+ mosStatus: StatusCode
mosLastUpdate: Time
mosDevices: OnLineOffLineList
- playoutStatus: PeripheralDeviceAPI.StatusCode
+ playoutStatus: StatusCode
playoutDevices: OnLineOffLineList
}
@@ -124,7 +125,7 @@ export const RundownSystemStatus = translateWithTracker(
const [ingest, playout] = [ingestDevices, playoutDevices].map((devices) => {
const status = devices
.filter((i) => !i.ignore)
- .reduce((memo: PeripheralDeviceAPI.StatusCode, device: PeripheralDevice) => {
+ .reduce((memo: StatusCode, device: PeripheralDevice) => {
if (device.connected && memo.valueOf() < device.status.statusCode.valueOf()) {
return device.status.statusCode
} else if (!device.connected) {
diff --git a/meteor/client/ui/Settings.tsx b/meteor/client/ui/Settings.tsx
index 5922b49b14..f8ee9b1478 100644
--- a/meteor/client/ui/Settings.tsx
+++ b/meteor/client/ui/Settings.tsx
@@ -29,6 +29,7 @@ import { MeteorCall } from '../../lib/api/methods'
import { getUser, User } from '../../lib/collections/Users'
import { Settings as MeteorSettings } from '../../lib/Settings'
import { getAllowConfigure } from '../lib/localStorage'
+import { StatusCode } from '@sofie-automation/blueprints-integration'
class WelcomeToSettings extends React.Component {
render() {
@@ -81,7 +82,7 @@ const SettingsMenu = translateWithTracker
}
// Note The actual type of a device is determined by the Category, Type and SubType
diff --git a/packages/blueprints-integration/src/index.ts b/packages/blueprints-integration/src/index.ts
index 08edeb8a22..b992e48ab0 100644
--- a/packages/blueprints-integration/src/index.ts
+++ b/packages/blueprints-integration/src/index.ts
@@ -12,6 +12,7 @@ export * from './package'
export * from './packageInfo'
export * from './rundown'
export * from './showStyle'
+export * from './status'
export * from './studio'
export * from './timeline'
export * from './util'
diff --git a/packages/blueprints-integration/src/package.ts b/packages/blueprints-integration/src/package.ts
index 9409c0d26c..6d8903e896 100644
--- a/packages/blueprints-integration/src/package.ts
+++ b/packages/blueprints-integration/src/package.ts
@@ -4,6 +4,9 @@
* Example: A piece uses a media file for playout in CasparCG. The media file will then be an ExpectedPackage, which the Package Manager
* will fetch from a MAM and copy to the media-folder of CasparCG.
*/
+
+import { StatusCode } from './status'
+
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ExpectedPackage {
export type Any = ExpectedPackageMediaFile | ExpectedPackageQuantelClip
@@ -382,6 +385,22 @@ export namespace ExpectedPackageStatusAPI {
/** All good, the package is in place and ready to play*/
READY = 'ready',
}
+
+ export interface PackageContainerStatus {
+ status: StatusCode
+ statusReason: Reason
+ statusChanged: number
+
+ monitors: {
+ [monitorId: string]: PackageContainerMonitorStatus
+ }
+ }
+ export interface PackageContainerMonitorStatus {
+ label: string
+ status: StatusCode
+ statusReason: Reason
+ }
+
/** Contains textual descriptions for statuses. */
export interface Reason {
/** User-readable reason (to be displayed in GUI:s, to regular humans ) */
diff --git a/packages/blueprints-integration/src/status.ts b/packages/blueprints-integration/src/status.ts
new file mode 100644
index 0000000000..4d61403ecd
--- /dev/null
+++ b/packages/blueprints-integration/src/status.ts
@@ -0,0 +1,14 @@
+export enum StatusCode {
+ /** Status unknown, for example at startup */
+ UNKNOWN = 0,
+ /** All good and green */
+ GOOD = 1,
+ /** Not everything is OK, but normal operation is not affected */
+ WARNING_MINOR = 2,
+ /** Not everything is OK, normal operation might be affected */
+ WARNING_MAJOR = 3,
+ /** Normal operation is affected, automatic recover might be possible */
+ BAD = 4,
+ /** Normal operation is affected, automatic recover is not possible (manual interference is required) */
+ FATAL = 5,
+}
From fa55946a75a4ddb4417d357d4c31014a6cd47760 Mon Sep 17 00:00:00 2001
From: Johan Nyman
Date: Thu, 16 Sep 2021 07:14:29 +0200
Subject: [PATCH 088/112] chore: refactor: rename and make indepentent:
PeripheralDeviceStatus -> StatusPill
So that it can be used in other places
---
meteor/client/ui/Settings/DeviceSettings.tsx | 8 +-
meteor/client/ui/Status/StatusCodePill.tsx | 62 +++++++++++++
meteor/client/ui/Status/SystemStatus.tsx | 95 ++++++--------------
3 files changed, 93 insertions(+), 72 deletions(-)
create mode 100644 meteor/client/ui/Status/StatusCodePill.tsx
diff --git a/meteor/client/ui/Settings/DeviceSettings.tsx b/meteor/client/ui/Settings/DeviceSettings.tsx
index 110078b465..8e5db17f09 100644
--- a/meteor/client/ui/Settings/DeviceSettings.tsx
+++ b/meteor/client/ui/Settings/DeviceSettings.tsx
@@ -14,7 +14,7 @@ import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import { PeripheralDevicesAPI } from '../../lib/clientAPI'
import { NotificationCenter, Notification, NoticeLevel } from '../../lib/notifications/notifications'
-import { PeripheralDeviceStatus } from '../Status/SystemStatus'
+import { StatusCodePill } from '../Status/StatusCodePill'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'
import { GenericDeviceSettingsComponent } from './components/GenericDeviceSettingsComponent'
@@ -171,7 +171,11 @@ export default translateWithTracker
{device.type === PeripheralDeviceAPI.DeviceType.PACKAGE_MANAGER ? (
diff --git a/meteor/client/ui/Status/StatusCodePill.tsx b/meteor/client/ui/Status/StatusCodePill.tsx
new file mode 100644
index 0000000000..0b364a1ff0
--- /dev/null
+++ b/meteor/client/ui/Status/StatusCodePill.tsx
@@ -0,0 +1,62 @@
+import * as React from 'react'
+import * as reacti18next from 'react-i18next'
+import { PeripheralDeviceAPI } from '../../../lib/api/peripheralDevice'
+import { assertNever } from '../../../lib/lib'
+import ClassNames from 'classnames'
+import { StatusCode } from '@sofie-automation/blueprints-integration'
+import { statusCodeToString } from './SystemStatus'
+
+interface StatusCodePillProps {
+ connected: boolean
+ statusCode: StatusCode
+ messages?: string[]
+}
+export const StatusCodePill = reacti18next.withTranslation()(
+ class StatusCodePill extends React.Component
{
+ constructor(props: StatusCodePillProps & reacti18next.WithTranslation) {
+ super(props)
+ }
+ statusCodeString() {
+ const { t } = this.props
+
+ return this.props.connected ? statusCodeToString(t, this.props.statusCode) : t('Not Connected')
+ }
+ statusMessages() {
+ const messages = this.props.messages || []
+ return messages.length ? '"' + messages.join(', ') + '"' : ''
+ }
+ getStatusClassName(): string {
+ if (!this.props.connected) return 'device-status--unknown'
+
+ switch (this.props.statusCode) {
+ case PeripheralDeviceAPI.StatusCode.UNKNOWN:
+ return 'device-status--unknown'
+ case PeripheralDeviceAPI.StatusCode.GOOD:
+ return 'device-status--good'
+ case PeripheralDeviceAPI.StatusCode.WARNING_MINOR:
+ return 'device-status--minor-warning'
+ case PeripheralDeviceAPI.StatusCode.WARNING_MAJOR:
+ return 'device-status--warning'
+ case PeripheralDeviceAPI.StatusCode.BAD:
+ return 'device-status--bad'
+ case PeripheralDeviceAPI.StatusCode.FATAL:
+ return 'device-status--fatal'
+ default:
+ assertNever(this.props.statusCode)
+ return 'unknown-' + this.props.statusCode
+ }
+ }
+ render() {
+ return (
+
+
+ {this.statusCodeString()}
+
+
+ {this.statusMessages()}
+
+
+ )
+ }
+ }
+)
diff --git a/meteor/client/ui/Status/SystemStatus.tsx b/meteor/client/ui/Status/SystemStatus.tsx
index 0569131f5a..652f5bc2b7 100644
--- a/meteor/client/ui/Status/SystemStatus.tsx
+++ b/meteor/client/ui/Status/SystemStatus.tsx
@@ -5,7 +5,7 @@ import * as reacti18next from 'react-i18next'
import * as i18next from 'i18next'
import { PeripheralDeviceAPI } from '../../../lib/api/peripheralDevice'
import Moment from 'react-moment'
-import { getCurrentTime, getHash, unprotectString } from '../../../lib/lib'
+import { assertNever, getCurrentTime, getHash, unprotectString } from '../../../lib/lib'
import { Link } from 'react-router-dom'
import Tooltip from 'rc-tooltip'
import { faTrash, faEye } from '@fortawesome/free-solid-svg-icons'
@@ -25,6 +25,7 @@ import { doUserAction, UserAction } from '../../lib/userAction'
import { MeteorCall } from '../../../lib/api/methods'
import { RESTART_SALT } from '../../../lib/api/userActions'
import { CASPARCG_RESTART_TIME } from '../../../lib/constants'
+import { StatusCodePill } from './StatusCodePill'
interface IDeviceItemProps {
// key: string,
@@ -35,20 +36,24 @@ interface IDeviceItemProps {
}
interface IDeviceItemState {}
-export function statusCodeToString(t: i18next.TFunction, statusCode: PeripheralDeviceAPI.StatusCode) {
- return statusCode === PeripheralDeviceAPI.StatusCode.UNKNOWN
- ? t('Unknown')
- : statusCode === PeripheralDeviceAPI.StatusCode.GOOD
- ? t('Good')
- : statusCode === PeripheralDeviceAPI.StatusCode.WARNING_MINOR
- ? t('Minor Warning')
- : statusCode === PeripheralDeviceAPI.StatusCode.WARNING_MAJOR
- ? t('Warning')
- : statusCode === PeripheralDeviceAPI.StatusCode.BAD
- ? t('Bad')
- : statusCode === PeripheralDeviceAPI.StatusCode.FATAL
- ? t('Fatal')
- : t('Unknown')
+export function statusCodeToString(t: i18next.TFunction, statusCode: StatusCode) {
+ switch (statusCode) {
+ case PeripheralDeviceAPI.StatusCode.UNKNOWN:
+ return t('Unknown')
+ case PeripheralDeviceAPI.StatusCode.GOOD:
+ return t('Good')
+ case PeripheralDeviceAPI.StatusCode.WARNING_MINOR:
+ return t('Minor Warning')
+ case PeripheralDeviceAPI.StatusCode.WARNING_MAJOR:
+ return t('Warning')
+ case PeripheralDeviceAPI.StatusCode.BAD:
+ return t('Bad')
+ case PeripheralDeviceAPI.StatusCode.FATAL:
+ return t('Fatal')
+ default:
+ assertNever(statusCode)
+ return t('Unknown')
+ }
}
export const DeviceItem = reacti18next.withTranslation()(
@@ -197,7 +202,11 @@ export const DeviceItem = reacti18next.withTranslation()(
return (
-
+
{t('Last seen')}:
@@ -694,57 +703,3 @@ export default translateWithTracker
{
- constructor(props: PeripheralDeviceStatusProps & reacti18next.WithTranslation) {
- super(props)
- }
- statusCodeString() {
- const { t } = this.props
-
- return this.props.device.connected
- ? statusCodeToString(t, this.props.device.status.statusCode)
- : t('Not Connected')
- }
- statusMessages() {
- const messages = ((this.props.device || {}).status || {}).messages || []
- return messages.length ? '"' + messages.join(', ') + '"' : ''
- }
- render() {
- const statusClassNames = [
- 'device-status',
- this.props.device.status.statusCode === PeripheralDeviceAPI.StatusCode.UNKNOWN || !this.props.device.connected
- ? 'device-status--unknown'
- : this.props.device.status.statusCode === PeripheralDeviceAPI.StatusCode.GOOD
- ? 'device-status--good'
- : this.props.device.status.statusCode === PeripheralDeviceAPI.StatusCode.WARNING_MINOR
- ? 'device-status--minor-warning'
- : this.props.device.status.statusCode === PeripheralDeviceAPI.StatusCode.WARNING_MAJOR
- ? 'device-status--warning'
- : this.props.device.status.statusCode === PeripheralDeviceAPI.StatusCode.BAD
- ? 'device-status--bad'
- : this.props.device.status.statusCode === PeripheralDeviceAPI.StatusCode.FATAL
- ? 'device-status--fatal'
- : '',
- ].join(' ')
- return (
-
-
- {this.statusCodeString()}
-
-
- {this.statusMessages()}
-
-
- )
- }
- }
-)
From dac1cdba4e4fce73272cccf839e932efa8143ddf Mon Sep 17 00:00:00 2001
From: Johan Nyman
Date: Thu, 16 Sep 2021 07:15:31 +0200
Subject: [PATCH 089/112] feat: Add PackageContainerStatuses, used by Package
Manager to report on statuses of the PackageContainers and Monitors.
[publish]
---
meteor/client/lib/userAction.ts | 2 +
.../package-status/PackageContainerStatus.tsx | 76 +++++++++++
.../client/ui/Status/package-status/index.tsx | 125 ++++++++++++++----
.../Status/package-status/package-status.scss | 53 ++++++++
meteor/lib/api/peripheralDevice.ts | 3 +
meteor/lib/api/pubsub.ts | 1 +
meteor/lib/api/userActions.ts | 6 +
.../lib/collections/PackageContainerStatus.ts | 53 ++++++++
meteor/lib/userAction.ts | 1 +
.../api/integration/expectedPackages.ts | 106 ++++++++++++++-
meteor/server/api/packageManager.ts | 11 ++
meteor/server/api/peripheralDevice.ts | 21 +++
meteor/server/api/userActions.ts | 10 ++
meteor/server/publications/studio.ts | 11 ++
.../src/lib/corePeripherals.ts | 3 +
15 files changed, 454 insertions(+), 28 deletions(-)
create mode 100644 meteor/client/ui/Status/package-status/PackageContainerStatus.tsx
create mode 100644 meteor/lib/collections/PackageContainerStatus.ts
diff --git a/meteor/client/lib/userAction.ts b/meteor/client/lib/userAction.ts
index 719df038d3..708c8d1cd0 100644
--- a/meteor/client/lib/userAction.ts
+++ b/meteor/client/lib/userAction.ts
@@ -72,6 +72,8 @@ function userActionToLabel(userAction: UserAction, t: i18next.TFunction) {
return t('Aborting all Media Workflows')
case UserAction.PACKAGE_MANAGER_RESTART_WORK:
return t('Package Manager: Restart work')
+ case UserAction.PACKAGE_MANAGER_RESTART_PACKAGE_CONTAINER:
+ return t('Package Manager: Restart Package Container')
case UserAction.GENERATE_RESTART_TOKEN:
return t('Generating restart token')
case UserAction.RESTART_CORE:
diff --git a/meteor/client/ui/Status/package-status/PackageContainerStatus.tsx b/meteor/client/ui/Status/package-status/PackageContainerStatus.tsx
new file mode 100644
index 0000000000..d6eec7a19a
--- /dev/null
+++ b/meteor/client/ui/Status/package-status/PackageContainerStatus.tsx
@@ -0,0 +1,76 @@
+import * as React from 'react'
+import { Translated } from '../../../lib/ReactMeteorData/react-meteor-data'
+import Tooltip from 'rc-tooltip'
+import { withTranslation } from 'react-i18next'
+import { PackageContainerStatusDB } from '../../../../lib/collections/PackageContainerStatus'
+import { StatusCodePill } from '../StatusCodePill'
+import { doUserAction, UserAction } from '../../../lib/userAction'
+import { MeteorCall } from '../../../../lib/api/methods'
+
+interface IPackageContainerStatusProps {
+ packageContainerStatus: PackageContainerStatusDB
+}
+
+export const PackageContainerStatus = withTranslation()(
+ class PackageContainerStatus extends React.Component, {}> {
+ constructor(props) {
+ super(props)
+
+ this.state = {}
+ }
+
+ restartPackageContainer(e: React.MouseEvent) {
+ doUserAction(this.props.t, e, UserAction.PACKAGE_MANAGER_RESTART_PACKAGE_CONTAINER, (e) =>
+ MeteorCall.userAction.packageManagerRestartPackageContainer(
+ e,
+ this.props.packageContainerStatus.deviceId,
+ this.props.packageContainerStatus.containerId
+ )
+ )
+ }
+
+ render() {
+ const { t } = this.props
+ const packageContainerStatus = this.props.packageContainerStatus
+
+ return (
+ <>
+
+
+ {packageContainerStatus.containerId}
+
+
+
+
+
+ {packageContainerStatus.status.statusReason.user}
+
+
+
+ this.restartPackageContainer(e)}>
+ {t('Restart')}
+
+
+
+ {Object.entries(packageContainerStatus.status.monitors).map(([monitorId, monitor]) => {
+ return (
+
+
+ {monitorId}
+
+
+
+
+
+ {monitor.statusReason.user}
+
+
+
+
+ )
+ })}
+ >
+ )
+ }
+ }
+)
diff --git a/meteor/client/ui/Status/package-status/index.tsx b/meteor/client/ui/Status/package-status/index.tsx
index 8f8472102b..394b4c3d04 100644
--- a/meteor/client/ui/Status/package-status/index.tsx
+++ b/meteor/client/ui/Status/package-status/index.tsx
@@ -13,30 +13,64 @@ import { doUserAction, UserAction } from '../../../lib/userAction'
import { Studios } from '../../../../lib/collections/Studios'
import { Meteor } from 'meteor/meteor'
import { PackageStatus } from './PackageStatus'
+import { PackageContainerStatusDB, PackageContainerStatuses } from '../../../../lib/collections/PackageContainerStatus'
+import { PackageContainerStatus } from './PackageContainerStatus'
+import { Spinner } from '../../../lib/Spinner'
interface IIExpectedPackagesStatusTrackedProps {
expectedPackageWorkStatuses: ExpectedPackageWorkStatus[]
expectedPackages: ExpectedPackageDB[]
+ packageContainerStatuses: PackageContainerStatusDB[]
+}
+interface IIExpectedPackagesStatusState {
+ allSubsReady: boolean
}
export const ExpectedPackagesStatus = translateWithTracker<{}, {}, IIExpectedPackagesStatusTrackedProps>(() => {
return {
expectedPackageWorkStatuses: ExpectedPackageWorkStatuses.find({}).fetch(),
expectedPackages: ExpectedPackages.find({}).fetch(),
+ packageContainerStatuses: PackageContainerStatuses.find().fetch(),
}
})(
- class PackageManagerStatus extends MeteorReactComponent, {}> {
+ class PackageManagerStatus extends MeteorReactComponent<
+ Translated,
+ IIExpectedPackagesStatusState
+ > {
constructor(props) {
super(props)
+ this.state = {
+ allSubsReady: false,
+ }
}
componentDidMount() {
// Subscribe to data:
- this.subscribe(PubSub.expectedPackageWorkStatuses, {
- studioId: 'studio0', // hack
- })
- this.subscribe(PubSub.expectedPackages, {
- studioId: 'studio0', // hack
+ this.autorun(() => {
+ // Hack: We should add a studio selector in the GUI instead
+ const studioIds = Studios.find()
+ .fetch()
+ .map((studio) => studio._id)
+
+ console.log('studioIds', studioIds)
+ const subs = [
+ this.subscribe(PubSub.expectedPackageWorkStatuses, {
+ studioId: { $in: studioIds },
+ }),
+ this.subscribe(PubSub.expectedPackages, {
+ studioId: { $in: studioIds },
+ }),
+ this.subscribe(PubSub.packageContainerStatuses, {
+ studioId: { $in: studioIds },
+ }),
+ ]
+
+ this.autorun(() => {
+ const allSubsReady = subs.reduce((memo, sub) => {
+ return memo && sub.ready()
+ }, true)
+ this.setState({ allSubsReady: studioIds.length > 0 && allSubsReady })
+ })
})
}
restartAllExpectations(e: React.MouseEvent): void {
@@ -126,6 +160,16 @@ export const ExpectedPackagesStatus = translateWithTracker<{}, {}, IIExpectedPac
)
})
}
+ renderPackageContainerStatuses() {
+ return this.props.packageContainerStatuses.map((packageContainerStatus) => {
+ return (
+
+ )
+ })
+ }
render() {
const { t } = this.props
@@ -134,26 +178,55 @@ export const ExpectedPackagesStatus = translateWithTracker<{}, {}, IIExpectedPac
-
- this.restartAllExpectations(e)}>
- {t('Restart All')}
-
-
-
-
-
-
- {t('Status')}
- {t('Name')}
- {t('Created')}
-
- {/* {t('Info')} */}
-
- {this.renderExpectedPackageStatuses()}
-
-
-
-
+
+ {this.state.allSubsReady ? (
+ <>
+
+
+
+ {t('Package container status')}
+
+
+
+
+
+
+ {t('Id')}
+ {t('Status')}
+
+
+ {this.renderPackageContainerStatuses()}
+
+
+
+
+
+
+
+
+ this.restartAllExpectations(e)}>
+ {t('Restart All jobs')}
+
+
+
+
+
+
+
+ {t('Status')}
+ {t('Name')}
+ {t('Created')}
+
+
+ {this.renderExpectedPackageStatuses()}
+
+
+ >
+ ) : (
+
+ )}
)
}
diff --git a/meteor/client/ui/Status/package-status/package-status.scss b/meteor/client/ui/Status/package-status/package-status.scss
index d429287c0a..a1ca980f88 100644
--- a/meteor/client/ui/Status/package-status/package-status.scss
+++ b/meteor/client/ui/Status/package-status/package-status.scss
@@ -136,6 +136,59 @@
}
}
}
+
+ .packageContainer-status-list {
+ width: 100%;
+
+ .packageContainer-status__header {
+ background-color: #898989;
+ color: #fff;
+
+ > th {
+ padding: 0.25em 0.5em 0.25em 0.5em;
+ }
+ }
+
+ .packageContainer {
+ > td {
+ padding: 0.5em 0.5em 0.5em 0.5em;
+ background-color: #f6f6f6;
+ vertical-align: middle;
+
+ border-bottom: 1px solid #bbbbbb;
+
+ cursor: pointer;
+
+ &:first-child {
+ }
+ &:nth-child(2) {
+ padding-left: 0;
+ width: 7em;
+ }
+ }
+ }
+ .packageContainer-monitor {
+ > td {
+ padding: 0.5em 0.5em 0.5em 0em;
+ background-color: #c6c6c6;
+ vertical-align: middle;
+ border-bottom: 1px solid #919191;
+ cursor: pointer;
+
+ font-size: 90%;
+ position: relative;
+
+ &:first-child {
+ width: 2em;
+ background-color: transparent;
+ border-bottom: none;
+ }
+ &:nth-child(2) {
+ padding-left: 1em;
+ }
+ }
+ }
+ }
}
.job-status-icon {
display: inline-block;
diff --git a/meteor/lib/api/peripheralDevice.ts b/meteor/lib/api/peripheralDevice.ts
index 87181abfe2..4b178d652a 100644
--- a/meteor/lib/api/peripheralDevice.ts
+++ b/meteor/lib/api/peripheralDevice.ts
@@ -431,6 +431,9 @@ export enum PeripheralDeviceAPIMethods {
'updatePackageContainerPackageStatuses' = 'peripheralDevice.packageManager.updatePackageContainerPackageStatuses',
'removeAllPackageContainerPackageStatusesOfDevice' = 'peripheralDevice.packageManager.removeAllPackageContainerPackageStatusesOfDevice',
+ 'updatePackageContainerStatuses' = 'peripheralDevice.packageManager.updatePackageContainerStatuses',
+ 'removeAllPackageContainerStatusesOfDevice' = 'peripheralDevice.packageManager.removeAllPackageContainerStatusesOfDevice',
+
'fetchPackageInfoMetadata' = 'peripheralDevice.packageManager.fetchPackageInfoMetadata',
'updatePackageInfo' = 'peripheralDevice.packageManager.updatePackageInfo',
'removePackageInfo' = 'peripheralDevice.packageManager.removePackageInfo',
diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts
index be0d0c78e8..72acfc9064 100644
--- a/meteor/lib/api/pubsub.ts
+++ b/meteor/lib/api/pubsub.ts
@@ -49,6 +49,7 @@ export enum PubSub {
expectedPackages = 'expectedPackages',
expectedPackageWorkStatuses = 'expectedPackageWorkStatuses',
packageContainerPackageStatuses = 'packageContainerStatuses',
+ packageContainerStatuses = 'packageContainerStatuses',
packageInfos = 'packageInfos',
// custom publications:
mappingsForDevice = 'mappingsForDevice',
diff --git a/meteor/lib/api/userActions.ts b/meteor/lib/api/userActions.ts
index f13f9d22bf..71d1825317 100644
--- a/meteor/lib/api/userActions.ts
+++ b/meteor/lib/api/userActions.ts
@@ -164,6 +164,11 @@ export interface NewUserActionAPI extends MethodContext {
deviceId: PeripheralDeviceId,
workId: string
): Promise
>
+ packageManagerRestartPackageContainer(
+ userEvent: string,
+ deviceId: PeripheralDeviceId,
+ containerId: string
+ ): Promise>
regenerateRundownPlaylist(userEvent: string, playlistId: RundownPlaylistId): Promise>
generateRestartToken(userEvent: string): Promise>
restartCore(userEvent: string, token: string): Promise>
@@ -274,6 +279,7 @@ export enum UserActionAPIMethods {
'packageManagerRestartExpectation' = 'userAction.packagemanager.restartExpectation',
'packageManagerRestartAllExpectations' = 'userAction.packagemanager.restartAllExpectations',
'packageManagerAbortExpectation' = 'userAction.packagemanager.abortExpectation',
+ 'packageManagerRestartPackageContainer' = 'userAction.packagemanager.restartPackageContainer',
'regenerateRundownPlaylist' = 'userAction.ingest.regenerateRundownPlaylist',
diff --git a/meteor/lib/collections/PackageContainerStatus.ts b/meteor/lib/collections/PackageContainerStatus.ts
new file mode 100644
index 0000000000..0ceba35ed7
--- /dev/null
+++ b/meteor/lib/collections/PackageContainerStatus.ts
@@ -0,0 +1,53 @@
+import { registerCollection, ProtectedString, Time, protectString } from '../lib'
+import { createMongoCollection } from './lib'
+import { StudioId } from './Studios'
+import { registerIndex } from '../database'
+import { ExpectedPackageStatusAPI } from '@sofie-automation/blueprints-integration'
+import { PeripheralDeviceId } from './PeripheralDevices'
+
+/**
+ * The PackageContainerStatuses-collection contains statuses about PackageContainers
+ * PackageContainerStatuses are populated by the Package Manager-device and are used to track
+ * jobs that runs on the PackageContainer, such as cronjobs and monitors
+ *
+ * Note: A "Package Container" is a generic term for "something that contains packages".
+ * One example of this could be a Media-folder (the "package container") which contains Media-files ("packages").
+ */
+
+/** Id of a package container */
+export type PackageContainerId = ProtectedString<'PackageContainerId'>
+
+export interface PackageContainerStatusDB {
+ _id: PackageContainerId // unique id, see getPackageContainerId()
+
+ /** The studio this PackageContainer is defined in */
+ studioId: StudioId
+
+ /** The id of the PackageContainer */
+ containerId: string
+
+ /** Which PeripheralDevice this update came from */
+ deviceId: PeripheralDeviceId
+
+ /** The status of the PackageContainer */
+ status: ExpectedPackageStatusAPI.PackageContainerStatus
+
+ modified: Time
+}
+
+export const PackageContainerStatuses = createMongoCollection(
+ 'packageContainerStatuses'
+)
+registerCollection('PackageContainerStatuses', PackageContainerStatuses)
+
+registerIndex(PackageContainerStatuses, {
+ studioId: 1,
+ containerId: 1,
+})
+registerIndex(PackageContainerStatuses, {
+ deviceId: 1,
+})
+
+export function getPackageContainerId(studioId: StudioId, containerId: string): PackageContainerId {
+ return protectString(`${studioId}_${containerId}`)
+}
diff --git a/meteor/lib/userAction.ts b/meteor/lib/userAction.ts
index 69e3d340b3..eaa82712e3 100644
--- a/meteor/lib/userAction.ts
+++ b/meteor/lib/userAction.ts
@@ -31,6 +31,7 @@ export enum UserAction {
PRIORITIZE_MEDIA_WORKFLOW,
ABORT_ALL_MEDIA_WORKFLOWS,
PACKAGE_MANAGER_RESTART_WORK,
+ PACKAGE_MANAGER_RESTART_PACKAGE_CONTAINER,
GENERATE_RESTART_TOKEN,
RESTART_CORE,
USER_LOG_PLAYER_METHOD,
diff --git a/meteor/server/api/integration/expectedPackages.ts b/meteor/server/api/integration/expectedPackages.ts
index 9202f2913e..3645d499d6 100644
--- a/meteor/server/api/integration/expectedPackages.ts
+++ b/meteor/server/api/integration/expectedPackages.ts
@@ -20,6 +20,12 @@ import {
import { getPackageInfoId, PackageInfoDB, PackageInfos } from '../../../lib/collections/PackageInfos'
import { BulkWriteOperation } from 'mongodb'
import { onUpdatedPackageInfo } from '../ingest/packageInfo'
+import {
+ getPackageContainerId,
+ PackageContainerId,
+ PackageContainerStatusDB,
+ PackageContainerStatuses,
+} from '../../../lib/collections/PackageContainerStatus'
export namespace PackageManagerIntegration {
export async function updateExpectedPackageWorkStatuses(
@@ -138,7 +144,11 @@ export namespace PackageManagerIntegration {
const peripheralDevice = checkAccessAndGetPeripheralDevice(deviceId, deviceToken, context)
await ExpectedPackageWorkStatuses.removeAsync({
- deviceId: peripheralDevice._id,
+ $or: [
+ { deviceId: peripheralDevice._id },
+ // Since we only have one PM in a studio, we can remove everything in the studio:
+ { studioId: peripheralDevice.studioId },
+ ],
})
}
@@ -229,9 +239,101 @@ export namespace PackageManagerIntegration {
const peripheralDevice = checkAccessAndGetPeripheralDevice(deviceId, deviceToken, context)
await PackageContainerPackageStatuses.removeAsync({
- deviceId: peripheralDevice._id,
+ $or: [
+ { deviceId: peripheralDevice._id },
+ // Since we only have one PM in a studio, we can remove everything in the studio:
+ { studioId: peripheralDevice.studioId },
+ ],
+ })
+ }
+
+ export async function updatePackageContainerStatuses(
+ context: MethodContext,
+ deviceId: PeripheralDeviceId,
+ deviceToken: string,
+ changes: (
+ | {
+ containerId: string
+ type: 'delete'
+ }
+ | {
+ containerId: string
+ type: 'update'
+ status: ExpectedPackageStatusAPI.PackageContainerStatus
+ }
+ )[]
+ ): Promise {
+ const peripheralDevice = checkAccessAndGetPeripheralDevice(deviceId, deviceToken, context)
+ if (!peripheralDevice.studioId)
+ throw new Meteor.Error(400, 'Device "' + peripheralDevice._id + '" has no studio')
+
+ const studioId = peripheralDevice.studioId
+
+ const removedIds: PackageContainerId[] = []
+ const ps: Promise[] = []
+ for (const change of changes) {
+ check(change.containerId, String)
+
+ const id = getPackageContainerId(peripheralDevice.studioId, change.containerId)
+
+ if (change.type === 'delete') {
+ removedIds.push(id)
+ } else if (change.type === 'update') {
+ ps.push(
+ Promise.resolve().then(async () => {
+ const updateCount = await PackageContainerStatuses.updateAsync(id, {
+ $set: {
+ status: change.status,
+ modified: getCurrentTime(),
+ },
+ })
+ if (updateCount === 0) {
+ // The PackageContainerStatus doesn't exist
+ // Create it on the fly:
+
+ await PackageContainerStatuses.upsertAsync(id, {
+ $set: literal({
+ _id: id,
+ studioId: studioId,
+ containerId: change.containerId,
+ deviceId: peripheralDevice._id,
+ status: change.status,
+ modified: getCurrentTime(),
+ }),
+ })
+ }
+ })
+ )
+ } else {
+ assertNever(change)
+ }
+ }
+ if (removedIds.length) {
+ ps.push(
+ PackageContainerStatuses.removeAsync({
+ deviceId: peripheralDevice._id,
+ _id: { $in: removedIds },
+ })
+ )
+ }
+ await Promise.all(ps)
+ }
+ export async function removeAllPackageContainerStatusesOfDevice(
+ context: MethodContext,
+ deviceId: PeripheralDeviceId,
+ deviceToken: string
+ ): Promise {
+ const peripheralDevice = checkAccessAndGetPeripheralDevice(deviceId, deviceToken, context)
+
+ await PackageContainerStatuses.removeAsync({
+ $or: [
+ { deviceId: peripheralDevice._id },
+ // Since we only have one PM in a studio, we can remove everything in the studio:
+ { studioId: peripheralDevice.studioId },
+ ],
})
}
+
export async function fetchPackageInfoMetadata(
context: MethodContext,
deviceId: PeripheralDeviceId,
diff --git a/meteor/server/api/packageManager.ts b/meteor/server/api/packageManager.ts
index e74a3f2309..68a18b5a6f 100644
--- a/meteor/server/api/packageManager.ts
+++ b/meteor/server/api/packageManager.ts
@@ -45,4 +45,15 @@ export namespace PackageManagerAPI {
})
)
}
+ export function restartPackageContainer(
+ context: MethodContext,
+ deviceId: PeripheralDeviceId,
+ containerId: string
+ ): any {
+ check(deviceId, String)
+ check(containerId, String)
+ PeripheralDeviceContentWriteAccess.peripheralDevice(context, deviceId)
+
+ waitForPromise(PeripheralDeviceAPI.executeFunctionAsync(deviceId, 'restartPackageContainer', containerId))
+ }
}
diff --git a/meteor/server/api/peripheralDevice.ts b/meteor/server/api/peripheralDevice.ts
index e7b69a40aa..bb61f60e4f 100644
--- a/meteor/server/api/peripheralDevice.ts
+++ b/meteor/server/api/peripheralDevice.ts
@@ -1173,6 +1173,27 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri
async removeAllPackageContainerPackageStatusesOfDevice(deviceId: PeripheralDeviceId, deviceToken: string) {
await PackageManagerIntegration.removeAllPackageContainerPackageStatusesOfDevice(this, deviceId, deviceToken)
}
+ async updatePackageContainerStatuses(
+ deviceId: PeripheralDeviceId,
+ deviceToken: string,
+ changes: (
+ | {
+ containerId: string
+ type: 'delete'
+ }
+ | {
+ containerId: string
+ type: 'update'
+ status: ExpectedPackageStatusAPI.PackageContainerStatus
+ }
+ )[]
+ ): Promise {
+ await PackageManagerIntegration.updatePackageContainerStatuses(this, deviceId, deviceToken, changes)
+ }
+ async removeAllPackageContainerStatusesOfDevice(deviceId: PeripheralDeviceId, deviceToken: string) {
+ await PackageManagerIntegration.removeAllPackageContainerStatusesOfDevice(this, deviceId, deviceToken)
+ }
+
async fetchPackageInfoMetadata(
deviceId: PeripheralDeviceId,
deviceToken: string,
diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts
index c7952ec230..75527997d7 100644
--- a/meteor/server/api/userActions.ts
+++ b/meteor/server/api/userActions.ts
@@ -662,6 +662,13 @@ export function packageManagerRestartAllExpectations(context: MethodContext, stu
export function packageManagerAbortExpectation(context: MethodContext, deviceId: PeripheralDeviceId, workId: string) {
return ClientAPI.responseSuccess(PackageManagerAPI.abortExpectation(context, deviceId, workId))
}
+export function packageManagerRestartPackageContainer(
+ context: MethodContext,
+ deviceId: PeripheralDeviceId,
+ containerId: string
+) {
+ return ClientAPI.responseSuccess(PackageManagerAPI.restartPackageContainer(context, deviceId, containerId))
+}
export async function bucketsRemoveBucket(context: MethodContext, id: BucketId) {
check(id, String)
@@ -1143,6 +1150,9 @@ class ServerUserActionAPI extends MethodContextAPI implements NewUserActionAPI {
async packageManagerAbortExpectation(_userEvent: string, deviceId: PeripheralDeviceId, workId: string) {
return makePromise(() => packageManagerAbortExpectation(this, deviceId, workId))
}
+ async packageManagerRestartPackageContainer(_userEvent: string, deviceId: PeripheralDeviceId, containerId: string) {
+ return makePromise(() => packageManagerRestartPackageContainer(this, deviceId, containerId))
+ }
async regenerateRundownPlaylist(_userEvent: string, playlistId: RundownPlaylistId) {
return traceAction(UserActionAPIMethods.regenerateRundownPlaylist, regenerateRundownPlaylist, this, playlistId)
}
diff --git a/meteor/server/publications/studio.ts b/meteor/server/publications/studio.ts
index 650c88ef82..94ec55df4c 100644
--- a/meteor/server/publications/studio.ts
+++ b/meteor/server/publications/studio.ts
@@ -24,6 +24,7 @@ import {
} from '../../lib/collections/PackageContainerPackageStatus'
import { Match } from 'meteor/check'
import { PackageInfos } from '../../lib/collections/PackageInfos'
+import { PackageContainerStatuses } from '../../lib/collections/PackageContainerStatus'
meteorPublish(PubSub.studios, function (selector0, token) {
const { cred, selector } = AutoFillSelector.organizationId(this.userId, selector0, token)
@@ -103,6 +104,16 @@ meteorPublish(PubSub.expectedPackageWorkStatuses, function (selector, token) {
}
return null
})
+meteorPublish(PubSub.packageContainerStatuses, function (selector, token) {
+ if (!selector) throw new Meteor.Error(400, 'selector argument missing')
+ const modifier: FindOptions = {
+ fields: {},
+ }
+ if (StudioReadAccess.studioContent(selector, { userId: this.userId, token })) {
+ return PackageContainerStatuses.find(selector, modifier)
+ }
+ return null
+})
meteorPublish(PubSub.packageInfos, function (selector, token) {
if (!selector) throw new Meteor.Error(400, 'selector argument missing')
const modifier: FindOptions = {
diff --git a/packages/server-core-integration/src/lib/corePeripherals.ts b/packages/server-core-integration/src/lib/corePeripherals.ts
index 6e6db7e8eb..5c314151a3 100644
--- a/packages/server-core-integration/src/lib/corePeripherals.ts
+++ b/packages/server-core-integration/src/lib/corePeripherals.ts
@@ -157,6 +157,9 @@ export namespace PeripheralDeviceAPI {
'updatePackageContainerPackageStatuses' = 'peripheralDevice.packageManager.updatePackageContainerPackageStatuses',
'removeAllPackageContainerPackageStatusesOfDevice' = 'peripheralDevice.packageManager.removeAllPackageContainerPackageStatusesOfDevice',
+ 'updatePackageContainerStatuses' = 'peripheralDevice.packageManager.updatePackageContainerStatuses',
+ 'removeAllPackageContainerStatusesOfDevice' = 'peripheralDevice.packageManager.removeAllPackageContainerStatusesOfDevice',
+
'fetchPackageInfoMetadata' = 'peripheralDevice.packageManager.fetchPackageInfoMetadata',
'updatePackageInfo' = 'peripheralDevice.packageManager.updatePackageInfo',
'removePackageInfo' = 'peripheralDevice.packageManager.removePackageInfo',
From 6f29254b51d3b8c8cde294586bc07cff079bc565 Mon Sep 17 00:00:00 2001
From: Tom Lee
Date: Wed, 22 Sep 2021 10:49:21 +0100
Subject: [PATCH 090/112] chore: Clean up dashboard element styling
---
meteor/client/ui/Shelf/AdLibRegionPanel.tsx | 16 +++++++---------
meteor/client/ui/Shelf/ColoredBoxPanel.tsx | 17 +++++------------
meteor/client/ui/Shelf/DashboardPanel.tsx | 6 ++++--
meteor/client/ui/Shelf/EndWordsPanel.tsx | 11 ++---------
meteor/client/ui/Shelf/ExternalFramePanel.tsx | 16 +++++++---------
meteor/client/ui/Shelf/PartNamePanel.tsx | 11 ++---------
meteor/client/ui/Shelf/PartTimingPanel.tsx | 11 ++---------
meteor/client/ui/Shelf/PieceCountdownPanel.tsx | 17 +++++------------
.../client/ui/Shelf/PlaylistEndTimerPanel.tsx | 11 ++---------
meteor/client/ui/Shelf/PlaylistNamePanel.tsx | 11 ++---------
.../client/ui/Shelf/PlaylistStartTimerPanel.tsx | 11 ++---------
meteor/client/ui/Shelf/SegmentNamePanel.tsx | 11 ++---------
meteor/client/ui/Shelf/SegmentTimingPanel.tsx | 11 ++---------
meteor/client/ui/Shelf/ShowStylePanel.tsx | 11 ++---------
meteor/client/ui/Shelf/StudioNamePanel.tsx | 11 ++---------
meteor/client/ui/Shelf/SystemStatusPanel.tsx | 11 ++---------
meteor/client/ui/Shelf/TextLabelPanel.tsx | 11 ++---------
meteor/client/ui/Shelf/TimeOfDayPanel.tsx | 11 ++---------
.../client/ui/Shelf/TimelineDashboardPanel.tsx | 4 ++--
19 files changed, 56 insertions(+), 163 deletions(-)
diff --git a/meteor/client/ui/Shelf/AdLibRegionPanel.tsx b/meteor/client/ui/Shelf/AdLibRegionPanel.tsx
index 1f6a2bfcb4..77a3b2a060 100644
--- a/meteor/client/ui/Shelf/AdLibRegionPanel.tsx
+++ b/meteor/client/ui/Shelf/AdLibRegionPanel.tsx
@@ -8,7 +8,7 @@ import {
} from '../../../lib/collections/RundownLayouts'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
import {
- dashboardElementPosition,
+ dashboardElementStyle,
IDashboardPanelTrackedProps,
getUnfinishedPieceInstancesGrouped,
getNextPieceInstancesGrouped,
@@ -143,14 +143,12 @@ export class AdLibRegionPanelInner extends MeteorReactComponent<
return (
diff --git a/meteor/client/ui/Shelf/DashboardPanel.tsx b/meteor/client/ui/Shelf/DashboardPanel.tsx
index 5894517abb..0651f98d85 100644
--- a/meteor/client/ui/Shelf/DashboardPanel.tsx
+++ b/meteor/client/ui/Shelf/DashboardPanel.tsx
@@ -69,11 +69,12 @@ interface DashboardPositionableElement {
y: number
width: number
height: number
+ scale?: number
}
type AdLibPieceUiWithNext = AdLibPieceUi & { isNext: boolean }
-export function dashboardElementPosition(el: DashboardPositionableElement): React.CSSProperties {
+export function dashboardElementStyle(el: DashboardPositionableElement): React.CSSProperties {
return {
width:
el.width >= 0
@@ -107,6 +108,7 @@ export function dashboardElementPosition(el: DashboardPositionableElement): Reac
: el.height < 0
? `calc(${-1 * el.height - 1} * var(--dashboard-button-grid-height))`
: undefined,
+ fontSize: (el.scale || 1) * 1.5 + 'em',
}
}
@@ -601,7 +603,7 @@ export class DashboardPanelInner extends MeteorReactComponent<
'dashboard-panel--take': filter.displayTakeButtons,
})}
ref={this.setRef}
- style={dashboardElementPosition(filter)}
+ style={dashboardElementStyle(filter)}
>
{this.props.filter.name}
{filter.enableSearch && (
diff --git a/meteor/client/ui/Shelf/EndWordsPanel.tsx b/meteor/client/ui/Shelf/EndWordsPanel.tsx
index f0b8d8c387..e0d463e58c 100644
--- a/meteor/client/ui/Shelf/EndWordsPanel.tsx
+++ b/meteor/client/ui/Shelf/EndWordsPanel.tsx
@@ -7,7 +7,7 @@ import {
RundownLayoutEndWords,
} from '../../../lib/collections/RundownLayouts'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
-import { dashboardElementPosition } from './DashboardPanel'
+import { dashboardElementStyle } from './DashboardPanel'
import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData'
import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
@@ -51,14 +51,7 @@ export class EndWordsPanelInner extends MeteorReactComponent<
'end-words-panel timing',
isDashboardLayout ? (panel as DashboardLayoutEndsWords).customClasses : undefined
)}
- style={_.extend(
- isDashboardLayout
- ? {
- ...dashboardElementPosition({ ...(this.props.panel as DashboardLayoutEndsWords) }),
- fontSize: ((panel as DashboardLayoutEndsWords).scale || 1) * 1.5 + 'em',
- }
- : {}
- )}
+ style={isDashboardLayout ? dashboardElementStyle(this.props.panel as DashboardLayoutEndsWords) : {}}
>
{!this.props.panel.hideLabel &&
{t('End Words')} }
diff --git a/meteor/client/ui/Shelf/ExternalFramePanel.tsx b/meteor/client/ui/Shelf/ExternalFramePanel.tsx
index 0e665eee71..00d1b805e5 100644
--- a/meteor/client/ui/Shelf/ExternalFramePanel.tsx
+++ b/meteor/client/ui/Shelf/ExternalFramePanel.tsx
@@ -9,7 +9,7 @@ import {
DashboardLayoutExternalFrame,
} from '../../../lib/collections/RundownLayouts'
import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
-import { dashboardElementPosition } from './DashboardPanel'
+import { dashboardElementStyle } from './DashboardPanel'
import { literal, protectString } from '../../../lib/lib'
import { RundownPlaylist, RundownPlaylistId } from '../../../lib/collections/RundownPlaylists'
import { PartInstanceId, PartInstances, PartInstance } from '../../../lib/collections/PartInstances'
@@ -508,14 +508,12 @@ export const ExternalFramePanel = withTranslation()(
? (this.props.panel as DashboardLayoutExternalFrame).customClasses
: undefined
)}
- style={_.extend(
- RundownLayoutsAPI.isDashboardLayout(this.props.layout)
- ? dashboardElementPosition(this.props.panel as DashboardLayoutExternalFrame)
- : {},
- {
- visibility: this.props.visible ? 'visible' : 'hidden',
- }
- )}
+ style={{
+ visibility: this.props.visible ? 'visible' : 'hidden',
+ ...(RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ ? dashboardElementStyle(this.props.panel as DashboardLayoutExternalFrame)
+ : {}),
+ }}
>