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 (