diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index 3247c10a91..762dfabbf8 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -240,7 +240,7 @@ export async function checkPieceContentStatusAndDependencies( blacks: [], scenes: [], - thumbnailUrl: undefined, + thumbnailUrl: '/dev/fakeThumbnail.png', previewUrl: '/dev/fakePreview.mp4', packageName: null, diff --git a/packages/blueprints-integration/src/previews.ts b/packages/blueprints-integration/src/previews.ts index 038461a359..fbfb31ac65 100644 --- a/packages/blueprints-integration/src/previews.ts +++ b/packages/blueprints-integration/src/previews.ts @@ -1,4 +1,4 @@ -import { SplitsContentBoxContent, SplitsContentBoxProperties } from './content.js' +import { SourceLayerType, SplitsContentBoxContent, SplitsContentBoxProperties } from './content.js' import { NoteSeverity } from './lib.js' import { ITranslatableMessage } from './translations.js' @@ -6,6 +6,10 @@ export interface PopupPreview

{ name?: string preview?: P warnings?: InvalidPreview[] + /** + * Add custom content preview content + */ + additionalPreviewContent?: Array } export type Previews = TablePreview | ScriptPreview | HTMLPreview | SplitPreview | VTPreview | BlueprintImagePreview @@ -19,6 +23,55 @@ export enum PreviewType { BlueprintImage = 'blueprintImage', } +// The PreviewContent types are a partly replica of the types in PreviewPopUpContext.tsx +export type PreviewContent = + | { + type: 'iframe' + href: string + postMessage?: any + dimensions?: { width: number; height: number } + } + | { + type: 'image' + src: string + } + | { + type: 'video' + src: string + } + | { + type: 'script' + script?: string + firstWords?: string + lastWords?: string + comment?: string + lastModified?: number + } + | { + type: 'title' + content: string + } + | { + type: 'inOutWords' + in?: string + out: string + } + | { + type: 'layerInfo' + layerType: SourceLayerType + text: Array + inTime?: number | string + outTime?: number | string + duration?: number | string + } + | { + type: 'separationLine' + } + | { + type: 'data' + content: { key: string; value: string }[] + } + interface PreviewBase { type: PreviewType } diff --git a/packages/webui/public/dev/fakeThumbnail.png b/packages/webui/public/dev/fakeThumbnail.png new file mode 100644 index 0000000000..669e7837dd Binary files /dev/null and b/packages/webui/public/dev/fakeThumbnail.png differ diff --git a/packages/webui/src/client/styles/shelf/dashboard-rundownView.scss b/packages/webui/src/client/styles/shelf/dashboard-rundownView.scss index 2f4e5c328a..6e0e1eff21 100644 --- a/packages/webui/src/client/styles/shelf/dashboard-rundownView.scss +++ b/packages/webui/src/client/styles/shelf/dashboard-rundownView.scss @@ -11,6 +11,9 @@ } .dashboard-panel__panel__button { + margin-top: 10px; + height: 110px; + max-width: 170px !important; > .dashboard-panel__panel__button__content { display: grid; grid-template-columns: 1fr min-content; @@ -31,7 +34,7 @@ > .dashboard-panel__panel__button__thumbnail { position: relative; - height: auto; + height: 85px; z-index: 1; overflow: hidden; grid-column: auto / span 2; diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss index ab5105bcc2..e7182b8e32 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss @@ -3,17 +3,16 @@ .preview-popUp { border: 1px solid var(--sofie-segment-layer-hover-popup-border); background: var(--sofie-segment-layer-hover-popup-background); - box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.5); + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.8); border-radius: 5px; overflow: hidden; pointer-events: none; - box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.6); - z-index: 9999; &--large { width: 482px; + padding-bottom: 10px; --preview-max-dimension: 480; } @@ -25,18 +24,65 @@ &--hidden { visibility: none; } + + font-family: Roboto Flex; + + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 110%; + /* identical to box height, or 15px */ + letter-spacing: 0.02em; + font-feature-settings: + 'tnum', + 'liga' off; + color: #ffffff; + font-variation-settings: + 'GRAD' 0, + 'opsz' 15, + 'slnt' 0, + 'wdth' 30, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 548, + 'YTUC' 712; } .preview-popUp__preview { width: 100%; - font-family: 'Roboto Condensed'; - font-size: 0.9375rem; // 15px; .preview-popUp__script, .preview-popUp__script-comment, .preview-popUp__script-last-modified { - padding: 0.4em 0.4em 0.4em 0.6em; - font-style: italic; + padding: 5px; + padding-left: 2%; + padding-right: 2%; + font-weight: 300; + font-size: 16px; + line-height: 120%; + letter-spacing: 0.03em; + font-feature-settings: + 'tnum', + 'liga' off; + + color: #ffffff; + font-variation-settings: + 'GRAD' 0, + 'opsz' 16, + 'slnt' -10, + 'wdth' 75, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 548, + 'YTUC' 712; } .preview-popUp__script-comment, @@ -54,6 +100,72 @@ letter-spacing: 0.02rem; padding: 5px; + padding-left: 2%; + } + + .preview-popUp__element-with-time-info { + width: 100%; + display: flex; + + margin-bottom: 7px; + + .preview-popUp__element-with-time-info__layer-color { + height: 13px; + aspect-ratio: 1; + margin-left: 2%; + margin-top: 7px; + flex-shrink: 0; + @include item-type-colors(); + } + + .preview-popUp__element-with-time-info__text { + margin: 5px; + width: calc(100% - 35px); + flex-grow: 1; + } + + .preview-popUp__element-with-time-info__timing { + margin-left: 5px; + overflow: none; + white-space: nowrap; + text-overflow: ellipsis; + font-feature-settings: 'liga' off; + + font-weight: 500; + line-height: 100%; /* 15px */ + + .label { + font-weight: 100; + line-height: 100%; + /* identical to box height, or 15px */ + letter-spacing: 0.02em; + font-feature-settings: + 'tnum', + 'liga' off; + color: #b2b2b2; + font-variation-settings: + 'GRAD' 0, + 'opsz' 30, + 'slnt' 0, + 'wdth' 25, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 548, + 'YTUC' 712; + } + } + } + + .preview-popup__separation-line { + width: 96%; + margin-left: 2%; + background-color: #5b5b5b; + margin-top: 0px; + margin-bottom: 0px; } .preview-popUp__warning { @@ -174,21 +286,42 @@ } .preview-popUp__in-out-words { - letter-spacing: 0em; + font-weight: 300; + font-size: 16px; + line-height: 100%; + letter-spacing: 0.02em; + font-feature-settings: + 'tnum', + 'liga' off; + color: #ffffff; + font-variation-settings: + 'GRAD' 0, + 'opsz' 16, + 'slnt' -10, + 'wdth' 75, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 548, + 'YTUC' 712; width: 100%; overflow: hidden; text-overflow: clip; white-space: nowrap; - margin-top: -25px; //Pull up the in/out words a bit - padding: 7px; + padding: 5px; + padding-left: 2%; + padding-right: 2%; .separation-line { width: 100%; height: 1px; background-color: #5b5b5b; - margin-bottom: 5px; + margin-bottom: 7px; } .in-words, @@ -201,7 +334,7 @@ } .out-words { - direction: rtl; + text-align: right; } } diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContent.tsx b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContent.tsx index b07d940678..5c1c3a35d0 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContent.tsx +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContent.tsx @@ -1,5 +1,4 @@ import React from 'react' -import { PreviewContent } from './PreviewPopUpContext.js' import { WarningIconSmall } from '../../lib/ui/icons/notifications.js' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { TFunction, useTranslation } from 'react-i18next' @@ -11,9 +10,11 @@ import { RundownUtils } from '../../lib/rundown.js' import { PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' import { PieceLifespan } from '@sofie-automation/blueprints-integration' +import { LayerInfoPreview } from './Previews/LayerInfoPreview.js' +import { PreviewContentUI } from './PreviewPopUpContext.js' interface PreviewPopUpContentProps { - content: PreviewContent + content: PreviewContentUI time: number | null } @@ -38,7 +39,6 @@ export function PreviewPopUpContent({ content, time }: PreviewPopUpContentProps) case 'inOutWords': return (

-
{content.in}
{content.out}
@@ -59,6 +59,10 @@ export function PreviewPopUpContent({ content, time }: PreviewPopUpContentProps) ) + case 'layerInfo': + return + case 'separationLine': + return
case 'boxLayout': return case 'warning': @@ -108,17 +112,17 @@ function getDurationText( function getLifeSpanText(t: TFunction, lifespan: PieceLifespan): string { switch (lifespan) { case PieceLifespan.WithinPart: - return t('Until next take') + return t('Until Next Take') case PieceLifespan.OutOnSegmentChange: - return t('Until next segment') + return t('Until Next Segment') case PieceLifespan.OutOnSegmentEnd: - return t('Until end of segment') + return t('Until End of Segment') case PieceLifespan.OutOnRundownChange: - return t('Until next rundown') + return t('Until Next Rundown') case PieceLifespan.OutOnRundownEnd: - return t('Until end of rundown') + return t('Until End of Rundown') case PieceLifespan.OutOnShowStyleEnd: - return t('Until end of showstyle') + return t('Until End of Showstyle') default: return '' } diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx index 53459b8aeb..9e446287d3 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx @@ -6,6 +6,7 @@ import { JSONBlobParse, NoraPayload, PieceLifespan, + PreviewContent, PreviewType, ScriptContent, SourceLayerType, @@ -33,11 +34,11 @@ export function convertSourceLayerItemToPreview( item: ReadonlyObjectDeep | IAdLibListItem, contentStatus?: ReadonlyObjectDeep, timeAsRendered?: { in?: number | null; dur?: number | null } -): { contents: PreviewContent[]; options: Readonly> } { +): { contents: PreviewContentUI[]; options: Readonly> } { // first try to read the popup preview if (item.content.popUpPreview) { const popupPreview = item.content.popUpPreview - const contents: PreviewContent[] = [] + const contents: PreviewContentUI[] = [] const options: Partial = {} if (popupPreview.name) { @@ -99,6 +100,7 @@ export function convertSourceLayerItemToPreview( break case PreviewType.VT: if (popupPreview.preview.outWords) { + contents.push({ type: 'separationLine' }) contents.push({ type: 'inOutWords', in: popupPreview.preview.inWords, @@ -120,10 +122,14 @@ export function convertSourceLayerItemToPreview( } break } + // Add any additional preview content to the popup: + popupPreview.additionalPreviewContent?.forEach((content) => { + contents.push(content as PreviewContentUI) + }) } if (popupPreview.warnings) { - contents.push(...popupPreview.warnings.map((w): PreviewContent => ({ type: 'warning', content: w.reason }))) + contents.push(...popupPreview.warnings.map((w): PreviewContentUI => ({ type: 'warning', content: w.reason }))) } return { contents, options } @@ -136,7 +142,7 @@ export function convertSourceLayerItemToPreview( const content = item.content as VTContent return { - contents: _.compact<(PreviewContent | undefined)[]>([ + contents: _.compact<(PreviewContentUI | undefined)[]>([ { type: 'title', content: content.fileName, @@ -159,11 +165,11 @@ export function convertSourceLayerItemToPreview( src: contentStatus.thumbnailUrl, } : undefined, - ...(contentStatus?.messages?.map((m) => ({ + ...(contentStatus?.messages?.map((m) => ({ type: 'warning', content: m as any, })) || []), - ]) as PreviewContent[], + ]) as PreviewContentUI[], options: { size: contentStatus?.previewUrl ? 'large' : undefined, }, @@ -220,7 +226,7 @@ export function convertSourceLayerItemToPreview( current: item.content.step.current, count: item.content.step.count, }, - ]) as PreviewContent[], + ]) as PreviewContentUI[], options: { size: 'large' }, } } catch (e) { @@ -237,7 +243,7 @@ export function convertSourceLayerItemToPreview( current: item.content.step.current, count: item.content.step.count, }, - ]) as PreviewContent[], + ]) as PreviewContentUI[], options: {}, } } @@ -287,43 +293,9 @@ export function convertSourceLayerItemToPreview( return { contents: [], options: {} } } - -export type PreviewContent = - | { - type: 'iframe' - href: string - postMessage?: any - dimensions?: { width: number; height: number } - } - | { - type: 'image' - src: string - } - | { - type: 'video' - src: string - } - | { - type: 'script' - script?: string - firstWords?: string - lastWords?: string - comment?: string - lastModified?: number - } - | { - type: 'title' - content: string - } - | { - type: 'inOutWords' - in?: string - out: string - } - | { - type: 'data' - content: { key: string; value: string }[] - } +// PreviewContentUI should be the same as PreviewContent, but we need to extend it with some more types: +export type PreviewContentUI = + | PreviewContent | { type: 'boxLayout' boxSourceConfiguration: ReadonlyDeep<(SplitsContentBoxContent & SplitsContentBoxProperties)[]> @@ -351,7 +323,7 @@ export interface IPreviewPopUpSession { * Update the open preview with new content or modify the content already being previewed, such as change current showing * time in the video, etc. */ - readonly update: (content?: PreviewContent[]) => void + readonly update: (content?: PreviewContentUI[]) => void /** * Set the time that the current pointer position is representing in the scope of the preview contents */ @@ -390,7 +362,7 @@ export interface IPreviewPopUpContext { */ requestPreview( anchor: HTMLElement | VirtualElement, - content: PreviewContent[], + content: PreviewContentUI[], opts?: PreviewRequestOptions ): IPreviewPopUpSession } @@ -415,7 +387,7 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre const previewRef = useRef(null) const [previewSession, setPreviewSession] = useState(null) - const [previewContent, setPreviewContent] = useState(null) + const [previewContent, setPreviewContent] = useState(null) const [t, setTime] = useState(null) const context: IPreviewPopUpContext = { diff --git a/packages/webui/src/client/ui/PreviewPopUp/Previews/LayerInfoPreview.tsx b/packages/webui/src/client/ui/PreviewPopUp/Previews/LayerInfoPreview.tsx new file mode 100644 index 0000000000..cc87ee3a49 --- /dev/null +++ b/packages/webui/src/client/ui/PreviewPopUp/Previews/LayerInfoPreview.tsx @@ -0,0 +1,53 @@ +import { PreviewContent } from '@sofie-automation/blueprints-integration' +import { RundownUtils } from '../../../lib/rundown' +import { useTranslation } from 'react-i18next' +import classNames from 'classnames' + +type layerInfoContent = Extract + +export function LayerInfoPreview(content: layerInfoContent): React.ReactElement { + const { t } = useTranslation() + const sourceLayerClassName = + content.layerType !== undefined ? RundownUtils.getSourceLayerClassName(content.layerType) : undefined + + return ( +
+
+
+ {content.text.map((line, index) => ( +
+ {line} +
+ ))} +
+ {content.inTime !== undefined && ( + <> + {t('IN')}: + {typeof content.inTime === 'number' + ? RundownUtils.formatTimeToShortTime(content.inTime || 0) + : content.inTime} + + )} +  {' '} + {content.duration !== undefined && ( + <> + {t('DURATION')}: + {typeof content.duration === 'number' + ? RundownUtils.formatTimeToShortTime(content.duration || 0) + : content.duration} + + )} +  {' '} + {content.outTime !== undefined && ( + <> + {t('OUT')}: + {typeof content.outTime === 'number' + ? RundownUtils.formatTimeToShortTime(content.outTime || 0) + : content.outTime} + + )} +
+
+
+ ) +}