@arcanewizards/timecode-toolbox 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +55 -0
- package/CHANGELOG.md +24 -0
- package/eslint.config.mjs +49 -0
- package/package.json +74 -0
- package/src/app.tsx +147 -0
- package/src/components/backend/index.ts +6 -0
- package/src/components/backend/toolbox-root.ts +119 -0
- package/src/components/frontend/constants.ts +81 -0
- package/src/components/frontend/entrypoint.ts +12 -0
- package/src/components/frontend/frontend.css +108 -0
- package/src/components/frontend/index.tsx +46 -0
- package/src/components/frontend/toolbox/content.tsx +45 -0
- package/src/components/frontend/toolbox/context.tsx +63 -0
- package/src/components/frontend/toolbox/core/size-aware-div.tsx +51 -0
- package/src/components/frontend/toolbox/core/timecode-display.tsx +592 -0
- package/src/components/frontend/toolbox/generators.tsx +318 -0
- package/src/components/frontend/toolbox/inputs.tsx +484 -0
- package/src/components/frontend/toolbox/outputs.tsx +581 -0
- package/src/components/frontend/toolbox/preferences.ts +25 -0
- package/src/components/frontend/toolbox/root.tsx +335 -0
- package/src/components/frontend/toolbox/settings.tsx +54 -0
- package/src/components/frontend/toolbox/types.ts +28 -0
- package/src/components/frontend/toolbox/util.tsx +61 -0
- package/src/components/proto.ts +420 -0
- package/src/config.ts +7 -0
- package/src/generators/clock.tsx +206 -0
- package/src/generators/index.tsx +15 -0
- package/src/index.ts +38 -0
- package/src/inputs/artnet.tsx +305 -0
- package/src/inputs/index.tsx +13 -0
- package/src/inputs/tcnet.tsx +272 -0
- package/src/outputs/artnet.tsx +170 -0
- package/src/outputs/index.tsx +11 -0
- package/src/start.ts +47 -0
- package/src/tree.ts +133 -0
- package/src/types.ts +12 -0
- package/src/urls.ts +49 -0
- package/src/util.ts +82 -0
- package/tailwind.config.cjs +7 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FC,
|
|
3
|
+
ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import {
|
|
11
|
+
isTimecodeGroup,
|
|
12
|
+
isTimecodeInstance,
|
|
13
|
+
TimecodeGroup,
|
|
14
|
+
TimecodeInstance,
|
|
15
|
+
TimecodeState,
|
|
16
|
+
TimecodeTotalTime,
|
|
17
|
+
TimecodeInstanceId,
|
|
18
|
+
isOutputInstanceId,
|
|
19
|
+
isInputInstanceId,
|
|
20
|
+
isGeneratorInstanceId,
|
|
21
|
+
} from '../../../proto';
|
|
22
|
+
import { displayMillis } from '../util';
|
|
23
|
+
import { StageContext } from '@arcanejs/toolkit-frontend';
|
|
24
|
+
import {
|
|
25
|
+
cnd,
|
|
26
|
+
cssSigilColorUsageVariables,
|
|
27
|
+
SigilColor,
|
|
28
|
+
sigilColorUsage,
|
|
29
|
+
} from '@arcanewizards/sigil/frontend/styling';
|
|
30
|
+
import { cn } from '@arcanejs/toolkit-frontend/util';
|
|
31
|
+
import {
|
|
32
|
+
ControlButton,
|
|
33
|
+
ControlButtonGroup,
|
|
34
|
+
} from '@arcanewizards/sigil/frontend/controls';
|
|
35
|
+
import { TooltipWrapper } from '@arcanewizards/sigil/frontend/tooltip';
|
|
36
|
+
import { STRINGS } from '../../constants';
|
|
37
|
+
import { AssignToOutputCallback } from '../types';
|
|
38
|
+
import { Icon } from '@arcanejs/toolkit-frontend/components/core';
|
|
39
|
+
import { SizeAwareDiv } from './size-aware-div';
|
|
40
|
+
import {
|
|
41
|
+
ApplicationStateContext,
|
|
42
|
+
ConfigContext,
|
|
43
|
+
useApplicationHandlers,
|
|
44
|
+
} from '../context';
|
|
45
|
+
import { getTreeValue } from '../../../../tree';
|
|
46
|
+
import { useBrowserContext } from '@arcanewizards/sigil/frontend';
|
|
47
|
+
import { WINDOW_MODE_TIMECODE, withUrlFragment } from '../../../../urls';
|
|
48
|
+
import {
|
|
49
|
+
augmentUpstreamTimecodeWithOutputMetadata,
|
|
50
|
+
getTimecodeInstance,
|
|
51
|
+
} from '../../../../util';
|
|
52
|
+
|
|
53
|
+
type ActiveTimecodeTextProps = {
|
|
54
|
+
effectiveStartTimeMillis: number;
|
|
55
|
+
speed: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const ActiveTimecodeText: FC<ActiveTimecodeTextProps> = ({
|
|
59
|
+
effectiveStartTimeMillis,
|
|
60
|
+
speed,
|
|
61
|
+
}) => {
|
|
62
|
+
const [millis, setMillis] = useState(0);
|
|
63
|
+
|
|
64
|
+
const { timeDifferenceMs } = useContext(StageContext);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
let animationFrame: number | null = null;
|
|
68
|
+
|
|
69
|
+
const updateMillis = () => {
|
|
70
|
+
const newMillis =
|
|
71
|
+
(Date.now() - (timeDifferenceMs ?? 0) - effectiveStartTimeMillis) *
|
|
72
|
+
speed;
|
|
73
|
+
setMillis(newMillis);
|
|
74
|
+
animationFrame = requestAnimationFrame(updateMillis);
|
|
75
|
+
};
|
|
76
|
+
updateMillis();
|
|
77
|
+
return () => {
|
|
78
|
+
if (animationFrame !== null) {
|
|
79
|
+
cancelAnimationFrame(animationFrame);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}, [effectiveStartTimeMillis, speed, timeDifferenceMs]);
|
|
83
|
+
|
|
84
|
+
return displayMillis(millis);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type TimelineProps = {
|
|
88
|
+
state: TimecodeState;
|
|
89
|
+
totalTime: TimecodeTotalTime;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const Timeline: FC<TimelineProps> = ({ state, totalTime }) => {
|
|
93
|
+
const [millis, setMillis] = useState(0);
|
|
94
|
+
|
|
95
|
+
const { timeDifferenceMs } = useContext(StageContext);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (state.state === 'none') {
|
|
99
|
+
setMillis(0);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (state.state === 'stopped') {
|
|
104
|
+
setMillis(state.positionMillis);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let animationFrame: number | null = null;
|
|
109
|
+
|
|
110
|
+
const updateMillis = () => {
|
|
111
|
+
const newMillis =
|
|
112
|
+
(Date.now() -
|
|
113
|
+
(timeDifferenceMs ?? 0) -
|
|
114
|
+
state.effectiveStartTimeMillis) *
|
|
115
|
+
state.speed;
|
|
116
|
+
setMillis(newMillis);
|
|
117
|
+
animationFrame = requestAnimationFrame(updateMillis);
|
|
118
|
+
};
|
|
119
|
+
updateMillis();
|
|
120
|
+
return () => {
|
|
121
|
+
if (animationFrame !== null) {
|
|
122
|
+
cancelAnimationFrame(animationFrame);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}, [state, timeDifferenceMs]);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="w-full border border-timecode-usage-foreground p-px">
|
|
129
|
+
<div className="relative h-1 w-full overflow-hidden">
|
|
130
|
+
<div
|
|
131
|
+
className="absolute inset-y-0 left-0 bg-timecode-usage-foreground"
|
|
132
|
+
style={{
|
|
133
|
+
width: `${Math.min((millis / totalTime.timeMillis) * 100, 100)}%`,
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
type UniversalConfig = {
|
|
142
|
+
delayMs: number | null;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
type TimecodeDisplayProps = {
|
|
146
|
+
id: TimecodeInstanceId;
|
|
147
|
+
timecode: TimecodeInstance;
|
|
148
|
+
config: UniversalConfig;
|
|
149
|
+
headerComponents?: React.ReactNode;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const TimecodeDisplay: FC<TimecodeDisplayProps> = ({
|
|
153
|
+
id,
|
|
154
|
+
timecode: { state, metadata },
|
|
155
|
+
config,
|
|
156
|
+
headerComponents,
|
|
157
|
+
}) => {
|
|
158
|
+
const { handlers, callHandler } = useApplicationHandlers();
|
|
159
|
+
|
|
160
|
+
const hooks = id && getTreeValue(handlers, id);
|
|
161
|
+
|
|
162
|
+
const play = useCallback(() => {
|
|
163
|
+
if (id) {
|
|
164
|
+
callHandler({ handler: 'play', path: id, args: [] });
|
|
165
|
+
}
|
|
166
|
+
}, [callHandler, id]);
|
|
167
|
+
|
|
168
|
+
const pause = useCallback(() => {
|
|
169
|
+
if (id) {
|
|
170
|
+
callHandler({ handler: 'pause', path: id, args: [] });
|
|
171
|
+
}
|
|
172
|
+
}, [callHandler, id]);
|
|
173
|
+
|
|
174
|
+
const back5seconds = useCallback(() => {
|
|
175
|
+
if (id) {
|
|
176
|
+
callHandler({ handler: 'seekRelative', path: id, args: [-5000] });
|
|
177
|
+
}
|
|
178
|
+
}, [callHandler, id]);
|
|
179
|
+
|
|
180
|
+
const forward5seconds = useCallback(() => {
|
|
181
|
+
if (id) {
|
|
182
|
+
callHandler({ handler: 'seekRelative', path: id, args: [5000] });
|
|
183
|
+
}
|
|
184
|
+
}, [callHandler, id]);
|
|
185
|
+
|
|
186
|
+
const beginning = useCallback(() => {
|
|
187
|
+
if (id) {
|
|
188
|
+
callHandler({ handler: 'beginning', path: id, args: [] });
|
|
189
|
+
}
|
|
190
|
+
}, [callHandler, id]);
|
|
191
|
+
|
|
192
|
+
const toggle = useCallback(() => {
|
|
193
|
+
if (hooks?.play && hooks?.pause) {
|
|
194
|
+
if (state.state === 'none' || state.state === 'stopped') {
|
|
195
|
+
play();
|
|
196
|
+
} else {
|
|
197
|
+
pause();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}, [hooks, play, pause, state.state]);
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div className="flex grow flex-col gap-px">
|
|
204
|
+
<div
|
|
205
|
+
className={cn(
|
|
206
|
+
'flex grow flex-col p-0.5',
|
|
207
|
+
cnd(
|
|
208
|
+
state?.state === 'lagging',
|
|
209
|
+
'bg-sigil-usage-red-background text-sigil-usage-red-text',
|
|
210
|
+
'bg-sigil-bg-light text-timecode-usage-foreground',
|
|
211
|
+
),
|
|
212
|
+
)}
|
|
213
|
+
>
|
|
214
|
+
{headerComponents && (
|
|
215
|
+
<div className="flex flex-wrap gap-0.25">{headerComponents}</div>
|
|
216
|
+
)}
|
|
217
|
+
<SizeAwareDiv
|
|
218
|
+
className={cn(
|
|
219
|
+
'relative min-h-timecode-min-height grow',
|
|
220
|
+
cnd(state?.state === 'stopped', 'opacity-50'),
|
|
221
|
+
cnd(
|
|
222
|
+
hooks?.play && hooks?.pause,
|
|
223
|
+
`
|
|
224
|
+
cursor-pointer
|
|
225
|
+
hover:opacity-100
|
|
226
|
+
`,
|
|
227
|
+
),
|
|
228
|
+
)}
|
|
229
|
+
onClick={toggle}
|
|
230
|
+
>
|
|
231
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
232
|
+
<span className={cn('font-mono text-timecode-adaptive')}>
|
|
233
|
+
{state.state === 'none' ? (
|
|
234
|
+
'--:--:--:---'
|
|
235
|
+
) : state.state === 'stopped' ? (
|
|
236
|
+
displayMillis(state.positionMillis)
|
|
237
|
+
) : (
|
|
238
|
+
<ActiveTimecodeText
|
|
239
|
+
effectiveStartTimeMillis={state.effectiveStartTimeMillis}
|
|
240
|
+
speed={state.speed}
|
|
241
|
+
/>
|
|
242
|
+
)}
|
|
243
|
+
</span>
|
|
244
|
+
</div>
|
|
245
|
+
</SizeAwareDiv>
|
|
246
|
+
{hooks?.pause || hooks?.play ? (
|
|
247
|
+
<div className="flex justify-center gap-px">
|
|
248
|
+
{hooks.beginning && (
|
|
249
|
+
<ControlButton
|
|
250
|
+
onClick={beginning}
|
|
251
|
+
variant="large"
|
|
252
|
+
icon="skip_previous"
|
|
253
|
+
disabled={!hooks?.beginning}
|
|
254
|
+
title={STRINGS.controls.beginning}
|
|
255
|
+
className="text-timecode-usage-foreground!"
|
|
256
|
+
/>
|
|
257
|
+
)}
|
|
258
|
+
{hooks.seekRelative && (
|
|
259
|
+
<ControlButton
|
|
260
|
+
onClick={back5seconds}
|
|
261
|
+
variant="large"
|
|
262
|
+
icon="replay_5"
|
|
263
|
+
disabled={!hooks?.seekRelative}
|
|
264
|
+
title={STRINGS.controls.back5seconds}
|
|
265
|
+
className="text-timecode-usage-foreground!"
|
|
266
|
+
/>
|
|
267
|
+
)}
|
|
268
|
+
{state.state === 'none' || state.state === 'stopped' ? (
|
|
269
|
+
<ControlButton
|
|
270
|
+
onClick={play}
|
|
271
|
+
variant="large"
|
|
272
|
+
icon="play_arrow"
|
|
273
|
+
disabled={!hooks?.play}
|
|
274
|
+
title={STRINGS.controls.play}
|
|
275
|
+
className="text-timecode-usage-foreground!"
|
|
276
|
+
/>
|
|
277
|
+
) : (
|
|
278
|
+
<ControlButton
|
|
279
|
+
onClick={pause}
|
|
280
|
+
variant="large"
|
|
281
|
+
icon="pause"
|
|
282
|
+
disabled={!hooks?.pause}
|
|
283
|
+
title={STRINGS.controls.pause}
|
|
284
|
+
className="text-timecode-usage-foreground!"
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
287
|
+
{hooks.seekRelative && (
|
|
288
|
+
<ControlButton
|
|
289
|
+
onClick={forward5seconds}
|
|
290
|
+
variant="large"
|
|
291
|
+
icon="forward_5"
|
|
292
|
+
disabled={!hooks?.seekRelative}
|
|
293
|
+
title={STRINGS.controls.forward5seconds}
|
|
294
|
+
className="text-timecode-usage-foreground!"
|
|
295
|
+
/>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
) : null}
|
|
299
|
+
{metadata?.totalTime && (
|
|
300
|
+
<Timeline state={state} totalTime={metadata.totalTime} />
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
{(state.smpteMode !== null ||
|
|
304
|
+
state.accuracyMillis !== null ||
|
|
305
|
+
config.delayMs !== null) && (
|
|
306
|
+
<div className="flex gap-px">
|
|
307
|
+
{config.delayMs !== null && (
|
|
308
|
+
<div className="grow basis-0 truncate bg-sigil-bg-light p-0.5">
|
|
309
|
+
{STRINGS.delay(config.delayMs)}
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
312
|
+
{state.smpteMode !== null && (
|
|
313
|
+
<div className="grow basis-0 truncate bg-sigil-bg-light p-0.5">
|
|
314
|
+
{STRINGS.smtpeModes[state.smpteMode]}
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
{state.accuracyMillis !== null && (
|
|
318
|
+
<div className="grow basis-0 truncate bg-sigil-bg-light p-0.5">
|
|
319
|
+
{STRINGS.accuracy(state.accuracyMillis)}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
{metadata?.artist || metadata?.title ? (
|
|
325
|
+
<TooltipWrapper
|
|
326
|
+
tooltip={
|
|
327
|
+
<>
|
|
328
|
+
{metadata.title && (
|
|
329
|
+
<div>
|
|
330
|
+
<span className="font-bold">Title:</span> {metadata.title}
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
{metadata.artist && (
|
|
334
|
+
<div>
|
|
335
|
+
<span className="font-bold">Artist:</span> {metadata.artist}
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</>
|
|
339
|
+
}
|
|
340
|
+
>
|
|
341
|
+
<div className="flex gap-px">
|
|
342
|
+
{metadata.title && (
|
|
343
|
+
<div className="grow truncate bg-sigil-bg-light p-0.5 font-bold">
|
|
344
|
+
{metadata.title}
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
{metadata.artist && (
|
|
348
|
+
<div className="grow truncate bg-sigil-bg-light p-0.5">
|
|
349
|
+
{metadata.artist}
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
</TooltipWrapper>
|
|
354
|
+
) : null}
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
type TimecodeTreeDisplayProps = {
|
|
360
|
+
config: UniversalConfig;
|
|
361
|
+
/**
|
|
362
|
+
* Outputs will not have this set, inputs and generators will.
|
|
363
|
+
*/
|
|
364
|
+
id: TimecodeInstanceId;
|
|
365
|
+
type: string;
|
|
366
|
+
name: string[];
|
|
367
|
+
color: SigilColor | undefined;
|
|
368
|
+
timecode: TimecodeGroup | TimecodeInstance | null;
|
|
369
|
+
namePlaceholder: string;
|
|
370
|
+
buttons: ReactNode;
|
|
371
|
+
/**
|
|
372
|
+
* If set, calling this will assign the instance to the given output on
|
|
373
|
+
*/
|
|
374
|
+
assignToOutput: AssignToOutputCallback;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const EMPTY_TIMECODE: TimecodeInstance = {
|
|
378
|
+
name: null,
|
|
379
|
+
state: {
|
|
380
|
+
state: 'none',
|
|
381
|
+
accuracyMillis: null,
|
|
382
|
+
smpteMode: null,
|
|
383
|
+
onAir: null,
|
|
384
|
+
},
|
|
385
|
+
metadata: null,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const extendId = <T extends TimecodeInstanceId>(id: T, key: string): T => {
|
|
389
|
+
return [id[0], ...id.slice(1), key] as unknown as T;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
export const TimecodeTreeDisplay: FC<TimecodeTreeDisplayProps> = ({
|
|
393
|
+
config,
|
|
394
|
+
id,
|
|
395
|
+
type,
|
|
396
|
+
name,
|
|
397
|
+
color,
|
|
398
|
+
timecode,
|
|
399
|
+
namePlaceholder,
|
|
400
|
+
buttons,
|
|
401
|
+
assignToOutput,
|
|
402
|
+
}) => {
|
|
403
|
+
const { openNewWidow } = useBrowserContext();
|
|
404
|
+
|
|
405
|
+
const openInNewWindow = useCallback(() => {
|
|
406
|
+
if (id) {
|
|
407
|
+
openNewWidow(withUrlFragment({ values: { tc: id } }).href, {
|
|
408
|
+
canUseExisting: false,
|
|
409
|
+
mode: WINDOW_MODE_TIMECODE,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}, [id, openNewWidow]);
|
|
413
|
+
|
|
414
|
+
name = timecode?.name ? [...name, timecode.name] : name;
|
|
415
|
+
if (isTimecodeGroup(timecode) && Object.values(timecode.timecodes).length) {
|
|
416
|
+
return Object.entries(timecode.timecodes).map(([key, child]) => (
|
|
417
|
+
<TimecodeTreeDisplay
|
|
418
|
+
config={config}
|
|
419
|
+
id={extendId(id, key)}
|
|
420
|
+
key={key}
|
|
421
|
+
type={type}
|
|
422
|
+
name={name}
|
|
423
|
+
color={timecode.color ?? color}
|
|
424
|
+
timecode={child}
|
|
425
|
+
namePlaceholder={namePlaceholder}
|
|
426
|
+
buttons={buttons}
|
|
427
|
+
assignToOutput={assignToOutput}
|
|
428
|
+
/>
|
|
429
|
+
));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<div
|
|
434
|
+
className="relative flex grow flex-col text-timecode-usage-foreground"
|
|
435
|
+
style={
|
|
436
|
+
color &&
|
|
437
|
+
cssSigilColorUsageVariables('timecode-usage', sigilColorUsage(color))
|
|
438
|
+
}
|
|
439
|
+
>
|
|
440
|
+
<TimecodeDisplay
|
|
441
|
+
id={id}
|
|
442
|
+
timecode={isTimecodeInstance(timecode) ? timecode : EMPTY_TIMECODE}
|
|
443
|
+
config={config}
|
|
444
|
+
headerComponents={
|
|
445
|
+
<>
|
|
446
|
+
<div className="flex grow items-start gap-0.25">
|
|
447
|
+
<div
|
|
448
|
+
className="
|
|
449
|
+
m-0.25 rounded-md border border-sigil-bg-light
|
|
450
|
+
bg-timecode-usage-foreground px-1 py-0.25 text-sigil-control
|
|
451
|
+
text-timecode-usage-text
|
|
452
|
+
"
|
|
453
|
+
>
|
|
454
|
+
{type}
|
|
455
|
+
</div>
|
|
456
|
+
<div
|
|
457
|
+
className={cn(
|
|
458
|
+
'grow basis-0 truncate p-0.5',
|
|
459
|
+
cnd(name.length, 'font-bold', 'italic opacity-50'),
|
|
460
|
+
)}
|
|
461
|
+
>
|
|
462
|
+
{name.length ? name.join(' / ') : namePlaceholder}
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
<ControlButtonGroup className="rounded-md bg-sigil-bg-light">
|
|
466
|
+
<ControlButton
|
|
467
|
+
variant="toolbar"
|
|
468
|
+
icon="open_in_new"
|
|
469
|
+
title={STRINGS.openInNewWindow}
|
|
470
|
+
onClick={openInNewWindow}
|
|
471
|
+
/>
|
|
472
|
+
{buttons}
|
|
473
|
+
</ControlButtonGroup>
|
|
474
|
+
</>
|
|
475
|
+
}
|
|
476
|
+
/>
|
|
477
|
+
{assignToOutput && id && !isOutputInstanceId(id) && (
|
|
478
|
+
<SizeAwareDiv
|
|
479
|
+
className="
|
|
480
|
+
absolute inset-0 flex cursor-pointer items-center justify-center
|
|
481
|
+
bg-timecode-backdrop text-timecode-usage-text
|
|
482
|
+
hover:bg-timecode-backdrop-hover
|
|
483
|
+
"
|
|
484
|
+
onClick={() => assignToOutput(id)}
|
|
485
|
+
>
|
|
486
|
+
<Icon icon="link" className="text-block-icon" />
|
|
487
|
+
</SizeAwareDiv>
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
);
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
type FullscreenTimecodeConfig = {
|
|
494
|
+
config: UniversalConfig;
|
|
495
|
+
type: string;
|
|
496
|
+
name: string[];
|
|
497
|
+
color: SigilColor | undefined;
|
|
498
|
+
namePlaceholder: string;
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
export const FullscreenTimecodeDisplay: FC<{ id: TimecodeInstanceId }> = ({
|
|
502
|
+
id,
|
|
503
|
+
}) => {
|
|
504
|
+
const { config } = useContext(ConfigContext);
|
|
505
|
+
const applicationState = useContext(ApplicationStateContext);
|
|
506
|
+
|
|
507
|
+
const timecode: TimecodeInstance | null = useMemo(() => {
|
|
508
|
+
if (isInputInstanceId(id) || isGeneratorInstanceId(id)) {
|
|
509
|
+
return getTimecodeInstance(applicationState, id);
|
|
510
|
+
} else {
|
|
511
|
+
const c = config.outputs[id[1]];
|
|
512
|
+
if (!c) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
return augmentUpstreamTimecodeWithOutputMetadata(
|
|
516
|
+
c.link ? getTimecodeInstance(applicationState, c.link) : null,
|
|
517
|
+
c,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}, [applicationState, id, config.outputs]);
|
|
521
|
+
|
|
522
|
+
const instanceConfig: FullscreenTimecodeConfig | null = useMemo(() => {
|
|
523
|
+
if (isInputInstanceId(id)) {
|
|
524
|
+
const c = config.inputs[id[1]];
|
|
525
|
+
if (!c) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
config: { delayMs: c.delayMs ?? null },
|
|
530
|
+
type: STRINGS.protocols[c.definition.type].short,
|
|
531
|
+
name: c.name ? [c.name] : [],
|
|
532
|
+
color: c.color,
|
|
533
|
+
namePlaceholder: `Unnamed Input`,
|
|
534
|
+
};
|
|
535
|
+
} else if (isGeneratorInstanceId(id)) {
|
|
536
|
+
const c = config.generators[id[1]];
|
|
537
|
+
if (!c) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
config: { delayMs: c.delayMs ?? null },
|
|
542
|
+
type: STRINGS.generators.type[c.definition.type],
|
|
543
|
+
name: c.name ? [c.name] : [],
|
|
544
|
+
color: c.color,
|
|
545
|
+
namePlaceholder: `Unnamed Generator`,
|
|
546
|
+
};
|
|
547
|
+
} else {
|
|
548
|
+
const c = config.outputs[id[1]];
|
|
549
|
+
if (!c) {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
config: { delayMs: c.delayMs ?? null },
|
|
554
|
+
type: STRINGS.protocols[c.definition.type].short,
|
|
555
|
+
name: c.name ? [c.name] : [],
|
|
556
|
+
color: c.color,
|
|
557
|
+
namePlaceholder: `Unnamed Output`,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}, [id, config]);
|
|
561
|
+
|
|
562
|
+
if (!instanceConfig) {
|
|
563
|
+
return (
|
|
564
|
+
<SizeAwareDiv
|
|
565
|
+
className="
|
|
566
|
+
flex grow flex-col items-center justify-center gap-1 bg-sigil-bg-light
|
|
567
|
+
p-1 text-sigil-foreground-muted
|
|
568
|
+
"
|
|
569
|
+
>
|
|
570
|
+
<Icon icon="question_mark" className="text-block-icon" />
|
|
571
|
+
<div className="text-center">{STRINGS.errors.unknownTimecodeID}</div>
|
|
572
|
+
</SizeAwareDiv>
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return (
|
|
577
|
+
<div
|
|
578
|
+
className="
|
|
579
|
+
flex h-0 grow flex-col gap-px overflow-y-auto bg-sigil-border
|
|
580
|
+
scrollbar-sigil
|
|
581
|
+
"
|
|
582
|
+
>
|
|
583
|
+
<TimecodeTreeDisplay
|
|
584
|
+
id={id}
|
|
585
|
+
timecode={timecode}
|
|
586
|
+
assignToOutput={null}
|
|
587
|
+
buttons={null}
|
|
588
|
+
{...instanceConfig}
|
|
589
|
+
/>
|
|
590
|
+
</div>
|
|
591
|
+
);
|
|
592
|
+
};
|