diff --git a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx
index 6a187743c3..db6eeb6243 100644
--- a/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx
+++ b/meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx
@@ -336,7 +336,7 @@ export function useTracker(
* @param {...any[]} args A list of arugments for the subscription. This is used for optimizing the subscription across
* renders so that it isn't torn down and created for every render.
*/
-export function useSubscription(sub: PubSub, ...args: any[]) {
+export function useSubscription(sub: PubSub, ...args: any[]): boolean {
const [ready, setReady] = useState(false)
useEffect(() => {
diff --git a/meteor/client/lib/reactiveData/reactiveDataHelper.ts b/meteor/client/lib/reactiveData/reactiveDataHelper.ts
index 4508a979f6..22904913b3 100644
--- a/meteor/client/lib/reactiveData/reactiveDataHelper.ts
+++ b/meteor/client/lib/reactiveData/reactiveDataHelper.ts
@@ -54,6 +54,23 @@ const isolatedAutorunsMem: {
}
} = {}
+/**
+ * Create a reactive computation that will be run independently of the outer one. If the same function (using the same
+ * name and parameters) will be used again, this computation will only be computed once on invalidation and it's
+ * result will be memoized and reused on every other call.
+ *
+ * The function will be considered "same", if `functionName` and `params` match.
+ *
+ * If the `fnc` computation is invalidated, the outer computations will only be invalidated if the value returned from
+ * `fnc` fails a deep equality check (_.isEqual).
+ *
+ * @export
+ * @template T
+ * @param {T} fnc The computation function to be memoized and calculated separately from the outer one.
+ * @param {string} functionName The name of this computation function
+ * @param {...Parameters} params Params `fnc` depends on from the outer scope. All parameters will be passed through to the function.
+ * @return {*} {ReturnType}
+ */
export function memoizedIsolatedAutorun any>(
fnc: T,
functionName: string,
@@ -105,14 +122,22 @@ export function memoizedIsolatedAutorun any>(
return result
}
-export function slowDownReactivity any>(
- fnc: T,
- delay: number,
- ...params: Parameters
-): ReturnType {
+/**
+ * Slow down the reactivity of the inner function `fnc` to the outer computation.
+ *
+ * This is essentially a `throttle` for reactivity. If the inner `fnc` computation is invalidated, it will wait `delay`
+ * time to invalidate the outer computation.
+ *
+ * @export
+ * @template T
+ * @param {T} fnc The wrapped computation
+ * @param {number} delay The amount of time to wait before invalidating the outer function
+ * @return {*} {ReturnType}
+ */
+export function slowDownReactivity any>(fnc: T, delay: number): ReturnType {
// if the delay is <= 0, call straight away and register a direct dependency
if (delay <= 0) {
- return fnc(...(params as any))
+ return fnc()
}
// if the delay is > 0, slow down the reactivity
@@ -125,7 +150,7 @@ export function slowDownReactivity any>(
const parentComputation = Tracker.currentComputation
const computation = Tracker.nonreactive(() => {
const computation = Tracker.autorun(() => {
- result = fnc(...(params as any))
+ result = fnc()
})
computation.onInvalidate(() => {
// if the parent hasn't been invalidated and there is no scheduled invalidation
diff --git a/meteor/client/lib/rundownLayouts.ts b/meteor/client/lib/rundownLayouts.ts
new file mode 100644
index 0000000000..3d820da198
--- /dev/null
+++ b/meteor/client/lib/rundownLayouts.ts
@@ -0,0 +1,131 @@
+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, true)
+ let activePieceInstance: PieceInstance | undefined
+ const activeLayers = unfinishedPieces.map((p) => p.piece.sourceLayerId)
+ const containsEveryRequiredLayer = panel.requireAllAdditionalSourcelayers
+ ? panel.additionalLayers?.length && panel.additionalLayers.every((s) => activeLayers.includes(s))
+ : false
+ const containsRequiredLayer = containsEveryRequiredLayer
+ ? true
+ : panel.additionalLayers && panel.additionalLayers.length
+ ? panel.additionalLayers.some((s) => activeLayers.includes(s))
+ : false
+
+ if (
+ (!panel.requireAllAdditionalSourcelayers || containsEveryRequiredLayer) &&
+ (!panel.additionalLayers?.length || containsRequiredLayer)
+ ) {
+ activePieceInstance =
+ panel.requiredLayerIds && panel.requiredLayerIds.length
+ ? _.flatten(Object.values(unfinishedPieces)).find((piece: PieceInstance) => {
+ return (
+ (panel.requiredLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 &&
+ piece.partInstanceId === playlist.currentPartInstanceId
+ )
+ })
+ : undefined
+ }
+ return {
+ active:
+ activePieceInstance !== undefined || (!panel.requiredLayerIds?.length && !panel.additionalLayers?.length),
+ activePieceInstance,
+ }
+}
+
+export function getUnfinishedPieceInstancesReactive(playlist: RundownPlaylist, includeNonAdLibPieces: boolean) {
+ let prospectivePieces: PieceInstance[] = []
+ const now = getCurrentTime()
+ if (playlist.activationId && playlist.currentPartInstanceId) {
+ prospectivePieces = PieceInstances.find({
+ startedPlayback: {
+ $exists: true,
+ },
+ playlistActivationId: playlist.activationId,
+ $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/lib/triggers/TriggersHandler.tsx b/meteor/client/lib/triggers/TriggersHandler.tsx
index 85541e65b8..e157d07415 100644
--- a/meteor/client/lib/triggers/TriggersHandler.tsx
+++ b/meteor/client/lib/triggers/TriggersHandler.tsx
@@ -265,12 +265,20 @@ export const TriggersHandler: React.FC = function TriggersHandler(
ordered: 'modifiersFirst',
preventDefaultPartials: false,
})
+ localSorensen.bind(['F5', 'Control+F5'], preventDefault, {
+ global: true,
+ exclusive: true,
+ ordered: false,
+ preventDefaultPartials: false,
+ })
}
}
return () => {
localSorensen.unbind('Escape', poisonHotkeys)
localSorensen.unbind('Control+KeyF', preventDefault)
+ localSorensen.unbind('F5', preventDefault)
+ localSorensen.unbind('Control+F5', preventDefault)
}
}, [initialized]) // run once once Sorensen is initialized
diff --git a/meteor/client/lib/ui/scriptPreview.ts b/meteor/client/lib/ui/scriptPreview.ts
new file mode 100644
index 0000000000..25d79240b4
--- /dev/null
+++ b/meteor/client/lib/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/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/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/countdown/presenter.scss b/meteor/client/styles/countdown/presenter.scss
index 8461d4f28f..cbe7616cc5 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,147 @@
.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-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/rundownView.scss b/meteor/client/styles/rundownView.scss
index b9bce5880c..00d0f954e9 100644
--- a/meteor/client/styles/rundownView.scss
+++ b/meteor/client/styles/rundownView.scss
@@ -670,6 +670,24 @@ svg.icon {
}
}
+ &.has-break {
+ width: 12.5rem;
+
+ .segment-timeline__title {
+ background: $rundown-divider-background-color;
+
+ h2 {
+ 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 {
.segment-timeline__title {
color: $segment-title-text-color-live;
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/endTimerPanel.scss b/meteor/client/styles/shelf/endTimerPanel.scss
new file mode 100644
index 0000000000..af6110088c
--- /dev/null
+++ b/meteor/client/styles/shelf/endTimerPanel.scss
@@ -0,0 +1,3 @@
+.playlist-end-time-panel {
+ position: absolute;
+}
diff --git a/meteor/client/styles/shelf/endWordsPanel.scss b/meteor/client/styles/shelf/endWordsPanel.scss
new file mode 100644
index 0000000000..8c81d3161d
--- /dev/null
+++ b/meteor/client/styles/shelf/endWordsPanel.scss
@@ -0,0 +1,18 @@
+.end-words-panel {
+ overflow: hidden;
+ position: absolute;
+
+ .timing-clock {
+ width: 100%;
+ }
+
+ .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/partNamePanel.scss b/meteor/client/styles/shelf/partNamePanel.scss
new file mode 100644
index 0000000000..7b315f9a26
--- /dev/null
+++ b/meteor/client/styles/shelf/partNamePanel.scss
@@ -0,0 +1,40 @@
+@import '../itemTypeColors';
+
+.part-name-panel {
+ position: absolute;
+ margin: 0 0;
+ min-width: auto;
+ text-align: center;
+ isolation: isolate;
+ padding: 1vw 1vh;
+ overflow: hidden;
+
+ @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/partTimingPanel.scss b/meteor/client/styles/shelf/partTimingPanel.scss
new file mode 100644
index 0000000000..c65278ee70
--- /dev/null
+++ b/meteor/client/styles/shelf/partTimingPanel.scss
@@ -0,0 +1,8 @@
+.part-timing-panel {
+ position: absolute;
+
+ .overtime {
+ color: var(--general-late-color);
+ font-weight: 500;
+ }
+}
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/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/styles/shelf/segmentTimingPanel.scss b/meteor/client/styles/shelf/segmentTimingPanel.scss
new file mode 100644
index 0000000000..afb651aed5
--- /dev/null
+++ b/meteor/client/styles/shelf/segmentTimingPanel.scss
@@ -0,0 +1,9 @@
+.segment-timing-panel {
+ position: absolute;
+ overflow: hidden;
+
+ .negative {
+ color: var(--general-late-color);
+ font-weight: 500;
+ }
+}
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/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/styles/shelf/studioNamePanel.scss b/meteor/client/styles/shelf/studioNamePanel.scss
new file mode 100644
index 0000000000..047aa0dc10
--- /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-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/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/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 05017f0000..5cbbf2c17c 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, {
@@ -298,12 +385,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) {
@@ -424,6 +550,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/FloatingInspectors/MicFloatingInspector.tsx b/meteor/client/ui/FloatingInspectors/MicFloatingInspector.tsx
index 94e7e210aa..d4abfc4256 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 '../../lib/ui/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
- )
+ const { startOfScript, endOfScript, breakScript } = getScriptPreview(props.content.fullScript || '')
return (
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/RundownView.tsx b/meteor/client/ui/RundownView.tsx
index 12ccb6b415..38bd1a5179 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'
@@ -94,6 +93,16 @@ import { RundownLayoutsAPI } from '../../lib/api/rundownLayouts'
import { TriggersHandler } from '../lib/triggers/TriggersHandler'
import { PlaylistTiming } from '../../lib/rundown/rundownTiming'
import { SorensenContext } from '../lib/SorensenContext'
+import { BreakSegment } from './SegmentTimeline/BreakSegment'
+import { PlaylistStartTiming } from './RundownView/RundownTiming/PlaylistStartTiming'
+import { RundownName } from './RundownView/RundownTiming/RundownName'
+import { TimeOfDay } from './RundownView/RundownTiming/TimeOfDay'
+import { PlaylistEndTiming } from './RundownView/RundownTiming/PlaylistEndTiming'
+import { NextBreakTiming } from './RundownView/RundownTiming/NextBreakTiming'
+import { ShowStyleVariant, ShowStyleVariants } from '../../lib/collections/ShowStyleVariants'
+import { BucketAdLibItem } from './Shelf/RundownViewBuckets'
+import { IAdLibListItem } from './Shelf/AdLibListItem'
+import { ShelfDashboardLayout } from './Shelf/ShelfDashboardLayout'
export const MAGIC_TIME_SCALE_FACTOR = 0.03
@@ -234,105 +243,35 @@ 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 } = this.props
+ const { t, rundownPlaylist, currentRundown } = this.props
if (!rundownPlaylist) return null
const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing)
const expectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing)
const expectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing)
+ const showEndTiming =
+ !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 &&
+ this.props.layout?.showNextBreakTiming &&
+ !(this.props.timingDurations.breakIsLastRundown && this.props.layout.lastRundownIsNotBreak)
return (
- {rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
-
- {t('Started')}
-
-
- ) : PlaylistTiming.isPlaylistTimingForwardTime(rundownPlaylist.timing) ? (
-
- {t('Planned Start')}
-
-
- ) : PlaylistTiming.isPlaylistTimingBackTime(rundownPlaylist.timing) &&
- rundownPlaylist.timing.expectedDuration ? (
-
- {t('Expected Start')}
-
-
- ) : null}
- {rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
- expectedStart ? (
-
- {this.renderRundownName()}
- {RundownUtils.formatDiffToTimecode(
- rundownPlaylist.startedPlayback - expectedStart!,
- true,
- false,
- true,
- true,
- true
- )}
-
- ) : (
-
{this.renderRundownName()}
- )
- ) : (
- (expectedStart ? (
-
expectedStart,
- })}
- >
- {this.renderRundownName()}
- {RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)}
-
- ) : (
-
{this.renderRundownName()}
- )) || undefined
- )}
-
-
-
+
+
+
{rundownPlaylist.currentPartInstanceId && (
)}
- {expectedEnd ? (
-
- {!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}
-
- ) : (
-
- {this.props.timingDurations ? (
- rundownPlaylist.loop ? (
- this.props.timingDurations.partCountdown &&
- rundownPlaylist.activationId &&
- rundownPlaylist.currentPartInstanceId ? (
-
- {t('Next Loop at')}
-
-
- ) : null
- ) : (
-
-
- {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
- expectedDuration?: number
- expectedEnd: number
- endLabel?: string
-}
-
-const PlaylistEndTiming = withTranslation()(
- withTiming()(
- class PlaylistEndTiming extends React.Component>> {
- render() {
- const { t } = this.props
-
- return (
-
- {!this.props.loop && (
-
- {t(this.props.endLabel || 'Planned End')}
-
-
- )}
- {!this.props.loop && (
-
- {RundownUtils.formatDiffToTimecode(getCurrentTime() - this.props.expectedEnd, true, true, true)}
-
- )}
- {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() {
- const { t, rundownsBeforeBreak } = this.props
- const 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
- }
-
- const expectedEnd = PlaylistTiming.getExpectedEnd(breakRundown.timing)
-
- return (
-
-
- {this.props.breakText ?? t('Next Break')}
-
-
- {!this.props.loop && expectedEnd ? (
-
- {RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedEnd, true, true, true)}
-
+ {showEndTiming ? (
+
) : null}
- {accumulatedExpectedDurations ? (
- (accumulatedExpectedDurations || 0),
- })}
- >
- {t('Diff')}
- {RundownUtils.formatDiffToTimecode(
- (rundownAsPlayedDuration || 0) - accumulatedExpectedDurations,
- true,
- false,
- true,
- true,
- true,
- undefined,
- true
- )}
-
+ {showNextBreakTiming ? (
+
) : null}
-
+
)
}
}
@@ -575,6 +316,8 @@ interface HotkeyDefinition {
interface IRundownHeaderProps {
playlist: RundownPlaylist
+ showStyleBase: ShowStyleBase
+ showStyleVariant: ShowStyleVariant
currentRundown: Rundown | undefined
studio: Studio
rundownIds: RundownId[]
@@ -589,6 +332,8 @@ interface IRundownHeaderProps {
interface IRundownHeaderState {
isError: boolean
errorMessage?: string
+ shouldQueue: boolean
+ selectedPiece: BucketAdLibItem | IAdLibListItem | PieceUi | undefined
}
const RundownHeader = withTranslation()(
@@ -606,6 +351,8 @@ const RundownHeader = withTranslation()(
this.state = {
isError: false,
+ shouldQueue: false,
+ selectedPiece: undefined,
}
}
componentDidMount() {
@@ -1160,6 +907,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 (
@@ -1233,7 +992,7 @@ const RundownHeader = withTranslation()(
playlist={this.props.playlist}
oneMinuteBeforeAction={this.resetAndActivateRundown}
/>
-
+
+ {this.props.layout && RundownLayoutsAPI.isDashboardLayout(this.props.layout) ? (
+
+ ) : (
+
+
+
+
+ )}
@@ -1255,18 +1043,6 @@ const RundownHeader = withTranslation()(
-
-
@@ -1321,7 +1097,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
@@ -1344,6 +1120,7 @@ interface ITrackedProps {
rundownsToShowstyles: Map
studio?: Studio
showStyleBase?: ShowStyleBase
+ showStyleVariant?: ShowStyleVariant
rundownLayouts?: Array
buckets: Bucket[]
casparCGPlayoutDevices?: PeripheralDevice[]
@@ -1428,6 +1205,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:
@@ -1545,7 +1323,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
@@ -1556,7 +1334,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) {
@@ -1576,8 +1356,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) {
@@ -1619,7 +1401,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) {
@@ -1696,13 +1480,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),
@@ -2204,7 +1994,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 && (
((
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}
@@ -2267,12 +2058,20 @@ export const RundownView = translateWithTracker((
ownCurrentPartInstance={ownCurrentPartInstance}
ownNextPartInstance={ownNextPartInstance}
isFollowingOnAirSegment={segmentIndex === currentSegmentIndex + 1}
+ countdownToSegmentRequireLayers={
+ this.state.rundownViewLayout?.countdownToSegmentRequireLayers
+ }
+ fixedSegmentDuration={this.state.rundownViewLayout?.fixedSegmentDuration}
/>
)
}
})}
+ {this.state.rundownViewLayout?.showBreaksAsSegments &&
+ rundownAndSegments.rundown.endOfRundownIsShowBreak && (
+
+ )}
)
})
@@ -2506,7 +2305,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 &&
@@ -2664,6 +2469,8 @@ export const RundownView = translateWithTracker((
inActiveRundownView={this.props.inActiveRundownView}
currentRundown={this.state.currentRundown || this.props.rundowns[0]}
layout={this.state.rundownHeaderLayout}
+ showStyleBase={this.props.showStyleBase}
+ showStyleVariant={this.props.showStyleVariant}
/>
@@ -2725,6 +2532,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}
@@ -2760,7 +2568,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 (
@@ -2774,6 +2588,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}
@@ -2797,7 +2612,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/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/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/RundownView/RundownTiming/NextBreakTiming.tsx b/meteor/client/ui/RundownView/RundownTiming/NextBreakTiming.tsx
new file mode 100644
index 0000000000..56815e72be
--- /dev/null
+++ b/meteor/client/ui/RundownView/RundownTiming/NextBreakTiming.tsx
@@ -0,0 +1,84 @@
+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'
+import { PlaylistTiming } from '../../../../lib/rundown/rundownTiming'
+
+interface INextBreakTimingProps {
+ loop?: boolean
+ rundownsBeforeBreak: Rundown[]
+ breakText?: string
+ lastChild?: boolean
+}
+
+export const NextBreakTiming = withTranslation()(
+ withTiming()(
+ class NextBreakEndTiming extends React.Component>> {
+ render() {
+ const { t, rundownsBeforeBreak } = this.props
+ const 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
+ }
+
+ const expectedEnd = PlaylistTiming.getExpectedEnd(breakRundown.timing)
+
+ return (
+
+
+ {t(this.props.breakText || 'Next Break')}
+
+
+ {!this.props.loop && expectedEnd ? (
+
+ {RundownUtils.formatDiffToTimecode(getCurrentTime() - 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..f19f665441
--- /dev/null
+++ b/meteor/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx
@@ -0,0 +1,138 @@
+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'
+import { RundownPlaylist } from '../../../../lib/collections/RundownPlaylists'
+
+interface IEndTimingProps {
+ rundownPlaylist: RundownPlaylist
+ loop?: boolean
+ expectedStart?: number
+ expectedDuration?: number
+ expectedEnd?: number
+ endLabel?: string
+ hidePlannedEndLabel?: boolean
+ hideDiffLabel?: boolean
+ hidePlannedEnd?: boolean
+ hideCountdown?: boolean
+ hideDiff?: boolean
+ rundownCount: number
+}
+
+export const PlaylistEndTiming = withTranslation()(
+ withTiming()(
+ class PlaylistEndTiming extends React.Component>> {
+ render() {
+ const { t } = this.props
+ const { rundownPlaylist, expectedStart, expectedEnd, expectedDuration } = this.props
+
+ return (
+
+ {!this.props.hidePlannedEnd ? (
+ this.props.expectedEnd ? (
+ !rundownPlaylist.startedPlayback ? (
+
+ {!this.props.hidePlannedEndLabel && (
+ {this.props.endLabel ?? t('Planned End')}
+ )}
+
+
+ ) : (
+
+ {!this.props.hidePlannedEndLabel && (
+ {this.props.endLabel ?? t('Expected End')}
+ )}
+
+
+ )
+ ) : this.props.timingDurations ? (
+ this.props.rundownPlaylist.loop ? (
+ this.props.timingDurations.partCountdown &&
+ rundownPlaylist.activationId &&
+ rundownPlaylist.currentPartInstanceId ? (
+
+ {!this.props.hidePlannedEndLabel && (
+ {t('Next Loop at')}
+ )}
+
+
+ ) : null
+ ) : (
+
+ {!this.props.hidePlannedEndLabel && (
+ {this.props.endLabel ?? t('Expected End')}
+ )}
+
+
+ )
+ ) : null
+ ) : null}
+ {!this.props.loop &&
+ !this.props.hideCountdown &&
+ (expectedEnd ? (
+
+ {RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedEnd, true, true, true)}
+
+ ) : expectedStart && expectedDuration ? (
+
+ {RundownUtils.formatDiffToTimecode(
+ getCurrentTime() - (expectedStart + expectedDuration),
+ true,
+ true,
+ true
+ )}
+
+ ) : null)}
+ {!this.props.hideDiff ? (
+ this.props.timingDurations ? ( // TEMPORARY: disable the diff counter for playlists longer than one rundown -- Jan Starzak, 2021-05-06
+
+ (expectedDuration ?? this.props.timingDurations.totalPlaylistDuration ?? 0),
+ })}
+ >
+ {!this.props.hideDiffLabel && {t('Diff')}}
+ {RundownUtils.formatDiffToTimecode(
+ (this.props.timingDurations.asPlayedPlaylistDuration || 0) -
+ (expectedDuration ?? this.props.timingDurations.totalPlaylistDuration ?? 0),
+ true,
+ false,
+ true,
+ true,
+ true,
+ undefined,
+ true
+ )}
+
+ ) : null
+ ) : 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..6e64c8583f
--- /dev/null
+++ b/meteor/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx
@@ -0,0 +1,77 @@
+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'
+import { PlaylistTiming } from '../../../../lib/rundown/rundownTiming'
+
+interface IEndTimingProps {
+ rundownPlaylist: RundownPlaylist
+ hidePlannedStart?: boolean
+ hideDiff?: boolean
+ plannedStartText?: string
+}
+
+export const PlaylistStartTiming = withTranslation()(
+ withTiming()(
+ class PlaylistStartTiming extends React.Component>> {
+ render() {
+ const { t, rundownPlaylist } = this.props
+ const playlistExpectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing)
+ const playlistExpectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing)
+ const playlistExpectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing)
+ const expectedStart = playlistExpectedStart
+ ? playlistExpectedStart
+ : playlistExpectedDuration && playlistExpectedEnd
+ ? playlistExpectedEnd - playlistExpectedDuration
+ : undefined
+
+ return (
+
+ {!this.props.hidePlannedStart &&
+ (rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? (
+
+ {t('Started')}
+
+
+ ) : playlistExpectedStart ? (
+
+ {this.props.plannedStartText || t('Planned Start')}
+
+
+ ) : playlistExpectedEnd && playlistExpectedDuration ? (
+
+ {this.props.plannedStartText || 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/SegmentDuration.tsx b/meteor/client/ui/RundownView/RundownTiming/SegmentDuration.tsx
index 673e650c1d..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,11 @@ 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 */
+ fixed?: boolean
}
/**
@@ -33,9 +39,19 @@ export const SegmentDuration = withTiming()(function
return (
<>
{props.label}
-
- {RundownUtils.formatDiffToTimecode(duration, false, false, true, false, true, '+')}
-
+ {props.fixed ? (
+
+ {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/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/SegmentTimeline/BreakSegment.tsx b/meteor/client/ui/SegmentTimeline/BreakSegment.tsx
new file mode 100644
index 0000000000..24ec30abcf
--- /dev/null
+++ b/meteor/client/ui/SegmentTimeline/BreakSegment.tsx
@@ -0,0 +1,42 @@
+import React from 'react'
+import { WithTranslation, withTranslation } from 'react-i18next'
+import Moment from 'react-moment'
+import { MeteorReactComponent } from '../../lib/MeteorReactComponent'
+import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData'
+import { RundownUtils } from '../../lib/rundown'
+import { WithTiming, withTiming } from '../RundownView/RundownTiming/withTiming'
+
+interface IProps {
+ breakTime: number | undefined
+}
+
+class BreakSegmentInner extends MeteorReactComponent>> {
+ render() {
+ const { t } = this.props
+ const displayTimecode =
+ this.props.breakTime && this.props.timingDurations.currentTime
+ ? this.props.breakTime - this.props.timingDurations.currentTime
+ : undefined
+
+ return (
+
+
+
+ {this.props.breakTime && }
+ {t('BREAK')}
+
+
+ {displayTimecode && (
+
+ {t('Break In')}
+
+ {RundownUtils.formatDiffToTimecode(displayTimecode, false, undefined, undefined, undefined, true)}
+
+
+ )}
+
+ )
+ }
+}
+
+export const BreakSegment = withTranslation()(withTiming()(BreakSegmentInner))
diff --git a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
index 23e7176467..e2a0a4bc37 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimeline.tsx
@@ -35,6 +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 { RundownTimingContext } from '../../../lib/rundown/rundownTiming'
import { PartInstanceId } from '../../../lib/collections/PartInstances'
import { SegmentTimelineSmallPartFlag } from './SmallParts/SegmentTimelineSmallPartFlag'
import { UIStateStorage } from '../../lib/UIStateStorage'
@@ -68,6 +69,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
@@ -80,6 +82,8 @@ interface IProps {
segmentRef?: (el: SegmentTimelineClass, segmentId: SegmentId) => void
isLastSegment: boolean
lastValidPartIndex: number | undefined
+ showCountdownToSegment: boolean
+ fixedSegmentDuration: boolean | undefined
}
interface IStateHeader {
timelineWidth: number
@@ -701,11 +705,13 @@ export class SegmentTimelineClass extends React.Component, IS
{t('On Air')}
-
+ {this.props.displayLiveLineCounter && (
+
+ )}
{this.props.autoNextPart ? (
) : (
@@ -1023,25 +1029,29 @@ export class SegmentTimelineClass extends React.Component
, IS
{t('Duration')}}
+ fixed={this.props.fixedSegmentDuration}
/>
)}
- {this.props.playlist && this.props.parts && this.props.parts.length > 0 && (
-
{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 0634498a6e..d628408524 100644
--- a/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
+++ b/meteor/client/ui/SegmentTimeline/SegmentTimelineContainer.tsx
@@ -45,6 +45,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
@@ -110,6 +112,9 @@ interface IProps {
ownCurrentPartInstance: PartInstance | undefined
ownNextPartInstance: PartInstance | undefined
isFollowingOnAirSegment: boolean
+ rundownViewLayout: RundownViewLayout | undefined
+ countdownToSegmentRequireLayers: string[] | undefined
+ fixedSegmentDuration: boolean | undefined
}
interface IState {
scrollLeft: number
@@ -136,6 +141,8 @@ interface ITrackedProps {
hasGuestItems: boolean
hasAlreadyPlayed: boolean
lastValidPartIndex: number | undefined
+ displayLiveLineCounter: boolean
+ showCountdownToSegment: boolean
}
export const SegmentTimelineContainer = translateWithTracker(
(props: IProps) => {
@@ -151,6 +158,8 @@ 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,
@@ -266,6 +290,8 @@ export const SegmentTimelineContainer = translateWithTracker {
@@ -279,7 +305,8 @@ export const SegmentTimelineContainer = translateWithTracker
)) ||
null
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
{device.type === PeripheralDeviceAPI.DeviceType.PACKAGE_MANAGER ? (
diff --git a/meteor/client/ui/Settings/RundownLayoutEditor.tsx b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
index 2c56ad7de8..166784e693 100644
--- a/meteor/client/ui/Settings/RundownLayoutEditor.tsx
+++ b/meteor/client/ui/Settings/RundownLayoutEditor.tsx
@@ -334,7 +334,13 @@ export default 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/FilterEditor.tsx b/meteor/client/ui/Settings/components/FilterEditor.tsx
index b676db1602..2ac299b834 100644
--- a/meteor/client/ui/Settings/components/FilterEditor.tsx
+++ b/meteor/client/ui/Settings/components/FilterEditor.tsx
@@ -10,18 +10,33 @@ import {
RundownLayoutAdLibRegion,
RundownLayoutAdLibRegionRole,
RundownLayoutBase,
+ RundownLayoutColoredBox,
RundownLayoutElementBase,
RundownLayoutElementType,
+ RundownLayoutEndWords,
RundownLayoutExternalFrame,
RundownLayoutFilterBase,
+ RundownLayoutPartName,
+ RundownLayoutPartTiming,
RundownLayoutPieceCountdown,
+ RundownLayoutPlaylistEndTimer,
+ RundownLayoutPlaylistName,
+ RundownLayoutPlaylistStartTimer,
RundownLayouts,
+ RundownLayoutSegmentName,
+ RundownLayoutSegmentTiming,
+ RundownLayoutShowStyleDisplay,
+ RundownLayoutStudioName,
+ RundownLayoutSytemStatus,
+ RundownLayoutTextLabel,
+ RundownLayoutTimeOfDay,
} from '../../../../lib/collections/RundownLayouts'
import { EditAttribute } from '../../../lib/EditAttribute'
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
@@ -50,7 +65,7 @@ export default withTranslation()(
})
}
- renderFilter(
+ renderRundownLayoutFilter(
item: RundownLayoutBase,
tab: RundownLayoutFilterBase,
index: number,
@@ -610,73 +625,9 @@ export default withTranslation()(
/>
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
{isDashboardLayout && (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
)}
-
-
-
)
}
@@ -800,77 +738,22 @@ export default withTranslation()(
/>
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index)}
{isDashboardLayout && (
-
-
-
-
-
-
-
-
-
- {isDashboardLayout && (
-
-
-
-
-
- )}
)}
@@ -887,19 +770,6 @@ export default withTranslation()(
const { t } = this.props
return (
-
-
-
(v && v.length > 0 ? v : undefined)}
/>
- {isDashboardLayout && (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
)
}
+ renderPlaylistStartTimer(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPlaylistStartTimer,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderPlaylistEndTimer(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPlaylistEndTimer,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderEndWords(
+ item: RundownLayoutBase,
+ tab: RundownLayoutEndWords,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+
+
+
+
+ {this.renderRequiresActiveLayerSettings(
+ item,
+ index,
+ t('Script Source Layers'),
+ t('Source layers containing script')
+ )}
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderSegmentTiming(
+ item: RundownLayoutBase,
+ tab: RundownLayoutSegmentTiming,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+
+ return (
+
+
+
+
+
+
+
+ {this.renderRequiresActiveLayerSettings(item, index, t('Require Piece on Source Layer'), '')}
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderPartTiming(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPartTiming,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+
+ return (
+
+
+
+
+
+
+
+ {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 (
+
+
+
+
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderPlaylistName(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPlaylistName,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderStudioName(
+ item: RundownLayoutBase,
+ tab: RundownLayoutStudioName,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderSegmentName(
+ item: RundownLayoutBase,
+ tab: RundownLayoutSegmentName,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderPartName(
+ item: RundownLayoutBase,
+ tab: RundownLayoutPartName,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+
+
+
+
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderColoredBox(
+ item: RundownLayoutBase,
+ tab: RundownLayoutColoredBox,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderShowStyleDisplay(
+ item: RundownLayoutBase,
+ tab: RundownLayoutShowStyleDisplay,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderSystemStatus(
+ item: RundownLayoutBase,
+ tab: RundownLayoutSytemStatus,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderTimeOfDay(
+ item: RundownLayoutBase,
+ tab: RundownLayoutTimeOfDay,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+
+
+
+
+ {isDashboardLayout && this.renderDashboardLayoutSettings(item, index, true)}
+
+ )
+ }
+
+ renderRequiresActiveLayerSettings(
+ item: RundownLayoutBase,
+ index: number,
+ activeLayerTitle: string,
+ activeLayersLabel
+ ) {
+ const { t } = this.props
+ return (
+
+
+
+ (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)}
+ />
+ {activeLayersLabel}
+
+
+
+ (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)}
+ />
+
+ {t('Specify additional layers where at least one layer must have an active piece')}
+
+
+
+
+
+
+ )
+ }
+
+ renderDashboardLayoutSettings(item: RundownLayoutBase, index: number, scalable?: boolean) {
+ const { t } = this.props
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {scalable && (
+
+
+
+ )}
+
+
+
+
+ )
+ }
+
+ renderFilter(
+ item: RundownLayoutBase,
+ filter: RundownLayoutElementBase,
+ index: number,
+ isRundownLayout: boolean,
+ isDashboardLayout: boolean
+ ) {
+ if (RundownLayoutsAPI.isFilter(filter)) {
+ return this.renderRundownLayoutFilter(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isExternalFrame(filter)) {
+ return this.renderFrame(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isAdLibRegion(filter)) {
+ return this.renderAdLibRegion(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isPieceCountdown(filter)) {
+ return this.renderPieceCountdown(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isPlaylistStartTimer(filter)) {
+ return this.renderPlaylistStartTimer(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isPlaylistEndTimer(filter)) {
+ return this.renderPlaylistEndTimer(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isEndWords(filter)) {
+ return this.renderEndWords(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isSegmentTiming(filter)) {
+ return this.renderSegmentTiming(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isPartTiming(filter)) {
+ return this.renderPartTiming(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isTextLabel(filter)) {
+ return this.renderTextLabel(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isPlaylistName(filter)) {
+ return this.renderPlaylistName(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isStudioName(filter)) {
+ return this.renderStudioName(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isSegmentName(filter)) {
+ return this.renderSegmentName(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isPartName(filter)) {
+ return this.renderPartName(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isColoredBox(filter)) {
+ return this.renderColoredBox(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isTimeOfDay(filter)) {
+ return this.renderTimeOfDay(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isShowStyleDisplay(filter)) {
+ return this.renderShowStyleDisplay(item, filter, index, isRundownLayout, isDashboardLayout)
+ } else if (RundownLayoutsAPI.isSystemStatus(filter)) {
+ return this.renderSystemStatus(item, filter, index, isRundownLayout, isDashboardLayout)
+ }
+ }
+
render() {
const { t } = this.props
@@ -1033,33 +1728,7 @@ export default withTranslation()(
- {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}
+ {this.renderFilter(this.props.item, this.props.filter, this.props.index, isRundownLayout, isDashboardLayout)}
)
}
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownHeaderLayoutSettings.tsx
index c0fffd8cee..5b1edcb081 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,14 +16,14 @@ export default withTranslation()(
render() {
const { t } = this.props
- return (
+ return this.props.item.type === RundownLayoutType.RUNDOWN_HEADER_LAYOUT ? (
- )
+ ) : null
}
}
)
diff --git a/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx b/meteor/client/ui/Settings/components/rundownLayouts/RundownViewLayoutSettings.tsx
index c176c65d90..d86e167844 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,152 @@ export default withTranslation()(
>
+
+
+ (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)}
+ />
+
+ {t('One of these source layers must have an active piece for the live line countdown to be show')}
+
+
+
+
+ (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)}
+ />
+
+ {t('Specify additional layers where at least one layer must have an active piece')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (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)}
+ />
+
+ {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/AdLibRegionPanel.tsx b/meteor/client/ui/Shelf/AdLibRegionPanel.tsx
index 7d1843dd3e..7ee1dffe3a 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,
@@ -34,7 +34,7 @@ interface IAdLibRegionPanelProps {
type IAdLibRegionPanelTrackedProps = IDashboardPanelTrackedProps
-export class AdLibRegionPanelInner extends MeteorReactComponent<
+class AdLibRegionPanelInner extends MeteorReactComponent<
Translated,
IState
> {
@@ -143,14 +143,12 @@ export class AdLibRegionPanelInner extends MeteorReactComponent<
return (
, 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/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/DashboardPanel.tsx b/meteor/client/ui/Shelf/DashboardPanel.tsx
index 1bd4e48528..963be5e7c0 100644
--- a/meteor/client/ui/Shelf/DashboardPanel.tsx
+++ b/meteor/client/ui/Shelf/DashboardPanel.tsx
@@ -10,7 +10,7 @@ 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 { unprotectString, getCurrentTime } from '../../../lib/lib'
+import { unprotectString } from '../../../lib/lib'
import {
IAdLibPanelProps,
AdLibFetchAndFilterProps,
@@ -28,13 +28,13 @@ import {
} from '../../lib/lib'
import { Studio } from '../../../lib/collections/Studios'
import { PieceId } from '../../../lib/collections/Pieces'
-import { invalidateAt } from '../../lib/invalidatingTime'
import { PieceInstances, PieceInstance } from '../../../lib/collections/PieceInstances'
import { MeteorCall } from '../../../lib/api/methods'
import { PartInstanceId } from '../../../lib/collections/PartInstances'
import { ContextMenuTrigger } from '@jstarpl/react-contextmenu'
import { setShelfContextMenuContext, ContextType } from './ShelfContextMenu'
import { RundownUtils } from '../../lib/rundown'
+import { getUnfinishedPieceInstancesReactive } from '../../lib/rundownLayouts'
import { RundownPlaylist } from '../../../lib/collections/RundownPlaylists'
interface IState {
@@ -67,11 +67,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
@@ -105,6 +106,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',
}
}
@@ -480,7 +482,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 && (
@@ -564,88 +566,6 @@ export class DashboardPanelInner extends MeteorReactComponent<
}
}
-export function getUnfinishedPieceInstancesReactive(playlist: RundownPlaylist) {
- let prospectivePieces: PieceInstance[] = []
- const now = getCurrentTime()
- if (playlist.activationId && playlist.currentPartInstanceId) {
- prospectivePieces = PieceInstances.find({
- startedPlayback: {
- $exists: true,
- },
- playlistActivationId: playlist.activationId,
- $and: [
- {
- $or: [
- {
- stoppedPlayback: {
- $eq: 0,
- },
- },
- {
- stoppedPlayback: {
- $exists: false,
- },
- },
- ],
- },
- {
- $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(playlist: RundownPlaylist): PieceInstance[] {
let prospectivePieceInstances: PieceInstance[] = []
if (playlist.activationId && playlist.nextPartInstanceId) {
@@ -682,7 +602,7 @@ export function getNextPiecesReactive(playlist: RundownPlaylist): PieceInstance[
export function getUnfinishedPieceInstancesGrouped(
playlist: RundownPlaylist
): Pick
{
- const unfinishedPieceInstances = getUnfinishedPieceInstancesReactive(playlist)
+ const unfinishedPieceInstances = getUnfinishedPieceInstancesReactive(playlist, false)
const unfinishedAdLibIds: PieceId[] = unfinishedPieceInstances
.filter((piece) => !!piece.adLibSourceId)
diff --git a/meteor/client/ui/Shelf/EndWordsPanel.tsx b/meteor/client/ui/Shelf/EndWordsPanel.tsx
new file mode 100644
index 0000000000..e51038f83d
--- /dev/null
+++ b/meteor/client/ui/Shelf/EndWordsPanel.tsx
@@ -0,0 +1,73 @@
+import * as React from 'react'
+import * as _ from 'underscore'
+import ClassNames from 'classnames'
+import {
+ DashboardLayoutEndsWords,
+ RundownLayoutBase,
+ RundownLayoutEndWords,
+} from '../../../lib/collections/RundownLayouts'
+import { RundownLayoutsAPI } from '../../../lib/api/rundownLayouts'
+import { dashboardElementStyle } 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 '../../lib/ui/scriptPreview'
+import { getIsFilterActive } from '../../lib/rundownLayouts'
+
+interface IEndsWordsPanelProps {
+ visible?: boolean
+ layout: RundownLayoutBase
+ panel: RundownLayoutEndWords
+ playlist: RundownPlaylist
+}
+
+interface IEndsWordsPanelTrackedProps {
+ livePieceInstance?: PieceInstance
+}
+
+interface IState {}
+
+class EndWordsPanelInner extends MeteorReactComponent<
+ Translated,
+ IState
+> {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+
+ const { t, livePieceInstance, panel } = this.props
+ const content = livePieceInstance?.piece.content as Partial | undefined
+
+ const { endOfScript } = getScriptPreview(content?.fullScript || '')
+
+ return (
+
+
+ {!this.props.panel.hideLabel && {t('End Words')}}
+ {endOfScript}
+
+
+ )
+ }
+}
+
+export const EndWordsPanel = translateWithTracker(
+ (props: IEndsWordsPanelProps) => {
+ const { activePieceInstance } = getIsFilterActive(props.playlist, props.panel)
+ return { livePieceInstance: activePieceInstance }
+ },
+ (_data, props: IEndsWordsPanelProps, nextProps: IEndsWordsPanelProps) => {
+ return !_.isEqual(props, nextProps)
+ }
+)(EndWordsPanelInner)
diff --git a/meteor/client/ui/Shelf/ExternalFramePanel.tsx b/meteor/client/ui/Shelf/ExternalFramePanel.tsx
index 98040ee2a4..6ba57ed9a8 100644
--- a/meteor/client/ui/Shelf/ExternalFramePanel.tsx
+++ b/meteor/client/ui/Shelf/ExternalFramePanel.tsx
@@ -2,13 +2,14 @@ import * as React from 'react'
import { Meteor } from 'meteor/meteor'
import { Random } from 'meteor/random'
import * as _ from 'underscore'
+import ClassNames from 'classnames'
import {
RundownLayoutExternalFrame,
RundownLayoutBase,
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'
@@ -498,18 +499,22 @@ export const ExternalFramePanel = withTranslation()(
}
render() {
- const scale = this.props.panel.scale || 1
+ const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout)
+ const scale = isDashboardLayout ? (this.props.panel as DashboardLayoutExternalFrame).scale || 1 : 1
return (