Skip to content

Commit e2e0450

Browse files
committed
Add "AB Channel display" config to showstyle settings UI
1 parent ae0b339 commit e2e0450

File tree

6 files changed

+316
-1
lines changed

6 files changed

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

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

Lines changed: 4 additions & 0 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'
@@ -131,6 +132,9 @@ export default function ShowStyleBaseSettings({ match }: IProps): JSX.Element {
131132
<Route path={`${match.path}/hotkey-labels`}>
132133
<HotkeyLegendSettings showStyleBase={showStyleBase} />
133134
</Route>
135+
<Route path={`${match.path}/ab-channel-display`}>
136+
<AbChannelDisplaySettings showStyleBase={showStyleBase} />
137+
</Route>
134138

135139
{RundownLayoutsAPI.getSettingsManifest(t).map((region) => {
136140
return (

0 commit comments

Comments
 (0)