@fcannizzaro/streamdeck-react 0.1.10 → 0.1.12

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 (76) hide show
  1. package/LICENSE +190 -21
  2. package/README.md +2 -0
  3. package/dist/action.d.ts +2 -2
  4. package/dist/action.js +1 -2
  5. package/dist/adapter/index.d.ts +2 -0
  6. package/dist/adapter/physical-device.d.ts +2 -0
  7. package/dist/adapter/physical-device.js +153 -0
  8. package/dist/adapter/types.d.ts +127 -0
  9. package/dist/bundler-shared.d.ts +11 -0
  10. package/dist/bundler-shared.js +11 -0
  11. package/dist/context/event-bus.d.ts +1 -1
  12. package/dist/context/event-bus.js +1 -1
  13. package/dist/context/touchstrip-context.d.ts +2 -0
  14. package/dist/context/touchstrip-context.js +5 -0
  15. package/dist/devtools/bridge.d.ts +35 -7
  16. package/dist/devtools/bridge.js +152 -46
  17. package/dist/devtools/highlight.d.ts +5 -0
  18. package/dist/devtools/highlight.js +107 -57
  19. package/dist/devtools/index.js +6 -0
  20. package/dist/devtools/observers/lifecycle.d.ts +4 -4
  21. package/dist/devtools/server.d.ts +6 -1
  22. package/dist/devtools/server.js +6 -1
  23. package/dist/devtools/types.d.ts +50 -6
  24. package/dist/font-inline.d.ts +5 -1
  25. package/dist/font-inline.js +8 -3
  26. package/dist/hooks/animation.d.ts +154 -0
  27. package/dist/hooks/animation.js +381 -0
  28. package/dist/hooks/events.js +2 -6
  29. package/dist/hooks/sdk.js +11 -11
  30. package/dist/hooks/touchstrip.d.ts +6 -0
  31. package/dist/hooks/touchstrip.js +37 -0
  32. package/dist/hooks/utility.js +3 -2
  33. package/dist/index.d.ts +9 -2
  34. package/dist/index.js +4 -2
  35. package/dist/manifest-codegen.d.ts +38 -0
  36. package/dist/manifest-codegen.js +110 -0
  37. package/dist/plugin.js +86 -106
  38. package/dist/reconciler/host-config.js +19 -1
  39. package/dist/reconciler/vnode.d.ts +26 -2
  40. package/dist/reconciler/vnode.js +40 -10
  41. package/dist/render/buffer-pool.d.ts +19 -0
  42. package/dist/render/buffer-pool.js +51 -0
  43. package/dist/render/cache.d.ts +29 -0
  44. package/dist/render/cache.js +137 -5
  45. package/dist/render/image-cache.d.ts +54 -0
  46. package/dist/render/image-cache.js +144 -0
  47. package/dist/render/metrics.d.ts +57 -0
  48. package/dist/render/metrics.js +98 -0
  49. package/dist/render/pipeline.d.ts +36 -1
  50. package/dist/render/pipeline.js +304 -34
  51. package/dist/render/png.d.ts +1 -1
  52. package/dist/render/png.js +26 -11
  53. package/dist/render/render-pool.d.ts +24 -0
  54. package/dist/render/render-pool.js +130 -0
  55. package/dist/render/svg.d.ts +7 -0
  56. package/dist/render/svg.js +139 -0
  57. package/dist/render/worker.d.ts +1 -0
  58. package/dist/rollup.d.ts +23 -9
  59. package/dist/rollup.js +24 -9
  60. package/dist/roots/registry.d.ts +9 -11
  61. package/dist/roots/registry.js +39 -42
  62. package/dist/roots/root.d.ts +9 -6
  63. package/dist/roots/root.js +52 -29
  64. package/dist/roots/settings-equality.d.ts +5 -0
  65. package/dist/roots/settings-equality.js +24 -0
  66. package/dist/roots/{touchbar-root.d.ts → touchstrip-root.d.ts} +30 -8
  67. package/dist/roots/touchstrip-root.js +263 -0
  68. package/dist/types.d.ts +73 -23
  69. package/dist/vite.d.ts +22 -8
  70. package/dist/vite.js +24 -8
  71. package/package.json +7 -4
  72. package/dist/context/touchbar-context.d.ts +0 -2
  73. package/dist/context/touchbar-context.js +0 -5
  74. package/dist/hooks/touchbar.d.ts +0 -6
  75. package/dist/hooks/touchbar.js +0 -37
  76. package/dist/roots/touchbar-root.js +0 -175
package/dist/types.d.ts CHANGED
@@ -1,6 +1,18 @@
1
1
  import { ComponentType, ReactNode } from 'react';
2
- import { Action, Controller, Coordinates, DeviceType, DialAction, KeyAction, Size } from '@elgato/streamdeck';
3
2
  import { JsonObject, JsonValue } from '@elgato/utils';
3
+ import { AdapterActionHandle, StreamDeckAdapter } from './adapter/types';
4
+ /** Controller surface type. */
5
+ export type Controller = "Keypad" | "Encoder";
6
+ /** Grid coordinates for a key or encoder on a device. */
7
+ export interface Coordinates {
8
+ column: number;
9
+ row: number;
10
+ }
11
+ /** Device grid size (number of key columns and rows). */
12
+ export interface Size {
13
+ columns: number;
14
+ rows: number;
15
+ }
4
16
  export interface FontConfig {
5
17
  name: string;
6
18
  data: ArrayBuffer | Buffer;
@@ -62,41 +74,80 @@ export interface TouchStripLayout {
62
74
  }
63
75
  export type EncoderLayout = string | TouchStripLayout;
64
76
  export interface PluginConfig {
77
+ /** Stream Deck adapter. Defaults to physicalDevice() (Elgato SDK). */
78
+ adapter?: StreamDeckAdapter;
65
79
  fonts: FontConfig[];
66
80
  actions: ActionDefinition[];
67
81
  wrapper?: WrapperComponent;
68
- renderDebounceMs?: number;
69
82
  imageFormat?: "png" | "webp";
70
83
  caching?: boolean;
71
84
  devicePixelRatio?: number;
72
85
  onActionError?: (uuid: string, actionId: string, error: Error) => void;
73
86
  /** Enable the devtools WebSocket server. Browser devtools UI discovers it via port scanning. @default false */
74
87
  devtools?: boolean;
88
+ /** Enable performance diagnostics (render counters, duplicate detection, depth warnings). Defaults to `process.env.NODE_ENV !== 'production'`. */
89
+ debug?: boolean;
90
+ /** Maximum image cache size in bytes for key/dial renders. Set to 0 to disable. @default 16777216 (16 MB) */
91
+ imageCacheMaxBytes?: number;
92
+ /** Maximum TouchStrip raw buffer cache size in bytes. Set to 0 to disable. @default 8388608 (8 MB) */
93
+ touchStripCacheMaxBytes?: number;
94
+ /** Offload Takumi rendering to a worker thread. Set to false to disable. @default true */
95
+ useWorker?: boolean;
75
96
  }
76
97
  export interface Plugin {
77
98
  connect(): Promise<void>;
78
99
  }
100
+ export interface ManifestActions {
101
+ }
102
+ /** Action UUID — a union of manifest UUIDs when available, plain `string` otherwise. */
103
+ export type ActionUUID = [keyof ManifestActions] extends [never] ? string : Extract<keyof ManifestActions, string>;
104
+ type HasController<UUID extends string, C extends string> = UUID extends keyof ManifestActions ? ManifestActions[UUID] extends {
105
+ controllers: readonly (infer Item)[];
106
+ } ? C extends Item ? true : false : false : false;
107
+ type KeySurface<UUID extends string> = HasController<UUID, "Keypad"> extends true ? {
108
+ key: ComponentType;
109
+ } : {
110
+ key?: ComponentType;
111
+ };
112
+ type EncoderSurface<UUID extends string> = HasController<UUID, "Encoder"> extends true ? {
113
+ dial: ComponentType;
114
+ touchStrip?: ComponentType;
115
+ } | {
116
+ dial?: ComponentType;
117
+ touchStrip: ComponentType;
118
+ } : {
119
+ dial?: ComponentType;
120
+ touchStrip?: ComponentType;
121
+ };
79
122
  export interface ActionConfig<S extends JsonObject = JsonObject> {
80
123
  uuid: string;
81
124
  key?: ComponentType;
82
125
  dial?: ComponentType;
83
- /** Full-strip touchbar component. When set, replaces per-encoder `dial` display with a single shared React tree that spans the entire touch strip. */
84
- touchBar?: ComponentType;
85
- /** Target frame rate for the touchbar animation loop and render pipeline. Controls both `useTick` cadence (via `useTouchBar().fps`) and the render debounce. @default 60 */
86
- touchBarFPS?: number;
126
+ /** Full-strip TouchStrip component. When set, replaces per-encoder `dial` display with a single shared React tree that spans the entire touch strip. */
127
+ touchStrip?: ComponentType;
87
128
  /** Encoder feedback layout. Defaults to a full-width `pixmap` canvas layout. Custom layouts should include a `pixmap` item keyed as `canvas`. */
88
129
  dialLayout?: EncoderLayout;
89
130
  wrapper?: WrapperComponent;
90
131
  defaultSettings?: Partial<S>;
91
132
  }
133
+ /** Resolved action config shape. When `ManifestActions` is populated (via `streamdeck-env.d.ts`), this becomes a mapped type that iterates over every UUID in the manifest and produces a discriminated union. Each member intersects `KeySurface<UUID>` and `EncoderSurface<UUID>` to enforce controller-specific requirements. When `ManifestActions` is empty, it falls back to the permissive `ActionConfig<S>`. */
134
+ export type ActionConfigInput<S extends JsonObject = JsonObject> = [keyof ManifestActions] extends [
135
+ never
136
+ ] ? ActionConfig<S> : {
137
+ [UUID in ActionUUID]: {
138
+ uuid: UUID;
139
+ /** Encoder feedback layout. Defaults to a full-width `pixmap` canvas layout. Custom layouts should include a `pixmap` item keyed as `canvas`. */
140
+ dialLayout?: EncoderLayout;
141
+ wrapper?: WrapperComponent;
142
+ defaultSettings?: Partial<S>;
143
+ } & KeySurface<UUID> & EncoderSurface<UUID>;
144
+ }[ActionUUID];
92
145
  export interface ActionDefinition<S extends JsonObject = JsonObject> {
93
146
  uuid: string;
94
147
  key?: ComponentType;
95
148
  dial?: ComponentType;
96
- /** Full-strip touchbar component. When set, replaces per-encoder `dial` display with a single shared React tree that spans the entire touch strip. */
97
- touchBar?: ComponentType;
98
- /** Target frame rate for the touchbar animation loop and render pipeline. @default 60 */
99
- touchBarFPS?: number;
149
+ /** Full-strip TouchStrip component. When set, replaces per-encoder `dial` display with a single shared React tree that spans the entire touch strip. */
150
+ touchStrip?: ComponentType;
100
151
  /** Encoder feedback layout. Defaults to a full-width `pixmap` canvas layout. Custom layouts should include a `pixmap` item keyed as `canvas`. */
101
152
  dialLayout?: EncoderLayout;
102
153
  wrapper?: WrapperComponent;
@@ -104,7 +155,8 @@ export interface ActionDefinition<S extends JsonObject = JsonObject> {
104
155
  }
105
156
  export interface DeviceInfo {
106
157
  id: string;
107
- type: DeviceType;
158
+ /** Numeric device type matching Elgato DeviceType enum values (e.g. 7 = StreamDeckPlus). */
159
+ type: number;
108
160
  size: Size;
109
161
  name: string;
110
162
  }
@@ -121,8 +173,8 @@ export interface CanvasInfo {
121
173
  type: "key" | "dial" | "touch";
122
174
  }
123
175
  export interface StreamDeckAccess {
124
- action: Action | DialAction | KeyAction;
125
- sdk: typeof import('@elgato/streamdeck').default;
176
+ action: AdapterActionHandle;
177
+ adapter: StreamDeckAdapter;
126
178
  }
127
179
  export interface KeyDownPayload {
128
180
  settings: JsonObject;
@@ -161,7 +213,7 @@ export interface DialHints {
161
213
  touch?: string;
162
214
  longTouch?: string;
163
215
  }
164
- export interface TouchBarInfo {
216
+ export interface TouchStripInfo {
165
217
  /** Full render width in pixels (e.g., 800 for 4 encoders). */
166
218
  width: number;
167
219
  /** Strip height in pixels (always 100). */
@@ -170,22 +222,20 @@ export interface TouchBarInfo {
170
222
  columns: number[];
171
223
  /** Width of each encoder segment in pixels (always 200). */
172
224
  segmentWidth: number;
173
- /** Target frame rate for the animation loop. Pass to `useTick` for matched cadence. */
174
- fps: number;
175
225
  }
176
- export interface TouchBarTapPayload {
226
+ export interface TouchStripTapPayload {
177
227
  /** Absolute tap position across the full strip width. */
178
228
  tapPos: [x: number, y: number];
179
229
  hold: boolean;
180
230
  /** The encoder column that was touched. */
181
231
  column: number;
182
232
  }
183
- export interface TouchBarDialRotatePayload {
233
+ export interface TouchStripDialRotatePayload {
184
234
  column: number;
185
235
  ticks: number;
186
236
  pressed: boolean;
187
237
  }
188
- export interface TouchBarDialPressPayload {
238
+ export interface TouchStripDialPressPayload {
189
239
  column: number;
190
240
  }
191
241
  export interface EventMap {
@@ -205,9 +255,9 @@ export interface EventMap {
205
255
  title: string;
206
256
  settings: JsonObject;
207
257
  };
208
- touchBarTap: TouchBarTapPayload;
209
- touchBarDialRotate: TouchBarDialRotatePayload;
210
- touchBarDialDown: TouchBarDialPressPayload;
211
- touchBarDialUp: TouchBarDialPressPayload;
258
+ touchStripTap: TouchStripTapPayload;
259
+ touchStripDialRotate: TouchStripDialRotatePayload;
260
+ touchStripDialDown: TouchStripDialPressPayload;
261
+ touchStripDialUp: TouchStripDialPressPayload;
212
262
  }
213
263
  export {};
package/dist/vite.d.ts CHANGED
@@ -10,17 +10,31 @@ export interface StreamDeckReactOptions extends StreamDeckTargetOptions {
10
10
  * each successful build.
11
11
  */
12
12
  uuid?: string;
13
+ /**
14
+ * Path to the plugin `manifest.json`. When omitted, the plugin
15
+ * auto-detects by scanning the project root for a `*.sdPlugin/manifest.json`.
16
+ *
17
+ * Set to `false` to disable manifest type generation entirely.
18
+ */
19
+ manifest?: string | false;
13
20
  }
14
21
  /**
15
22
  * Vite plugin for Stream Deck React projects.
16
23
  *
17
- * - Inlines font files (`.ttf`, `.otf`, `.woff`, `.woff2`) imported by the
18
- * project into the bundle as `Buffer` instances so no runtime filesystem
19
- * access is needed.
20
- * - Copies platform-specific `@takumi-rs/core` native bindings (`.node` files)
21
- * into the bundle output directory.
22
- * - Strips devtools code in production builds (non-watch mode).
23
- * - Optionally restarts the Stream Deck plugin after each build when
24
- * {@link StreamDeckReactOptions.uuid} is provided.
24
+ * Responsibilities mapped to Vite lifecycle hooks:
25
+ *
26
+ * configResolved → detect dev/production mode, set strip flags
27
+ * buildStart → generate streamdeck-env.d.ts from manifest.json
28
+ * resolveId → redirect devtools imports (production) + font imports
29
+ * load → return noop devtools stub + inline font as base64 Buffer
30
+ * writeBundle → copy native .node bindings to output directory
31
+ * closeBundle → restart Stream Deck plugin (optional, via CLI)
32
+ *
33
+ * Font inlining:
34
+ * `.ttf`, `.otf`, `.woff`, `.woff2` imports are resolved to absolute
35
+ * paths and loaded as synthetic ES modules:
36
+ * `export default Buffer.from("<base64>", "base64");`
37
+ * This eliminates runtime filesystem access in the sandboxed
38
+ * Stream Deck Node.js environment.
25
39
  */
26
40
  export declare function streamDeckReact(options?: StreamDeckReactOptions): Plugin;
package/dist/vite.js CHANGED
@@ -1,19 +1,27 @@
1
1
  import { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_ID, copyNativeBindings, isLibraryDevtoolsImport, shouldStripDevtools } from "./bundler-shared.js";
2
2
  import { loadFont, resolveFontId } from "./font-inline.js";
3
+ import { generateManifestTypes } from "./manifest-codegen.js";
3
4
  import { resolve } from "node:path";
4
5
  import { exec } from "node:child_process";
5
6
  //#region src/vite.ts
6
7
  /**
7
8
  * Vite plugin for Stream Deck React projects.
8
9
  *
9
- * - Inlines font files (`.ttf`, `.otf`, `.woff`, `.woff2`) imported by the
10
- * project into the bundle as `Buffer` instances so no runtime filesystem
11
- * access is needed.
12
- * - Copies platform-specific `@takumi-rs/core` native bindings (`.node` files)
13
- * into the bundle output directory.
14
- * - Strips devtools code in production builds (non-watch mode).
15
- * - Optionally restarts the Stream Deck plugin after each build when
16
- * {@link StreamDeckReactOptions.uuid} is provided.
10
+ * Responsibilities mapped to Vite lifecycle hooks:
11
+ *
12
+ * configResolved → detect dev/production mode, set strip flags
13
+ * buildStart → generate streamdeck-env.d.ts from manifest.json
14
+ * resolveId → redirect devtools imports (production) + font imports
15
+ * load → return noop devtools stub + inline font as base64 Buffer
16
+ * writeBundle → copy native .node bindings to output directory
17
+ * closeBundle → restart Stream Deck plugin (optional, via CLI)
18
+ *
19
+ * Font inlining:
20
+ * `.ttf`, `.otf`, `.woff`, `.woff2` imports are resolved to absolute
21
+ * paths and loaded as synthetic ES modules:
22
+ * `export default Buffer.from("<base64>", "base64");`
23
+ * This eliminates runtime filesystem access in the sandboxed
24
+ * Stream Deck Node.js environment.
17
25
  */
18
26
  function streamDeckReact(options = {}) {
19
27
  let resolvedConfig;
@@ -29,6 +37,14 @@ function streamDeckReact(options = {}) {
29
37
  isDevelopment = isWatch || process.env.NODE_ENV === "development";
30
38
  stripDevtools = shouldStripDevtools(isWatch);
31
39
  },
40
+ buildStart() {
41
+ const warn = (msg) => resolvedConfig.logger.warn(msg);
42
+ const result = generateManifestTypes(resolvedConfig.root, options.manifest, warn);
43
+ if (result) {
44
+ this.addWatchFile(result.manifestPath);
45
+ if (result.written) resolvedConfig.logger.info("[@fcannizzaro/streamdeck-react] Generated src/streamdeck-env.d.ts");
46
+ }
47
+ },
32
48
  resolveId(source, importer) {
33
49
  if (stripDevtools && isLibraryDevtoolsImport(source, importer)) return NOOP_DEVTOOLS_ID;
34
50
  return resolveFontId(source, importer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fcannizzaro/streamdeck-react",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Build Stream Deck plugins with React — render components directly to keys, dials, and touch screens",
5
5
  "keywords": [
6
6
  "elgato",
@@ -13,7 +13,7 @@
13
13
  "bugs": {
14
14
  "url": "https://github.com/fcannizzaro/streamdeck-react/issues"
15
15
  },
16
- "license": "MIT",
16
+ "license": "Apache-2.0",
17
17
  "author": {
18
18
  "name": "Francesco Saverio Cannizzaro",
19
19
  "url": "https://github.com/fcannizzaro"
@@ -56,11 +56,10 @@
56
56
  },
57
57
  "scripts": {
58
58
  "build": "vite build",
59
- "check-types": "tsc --noEmit",
59
+ "typecheck": "tsc --noEmit",
60
60
  "test": "bun test ./src"
61
61
  },
62
62
  "dependencies": {
63
- "@elgato/streamdeck": "^2.0.2",
64
63
  "@takumi-rs/core": "^0.71.7",
65
64
  "@takumi-rs/helpers": "^0.71.7",
66
65
  "react-reconciler": "^0.33.0"
@@ -76,12 +75,16 @@
76
75
  "vite-plugin-dts": "^4.5.4"
77
76
  },
78
77
  "peerDependencies": {
78
+ "@elgato/streamdeck": "^2.0.2",
79
79
  "react": "^18.0.0 || ^19.0.0",
80
80
  "rollup": "^4.0.0",
81
81
  "typescript": "^5",
82
82
  "vite": "^7.0.0 || ^8.0.0"
83
83
  },
84
84
  "peerDependenciesMeta": {
85
+ "@elgato/streamdeck": {
86
+ "optional": true
87
+ },
85
88
  "rollup": {
86
89
  "optional": true
87
90
  },
@@ -1,2 +0,0 @@
1
- import { TouchBarInfo } from '../types';
2
- export declare const TouchBarContext: import('react').Context<TouchBarInfo>;
@@ -1,5 +0,0 @@
1
- import { createContext } from "react";
2
- //#region src/context/touchbar-context.ts
3
- var TouchBarContext = createContext(null);
4
- //#endregion
5
- export { TouchBarContext };
@@ -1,6 +0,0 @@
1
- import { TouchBarInfo, TouchBarTapPayload, TouchBarDialRotatePayload, TouchBarDialPressPayload } from '../types';
2
- export declare function useTouchBar(): TouchBarInfo;
3
- export declare function useTouchBarTap(callback: (payload: TouchBarTapPayload) => void): void;
4
- export declare function useTouchBarDialRotate(callback: (payload: TouchBarDialRotatePayload) => void): void;
5
- export declare function useTouchBarDialDown(callback: (payload: TouchBarDialPressPayload) => void): void;
6
- export declare function useTouchBarDialUp(callback: (payload: TouchBarDialPressPayload) => void): void;
@@ -1,37 +0,0 @@
1
- import { EventBusContext } from "../context/providers.js";
2
- import { TouchBarContext } from "../context/touchbar-context.js";
3
- import { useCallbackRef } from "./internal/useCallbackRef.js";
4
- import { useContext, useEffect } from "react";
5
- //#region src/hooks/touchbar.ts
6
- function useTouchBarEvent(event, callback) {
7
- const bus = useContext(EventBusContext);
8
- const callbackRef = useCallbackRef(callback);
9
- useEffect(() => {
10
- const handler = (payload) => {
11
- callbackRef.current(payload);
12
- };
13
- bus.on(event, handler);
14
- return () => bus.off(event, handler);
15
- }, [
16
- bus,
17
- callbackRef,
18
- event
19
- ]);
20
- }
21
- function useTouchBar() {
22
- return useContext(TouchBarContext);
23
- }
24
- function useTouchBarTap(callback) {
25
- useTouchBarEvent("touchBarTap", callback);
26
- }
27
- function useTouchBarDialRotate(callback) {
28
- useTouchBarEvent("touchBarDialRotate", callback);
29
- }
30
- function useTouchBarDialDown(callback) {
31
- useTouchBarEvent("touchBarDialDown", callback);
32
- }
33
- function useTouchBarDialUp(callback) {
34
- useTouchBarEvent("touchBarDialUp", callback);
35
- }
36
- //#endregion
37
- export { useTouchBar, useTouchBarDialDown, useTouchBarDialRotate, useTouchBarDialUp, useTouchBarTap };
@@ -1,175 +0,0 @@
1
- import { createVContainer } from "../reconciler/vnode.js";
2
- import { reconciler } from "../reconciler/renderer.js";
3
- import { renderToRaw, sliceToDataUri } from "../render/pipeline.js";
4
- import { EventBus } from "../context/event-bus.js";
5
- import { DeviceContext, EventBusContext, GlobalSettingsContext } from "../context/providers.js";
6
- import { TouchBarContext } from "../context/touchbar-context.js";
7
- import { createElement } from "react";
8
- //#region src/roots/touchbar-root.ts
9
- var SEGMENT_WIDTH = 200;
10
- var SEGMENT_HEIGHT = 100;
11
- var DEFAULT_TOUCHBAR_FPS = 60;
12
- var TouchBarRoot = class {
13
- eventBus = new EventBus();
14
- container;
15
- fiberRoot;
16
- columns = /* @__PURE__ */ new Map();
17
- globalSettings;
18
- setGlobalSettingsFn;
19
- renderDebounceMs;
20
- renderConfig;
21
- deviceInfo;
22
- disposed = false;
23
- fps;
24
- pluginWrapper;
25
- /** Last rendered per-column data URIs. Used by devtools snapshots. */
26
- lastSegmentUris = /* @__PURE__ */ new Map();
27
- /** Exposes the VContainer for devtools inspection. */
28
- get vcontainer() {
29
- return this.container;
30
- }
31
- /** Sorted column numbers for devtools observer. */
32
- get columnNumbers() {
33
- return [...this.columns.keys()].sort((a, b) => a - b);
34
- }
35
- /** Column → actionId map for devtools observer. */
36
- get columnActionMap() {
37
- const map = /* @__PURE__ */ new Map();
38
- for (const [col, entry] of this.columns) map.set(col, entry.actionId);
39
- return map;
40
- }
41
- globalSettingsValue;
42
- touchBarValue;
43
- constructor(component, deviceInfo, initialGlobalSettings, renderConfig, renderDebounceMs, onGlobalSettingsChange, pluginWrapper, touchBarFPS) {
44
- this.component = component;
45
- this.deviceInfo = deviceInfo;
46
- this.globalSettings = { ...initialGlobalSettings };
47
- this.renderConfig = renderConfig;
48
- this.fps = touchBarFPS ?? DEFAULT_TOUCHBAR_FPS;
49
- this.renderDebounceMs = touchBarFPS != null ? Math.max(1, Math.round(1e3 / touchBarFPS)) : renderDebounceMs;
50
- this.pluginWrapper = pluginWrapper;
51
- this.setGlobalSettingsFn = (partial) => {
52
- this.globalSettings = {
53
- ...this.globalSettings,
54
- ...partial
55
- };
56
- this.globalSettingsValue = {
57
- settings: this.globalSettings,
58
- setSettings: this.setGlobalSettingsFn
59
- };
60
- onGlobalSettingsChange(this.globalSettings);
61
- this.scheduleRerender();
62
- };
63
- this.globalSettingsValue = {
64
- settings: this.globalSettings,
65
- setSettings: this.setGlobalSettingsFn
66
- };
67
- this.touchBarValue = {
68
- width: 0,
69
- height: SEGMENT_HEIGHT,
70
- columns: [],
71
- segmentWidth: SEGMENT_WIDTH,
72
- fps: this.fps
73
- };
74
- this.container = createVContainer(() => {
75
- this.flush();
76
- });
77
- this.fiberRoot = reconciler.createContainer(this.container, 0, null, false, null, "", (err) => {
78
- console.error("[@fcannizzaro/streamdeck-react] TouchBar uncaught error:", err);
79
- }, (err) => {
80
- console.error("[@fcannizzaro/streamdeck-react] TouchBar caught error:", err);
81
- }, (err) => {
82
- console.error("[@fcannizzaro/streamdeck-react] TouchBar recoverable error:", err);
83
- }, () => {});
84
- this.eventBus.ownerId = `touchbar:${deviceInfo.id}`;
85
- }
86
- addColumn(column, actionId, sdkAction) {
87
- this.columns.set(column, {
88
- actionId,
89
- sdkAction
90
- });
91
- this.updateTouchBarInfo();
92
- this.scheduleRerender();
93
- }
94
- removeColumn(column) {
95
- this.columns.delete(column);
96
- if (this.columns.size === 0) return;
97
- this.updateTouchBarInfo();
98
- this.scheduleRerender();
99
- }
100
- get isEmpty() {
101
- return this.columns.size === 0;
102
- }
103
- findColumnByActionId(actionId) {
104
- for (const [column, entry] of this.columns) if (entry.actionId === actionId) return column;
105
- }
106
- updateTouchBarInfo() {
107
- const sortedColumns = [...this.columns.keys()].sort((a, b) => a - b);
108
- this.touchBarValue = {
109
- width: (sortedColumns.length > 0 ? sortedColumns[sortedColumns.length - 1] + 1 : 0) * SEGMENT_WIDTH,
110
- height: SEGMENT_HEIGHT,
111
- columns: sortedColumns,
112
- segmentWidth: SEGMENT_WIDTH,
113
- fps: this.fps
114
- };
115
- }
116
- render() {
117
- if (this.disposed) return;
118
- const element = this.buildTree();
119
- reconciler.updateContainer(element, this.fiberRoot, null, () => {});
120
- }
121
- buildTree() {
122
- let child = createElement(this.component);
123
- if (this.pluginWrapper) child = createElement(this.pluginWrapper, null, child);
124
- return createElement(TouchBarContext.Provider, { value: this.touchBarValue }, createElement(DeviceContext.Provider, { value: this.deviceInfo }, createElement(EventBusContext.Provider, { value: this.eventBus }, createElement(GlobalSettingsContext.Provider, { value: this.globalSettingsValue }, child))));
125
- }
126
- scheduleRerender() {
127
- if (this.disposed) return;
128
- this.render();
129
- }
130
- async flush() {
131
- if (this.disposed) return;
132
- if (this.renderDebounceMs > 0 && this.container.renderTimer !== null) clearTimeout(this.container.renderTimer);
133
- if (this.renderDebounceMs > 0) this.container.renderTimer = setTimeout(async () => {
134
- this.container.renderTimer = null;
135
- await this.doFlush();
136
- }, this.renderDebounceMs);
137
- else await this.doFlush();
138
- }
139
- async doFlush() {
140
- if (this.disposed || this.columns.size === 0) return;
141
- try {
142
- const width = this.touchBarValue.width;
143
- if (width === 0) return;
144
- const result = await renderToRaw(this.container, width, SEGMENT_HEIGHT, this.renderConfig);
145
- if (result === null || this.disposed) return;
146
- const feedbackPromises = [];
147
- for (const [column, entry] of this.columns) {
148
- const sliceUri = sliceToDataUri(result.buffer, result.width, result.height, column, SEGMENT_WIDTH, SEGMENT_HEIGHT);
149
- this.lastSegmentUris.set(column, sliceUri);
150
- feedbackPromises.push(entry.sdkAction.setFeedback({ canvas: sliceUri }));
151
- }
152
- await Promise.all(feedbackPromises);
153
- } catch (err) {
154
- console.error("[@fcannizzaro/streamdeck-react] TouchBar render error:", err);
155
- }
156
- }
157
- updateGlobalSettings(settings) {
158
- this.globalSettings = { ...settings };
159
- this.globalSettingsValue = {
160
- settings: this.globalSettings,
161
- setSettings: this.setGlobalSettingsFn
162
- };
163
- this.scheduleRerender();
164
- }
165
- unmount() {
166
- this.disposed = true;
167
- if (this.container.renderTimer !== null) clearTimeout(this.container.renderTimer);
168
- this.eventBus.emit("willDisappear", void 0);
169
- reconciler.updateContainer(null, this.fiberRoot, null, () => {});
170
- this.eventBus.removeAllListeners();
171
- this.columns.clear();
172
- }
173
- };
174
- //#endregion
175
- export { TouchBarRoot };