@fcannizzaro/streamdeck-react 0.1.10 → 0.1.11
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/LICENSE +190 -21
- package/README.md +2 -0
- package/dist/action.d.ts +2 -2
- package/dist/action.js +2 -2
- package/dist/bundler-shared.d.ts +11 -0
- package/dist/bundler-shared.js +11 -0
- package/dist/context/event-bus.d.ts +1 -1
- package/dist/context/event-bus.js +1 -1
- package/dist/context/touchstrip-context.d.ts +2 -0
- package/dist/context/touchstrip-context.js +5 -0
- package/dist/devtools/bridge.d.ts +35 -7
- package/dist/devtools/bridge.js +153 -46
- package/dist/devtools/highlight.d.ts +6 -0
- package/dist/devtools/highlight.js +106 -57
- package/dist/devtools/index.js +6 -0
- package/dist/devtools/observers/lifecycle.d.ts +4 -4
- package/dist/devtools/server.d.ts +6 -1
- package/dist/devtools/server.js +6 -1
- package/dist/devtools/types.d.ts +50 -6
- package/dist/font-inline.d.ts +5 -1
- package/dist/font-inline.js +8 -3
- package/dist/hooks/animation.d.ts +154 -0
- package/dist/hooks/animation.js +381 -0
- package/dist/hooks/events.js +1 -5
- package/dist/hooks/touchstrip.d.ts +6 -0
- package/dist/hooks/touchstrip.js +37 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +3 -2
- package/dist/manifest-codegen.d.ts +38 -0
- package/dist/manifest-codegen.js +110 -0
- package/dist/node_modules/.bun/xxhash-wasm@1.1.0/node_modules/xxhash-wasm/esm/xxhash-wasm.js +3157 -0
- package/dist/plugin.js +20 -9
- package/dist/reconciler/host-config.js +19 -1
- package/dist/reconciler/vnode.d.ts +26 -0
- package/dist/reconciler/vnode.js +41 -10
- package/dist/render/buffer-pool.d.ts +19 -0
- package/dist/render/buffer-pool.js +51 -0
- package/dist/render/cache.d.ts +41 -0
- package/dist/render/cache.js +159 -5
- package/dist/render/image-cache.d.ts +53 -0
- package/dist/render/image-cache.js +128 -0
- package/dist/render/metrics.d.ts +58 -0
- package/dist/render/metrics.js +101 -0
- package/dist/render/pipeline.d.ts +46 -1
- package/dist/render/pipeline.js +370 -36
- package/dist/render/png.d.ts +10 -1
- package/dist/render/png.js +31 -13
- package/dist/render/render-pool.d.ts +26 -0
- package/dist/render/render-pool.js +141 -0
- package/dist/render/svg.d.ts +7 -0
- package/dist/render/svg.js +139 -0
- package/dist/render/worker.d.ts +1 -0
- package/dist/rollup.d.ts +23 -9
- package/dist/rollup.js +24 -9
- package/dist/roots/flush-coordinator.d.ts +18 -0
- package/dist/roots/flush-coordinator.js +38 -0
- package/dist/roots/registry.d.ts +6 -4
- package/dist/roots/registry.js +47 -33
- package/dist/roots/root.d.ts +32 -2
- package/dist/roots/root.js +104 -14
- package/dist/roots/settings-equality.d.ts +5 -0
- package/dist/roots/settings-equality.js +24 -0
- package/dist/roots/touchstrip-root.d.ts +93 -0
- package/dist/roots/touchstrip-root.js +383 -0
- package/dist/types.d.ts +62 -16
- package/dist/vite.d.ts +22 -8
- package/dist/vite.js +24 -8
- package/package.json +5 -4
- package/dist/context/touchbar-context.d.ts +0 -2
- package/dist/context/touchbar-context.js +0 -5
- package/dist/hooks/touchbar.d.ts +0 -6
- package/dist/hooks/touchbar.js +0 -37
- package/dist/roots/touchbar-root.d.ts +0 -45
- package/dist/roots/touchbar-root.js +0 -175
package/dist/roots/registry.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { shallowEqualSettings } from "./settings-equality.js";
|
|
1
2
|
import { ReactRoot } from "./root.js";
|
|
2
|
-
import {
|
|
3
|
+
import { TouchStripRoot } from "./touchstrip-root.js";
|
|
4
|
+
import { FlushCoordinator } from "./flush-coordinator.js";
|
|
3
5
|
//#region src/roots/registry.ts
|
|
4
6
|
var SEGMENT_WIDTH = 200;
|
|
5
7
|
var KEY_SIZES = {
|
|
@@ -77,16 +79,17 @@ function getCanvasInfo(deviceType, surfaceType) {
|
|
|
77
79
|
type: "key"
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
|
-
var RootRegistry = class {
|
|
82
|
+
var RootRegistry = class RootRegistry {
|
|
81
83
|
roots = /* @__PURE__ */ new Map();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
touchStripRoots = /* @__PURE__ */ new Map();
|
|
85
|
+
touchStripActions = /* @__PURE__ */ new Map();
|
|
84
86
|
renderConfig;
|
|
85
87
|
renderDebounceMs;
|
|
86
88
|
sdkInstance;
|
|
87
89
|
globalSettings = {};
|
|
88
90
|
onGlobalSettingsChange;
|
|
89
91
|
wrapper;
|
|
92
|
+
flushCoordinator = new FlushCoordinator();
|
|
90
93
|
/** DevTools observer. Set externally by startDevtoolsServer(). null when devtools is off. */
|
|
91
94
|
observer = null;
|
|
92
95
|
constructor(renderConfig, renderDebounceMs, sdkInstance, onGlobalSettingsChange, wrapper) {
|
|
@@ -97,18 +100,19 @@ var RootRegistry = class {
|
|
|
97
100
|
this.wrapper = wrapper;
|
|
98
101
|
}
|
|
99
102
|
setGlobalSettings(settings) {
|
|
103
|
+
if (shallowEqualSettings(this.globalSettings, settings)) return;
|
|
100
104
|
this.globalSettings = settings;
|
|
101
105
|
for (const root of this.roots.values()) root.updateGlobalSettings(settings);
|
|
102
|
-
for (const tbRoot of this.
|
|
106
|
+
for (const tbRoot of this.touchStripRoots.values()) tbRoot.updateGlobalSettings(settings);
|
|
103
107
|
}
|
|
104
108
|
create(ev, component, definition) {
|
|
105
109
|
const contextId = ev.action.id;
|
|
106
|
-
if (this.roots.has(contextId) || this.
|
|
110
|
+
if (this.roots.has(contextId) || this.touchStripActions.has(contextId)) return;
|
|
107
111
|
const device = ev.action.device;
|
|
108
112
|
const controller = ev.action.controllerType;
|
|
109
113
|
const isEncoder = controller === "Encoder";
|
|
110
|
-
if (isEncoder && definition.
|
|
111
|
-
this.
|
|
114
|
+
if (isEncoder && definition.touchStrip) {
|
|
115
|
+
this.registerTouchStripColumn(ev, definition);
|
|
112
116
|
return;
|
|
113
117
|
}
|
|
114
118
|
let surfaceType = "key";
|
|
@@ -129,7 +133,7 @@ var RootRegistry = class {
|
|
|
129
133
|
const canvas = getCanvasInfo(device.type, surfaceType);
|
|
130
134
|
const root = new ReactRoot(component, actionInfo, deviceInfo, canvas, ev.payload.settings, this.globalSettings, ev.action, this.sdkInstance, this.renderConfig, this.renderDebounceMs, async (settings) => {
|
|
131
135
|
await ev.action.setSettings(settings);
|
|
132
|
-
}, this.onGlobalSettingsChange, this.wrapper, definition.wrapper, definition.dialLayout);
|
|
136
|
+
}, this.onGlobalSettingsChange, this.flushCoordinator, this.wrapper, definition.wrapper, definition.dialLayout);
|
|
133
137
|
root.eventBus.emitSticky("willAppear", {
|
|
134
138
|
settings: ev.payload.settings,
|
|
135
139
|
controller,
|
|
@@ -147,16 +151,16 @@ var RootRegistry = class {
|
|
|
147
151
|
} : void 0
|
|
148
152
|
});
|
|
149
153
|
}
|
|
150
|
-
|
|
154
|
+
registerTouchStripColumn(ev, definition) {
|
|
151
155
|
const actionId = ev.action.id;
|
|
152
156
|
const device = ev.action.device;
|
|
153
157
|
const deviceId = device.id;
|
|
154
158
|
const column = this.getEncoderColumn(ev);
|
|
155
159
|
if (column === void 0) {
|
|
156
|
-
console.warn("[@fcannizzaro/streamdeck-react] Cannot determine encoder column for
|
|
160
|
+
console.warn("[@fcannizzaro/streamdeck-react] Cannot determine encoder column for touchstrip action:", actionId);
|
|
157
161
|
return;
|
|
158
162
|
}
|
|
159
|
-
let tbRoot = this.
|
|
163
|
+
let tbRoot = this.touchStripRoots.get(deviceId);
|
|
160
164
|
if (!tbRoot) {
|
|
161
165
|
const deviceInfo = {
|
|
162
166
|
id: deviceId,
|
|
@@ -164,31 +168,31 @@ var RootRegistry = class {
|
|
|
164
168
|
size: device.size,
|
|
165
169
|
name: device.name
|
|
166
170
|
};
|
|
167
|
-
tbRoot = new
|
|
168
|
-
this.
|
|
169
|
-
this.observer?.
|
|
171
|
+
tbRoot = new TouchStripRoot(definition.touchStrip, deviceInfo, this.globalSettings, this.renderConfig, this.renderDebounceMs, this.onGlobalSettingsChange, this.wrapper, definition.touchStripFPS, this.flushCoordinator);
|
|
172
|
+
this.touchStripRoots.set(deviceId, tbRoot);
|
|
173
|
+
this.observer?.onTouchStripCreated(deviceId, tbRoot, deviceInfo);
|
|
170
174
|
}
|
|
171
175
|
tbRoot.addColumn(column, actionId, ev.action);
|
|
172
|
-
this.observer?.
|
|
173
|
-
this.
|
|
176
|
+
this.observer?.onTouchStripColumnChanged(deviceId, [...tbRoot.columnNumbers], tbRoot.columnActionMap);
|
|
177
|
+
this.touchStripActions.set(actionId, deviceId);
|
|
174
178
|
}
|
|
175
179
|
getEncoderColumn(ev) {
|
|
176
180
|
return ev.action.coordinates?.column;
|
|
177
181
|
}
|
|
178
182
|
destroy(contextId) {
|
|
179
|
-
const deviceId = this.
|
|
183
|
+
const deviceId = this.touchStripActions.get(contextId);
|
|
180
184
|
if (deviceId) {
|
|
181
|
-
const tbRoot = this.
|
|
185
|
+
const tbRoot = this.touchStripRoots.get(deviceId);
|
|
182
186
|
if (tbRoot) {
|
|
183
187
|
const column = tbRoot.findColumnByActionId(contextId);
|
|
184
188
|
if (column !== void 0) tbRoot.removeColumn(column);
|
|
185
189
|
if (tbRoot.isEmpty) {
|
|
186
|
-
this.observer?.
|
|
190
|
+
this.observer?.onTouchStripDestroyed(deviceId);
|
|
187
191
|
tbRoot.unmount();
|
|
188
|
-
this.
|
|
192
|
+
this.touchStripRoots.delete(deviceId);
|
|
189
193
|
}
|
|
190
194
|
}
|
|
191
|
-
this.
|
|
195
|
+
this.touchStripActions.delete(contextId);
|
|
192
196
|
return;
|
|
193
197
|
}
|
|
194
198
|
const root = this.roots.get(contextId);
|
|
@@ -198,29 +202,39 @@ var RootRegistry = class {
|
|
|
198
202
|
this.roots.delete(contextId);
|
|
199
203
|
}
|
|
200
204
|
}
|
|
205
|
+
static INTERACTION_EVENTS = new Set([
|
|
206
|
+
"keyDown",
|
|
207
|
+
"keyUp",
|
|
208
|
+
"dialRotate",
|
|
209
|
+
"dialDown",
|
|
210
|
+
"dialUp",
|
|
211
|
+
"touchTap"
|
|
212
|
+
]);
|
|
201
213
|
dispatch(contextId, event, payload) {
|
|
202
214
|
const root = this.roots.get(contextId);
|
|
203
215
|
if (root) {
|
|
216
|
+
if (RootRegistry.INTERACTION_EVENTS.has(event)) root.markInteraction();
|
|
204
217
|
root.eventBus.emit(event, payload);
|
|
205
218
|
this.observer?.onDispatch(contextId, event, payload);
|
|
206
219
|
return;
|
|
207
220
|
}
|
|
208
|
-
const deviceId = this.
|
|
221
|
+
const deviceId = this.touchStripActions.get(contextId);
|
|
209
222
|
if (deviceId) {
|
|
210
|
-
const tbRoot = this.
|
|
223
|
+
const tbRoot = this.touchStripRoots.get(deviceId);
|
|
211
224
|
if (tbRoot) {
|
|
212
|
-
|
|
225
|
+
if (RootRegistry.INTERACTION_EVENTS.has(event)) tbRoot.markInteraction();
|
|
226
|
+
this.dispatchToTouchStrip(tbRoot, contextId, event, payload);
|
|
213
227
|
this.observer?.onDispatch(contextId, event, payload);
|
|
214
228
|
}
|
|
215
229
|
}
|
|
216
230
|
}
|
|
217
|
-
|
|
231
|
+
dispatchToTouchStrip(tbRoot, actionId, event, payload) {
|
|
218
232
|
const column = tbRoot.findColumnByActionId(actionId);
|
|
219
233
|
if (column === void 0) return;
|
|
220
234
|
switch (event) {
|
|
221
235
|
case "touchTap": {
|
|
222
236
|
const tp = payload;
|
|
223
|
-
tbRoot.eventBus.emit("
|
|
237
|
+
tbRoot.eventBus.emit("touchStripTap", {
|
|
224
238
|
tapPos: [column * SEGMENT_WIDTH + tp.tapPos[0], tp.tapPos[1]],
|
|
225
239
|
hold: tp.hold,
|
|
226
240
|
column
|
|
@@ -229,7 +243,7 @@ var RootRegistry = class {
|
|
|
229
243
|
}
|
|
230
244
|
case "dialRotate": {
|
|
231
245
|
const dr = payload;
|
|
232
|
-
tbRoot.eventBus.emit("
|
|
246
|
+
tbRoot.eventBus.emit("touchStripDialRotate", {
|
|
233
247
|
column,
|
|
234
248
|
ticks: dr.ticks,
|
|
235
249
|
pressed: dr.pressed
|
|
@@ -237,10 +251,10 @@ var RootRegistry = class {
|
|
|
237
251
|
break;
|
|
238
252
|
}
|
|
239
253
|
case "dialDown":
|
|
240
|
-
tbRoot.eventBus.emit("
|
|
254
|
+
tbRoot.eventBus.emit("touchStripDialDown", { column });
|
|
241
255
|
break;
|
|
242
256
|
case "dialUp":
|
|
243
|
-
tbRoot.eventBus.emit("
|
|
257
|
+
tbRoot.eventBus.emit("touchStripDialUp", { column });
|
|
244
258
|
break;
|
|
245
259
|
}
|
|
246
260
|
}
|
|
@@ -251,9 +265,9 @@ var RootRegistry = class {
|
|
|
251
265
|
destroyAll() {
|
|
252
266
|
for (const [_, root] of this.roots) root.unmount();
|
|
253
267
|
this.roots.clear();
|
|
254
|
-
for (const [_, tbRoot] of this.
|
|
255
|
-
this.
|
|
256
|
-
this.
|
|
268
|
+
for (const [_, tbRoot] of this.touchStripRoots) tbRoot.unmount();
|
|
269
|
+
this.touchStripRoots.clear();
|
|
270
|
+
this.touchStripActions.clear();
|
|
257
271
|
}
|
|
258
272
|
};
|
|
259
273
|
//#endregion
|
package/dist/roots/root.d.ts
CHANGED
|
@@ -3,12 +3,14 @@ import { VContainer } from '../reconciler/vnode';
|
|
|
3
3
|
import { RenderConfig } from '../render/pipeline';
|
|
4
4
|
import { EventBus } from '../context/event-bus';
|
|
5
5
|
import { ActionInfo, CanvasInfo, DeviceInfo, EncoderLayout, StreamDeckAccess, WrapperComponent } from '../types';
|
|
6
|
+
import { FlushCoordinator, FlushableRoot } from './flush-coordinator';
|
|
6
7
|
import { JsonObject } from '@elgato/utils';
|
|
7
8
|
import { Action, DialAction, KeyAction } from '@elgato/streamdeck';
|
|
8
|
-
export declare class ReactRoot {
|
|
9
|
+
export declare class ReactRoot implements FlushableRoot {
|
|
9
10
|
private component;
|
|
10
11
|
private actionInfo;
|
|
11
12
|
private deviceInfo;
|
|
13
|
+
private flushCoordinator?;
|
|
12
14
|
private pluginWrapper?;
|
|
13
15
|
private actionWrapper?;
|
|
14
16
|
readonly eventBus: EventBus;
|
|
@@ -25,6 +27,20 @@ export declare class ReactRoot {
|
|
|
25
27
|
private sdkAction;
|
|
26
28
|
private sdkInstance;
|
|
27
29
|
private disposed;
|
|
30
|
+
private _renderCount;
|
|
31
|
+
private _lastRenderReport;
|
|
32
|
+
private static readonly RENDER_WARN_THRESHOLD;
|
|
33
|
+
private _rendering;
|
|
34
|
+
private _pendingFlush;
|
|
35
|
+
private _recentRenders;
|
|
36
|
+
private _lastInteraction;
|
|
37
|
+
private static readonly ANIMATION_WINDOW_MS;
|
|
38
|
+
private static readonly ANIMATION_THRESHOLD;
|
|
39
|
+
private static readonly INTERACTION_COOLDOWN_MS;
|
|
40
|
+
private static readonly IDLE_THRESHOLD_MS;
|
|
41
|
+
private _lastFlushTime;
|
|
42
|
+
/** Current render priority (lower = higher priority). Used by flush coordinator for ordering. */
|
|
43
|
+
get priority(): number;
|
|
28
44
|
/** Last data URI successfully sent to hardware. Used by devtools snapshots. */
|
|
29
45
|
lastDataUri: string | null;
|
|
30
46
|
/**
|
|
@@ -41,11 +57,25 @@ export declare class ReactRoot {
|
|
|
41
57
|
private streamDeckValue;
|
|
42
58
|
private settingsValue;
|
|
43
59
|
private globalSettingsValue;
|
|
44
|
-
constructor(component: ComponentType, actionInfo: ActionInfo, deviceInfo: DeviceInfo, canvas: CanvasInfo, initialSettings: JsonObject, initialGlobalSettings: JsonObject, sdkAction: Action | DialAction | KeyAction, sdkInstance: StreamDeckAccess["sdk"], renderConfig: RenderConfig, renderDebounceMs: number, onSettingsChange: (settings: JsonObject) => Promise<void>, onGlobalSettingsChange: (settings: JsonObject) => Promise<void>, pluginWrapper?: WrapperComponent | undefined, actionWrapper?: WrapperComponent | undefined, dialLayout?: EncoderLayout);
|
|
60
|
+
constructor(component: ComponentType, actionInfo: ActionInfo, deviceInfo: DeviceInfo, canvas: CanvasInfo, initialSettings: JsonObject, initialGlobalSettings: JsonObject, sdkAction: Action | DialAction | KeyAction, sdkInstance: StreamDeckAccess["sdk"], renderConfig: RenderConfig, renderDebounceMs: number, onSettingsChange: (settings: JsonObject) => Promise<void>, onGlobalSettingsChange: (settings: JsonObject) => Promise<void>, flushCoordinator?: FlushCoordinator | undefined, pluginWrapper?: WrapperComponent | undefined, actionWrapper?: WrapperComponent | undefined, dialLayout?: EncoderLayout);
|
|
45
61
|
private render;
|
|
46
62
|
private buildTree;
|
|
47
63
|
private scheduleRerender;
|
|
64
|
+
/** Record a user interaction (keyDown, dialRotate, etc.) for adaptive debounce. */
|
|
65
|
+
markInteraction(): void;
|
|
66
|
+
/** Compute effective debounce based on recent render frequency and interaction state. */
|
|
67
|
+
private get effectiveDebounceMs();
|
|
48
68
|
private flush;
|
|
69
|
+
/**
|
|
70
|
+
* Submit this root for flushing. Routes through the coordinator
|
|
71
|
+
* (priority-ordered) when available, or flushes directly.
|
|
72
|
+
*/
|
|
73
|
+
private submitFlush;
|
|
74
|
+
/**
|
|
75
|
+
* Execute the flush. Called by FlushCoordinator in priority order,
|
|
76
|
+
* or directly when no coordinator is present.
|
|
77
|
+
*/
|
|
78
|
+
executeFlush(): Promise<void>;
|
|
49
79
|
private doFlush;
|
|
50
80
|
updateSettings(settings: JsonObject): void;
|
|
51
81
|
updateGlobalSettings(settings: JsonObject): void;
|
package/dist/roots/root.js
CHANGED
|
@@ -3,6 +3,7 @@ import { reconciler } from "../reconciler/renderer.js";
|
|
|
3
3
|
import { renderToDataUri } from "../render/pipeline.js";
|
|
4
4
|
import { EventBus } from "../context/event-bus.js";
|
|
5
5
|
import { ActionContext, CanvasContext, DeviceContext, EventBusContext, GlobalSettingsContext, SettingsContext, StreamDeckContext } from "../context/providers.js";
|
|
6
|
+
import { partialHasChanges, shallowEqualSettings } from "./settings-equality.js";
|
|
6
7
|
import { createElement } from "react";
|
|
7
8
|
//#region src/roots/root.ts
|
|
8
9
|
var DEFAULT_DIAL_LAYOUT = {
|
|
@@ -18,7 +19,7 @@ var DEFAULT_DIAL_LAYOUT = {
|
|
|
18
19
|
]
|
|
19
20
|
}]
|
|
20
21
|
};
|
|
21
|
-
var ReactRoot = class {
|
|
22
|
+
var ReactRoot = class ReactRoot {
|
|
22
23
|
eventBus = new EventBus();
|
|
23
24
|
container;
|
|
24
25
|
fiberRoot;
|
|
@@ -33,6 +34,28 @@ var ReactRoot = class {
|
|
|
33
34
|
sdkAction;
|
|
34
35
|
sdkInstance;
|
|
35
36
|
disposed = false;
|
|
37
|
+
_renderCount = 0;
|
|
38
|
+
_lastRenderReport = 0;
|
|
39
|
+
static RENDER_WARN_THRESHOLD = 30;
|
|
40
|
+
_rendering = false;
|
|
41
|
+
_pendingFlush = false;
|
|
42
|
+
_recentRenders = [];
|
|
43
|
+
_lastInteraction = 0;
|
|
44
|
+
static ANIMATION_WINDOW_MS = 100;
|
|
45
|
+
static ANIMATION_THRESHOLD = 2;
|
|
46
|
+
static INTERACTION_COOLDOWN_MS = 500;
|
|
47
|
+
static IDLE_THRESHOLD_MS = 2e3;
|
|
48
|
+
_lastFlushTime = 0;
|
|
49
|
+
/** Current render priority (lower = higher priority). Used by flush coordinator for ordering. */
|
|
50
|
+
get priority() {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const cutoff = now - ReactRoot.ANIMATION_WINDOW_MS;
|
|
53
|
+
while (this._recentRenders.length > 0 && this._recentRenders[0] < cutoff) this._recentRenders.shift();
|
|
54
|
+
if (this._recentRenders.length > ReactRoot.ANIMATION_THRESHOLD) return 0;
|
|
55
|
+
if (now - this._lastInteraction < ReactRoot.INTERACTION_COOLDOWN_MS) return 1;
|
|
56
|
+
if (this._lastFlushTime > 0 && now - this._lastFlushTime > ReactRoot.IDLE_THRESHOLD_MS) return 3;
|
|
57
|
+
return 2;
|
|
58
|
+
}
|
|
36
59
|
/** Last data URI successfully sent to hardware. Used by devtools snapshots. */
|
|
37
60
|
lastDataUri = null;
|
|
38
61
|
/**
|
|
@@ -63,10 +86,11 @@ var ReactRoot = class {
|
|
|
63
86
|
streamDeckValue;
|
|
64
87
|
settingsValue;
|
|
65
88
|
globalSettingsValue;
|
|
66
|
-
constructor(component, actionInfo, deviceInfo, canvas, initialSettings, initialGlobalSettings, sdkAction, sdkInstance, renderConfig, renderDebounceMs, onSettingsChange, onGlobalSettingsChange, pluginWrapper, actionWrapper, dialLayout) {
|
|
89
|
+
constructor(component, actionInfo, deviceInfo, canvas, initialSettings, initialGlobalSettings, sdkAction, sdkInstance, renderConfig, renderDebounceMs, onSettingsChange, onGlobalSettingsChange, flushCoordinator, pluginWrapper, actionWrapper, dialLayout) {
|
|
67
90
|
this.component = component;
|
|
68
91
|
this.actionInfo = actionInfo;
|
|
69
92
|
this.deviceInfo = deviceInfo;
|
|
93
|
+
this.flushCoordinator = flushCoordinator;
|
|
70
94
|
this.pluginWrapper = pluginWrapper;
|
|
71
95
|
this.actionWrapper = actionWrapper;
|
|
72
96
|
this.canvas = canvas;
|
|
@@ -78,27 +102,33 @@ var ReactRoot = class {
|
|
|
78
102
|
this.renderDebounceMs = renderDebounceMs;
|
|
79
103
|
this.resolvedDialLayout = resolveDialLayout(dialLayout);
|
|
80
104
|
this.setSettingsFn = (partial) => {
|
|
81
|
-
this.settings
|
|
105
|
+
const hasChanges = partialHasChanges(this.settings, partial);
|
|
106
|
+
const nextSettings = hasChanges ? {
|
|
82
107
|
...this.settings,
|
|
83
108
|
...partial
|
|
84
|
-
};
|
|
109
|
+
} : this.settings;
|
|
110
|
+
onSettingsChange(nextSettings);
|
|
111
|
+
if (!hasChanges) return;
|
|
112
|
+
this.settings = nextSettings;
|
|
85
113
|
this.settingsValue = {
|
|
86
114
|
settings: this.settings,
|
|
87
115
|
setSettings: this.setSettingsFn
|
|
88
116
|
};
|
|
89
|
-
onSettingsChange(this.settings);
|
|
90
117
|
this.scheduleRerender();
|
|
91
118
|
};
|
|
92
119
|
this.setGlobalSettingsFn = (partial) => {
|
|
93
|
-
this.globalSettings
|
|
120
|
+
const hasChanges = partialHasChanges(this.globalSettings, partial);
|
|
121
|
+
const nextSettings = hasChanges ? {
|
|
94
122
|
...this.globalSettings,
|
|
95
123
|
...partial
|
|
96
|
-
};
|
|
124
|
+
} : this.globalSettings;
|
|
125
|
+
onGlobalSettingsChange(nextSettings);
|
|
126
|
+
if (!hasChanges) return;
|
|
127
|
+
this.globalSettings = nextSettings;
|
|
97
128
|
this.globalSettingsValue = {
|
|
98
129
|
settings: this.globalSettings,
|
|
99
130
|
setSettings: this.setGlobalSettingsFn
|
|
100
131
|
};
|
|
101
|
-
onGlobalSettingsChange(this.globalSettings);
|
|
102
132
|
this.scheduleRerender();
|
|
103
133
|
};
|
|
104
134
|
this.streamDeckValue = {
|
|
@@ -144,27 +174,86 @@ var ReactRoot = class {
|
|
|
144
174
|
if (this.disposed) return;
|
|
145
175
|
this.render();
|
|
146
176
|
}
|
|
177
|
+
/** Record a user interaction (keyDown, dialRotate, etc.) for adaptive debounce. */
|
|
178
|
+
markInteraction() {
|
|
179
|
+
this._lastInteraction = Date.now();
|
|
180
|
+
}
|
|
181
|
+
/** Compute effective debounce based on recent render frequency and interaction state. */
|
|
182
|
+
get effectiveDebounceMs() {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
const cutoff = now - ReactRoot.ANIMATION_WINDOW_MS;
|
|
185
|
+
while (this._recentRenders.length > 0 && this._recentRenders[0] < cutoff) this._recentRenders.shift();
|
|
186
|
+
if (this._recentRenders.length > ReactRoot.ANIMATION_THRESHOLD) return 0;
|
|
187
|
+
if (now - this._lastInteraction < ReactRoot.INTERACTION_COOLDOWN_MS) return Math.min(this.renderDebounceMs, 16);
|
|
188
|
+
return this.renderDebounceMs;
|
|
189
|
+
}
|
|
147
190
|
async flush() {
|
|
148
191
|
if (this.disposed) return;
|
|
149
|
-
if (this.
|
|
150
|
-
|
|
192
|
+
if (this._rendering) {
|
|
193
|
+
this._pendingFlush = true;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this._recentRenders.push(Date.now());
|
|
197
|
+
const debounce = this.effectiveDebounceMs;
|
|
198
|
+
if (debounce > 0 && this.container.renderTimer !== null) clearTimeout(this.container.renderTimer);
|
|
199
|
+
if (debounce > 0) this.container.renderTimer = setTimeout(() => {
|
|
151
200
|
this.container.renderTimer = null;
|
|
152
|
-
|
|
153
|
-
},
|
|
154
|
-
else
|
|
201
|
+
this.submitFlush();
|
|
202
|
+
}, debounce);
|
|
203
|
+
else this.submitFlush();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Submit this root for flushing. Routes through the coordinator
|
|
207
|
+
* (priority-ordered) when available, or flushes directly.
|
|
208
|
+
*/
|
|
209
|
+
submitFlush() {
|
|
210
|
+
if (this.disposed) return;
|
|
211
|
+
if (this.flushCoordinator) this.flushCoordinator.requestFlush(this);
|
|
212
|
+
else this.doFlush();
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Execute the flush. Called by FlushCoordinator in priority order,
|
|
216
|
+
* or directly when no coordinator is present.
|
|
217
|
+
*/
|
|
218
|
+
async executeFlush() {
|
|
219
|
+
await this.doFlush();
|
|
155
220
|
}
|
|
156
221
|
async doFlush() {
|
|
157
222
|
if (this.disposed) return;
|
|
223
|
+
this._rendering = true;
|
|
224
|
+
this._pendingFlush = false;
|
|
225
|
+
this._lastFlushTime = Date.now();
|
|
226
|
+
if (this.renderConfig.debug) {
|
|
227
|
+
this._renderCount++;
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
if (now - this._lastRenderReport > 1e3) {
|
|
230
|
+
if (this._renderCount > ReactRoot.RENDER_WARN_THRESHOLD) console.warn(`[@fcannizzaro/streamdeck-react] Action ${this.actionInfo.id} rendered ${this._renderCount}x in 1s`);
|
|
231
|
+
this._renderCount = 0;
|
|
232
|
+
this._lastRenderReport = now;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
158
235
|
try {
|
|
159
236
|
const dataUri = await renderToDataUri(this.container, this.canvas.width, this.canvas.height, this.renderConfig);
|
|
160
237
|
if (dataUri === null || this.disposed) return;
|
|
161
238
|
this.lastDataUri = dataUri;
|
|
162
|
-
if (!this.suppressHardwarePush)
|
|
239
|
+
if (!this.suppressHardwarePush) this.pushImage(dataUri).catch((err) => {
|
|
240
|
+
console.error("[@fcannizzaro/streamdeck-react] Hardware push error:", err);
|
|
241
|
+
});
|
|
163
242
|
} catch (err) {
|
|
164
243
|
console.error("[@fcannizzaro/streamdeck-react] Render error:", err);
|
|
244
|
+
} finally {
|
|
245
|
+
this._rendering = false;
|
|
246
|
+
if (this._pendingFlush && !this.disposed) {
|
|
247
|
+
this._pendingFlush = false;
|
|
248
|
+
await this.doFlush();
|
|
249
|
+
}
|
|
165
250
|
}
|
|
166
251
|
}
|
|
167
252
|
updateSettings(settings) {
|
|
253
|
+
if (shallowEqualSettings(this.settings, settings)) {
|
|
254
|
+
this.eventBus.emit("settingsChanged", settings);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
168
257
|
this.settings = { ...settings };
|
|
169
258
|
this.settingsValue = {
|
|
170
259
|
settings: this.settings,
|
|
@@ -174,6 +263,7 @@ var ReactRoot = class {
|
|
|
174
263
|
this.scheduleRerender();
|
|
175
264
|
}
|
|
176
265
|
updateGlobalSettings(settings) {
|
|
266
|
+
if (shallowEqualSettings(this.globalSettings, settings)) return;
|
|
177
267
|
this.globalSettings = { ...settings };
|
|
178
268
|
this.globalSettingsValue = {
|
|
179
269
|
settings: this.globalSettings,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { JsonObject } from '@elgato/utils';
|
|
2
|
+
/** Returns true when applying `partial` would change at least one key in `current`. */
|
|
3
|
+
export declare function partialHasChanges(current: JsonObject, partial: JsonObject): boolean;
|
|
4
|
+
/** Shallow equality for full settings objects. */
|
|
5
|
+
export declare function shallowEqualSettings(a: JsonObject, b: JsonObject): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/roots/settings-equality.ts
|
|
2
|
+
/** Returns true when applying `partial` would change at least one key in `current`. */
|
|
3
|
+
function partialHasChanges(current, partial) {
|
|
4
|
+
const keys = Object.keys(partial);
|
|
5
|
+
for (let i = 0; i < keys.length; i++) {
|
|
6
|
+
const key = keys[i];
|
|
7
|
+
if (!Object.is(current[key], partial[key])) return true;
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
/** Shallow equality for full settings objects. */
|
|
12
|
+
function shallowEqualSettings(a, b) {
|
|
13
|
+
const aKeys = Object.keys(a);
|
|
14
|
+
const bKeys = Object.keys(b);
|
|
15
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
16
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
17
|
+
const key = aKeys[i];
|
|
18
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
19
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { partialHasChanges, shallowEqualSettings };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
import { VContainer } from '../reconciler/vnode';
|
|
3
|
+
import { RenderConfig } from '../render/pipeline';
|
|
4
|
+
import { EventBus } from '../context/event-bus';
|
|
5
|
+
import { DeviceInfo, WrapperComponent } from '../types';
|
|
6
|
+
import { FlushCoordinator, FlushableRoot } from './flush-coordinator';
|
|
7
|
+
import { DialAction } from '@elgato/streamdeck';
|
|
8
|
+
import { JsonObject } from '@elgato/utils';
|
|
9
|
+
export declare class TouchStripRoot implements FlushableRoot {
|
|
10
|
+
private component;
|
|
11
|
+
private flushCoordinator?;
|
|
12
|
+
readonly eventBus: EventBus;
|
|
13
|
+
private container;
|
|
14
|
+
private fiberRoot;
|
|
15
|
+
private columns;
|
|
16
|
+
private globalSettings;
|
|
17
|
+
private setGlobalSettingsFn;
|
|
18
|
+
private renderDebounceMs;
|
|
19
|
+
private renderConfig;
|
|
20
|
+
private deviceInfo;
|
|
21
|
+
private disposed;
|
|
22
|
+
private fps;
|
|
23
|
+
private pluginWrapper?;
|
|
24
|
+
private _renderCount;
|
|
25
|
+
private _lastRenderReport;
|
|
26
|
+
private static readonly RENDER_WARN_THRESHOLD;
|
|
27
|
+
private _rendering;
|
|
28
|
+
private _pendingFlush;
|
|
29
|
+
private _recentRenders;
|
|
30
|
+
private _lastInteraction;
|
|
31
|
+
private static readonly ANIMATION_WINDOW_MS;
|
|
32
|
+
private static readonly ANIMATION_THRESHOLD;
|
|
33
|
+
private static readonly INTERACTION_COOLDOWN_MS;
|
|
34
|
+
private static readonly IDLE_THRESHOLD_MS;
|
|
35
|
+
private _lastFlushTime;
|
|
36
|
+
/** Current render priority (lower = higher priority). Used by flush coordinator. */
|
|
37
|
+
get priority(): number;
|
|
38
|
+
/** Last rendered per-column data URIs. Used by devtools snapshots. */
|
|
39
|
+
lastSegmentUris: Map<number, string>;
|
|
40
|
+
/**
|
|
41
|
+
* When true, doFlush skips pushing rendered segments to hardware.
|
|
42
|
+
* Set by the devtools bridge while a highlight overlay is active so
|
|
43
|
+
* that rapid re-renders don't overwrite the highlight on the device.
|
|
44
|
+
* The highlight path calls pushSegmentImages() directly (bypassing
|
|
45
|
+
* this flag).
|
|
46
|
+
*
|
|
47
|
+
* Mirrors ReactRoot.suppressHardwarePush — same pattern, different
|
|
48
|
+
* granularity (per-segment instead of single image).
|
|
49
|
+
*/
|
|
50
|
+
suppressHardwarePush: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Push per-column data URIs to the physical Stream Deck touch strip.
|
|
53
|
+
* Used by the devtools highlight overlay to bypass suppressHardwarePush.
|
|
54
|
+
*
|
|
55
|
+
* @param uris Map of column → data URI to push to hardware.
|
|
56
|
+
* Columns not present in the map are left unchanged.
|
|
57
|
+
*/
|
|
58
|
+
pushSegmentImages(uris: Map<number, string>): Promise<void>;
|
|
59
|
+
/** Exposes the VContainer for devtools inspection. */
|
|
60
|
+
get vcontainer(): VContainer;
|
|
61
|
+
/** Sorted column numbers for devtools observer. */
|
|
62
|
+
get columnNumbers(): number[];
|
|
63
|
+
/** Column → actionId map for devtools observer. */
|
|
64
|
+
get columnActionMap(): Map<number, string>;
|
|
65
|
+
private globalSettingsValue;
|
|
66
|
+
private touchStripValue;
|
|
67
|
+
constructor(component: ComponentType, deviceInfo: DeviceInfo, initialGlobalSettings: JsonObject, renderConfig: RenderConfig, renderDebounceMs: number, onGlobalSettingsChange: (settings: JsonObject) => Promise<void>, pluginWrapper?: WrapperComponent, touchStripFPS?: number, flushCoordinator?: FlushCoordinator | undefined);
|
|
68
|
+
addColumn(column: number, actionId: string, sdkAction: DialAction): void;
|
|
69
|
+
removeColumn(column: number): void;
|
|
70
|
+
get isEmpty(): boolean;
|
|
71
|
+
findColumnByActionId(actionId: string): number | undefined;
|
|
72
|
+
private updateTouchStripInfo;
|
|
73
|
+
private render;
|
|
74
|
+
private buildTree;
|
|
75
|
+
private scheduleRerender;
|
|
76
|
+
/** Record a user interaction for adaptive debounce. */
|
|
77
|
+
markInteraction(): void;
|
|
78
|
+
private get effectiveDebounceMs();
|
|
79
|
+
private flush;
|
|
80
|
+
/**
|
|
81
|
+
* Submit this root for flushing. Routes through the coordinator
|
|
82
|
+
* (priority-ordered) when available, or flushes directly.
|
|
83
|
+
*/
|
|
84
|
+
private submitFlush;
|
|
85
|
+
/**
|
|
86
|
+
* Execute the flush. Called by FlushCoordinator in priority order,
|
|
87
|
+
* or directly when no coordinator is present.
|
|
88
|
+
*/
|
|
89
|
+
executeFlush(): Promise<void>;
|
|
90
|
+
private doFlush;
|
|
91
|
+
updateGlobalSettings(settings: JsonObject): void;
|
|
92
|
+
unmount(): void;
|
|
93
|
+
}
|