@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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +55 -0
  2. package/CHANGELOG.md +24 -0
  3. package/eslint.config.mjs +49 -0
  4. package/package.json +74 -0
  5. package/src/app.tsx +147 -0
  6. package/src/components/backend/index.ts +6 -0
  7. package/src/components/backend/toolbox-root.ts +119 -0
  8. package/src/components/frontend/constants.ts +81 -0
  9. package/src/components/frontend/entrypoint.ts +12 -0
  10. package/src/components/frontend/frontend.css +108 -0
  11. package/src/components/frontend/index.tsx +46 -0
  12. package/src/components/frontend/toolbox/content.tsx +45 -0
  13. package/src/components/frontend/toolbox/context.tsx +63 -0
  14. package/src/components/frontend/toolbox/core/size-aware-div.tsx +51 -0
  15. package/src/components/frontend/toolbox/core/timecode-display.tsx +592 -0
  16. package/src/components/frontend/toolbox/generators.tsx +318 -0
  17. package/src/components/frontend/toolbox/inputs.tsx +484 -0
  18. package/src/components/frontend/toolbox/outputs.tsx +581 -0
  19. package/src/components/frontend/toolbox/preferences.ts +25 -0
  20. package/src/components/frontend/toolbox/root.tsx +335 -0
  21. package/src/components/frontend/toolbox/settings.tsx +54 -0
  22. package/src/components/frontend/toolbox/types.ts +28 -0
  23. package/src/components/frontend/toolbox/util.tsx +61 -0
  24. package/src/components/proto.ts +420 -0
  25. package/src/config.ts +7 -0
  26. package/src/generators/clock.tsx +206 -0
  27. package/src/generators/index.tsx +15 -0
  28. package/src/index.ts +38 -0
  29. package/src/inputs/artnet.tsx +305 -0
  30. package/src/inputs/index.tsx +13 -0
  31. package/src/inputs/tcnet.tsx +272 -0
  32. package/src/outputs/artnet.tsx +170 -0
  33. package/src/outputs/index.tsx +11 -0
  34. package/src/start.ts +47 -0
  35. package/src/tree.ts +133 -0
  36. package/src/types.ts +12 -0
  37. package/src/urls.ts +49 -0
  38. package/src/util.ts +82 -0
  39. package/tailwind.config.cjs +7 -0
  40. package/tsconfig.json +10 -0
  41. package/tsup.config.ts +10 -0
@@ -0,0 +1,335 @@
1
+ import { Debugger } from '@arcanewizards/sigil/frontend';
2
+ import {
3
+ FC,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useState,
9
+ } from 'react';
10
+ import {
11
+ TIMECODE_INSTANCE_ID,
12
+ TimecodeToolboxComponentCalls,
13
+ ToolboxConfig,
14
+ ToolboxRootComponent,
15
+ ToolboxRootConfigUpdate,
16
+ } from '../../proto';
17
+ import {
18
+ ToolbarDivider,
19
+ ToolbarRow,
20
+ ToolbarWrapper,
21
+ } from '@arcanewizards/sigil/frontend/toolbars';
22
+ import { ControlButton } from '@arcanewizards/sigil/frontend/controls';
23
+ import { ExternalLink } from './content';
24
+ import { STRINGS } from '../constants';
25
+ import { OutputSettingsDialog, OutputsSection } from './outputs';
26
+ import { GeneratorSettingsDialog, GeneratorsSection } from './generators';
27
+ import { InputSettingsDialog, InputsSection } from './inputs';
28
+ import { diffJson } from '@arcanejs/diff';
29
+ import { StageContext } from '@arcanejs/toolkit-frontend';
30
+ import {
31
+ ApplicationHandlersContext,
32
+ ApplicationHandlersContextData,
33
+ ApplicationStateContext,
34
+ ConfigContext,
35
+ ConfigContextData,
36
+ NetworkContext,
37
+ } from './context';
38
+ import { AssignToOutputCallback, DialogMode } from './types';
39
+ import { Settings } from './settings';
40
+ import { useBrowserPreferences } from './preferences';
41
+ import { useRootHintVariables } from '@arcanewizards/sigil/frontend/styling';
42
+ import { SizeAwareDiv } from './core/size-aware-div';
43
+ import { Icon } from '@arcanejs/toolkit-frontend/components/core';
44
+ import { getFragmentValue } from '../../../urls';
45
+ import { FullscreenTimecodeDisplay } from './core/timecode-display';
46
+
47
+ type Props = {
48
+ info: ToolboxRootComponent;
49
+ };
50
+
51
+ export const ToolboxRoot: FC<Props> = ({ info }) => {
52
+ const [windowMode, setWindowMode] = useState<'debug' | 'settings' | null>(
53
+ null,
54
+ );
55
+ const { config } = info;
56
+ const { sendMessage, call, connection, reconnect } = useContext(StageContext);
57
+ const [dialogMode, setDialogMode] = useState<DialogMode | null>(null);
58
+
59
+ const [assignToOutput, setAssignToOutput] = useState<string | null>(null);
60
+
61
+ const { preferences } = useBrowserPreferences();
62
+
63
+ useRootHintVariables(preferences.color);
64
+
65
+ useEffect(() => {
66
+ if (assignToOutput) {
67
+ const onEscape = (e: KeyboardEvent) => {
68
+ if (e.key === 'Escape') {
69
+ setAssignToOutput(null);
70
+ }
71
+ };
72
+ window.addEventListener('keydown', onEscape);
73
+ return () => {
74
+ window.removeEventListener('keydown', onEscape);
75
+ };
76
+ }
77
+ }, [assignToOutput]);
78
+
79
+ const updateConfig = useCallback(
80
+ (change: (current: ToolboxConfig) => ToolboxConfig) => {
81
+ const diff = diffJson(config, change(config));
82
+ sendMessage?.<ToolboxRootConfigUpdate>({
83
+ type: 'component-message',
84
+ namespace: 'timecode-toolbox',
85
+ component: 'toolbox-root',
86
+ componentKey: info.key,
87
+ action: 'update-config',
88
+ diff,
89
+ });
90
+ },
91
+ [sendMessage, info.key, config],
92
+ );
93
+
94
+ const configContext: ConfigContextData = useMemo(
95
+ () => ({
96
+ config,
97
+ updateConfig,
98
+ }),
99
+ [config, updateConfig],
100
+ );
101
+
102
+ const closeDialog = useCallback(() => setDialogMode(null), []);
103
+
104
+ const getNetworkInterfaces = useCallback(async () => {
105
+ if (!call) {
106
+ throw new Error('No call function available');
107
+ }
108
+ return call<
109
+ 'timecode-toolbox',
110
+ TimecodeToolboxComponentCalls,
111
+ 'toolbox-root-get-network-interfaces'
112
+ >({
113
+ namespace: 'timecode-toolbox',
114
+ type: 'component-call',
115
+ componentKey: info.key,
116
+ action: 'toolbox-root-get-network-interfaces',
117
+ });
118
+ }, [call, info.key]);
119
+
120
+ const networkContextValue = useMemo(() => {
121
+ return {
122
+ getNetworkInterfaces,
123
+ };
124
+ }, [getNetworkInterfaces]);
125
+
126
+ const assignToOutputCallback: AssignToOutputCallback = useMemo(() => {
127
+ if (!assignToOutput) {
128
+ return null;
129
+ }
130
+ const outputUuid = assignToOutput;
131
+ return (id) => {
132
+ updateConfig((current) => {
133
+ const output = current.outputs[outputUuid];
134
+ if (!output) {
135
+ return current;
136
+ }
137
+ return {
138
+ ...current,
139
+ outputs: {
140
+ ...current.outputs,
141
+ [outputUuid]: {
142
+ ...output,
143
+ link: id,
144
+ },
145
+ },
146
+ };
147
+ });
148
+ setAssignToOutput(null);
149
+ };
150
+ }, [assignToOutput, updateConfig]);
151
+
152
+ const callHandler: ApplicationHandlersContextData['callHandler'] =
153
+ useCallback(
154
+ async ({ path, handler, args }) => {
155
+ if (!call) {
156
+ throw new Error('No call function available');
157
+ }
158
+ return call<
159
+ 'timecode-toolbox',
160
+ TimecodeToolboxComponentCalls,
161
+ 'toolbox-root-call-handler'
162
+ >({
163
+ namespace: 'timecode-toolbox',
164
+ type: 'component-call',
165
+ componentKey: info.key,
166
+ action: 'toolbox-root-call-handler',
167
+ handler,
168
+ path,
169
+ args,
170
+ });
171
+ },
172
+ [call, info.key],
173
+ );
174
+
175
+ const handlers: ApplicationHandlersContextData = useMemo(
176
+ () => ({
177
+ handlers: info.handlers,
178
+ callHandler,
179
+ }),
180
+ [info.handlers, callHandler],
181
+ );
182
+
183
+ const windowedTimecodeId = useMemo(
184
+ () => getFragmentValue('tc', TIMECODE_INSTANCE_ID),
185
+ [],
186
+ );
187
+
188
+ const isMainWindow = windowedTimecodeId === null;
189
+
190
+ const root = useMemo(
191
+ () => (
192
+ <div className="flex h-screen flex-col">
193
+ <ToolbarWrapper>
194
+ <ToolbarRow>
195
+ <div
196
+ className="
197
+ flex h-full min-h-[36px] grow items-center justify-center px-1
198
+ app-title-bar
199
+ "
200
+ >
201
+ <span className="font-bold text-hint-gradient">
202
+ {STRINGS.title}
203
+ </span>
204
+ </div>
205
+ {isMainWindow && (
206
+ <>
207
+ <ToolbarDivider />
208
+ <ControlButton
209
+ onClick={() =>
210
+ setWindowMode((mode) =>
211
+ mode === 'settings' ? null : 'settings',
212
+ )
213
+ }
214
+ variant="titlebar"
215
+ icon="settings"
216
+ active={windowMode === 'settings'}
217
+ title={STRINGS.toggle(STRINGS.settings.title)}
218
+ />
219
+ <ControlButton
220
+ onClick={() =>
221
+ setWindowMode((mode) => (mode === 'debug' ? null : 'debug'))
222
+ }
223
+ variant="titlebar"
224
+ icon="bug_report"
225
+ active={windowMode === 'debug'}
226
+ title={STRINGS.toggle(STRINGS.debugger)}
227
+ />
228
+ </>
229
+ )}
230
+ </ToolbarRow>
231
+ </ToolbarWrapper>
232
+ <div className="relative flex h-0 grow flex-col">
233
+ {connection.state !== 'connected' ? (
234
+ <SizeAwareDiv
235
+ className="
236
+ flex grow flex-col items-center justify-center gap-1
237
+ bg-sigil-bg-light p-1 text-sigil-foreground-muted
238
+ "
239
+ >
240
+ <Icon icon="signal_disconnected" className="text-block-icon" />
241
+ <div className="text-center">{STRINGS.connectionError}</div>
242
+ <ControlButton onClick={reconnect} variant="large" icon="replay">
243
+ {STRINGS.reconnect}
244
+ </ControlButton>
245
+ </SizeAwareDiv>
246
+ ) : windowMode === 'debug' ? (
247
+ <Debugger title={STRINGS.debugger} className="size-full" />
248
+ ) : windowMode === 'settings' ? (
249
+ <Settings setWindowMode={setWindowMode} />
250
+ ) : windowedTimecodeId ? (
251
+ <FullscreenTimecodeDisplay id={windowedTimecodeId} />
252
+ ) : (
253
+ <div
254
+ className="
255
+ flex h-0 grow flex-col gap-px overflow-y-auto bg-sigil-border
256
+ scrollbar-sigil
257
+ "
258
+ >
259
+ <InputsSection
260
+ setDialogMode={setDialogMode}
261
+ assignToOutput={assignToOutputCallback}
262
+ />
263
+ <GeneratorsSection
264
+ setDialogMode={setDialogMode}
265
+ assignToOutput={assignToOutputCallback}
266
+ />
267
+ <OutputsSection
268
+ setDialogMode={setDialogMode}
269
+ assignToOutput={assignToOutput}
270
+ setAssignToOutput={setAssignToOutput}
271
+ />
272
+ </div>
273
+ )}
274
+ {dialogMode?.section.type === 'inputs' && (
275
+ <InputSettingsDialog
276
+ close={closeDialog}
277
+ input={dialogMode.section.input}
278
+ target={dialogMode.target}
279
+ />
280
+ )}
281
+ {dialogMode?.section.type === 'generators' && (
282
+ <GeneratorSettingsDialog
283
+ close={closeDialog}
284
+ generator={dialogMode.section.generator}
285
+ target={dialogMode.target}
286
+ />
287
+ )}
288
+ {dialogMode?.section.type === 'outputs' && (
289
+ <OutputSettingsDialog
290
+ close={closeDialog}
291
+ output={dialogMode.section.output}
292
+ target={dialogMode.target}
293
+ />
294
+ )}
295
+ </div>
296
+ {isMainWindow && (
297
+ <div
298
+ className="
299
+ flex justify-center border-t border-sigil-border bg-sigil-bg-dark
300
+ p-1 text-[80%]
301
+ "
302
+ >
303
+ {'Created by'}&nbsp;
304
+ <ExternalLink href="https://arcanewizards.com">
305
+ Arcane Wizards
306
+ </ExternalLink>
307
+ </div>
308
+ )}
309
+ </div>
310
+ ),
311
+ [
312
+ connection,
313
+ reconnect,
314
+ assignToOutput,
315
+ assignToOutputCallback,
316
+ closeDialog,
317
+ dialogMode,
318
+ windowMode,
319
+ isMainWindow,
320
+ windowedTimecodeId,
321
+ ],
322
+ );
323
+
324
+ return (
325
+ <ConfigContext.Provider value={configContext}>
326
+ <NetworkContext.Provider value={networkContextValue}>
327
+ <ApplicationStateContext.Provider value={info.state}>
328
+ <ApplicationHandlersContext.Provider value={handlers}>
329
+ {root}
330
+ </ApplicationHandlersContext.Provider>
331
+ </ApplicationStateContext.Provider>
332
+ </NetworkContext.Provider>
333
+ </ConfigContext.Provider>
334
+ );
335
+ };
@@ -0,0 +1,54 @@
1
+ import { FC } from 'react';
2
+
3
+ import { AppearanceSwitcher } from '@arcanewizards/sigil/frontend/appearance';
4
+ import { useBrowserPreferences } from './preferences';
5
+ import {
6
+ ControlButton,
7
+ ControlLabel,
8
+ } from '@arcanewizards/sigil/frontend/controls';
9
+ import {
10
+ ToolbarDivider,
11
+ ToolbarRow,
12
+ ToolbarWrapper,
13
+ } from '@arcanewizards/sigil/frontend/toolbars';
14
+ import { STRINGS } from '../constants';
15
+
16
+ type SettingsProps = {
17
+ setWindowMode: (mode: null) => void;
18
+ };
19
+
20
+ export const Settings: FC<SettingsProps> = ({ setWindowMode }) => {
21
+ const { preferences, updateBrowserPrefs } = useBrowserPreferences();
22
+
23
+ return (
24
+ <div className="flex grow flex-col">
25
+ <ToolbarWrapper>
26
+ <ToolbarRow>
27
+ <span className="grow p-1">{STRINGS.settings.title}</span>
28
+ <ToolbarDivider />
29
+ <ControlButton
30
+ onClick={() => setWindowMode(null)}
31
+ variant="titlebar"
32
+ icon="close"
33
+ title={STRINGS.close(STRINGS.settings.title)}
34
+ />
35
+ </ToolbarRow>
36
+ </ToolbarWrapper>
37
+ <div
38
+ className="
39
+ grow basis-0 overflow-y-auto bg-sigil-bg-light scrollbar-sigil
40
+ "
41
+ >
42
+ <div className="control-grid-large">
43
+ <ControlLabel>Appearance</ControlLabel>
44
+ <AppearanceSwitcher
45
+ color={preferences.color}
46
+ onColorChange={(color) =>
47
+ updateBrowserPrefs((current) => ({ ...current, color }))
48
+ }
49
+ />
50
+ </div>
51
+ </div>
52
+ </div>
53
+ );
54
+ };
@@ -0,0 +1,28 @@
1
+ import {
2
+ GeneratorDefinition,
3
+ InputDefinition,
4
+ InputOrGenInstance,
5
+ OutputDefinition,
6
+ } from '../../proto';
7
+
8
+ export type DialogMode = {
9
+ section:
10
+ | { type: 'inputs'; input: InputDefinition['type'] }
11
+ | { type: 'generators'; generator: GeneratorDefinition['type'] }
12
+ | { type: 'outputs'; output: OutputDefinition['type'] };
13
+ target:
14
+ | {
15
+ type: 'add';
16
+ }
17
+ | {
18
+ type: 'edit';
19
+ uuid: string;
20
+ };
21
+ };
22
+
23
+ export type SettingsProps<T> = {
24
+ data: T;
25
+ updateSettings: (change: (current: T) => T) => void;
26
+ };
27
+
28
+ export type AssignToOutputCallback = ((id: InputOrGenInstance) => void) | null;
@@ -0,0 +1,61 @@
1
+ import { FC, ReactNode } from 'react';
2
+
3
+ type PrimaryToolboxSectionProps = {
4
+ title: string;
5
+ children: ReactNode;
6
+ buttons: ReactNode;
7
+ };
8
+
9
+ export const PrimaryToolboxSection: FC<PrimaryToolboxSectionProps> = ({
10
+ title,
11
+ children,
12
+ buttons,
13
+ }) => {
14
+ return (
15
+ <div className="flex grow gap-px">
16
+ <div
17
+ className="
18
+ flex items-center justify-center bg-sigil-bg-light p-1
19
+ writing-mode-vertical-rl
20
+ "
21
+ >
22
+ {title}
23
+ </div>
24
+ <div className="flex grow flex-col gap-px">
25
+ <div className="flex grow flex-col gap-px">{children}</div>
26
+ <div className="flex w-full flex-wrap gap-1 bg-sigil-bg-light p-1">
27
+ {buttons}
28
+ </div>
29
+ </div>
30
+ </div>
31
+ );
32
+ };
33
+ /**
34
+ * Display the given number of milliseconds in a nice format to the user
35
+ */
36
+ export function displayMillis(totalMilliseconds: number): string {
37
+ if (totalMilliseconds < 0) {
38
+ return '-' + displayMillis(-totalMilliseconds);
39
+ }
40
+ let remaining = totalMilliseconds;
41
+ const hours = (remaining / 3600000) | 0;
42
+ remaining -= hours * 3600000;
43
+ const mins = (remaining / 60000) | 0;
44
+ remaining -= mins * 60000;
45
+ const seconds = (remaining / 1000) | 0;
46
+ remaining -= seconds * 1000;
47
+ const millis = remaining | 0;
48
+ return (
49
+ (hours < 10 ? '0' : '') +
50
+ hours +
51
+ ':' +
52
+ (mins < 10 ? '0' : '') +
53
+ mins +
54
+ ':' +
55
+ (seconds < 10 ? '0' : '') +
56
+ seconds +
57
+ ':' +
58
+ (millis < 10 ? '00' : millis < 100 ? '0' : '') +
59
+ millis
60
+ );
61
+ }