Skip to content
Open
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
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"electron-is-dev": "^2.0.0",
"eslint": "^9.39.2",
"express": "^5.2.1",
"ograf": "1.1.0",
"socket.io": "^4.8.3",
"type-fest": "^4.41.0",
"uuid": "^11.1.0"
Expand Down
14 changes: 13 additions & 1 deletion backend/src/background/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { SetOptional } from 'type-fest'
import type * as OGraf from 'ograf'

export interface Playlist {
/** Id of the playlist. */
Expand Down Expand Up @@ -133,7 +134,10 @@ export interface DBPiece {
export enum ManifestFieldType {
String = 'string',
Number = 'number',
Boolean = 'boolean'
Boolean = 'boolean',
OGrafForm = 'ograf-form',
Const = 'const',
Enum = 'enum'
}
export enum TypeManifestEntity {
Rundown = 'rundown',
Expand Down Expand Up @@ -163,6 +167,14 @@ export interface PayloadManifest {
label: string
type: ManifestFieldType
includeInName?: boolean
defaultValue?: any
/** Only set when type is ograf-form */
ografManifest?: OGraf.GraphicsManifest
/** Only set when type is enum */
enumValues?: {
label: string
value: string
}[]
}

export interface ApplicationSettings {
Expand Down
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"bootstrap": "^5.3.8",
"classnames": "^2.5.1",
"immutability-helper": "^3.1.1",
"ograf": "1.1.0",
"ograf-form": "0.1.3",
"react": "^19.2.3",
"react-bootstrap": "^2.10.10",
"react-datepicker": "^8.10.0",
Expand Down
88 changes: 86 additions & 2 deletions frontend/src/components/rundown/piecePropertiesForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { useForm } from '@tanstack/react-form'
import { Button, ButtonGroup, Form, Modal } from 'react-bootstrap'
import type { Piece } from '~backend/background/interfaces'
import { FieldInfo } from '../form'
import { useState } from 'react'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import * as OGrafForm from 'ograf-form'
import { useNavigate } from '@tanstack/react-router'
import { useAppDispatch, useAppSelector } from '~/store/app'
import { removePiece, updatePiece } from '~/store/pieces'
Expand Down Expand Up @@ -121,7 +122,7 @@ export function PiecePropertiesForm({ piece }: { piece: Piece }) {
children={(field) => (
<>
<Form.Group className="mb-3">
<Form.Label htmlFor={field.name}>{fieldInfo.label}:</Form.Label>
<Form.Label htmlFor={field.name}>{fieldInfo.label}:&nbsp;</Form.Label>

{fieldInfo.type === 'string' && (
<Form.Control
Expand Down Expand Up @@ -153,6 +154,36 @@ export function PiecePropertiesForm({ piece }: { piece: Piece }) {
onChange={(e) => field.handleChange(e.target.checked)}
/>
)}
{fieldInfo.type === 'ograf-form' && (
<ReactOGrafForm
schema={fieldInfo.ografManifest?.schema as OGrafForm.GDDSchema}
data={field.state.value}
onDataChangeCallback={(value: any) => {
field.handleChange(value)
}}
/>
)}

{fieldInfo.type === 'enum' && (
<>
<Form.Select
name={field.name}
value={String(field.state.value)}
onBlur={field.handleBlur}
onChange={(e) => {
console.log(e.target.value)
field.handleChange(e.target.value)
}}
>
{fieldInfo.enumValues?.map((ev, i) => (
<option key={i} value={ev.value}>
{ev.label}
</option>
))}
</Form.Select>
</>
)}
{fieldInfo.type === 'const' && <span>{field.state.value}</span>}
</Form.Group>
<FieldInfo field={field} />
</>
Expand Down Expand Up @@ -271,3 +302,56 @@ function DeletePieceButton({
</>
)
}

declare global {
namespace React {
namespace JSX {
interface IntrinsicElements {
'superflytv-ograf-form': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
schema: string // OGraf.GraphicsManifest['schema']
value: any
},
HTMLElement
>
}
}
}
}

function ReactOGrafForm(props: {
data: any
schema: OGrafForm.GDDSchema
onDataChangeCallback: (value: any) => void
}) {
const containerRef = useRef<HTMLDivElement>(null)
const ografFormRef = useRef<OGrafForm.SuperFlyTvOgrafDataForm>(null)

useLayoutEffect(() => {
if (ografFormRef.current) return // Already initialized
if (!containerRef.current) {
// Should really not happen
console.error('Div ref not set for OGraf form')
return
}

// Initialize the form OGraf form element:
ografFormRef.current = new OGrafForm.SuperFlyTvOgrafDataForm()
ografFormRef.current.addEventListener('change', () => {
if (ografFormRef.current) props.onDataChangeCallback(ografFormRef.current.value)
})
if (props.schema) ografFormRef.current.schema = props.schema as any
ografFormRef.current.value = props.data ?? {}

containerRef.current.appendChild(ografFormRef.current)
}, [])

useEffect(() => {
if (ografFormRef.current && props.schema) ografFormRef.current.schema = props.schema as any
}, [props.schema])
useEffect(() => {
if (ografFormRef.current) ografFormRef.current.value = props.data
}, [props.data])

return <div ref={containerRef} />
}
3 changes: 2 additions & 1 deletion frontend/src/components/rundown/piecesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ function NewPieceButton({
segmentId,
partId,
name: manifest && manifest.includeTypeInName ? manifest.name : 'New piece',
pieceType: selectedPieceType
pieceType: selectedPieceType,
manifest
})
)
.unwrap()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type {
ManifestFieldType,
TypeManifest,
TypeManifestEntity
} from '~backend/background/interfaces'
import type { AppDispatch } from '../../../../store/app'
import { importTypeManifest, updateTypeManifest } from '../../../../store/typeManifest'
import * as OGraf from 'ograf'
import { getDefaultDataFromSchema } from 'ograf-form'

export function isImportOGrafManifest(data: unknown): data is OGraf.GraphicsManifest {
if (typeof data !== 'object' || data === null) return false
if (Array.isArray(data)) return false

const manifest = data as OGraf.GraphicsManifest

return (
manifest.$schema === 'https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json'
)
}
export async function doImportOGrafManifest(
manifest: OGraf.GraphicsManifest,
typeManifests: TypeManifest[],
dispatch: AppDispatch
) {
const id = `ograf-${manifest.id}`

const existing = typeManifests.find((m) => m.id === id)

const typeManifest: TypeManifest = {
id,
colour: '#5555ff',
entityType: 'piece' as TypeManifestEntity,
name: manifest.name,
shortName: manifest.name,
includeTypeInName: true,

payload: [
{
id: 'ograf-id',
label: 'OGraf Id',
type: 'const' as ManifestFieldType,
defaultValue: manifest.id
},
{
id: 'type',
label: 'Type of Graphics',
type: 'enum' as ManifestFieldType,
enumValues: [
{ label: 'Full Screen', value: 'full-screen' },
{ label: 'Overlay 1', value: 'overlay1' },
{ label: 'Overlay 2', value: 'overlay2' },
{ label: 'Overlay 3', value: 'overlay3' }
],
defaultValue: 'overlay1'
},
{
id: 'ograf-data',
label: 'OGraf Data',
type: 'ograf-form' as ManifestFieldType,
ografManifest: manifest,
defaultValue: getDefaultDataFromSchema(manifest.schema)
}
]
}

if (existing) {
await dispatch(updateTypeManifest({ originalId: existing.id, typeManifest }))
} else {
await dispatch(importTypeManifest({ typeManifest }))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { TypeManifest } from '~backend/background/interfaces'
import type { AppDispatch } from '../../../../store/app'
import { importTypeManifest, updateTypeManifest } from '../../../../store/typeManifest'

export function isImportTypeManifest(arr: unknown): arr is TypeManifest[] {
return (
Array.isArray(arr) &&
arr.every((t) => 'id' in t && 'entityType' in t && 'name' in t && 'payload' in t)
)
}
export async function doImportTypeManifest(
importJson: TypeManifest[],
typeManifests: TypeManifest[],
dispatch: AppDispatch
) {
await Promise.all(
importJson.map(async (t) => {
const existing = typeManifests.find((m) => m.id === t.id)
try {
if (existing) {
await dispatch(updateTypeManifest({ originalId: existing.id, typeManifest: t }))
} else {
await dispatch(importTypeManifest({ typeManifest: t }))
}
} catch (e) {
console.error(e)
throw e
}
})
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ export function TypeManifestForm({
<option value={ManifestFieldType.String}>String</option>
<option value={ManifestFieldType.Number}>Number</option>
<option value={ManifestFieldType.Boolean}>Boolean</option>
<option value={ManifestFieldType.Enum}>Enum</option>
<option value={ManifestFieldType.Const}>Constant</option>
<option value={ManifestFieldType.OGrafForm}>OGraf data</option>
</Form.Select>
<FieldInfo field={field} />
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Accordion, Button, ButtonGroup } from 'react-bootstrap'
import { useAppDispatch } from '~/store/app'
import { addNewTypeManifest, importTypeManifest, updateTypeManifest } from '~/store/typeManifest'
import { addNewTypeManifest } from '~/store/typeManifest'
import { ipcAPI } from '~/lib/IPC'
import { TypeManifestEntity } from '~backend/background/interfaces'
import type { TypeManifest } from '~backend/background/interfaces'
import './typesForm.scss'
import { useToasts } from '~/components/toasts/useToasts'
import { TypeManifestForm } from './typeManifestForm'
import { doImportTypeManifest, isImportTypeManifest } from './lib/importType'
import { doImportOGrafManifest, isImportOGrafManifest } from './lib/importOGrafManifest'

export function TypeManifestsForm({
typeManifests,
Expand Down Expand Up @@ -36,35 +38,25 @@ export function TypeManifestsForm({
// Import types
const importTypes = () => {
ipcAPI.openFromFile({ title: `Import ${title}` }).then(async (imported) => {
const verify = (arr: unknown): arr is TypeManifest[] =>
Array.isArray(arr) &&
arr.every((t) => 'id' in t && 'entityType' in t && 'name' in t && 'payload' in t)
// Is OGraf manifest file?
// console.log('imported', imported)

if (!verify(imported)) {
toasts.show({ headerContent: `Import ${title}`, bodyContent: 'Invalid file' })
return
}

await Promise.all(
imported.map(async (t) => {
const existing = typeManifests.find((m) => m.id === t.id)
try {
if (existing) {
await dispatch(updateTypeManifest({ originalId: existing.id, typeManifest: t }))
} else {
await dispatch(importTypeManifest({ typeManifest: t }))
}
} catch (e) {
console.error(e)
toasts.show({
headerContent: `Import ${title}`,
bodyContent: 'Unexpected error'
})
}
try {
if (isImportOGrafManifest(imported)) {
await doImportOGrafManifest(imported, typeManifests, dispatch)
toasts.show({ headerContent: `Import ${title}`, bodyContent: 'Import complete' })
} else if (isImportTypeManifest(imported)) {
await doImportTypeManifest(imported, typeManifests, dispatch)
toasts.show({ headerContent: `Import ${title}`, bodyContent: 'Import complete' })
} else {
toasts.show({ headerContent: `Import ${title}`, bodyContent: 'Invalid file' })
}
} catch (e) {
toasts.show({
headerContent: `Import ${title}`,
bodyContent: 'Unexpected error, se console for details'
})
)

toasts.show({ headerContent: `Import ${title}`, bodyContent: 'Import complete' })
}
})
}

Expand Down
13 changes: 11 additions & 2 deletions frontend/src/store/pieces.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type {
MutationPieceCloneFromParToPart,
MutationPieceCopy,
Piece
Piece,
TypeManifest
} from '~backend/background/interfaces.js'
import { createSlice } from '@reduxjs/toolkit'
import { createAppAsyncThunk } from './app'
Expand All @@ -19,6 +20,7 @@ export interface NewPiecePayload {

name: string
pieceType: string
manifest?: TypeManifest
}
export interface UpdatePiecePayload {
piece: Piece
Expand All @@ -30,14 +32,21 @@ export interface RemovePiecePayload {
export const addNewPiece = createAppAsyncThunk(
'pieces/addNewPiece',
async (payload: NewPiecePayload) => {
const piecePayload: Record<string, any> = {}
if (payload.manifest) {
for (const payloadManifest of payload.manifest.payload) {
if (payloadManifest.defaultValue)
piecePayload[payloadManifest.id] = payloadManifest.defaultValue
}
}
return ipcAPI.addNewPiece({
name: payload.name,
playlistId: payload.playlistId,
rundownId: payload.rundownId,
segmentId: payload.segmentId,
partId: payload.partId,
pieceType: payload.pieceType,
payload: {}
payload: piecePayload
})
}
)
Expand Down
Loading
Loading