Skip to content

Commit a8960d3

Browse files
committed
Add "AB Channel display" config to showstyle settings UI
1 parent 230e18d commit a8960d3

File tree

6 files changed

+321
-5
lines changed

6 files changed

+321
-5
lines changed

meteor/server/publications/showStyleUI.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,20 @@ interface UIShowStyleBaseUpdateProps {
2626
invalidateShowStyle: boolean
2727
}
2828

29-
type ShowStyleBaseFields = '_id' | 'name' | 'outputLayersWithOverrides' | 'sourceLayersWithOverrides' | 'hotkeyLegend'
29+
type ShowStyleBaseFields =
30+
| '_id'
31+
| 'name'
32+
| 'outputLayersWithOverrides'
33+
| 'sourceLayersWithOverrides'
34+
| 'hotkeyLegend'
35+
| 'abChannelDisplay'
3036
const fieldSpecifier = literal<MongoFieldSpecifierOnesStrict<Pick<DBShowStyleBase, ShowStyleBaseFields>>>({
3137
_id: 1,
3238
name: 1,
3339
outputLayersWithOverrides: 1,
3440
sourceLayersWithOverrides: 1,
3541
hotkeyLegend: 1,
42+
abChannelDisplay: 1,
3643
})
3744

3845
async function setupUIShowStyleBasePublicationObservers(
@@ -78,6 +85,7 @@ async function manipulateUIShowStyleBasePublicationData(
7885
sourceLayers: resolvedSourceLayers,
7986
outputLayers: resolvedOutputLayers,
8087
hotkeyLegend: showStyleBase.hotkeyLegend,
88+
abChannelDisplay: showStyleBase.abChannelDisplay,
8189
}),
8290
]
8391
}

packages/job-worker/src/blueprints/context/lib.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export const IBlueprintPieceObjectsSampleKeys = allKeysOfObject<IBlueprintPiece>
104104
userEditOperations: true,
105105
userEditProperties: true,
106106
excludeDuringPartKeepalive: true,
107+
displayAbChannel: true,
107108
})
108109

109110
// Compile a list of the keys which are allowed to be set
@@ -252,6 +253,7 @@ export function convertPieceToBlueprints(piece: ReadonlyDeep<PieceInstancePiece>
252253
userEditOperations: translateUserEditsToBlueprint(piece.userEditOperations),
253254
userEditProperties: translateUserEditPropertiesToBlueprint(piece.userEditProperties),
254255
excludeDuringPartKeepalive: piece.excludeDuringPartKeepalive,
256+
displayAbChannel: piece.displayAbChannel,
255257
}
256258

257259
return obj

packages/webui/src/__mocks__/helpers/database.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ export function convertToUIShowStyleBase(showStyleBase: DBShowStyleBase): UIShow
554554
hotkeyLegend: showStyleBase.hotkeyLegend,
555555
sourceLayers: applyAndValidateOverrides(showStyleBase.sourceLayersWithOverrides).obj,
556556
outputLayers: applyAndValidateOverrides(showStyleBase.outputLayersWithOverrides).obj,
557+
abChannelDisplay: showStyleBase.abChannelDisplay,
557558
})
558559
}
559560
export function convertToUIStudio(studio: DBStudio): UIStudio {

packages/webui/src/client/ui/Settings/SettingsMenu.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ function SettingsMenuShowStyle({ showStyleBase }: Readonly<SettingsMenuShowStyle
357357
{ label: t('Source/Output Layers'), subPath: `layers` },
358358
{ label: t('Action Triggers'), subPath: `action-triggers` },
359359
{ label: t('Custom Hotkey Labels'), subPath: `hotkey-labels` },
360+
{ label: t('AB Channel Display'), subPath: `ab-channel-display` },
360361

361362
...RundownLayoutsAPI.getSettingsManifest(t).map((region) => {
362363
return { label: region.title, subPath: `layouts-${region._id}` }
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase'
2+
import { SourceLayerType } from '@sofie-automation/blueprints-integration'
3+
import { ShowStyleBases } from '../../../collections'
4+
import { useTranslation } from 'react-i18next'
5+
import { useCallback, useMemo } from 'react'
6+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
7+
import { faTrash } from '@fortawesome/free-solid-svg-icons'
8+
import { SourceLayers, OutputLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase'
9+
import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
10+
11+
interface AbChannelDisplaySettingsProps {
12+
showStyleBase: DBShowStyleBase
13+
}
14+
15+
export function AbChannelDisplaySettings({ showStyleBase }: Readonly<AbChannelDisplaySettingsProps>): JSX.Element {
16+
const { t } = useTranslation()
17+
18+
const sourceLayers: SourceLayers = useMemo(
19+
() => applyAndValidateOverrides(showStyleBase.sourceLayersWithOverrides).obj,
20+
[showStyleBase.sourceLayersWithOverrides]
21+
)
22+
23+
const outputLayers: OutputLayers = useMemo(
24+
() => applyAndValidateOverrides(showStyleBase.outputLayersWithOverrides).obj,
25+
[showStyleBase.outputLayersWithOverrides]
26+
)
27+
28+
const sourceLayerOptions = useMemo(() => {
29+
return Object.entries<SourceLayers[string]>(sourceLayers).map(([id, layer]) => ({
30+
value: id,
31+
label: layer?.name ?? id,
32+
}))
33+
}, [sourceLayers])
34+
35+
const outputLayerOptions = useMemo(() => {
36+
return Object.entries<OutputLayers[string]>(outputLayers).map(([id, layer]) => ({
37+
value: id,
38+
label: layer?.name ?? id,
39+
}))
40+
}, [outputLayers])
41+
42+
const sourceLayerTypeOptions = useMemo(() => {
43+
return [
44+
{ value: SourceLayerType.UNKNOWN, label: 'Unknown' },
45+
{ value: SourceLayerType.CAMERA, label: 'Camera' },
46+
{ value: SourceLayerType.VT, label: 'VT' },
47+
{ value: SourceLayerType.REMOTE, label: 'Remote' },
48+
{ value: SourceLayerType.SCRIPT, label: 'Script' },
49+
{ value: SourceLayerType.GRAPHICS, label: 'Graphics' },
50+
{ value: SourceLayerType.SPLITS, label: 'Splits' },
51+
{ value: SourceLayerType.AUDIO, label: 'Audio' },
52+
{ value: SourceLayerType.LOWER_THIRD, label: 'Lower Third' },
53+
{ value: SourceLayerType.LIVE_SPEAK, label: 'Live Speak' },
54+
{ value: SourceLayerType.TRANSITION, label: 'Transition' },
55+
{ value: SourceLayerType.LIGHTS, label: 'Lights' },
56+
{ value: SourceLayerType.LOCAL, label: 'Local' },
57+
]
58+
}, [])
59+
60+
const config = showStyleBase.abChannelDisplay ?? {
61+
sourceLayerIds: [],
62+
sourceLayerTypes: [SourceLayerType.VT, SourceLayerType.LIVE_SPEAK],
63+
outputLayerIds: [],
64+
showOnDirectorScreen: false,
65+
}
66+
67+
const updateConfig = useCallback(
68+
(updates: Partial<NonNullable<DBShowStyleBase['abChannelDisplay']>>) => {
69+
const newConfig: NonNullable<DBShowStyleBase['abChannelDisplay']> = {
70+
sourceLayerIds: config.sourceLayerIds,
71+
sourceLayerTypes: config.sourceLayerTypes,
72+
outputLayerIds: config.outputLayerIds,
73+
showOnDirectorScreen: config.showOnDirectorScreen,
74+
...updates,
75+
}
76+
77+
ShowStyleBases.update(showStyleBase._id, {
78+
$set: {
79+
abChannelDisplay: newConfig,
80+
},
81+
})
82+
},
83+
[showStyleBase._id, config]
84+
)
85+
86+
const toggleDirectorScreen = useCallback(() => {
87+
updateConfig({ showOnDirectorScreen: !config.showOnDirectorScreen })
88+
}, [updateConfig, config.showOnDirectorScreen])
89+
90+
const addSourceLayerId = useCallback(
91+
(layerId: string) => {
92+
if (!config.sourceLayerIds.includes(layerId)) {
93+
updateConfig({ sourceLayerIds: [...config.sourceLayerIds, layerId] })
94+
}
95+
},
96+
[updateConfig, config.sourceLayerIds]
97+
)
98+
99+
const removeSourceLayerId = useCallback(
100+
(layerId: string) => {
101+
updateConfig({ sourceLayerIds: config.sourceLayerIds.filter((id) => id !== layerId) })
102+
},
103+
[updateConfig, config.sourceLayerIds]
104+
)
105+
106+
const addSourceLayerType = useCallback(
107+
(type: SourceLayerType) => {
108+
if (!config.sourceLayerTypes.includes(type)) {
109+
updateConfig({ sourceLayerTypes: [...config.sourceLayerTypes, type] })
110+
}
111+
},
112+
[updateConfig, config.sourceLayerTypes]
113+
)
114+
115+
const removeSourceLayerType = useCallback(
116+
(type: SourceLayerType) => {
117+
updateConfig({ sourceLayerTypes: config.sourceLayerTypes.filter((t) => t !== type) })
118+
},
119+
[updateConfig, config.sourceLayerTypes]
120+
)
121+
122+
const addOutputLayerId = useCallback(
123+
(layerId: string) => {
124+
if (!config.outputLayerIds.includes(layerId)) {
125+
updateConfig({ outputLayerIds: [...config.outputLayerIds, layerId] })
126+
}
127+
},
128+
[updateConfig, config.outputLayerIds]
129+
)
130+
131+
const removeOutputLayerId = useCallback(
132+
(layerId: string) => {
133+
updateConfig({ outputLayerIds: config.outputLayerIds.filter((id) => id !== layerId) })
134+
},
135+
[updateConfig, config.outputLayerIds]
136+
)
137+
138+
return (
139+
<div className="studio-edit mod mhl mvn">
140+
<h2 className="mhn">{t('AB Resolver Channel Display')}</h2>
141+
<p className="mhn">
142+
{t(
143+
'Configure which pieces should display their assigned AB resolver channel (e.g., "Server A") on various screens. This helps operators identify which video server is playing each clip.'
144+
)}
145+
</p>
146+
147+
<div className="mod mvs mhs">
148+
<label className="field">
149+
<input
150+
type="checkbox"
151+
className="mod mas"
152+
checked={config.showOnDirectorScreen}
153+
onChange={toggleDirectorScreen}
154+
/>
155+
{t('Show on Director Screen')}
156+
</label>
157+
<p className="muted">{t('Display AB channel assignments on the director countdown screen')}</p>
158+
</div>
159+
160+
<div className="mod mvs mhs">
161+
<h3>{t('Filter by Source Layer')}</h3>
162+
<p className="muted">
163+
{t(
164+
'Select specific source layers that should display AB channel info. Leave empty to use type-based filtering.'
165+
)}
166+
</p>
167+
168+
<div className="mod mvs">
169+
<select
170+
className="input text-input input-l"
171+
title={t('Add source layer')}
172+
onChange={(e) => {
173+
if (e.target.value) {
174+
addSourceLayerId(e.target.value)
175+
e.target.value = ''
176+
}
177+
}}
178+
>
179+
<option value="">{t('Add source layer...')}</option>
180+
{sourceLayerOptions
181+
.filter((opt) => !config.sourceLayerIds.includes(opt.value))
182+
.map((opt) => (
183+
<option key={opt.value} value={opt.value}>
184+
{opt.label}
185+
</option>
186+
))}
187+
</select>
188+
</div>
189+
190+
<table className="table expando settings-studio-source-table">
191+
<tbody>
192+
{config.sourceLayerIds.map((layerId) => (
193+
<tr key={layerId}>
194+
<td>{sourceLayers[layerId]?.name ?? layerId}</td>
195+
<td className="actions">
196+
<button className="action-btn" title={t('Remove')} onClick={() => removeSourceLayerId(layerId)}>
197+
<FontAwesomeIcon icon={faTrash} />
198+
</button>
199+
</td>
200+
</tr>
201+
))}
202+
</tbody>
203+
</table>
204+
</div>
205+
206+
<div className="mod mvs mhs">
207+
<h3>{t('Filter by Source Layer Type')}</h3>
208+
<p className="muted">
209+
{t('Select source layer types that should display AB channel info (e.g., VT, Live Speak).')}
210+
</p>
211+
212+
<div className="mod mvs">
213+
<select
214+
className="input text-input input-l"
215+
title={t('Add source layer type')}
216+
onChange={(e) => {
217+
if (e.target.value) {
218+
addSourceLayerType(Number(e.target.value) as SourceLayerType)
219+
e.target.value = ''
220+
}
221+
}}
222+
>
223+
<option value="">{t('Add source layer type...')}</option>
224+
{sourceLayerTypeOptions
225+
.filter((opt) => !config.sourceLayerTypes.includes(opt.value))
226+
.map((opt) => (
227+
<option key={opt.value} value={opt.value}>
228+
{opt.label}
229+
</option>
230+
))}
231+
</select>
232+
</div>
233+
234+
<table className="table expando settings-studio-source-table">
235+
<tbody>
236+
{config.sourceLayerTypes.map((type) => {
237+
const option = sourceLayerTypeOptions.find((opt) => opt.value === type)
238+
return (
239+
<tr key={type}>
240+
<td>{option?.label ?? type}</td>
241+
<td className="actions">
242+
<button className="action-btn" title={t('Remove')} onClick={() => removeSourceLayerType(type)}>
243+
<FontAwesomeIcon icon={faTrash} />
244+
</button>
245+
</td>
246+
</tr>
247+
)
248+
})}
249+
</tbody>
250+
</table>
251+
</div>
252+
253+
<div className="mod mvs mhs">
254+
<h3>{t('Filter by Output Layer')}</h3>
255+
<p className="muted">
256+
{t(
257+
'Optionally restrict AB channel display to specific output layers (e.g., only PGM). Leave empty to show for all output layers.'
258+
)}
259+
</p>
260+
261+
<div className="mod mvs">
262+
<select
263+
className="input text-input input-l"
264+
title={t('Add output layer')}
265+
onChange={(e) => {
266+
if (e.target.value) {
267+
addOutputLayerId(e.target.value)
268+
e.target.value = ''
269+
}
270+
}}
271+
>
272+
<option value="">{t('Add output layer...')}</option>
273+
{outputLayerOptions
274+
.filter((opt) => !config.outputLayerIds.includes(opt.value))
275+
.map((opt) => (
276+
<option key={opt.value} value={opt.value}>
277+
{opt.label}
278+
</option>
279+
))}
280+
</select>
281+
</div>
282+
283+
<table className="table expando settings-studio-output-table">
284+
<tbody>
285+
{config.outputLayerIds.map((layerId) => (
286+
<tr key={layerId}>
287+
<td>{outputLayers[layerId]?.name ?? layerId}</td>
288+
<td className="actions">
289+
<button className="action-btn" title={t('Remove')} onClick={() => removeOutputLayerId(layerId)}>
290+
<FontAwesomeIcon icon={faTrash} />
291+
</button>
292+
</td>
293+
</tr>
294+
))}
295+
</tbody>
296+
</table>
297+
</div>
298+
</div>
299+
)
300+
}

packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { OutputLayerSettings } from './ShowStyle/OutputLayer.js'
1010
import { HotkeyLegendSettings } from './ShowStyle/HotkeyLegend.js'
1111
import { ShowStyleVariantsSettings } from './ShowStyle/VariantSettings.js'
1212
import { ShowStyleGenericProperties } from './ShowStyle/Generic.js'
13+
import { AbChannelDisplaySettings } from './ShowStyle/AbChannelDisplay.js'
1314
import { Switch, Route, Redirect } from 'react-router-dom'
1415
import { ErrorBoundary } from '../../lib/ErrorBoundary.js'
1516
import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
@@ -128,11 +129,14 @@ export default function ShowStyleBaseSettings({ match }: IProps): JSX.Element {
128129
outputLayers={outputLayers}
129130
/>
130131
</Route>
131-
<Route path={`${match.path}/hotkey-labels`}>
132-
<HotkeyLegendSettings showStyleBase={showStyleBase} />
133-
</Route>
132+
<Route path={`${match.path}/hotkey-labels`}>
133+
<HotkeyLegendSettings showStyleBase={showStyleBase} />
134+
</Route>
135+
<Route path={`${match.path}/ab-channel-display`}>
136+
<AbChannelDisplaySettings showStyleBase={showStyleBase} />
137+
</Route>
134138

135-
{RundownLayoutsAPI.getSettingsManifest(t).map((region) => {
139+
{RundownLayoutsAPI.getSettingsManifest(t).map((region) => {
136140
return (
137141
<Route key={region._id} path={`${match.path}/layouts-${region._id}`}>
138142
<RundownLayoutEditor

0 commit comments

Comments
 (0)