Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Hover Previews #1390

Draft
wants to merge 21 commits into
base: release53
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions meteor/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ COPY meteor /opt/core/meteor
COPY scripts /opt/core/scripts
WORKDIR /opt/core/meteor

# remove the dev only assets from the webui output
RUN rm -Rf /opt/core/packages/webui/dist/dev
# move the webui to the correct place
RUN rm -Rf /opt/core/meteor/public
RUN cp -R /opt/core/packages/webui/dist /opt/core/meteor/public
Expand Down
2 changes: 2 additions & 0 deletions meteor/server/api/rest/v1/typeConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@
allowPieceDirectPlay: apiStudioSettings.allowPieceDirectPlay ?? true, // Backwards compatible
enableBuckets: apiStudioSettings.enableBuckets ?? true, // Backwards compatible
enableEvaluationForm: apiStudioSettings.enableEvaluationForm ?? true, // Backwards compatible
mockPieceContentStatus: apiStudioSettings.mockPieceContentStatus,

Check warning on line 391 in meteor/server/api/rest/v1/typeConversion.ts

View check run for this annotation

Codecov / codecov/patch

meteor/server/api/rest/v1/typeConversion.ts#L391

Added line #L391 was not covered by tests
}
}

Expand All @@ -413,6 +414,7 @@
allowPieceDirectPlay: settings.allowPieceDirectPlay,
enableBuckets: settings.enableBuckets,
enableEvaluationForm: settings.enableEvaluationForm,
mockPieceContentStatus: settings.mockPieceContentStatus,

Check warning on line 417 in meteor/server/api/rest/v1/typeConversion.ts

View check run for this annotation

Codecov / codecov/patch

meteor/server/api/rest/v1/typeConversion.ts#L417

Added line #L417 was not covered by tests
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,27 @@
packageContainerPackageStatuses: [],
}

if (studio.settings.mockPieceContentStatus) {
return [
{
status: PieceStatusCode.OK,
messages: [],
progress: undefined,

freezes: [],
blacks: [],
scenes: [],

thumbnailUrl: undefined,
previewUrl: '/dev/fakePreview.mp4',

packageName: null,
contentDuration: 30 * 1000,
},
pieceDependencies,
]
}

Check warning on line 243 in meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts

View check run for this annotation

Codecov / codecov/patch

meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts#L225-L243

Added lines #L225 - L243 were not covered by tests

const ignoreMediaStatus = piece.content && piece.content.ignoreMediaObjectStatus
if (!ignoreMediaStatus) {
if (piece.expectedPackages) {
Expand Down
6 changes: 6 additions & 0 deletions packages/blueprints-integration/src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { JSONBlob } from '@sofie-automation/shared-lib/dist/lib/JSONBlob'
import { Time } from './common'
import { TSR, TimelineObjectCoreExt } from './timeline'
import { SourceLayerType } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle'
import { PopupPreview } from './previews'

export type WithTimeline<T extends BaseContent> = T & {
timelineObjects: TimelineObjectCoreExt<TSR.TSRTimelineContent>[]
Expand All @@ -19,6 +20,11 @@ export interface BaseContent {
ignoreBlackFrames?: boolean
ignoreFreezeFrame?: boolean
ignoreAudioFormat?: boolean

/**
* Overwrite any default hover previews in Sofie
*/
popUpPreview?: PopupPreview
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand Down
1 change: 1 addition & 0 deletions packages/blueprints-integration/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from './util'
export * from './translations'
export * from './triggers'
export * from './userEditing'
export * from './previews'

export { MOS } from '@sofie-automation/shared-lib/dist/mos'

Expand Down
78 changes: 78 additions & 0 deletions packages/blueprints-integration/src/previews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { SplitsContentBoxContent, SplitsContentBoxProperties } from './content'
import { NoteSeverity } from './lib'
import { ITranslatableMessage } from './translations'

export interface PopupPreview<P extends Previews = Previews> {
name?: string
preview?: P
warnings?: InvalidPreview[]
}
export type Previews = TablePreview | ScriptPreview | HTMLPreview | SplitPreview | VTPreview | BlueprintImagePreview

export enum PreviewType {
Invalid = 'invalid',
Table = 'table',
Script = 'script',
HTML = 'html',
Split = 'split',
VT = 'vt',
BlueprintImage = 'blueprintImage',
}

interface PreviewBase {
type: PreviewType
}

export interface InvalidPreview extends PreviewBase {
type: PreviewType.Invalid

severity: NoteSeverity
reason: ITranslatableMessage
}
export interface TablePreview extends PreviewBase {
type: PreviewType.Table

entries: { key: string; value: string }[]
displayTiming: boolean
}
export interface ScriptPreview extends PreviewBase {
type: PreviewType.Script

fullText?: string
lastWords?: string
comment?: string
lastModified?: number
}
export interface HTMLPreview extends PreviewBase {
// todo - expose if and how steps can be controlled
type: PreviewType.HTML

name?: string

previewUrl: string
previewDimension?: { width: number; height: number }

postMessageOnLoad?: any

steps?: { current: number; total: number }
}
export interface SplitPreview extends PreviewBase {
type: PreviewType.Split

background?: string // file asset upload?
boxes: (SplitsContentBoxContent & SplitsContentBoxProperties)[]
}
export interface VTPreview extends PreviewBase {
type: PreviewType.VT

// note: the info required for the preview follows from package manager so there's nothing for blueprins here
// note: if we want to allow a preview for different media than saved on the piece (because perhaps the media is in a non-primary piece) should we allow to specifiy the package to preview?

inWords?: string // note - only displayed if outWords are present
outWords?: string
}
export interface BlueprintImagePreview extends PreviewBase {
type: PreviewType.BlueprintImage

image: string // to be put in as asset
}
5 changes: 5 additions & 0 deletions packages/corelib/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { IStudioSettings } from './dataModel/Studio'
import { customAlphabet as createNanoid } from 'nanoid'
import type { ITranslatableMessage } from './TranslatableMessage'
import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep'

/**
* Limited character set to use for id generation
Expand Down Expand Up @@ -63,6 +64,10 @@
// Use this instead of fast-clone directly, as this retains the type
return fastClone(o as any)
}
export function cloneObject<T extends object>(o: ReadonlyObjectDeep<T> | Readonly<T> | T): T {
// Use this instead of fast-clone directly, as this retains the type
return fastClone(o as any)
}

Check warning on line 70 in packages/corelib/src/lib.ts

View check run for this annotation

Codecov / codecov/patch

packages/corelib/src/lib.ts#L67-L70

Added lines #L67 - L70 were not covered by tests

/**
* Deeply freeze an object
Expand Down
4 changes: 2 additions & 2 deletions packages/job-worker/src/blueprints/context/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
CoreUserEditingDefinitionSofie,
} from '@sofie-automation/corelib/dist/dataModel/UserEditingDefinitions'
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'
import { assertNever, clone, Complete, literal, omit } from '@sofie-automation/corelib/dist/lib'
import { assertNever, clone, cloneObject, Complete, literal, omit } from '@sofie-automation/corelib/dist/lib'
import { unprotectString, unprotectStringArray } from '@sofie-automation/corelib/dist/protectedString'
import { ReadonlyDeep } from 'type-fest'
import {
Expand Down Expand Up @@ -222,7 +222,7 @@ function convertPieceGenericToBlueprintsInner(piece: ReadonlyDeep<PieceGeneric>)
expectedPackages: clone<ExpectedPackage.Any[] | undefined>(piece.expectedPackages),
hasSideEffects: piece.hasSideEffects,
content: {
...clone(piece.content),
...cloneObject(piece.content),
timelineObjects: deserializePieceTimelineObjectsBlob(piece.timelineObjectsString),
},
abSessions: clone<PieceAbSessionInfo[] | undefined>(piece.abSessions),
Expand Down
2 changes: 1 addition & 1 deletion packages/job-worker/src/ingest/expectedMediaItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function generateExpectedMediaItems<T extends ExpectedMediaItemBase>(
commonProps: Subtract<T, ExpectedMediaItemBase>,
studioId: StudioId,
label: string,
content: Partial<SomeContent> | undefined,
content: Partial<SomeContent | ReadonlyDeep<SomeContent>> | undefined,
pieceType: string
): T[] {
const result: T[] = []
Expand Down
5 changes: 5 additions & 0 deletions packages/shared-lib/src/core/model/StudioSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,9 @@ export interface IStudioSettings {
* Doubleclick changes behaviour as selector for userediting
*/
enableUserEdits?: boolean

/**
* Override the piece content statuses with fake info - used for developing the UI
*/
mockPieceContentStatus?: boolean
}
103 changes: 103 additions & 0 deletions packages/webui/public/dev/dragTest.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<!--
This page emulates a MOS plugin (typically) provided by the MAM system that can provide
MOS-items to the UI to be imported into the Blueprints either as Bucket items or in conjunction
with the user-editing features (i.e. drag on top of a part)
-->

<html>
<head>
<style>
:root {
color: white;
}
</style>
</head>
<body>
<h1>Drag&Drop Test page</h1>

<button id="btn" draggable="true">Media Item #1</button>

<script>
const btn = document.getElementById('btn')
btn.addEventListener('dragstart', ({ dataTransfer }) => {
const ncsItem = createNcsItem()

dataTransfer.setData('text', new XMLSerializer().serializeToString(ncsItem))
})

function createNcsItem() {
return objectToXml(
{
ncsItem: {
item: {
itemID: 'OM_2.18516582,10.8570667.23',
itemSlug: 'TEST/SOFIE PKG 1/1519/23/9',
objID: '105.20913908',
objSlug: 'TEST/SOFIE PKG 1/1519/23/9',
objDur: 1315,
objTB: 25,
mosID: 'BIGTED.NBH.BBC.MOS',
mosPlugInID: 'BIGTED.NBH.BBC.MOS',
itemEdDur: 1325,
mosExternalMetadata: {
mosScope: 'PLAYLIST',
mosSchema: '',
mosPayload: {
ModTime: '2024-09-23T14:21:08.905Z',
ModBy: 'ActiveXWeb',
Creator: 'ActiveXWeb',
},
},
},
},
},
'mos'
)
}

function objectToXml(obj, rootName) {
const doc = new Document()
const root = doc.createElement(rootName)

addNodes(obj, root)

doc.appendChild(root)
return doc
}

function addNodes(obj, rootNode) {
const doc = rootNode.ownerDocument

for (const name of Object.keys(obj)) {
const value = obj[name]

if (Array.isArray(value)) {
value.forEach((element) => {
rootNode.appendChild(createNode(name, element, doc))
})
} else if (name.startsWith('@')) {
rootNode.setAttribute(name.substring(1), String(value))
} else {
rootNode.appendChild(createNode(name, value, doc))
}
}
}

function createNode(name, value, doc) {
if (name === '#textContent') {
return doc.createTextNode(String(value))
}

const node = doc.createElement(name)

if (typeof value === 'object' && value !== null) {
addNodes(value, node)
} else {
node.textContent = value
}

return node
}
</script>
</body>
</html>
Binary file added packages/webui/public/dev/fakePreview.mp4
Binary file not shown.
40 changes: 40 additions & 0 deletions packages/webui/public/dev/templatePreview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!--
This page emulates an HTML preview coming from the graphics system to
be rendered inside the Sofie UI as a hover preview.
-->

<html>
<head>
<style>
body {
width: 1920;
height: 1080;
overflow: hidden;
}

#test {
position: absolute;
width: 500px;
height: 80px;
font-size: 2.5em;
background: #fff;
bottom: 120px;
left: 80px;
}
</style>
</head>
<body>
<div id="test">Test Box</div>

<script>
const el = document.getElementById('test')

window.addEventListener('message', (ev) => {
console.log('recv', ev.data)
if (ev.data.event === 'sofie-update') {
el.innerText = ev.data.payload
}
})
</script>
</body>
</html>
4 changes: 2 additions & 2 deletions packages/webui/src/client/lib/VideoPreviewPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function setVideoElementPosition(
if (loop && vEl.duration > 0) {
targetTime =
targetTime % ((itemDuration > 0 ? Math.min(vEl.duration * 1000, itemDuration) : vEl.duration * 1000) * 1000)
} else {
} else if (itemDuration > 0) {
targetTime = Math.min(timePosition, itemDuration)
}
vEl.currentTime = targetTime / 1000
Expand Down Expand Up @@ -55,7 +55,7 @@ export function VideoPreviewPlayer({
<div
className={classNames('video-preview-player__frame-marker', {
'video-preview-player__frame-marker--first-frame': offsetTimePosition === 0,
'video-preview-player__frame-marker--last-frame': offsetTimePosition >= itemDuration,
'video-preview-player__frame-marker--last-frame': itemDuration > 0 && offsetTimePosition >= itemDuration,
})}
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { JSONBlobParse, NoraContent } from '@sofie-automation/blueprints-integration'
import { PieceGeneric } from '@sofie-automation/corelib/dist/dataModel/Piece'
import { objectToXML } from '../util/object-to-xml'
import { ReadonlyDeep } from 'type-fest'

export { createMosObjectXmlStringNoraBluePrintPiece }

function createMosObjectXmlStringNoraBluePrintPiece(piece: Pick<PieceGeneric, 'content' | 'externalId'>): string {
function createMosObjectXmlStringNoraBluePrintPiece(
piece: ReadonlyDeep<Pick<PieceGeneric, 'content' | 'externalId'>>
): string {
const noraContent = piece.content as NoraContent | undefined
const noraPayload = noraContent?.previewPayload ? JSONBlobParse(noraContent.previewPayload) : undefined
if (!noraContent || !noraPayload) {
Expand Down
Loading
Loading