├── .eslintignore
├── .gitattributes
├── .gitignore
├── .npmignore
├── API.md
├── LICENSE
├── README.md
├── example
├── README.md
├── action.html
├── background.html
├── easy-streamdeck-v2.0.1.js
├── icons
│ ├── actionDefaultImage.png
│ ├── actionDefaultImage@2x.png
│ ├── actionDefaultImage_blue.png
│ ├── actionDefaultImage_blue@2x.png
│ ├── actionDefaultImage_red.png
│ ├── actionDefaultImage_red@2x.png
│ ├── actionDefaultImage_yellow.png
│ ├── actionDefaultImage_yellow@2x.png
│ ├── actionIcon.png
│ ├── actionIcon@2x.png
│ ├── caret.svg
│ ├── pluginIcon.png
│ └── pluginIcon@2x.png
└── sdpi.css
├── index.js
├── manifest.json
├── package-lock.json
├── package.json
└── src
├── background
├── context.js
├── index.js
└── onmessage.js
├── common
├── boilers.js
├── connection.js
├── emitter.js
├── irn-client.js
└── utils.js
├── foreground
├── index.js
└── onmessage.js
└── index.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | example/*
2 | node_modules/*
3 | build/*
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
3 | .gitignore text eol=lf
4 | .gitattributes text eol=lf
5 | .npmignore text eol=lf
6 |
7 | TODO text eol=lf
8 | CONTRIBUTE text eol=lf
9 | CONTRIBUTING text eol=lf
10 | LICENSE text eol=lf
11 |
12 | *.js text eol=lf
13 | *.json text eol=lf
14 | *.htm text eol=lf
15 | *.html text eol=lf
16 | *.css text eol=lf
17 | *.md text eol=lf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # General ignores
2 | pids
3 | *.pid
4 | *.seed
5 | *.pid.lock
6 | *.tgz
7 | logs
8 | *.log
9 |
10 |
11 | # OS Ignores: Windows
12 | Thumbs.db
13 | ehthumbs.db
14 | ehthumbs_vista.db
15 | *.stackdump
16 | Desktop.ini
17 | $RECYCLE.BIN/
18 | *.cab
19 | *.msi
20 | *.msm
21 | *.msp
22 | *.lnk
23 |
24 |
25 | # OS Ignores: iOS/Mac
26 | .DS_Store
27 | .AppleDouble
28 | .LSOverride
29 | Icon
30 | ._*
31 | .DocumentRevisions-V100
32 | .fseventsd
33 | .Spotlight-V100
34 | .TemporaryItems
35 | .Trashes
36 | .VolumeIcon.icns
37 | .com.apple.timemachine.donotpresent
38 | .AppleDB
39 | .AppleDesktop
40 | Network Trash Folder
41 | Temporary Items
42 | .apdisk
43 |
44 |
45 | # OS Ignores: Linux
46 | .fuse_hidden*
47 | .directory
48 | .Trash-*
49 | .nfs*
50 |
51 |
52 | # Node-specific ignores
53 | npm-debug.log*
54 | yarn-debug.log*
55 | yarn-error.log*
56 | lib-cov
57 | coverage
58 | .nyc_output
59 | .grunt
60 | bower_components
61 | .lock-wscript
62 | node_modules/
63 | jspm_packages/
64 | typings/
65 | .npm
66 | .eslintcache
67 | .node_repl_history
68 | .yarn-integrity
69 | .env
70 |
71 |
72 | # Repo Specific
73 | build/
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | example/
2 | manifest.json
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 |
2 | # API
3 | When loaded in a browser-esq enviornment, easy-streamdeck is added to the global scope as `streamdeck` otherwise it is exported via `module.exports`
4 |
5 | ## Common
6 | Members shared by both the plugin/background and PropertyInspector/foreground instances.
7 |
8 | ### Properties
9 | Properties are read-only
10 |
11 | | Property | Type | Description |
12 | |------------|:------------------------------:|------------------------------------------------------------------|
13 | | `ready` | boolean | `true` if the library is ready, `false` otherwise |
14 | | `port` | number | The port that will be used to connect to Stream Deck's software |
15 | | `id` | string | The current context's UUID/opaqueValue |
16 | | `layer` | string | The current context's layer: `"plugin"` or `"propertyinspector"` |
17 | | `host` | [`host`](#host) | Data related to the host |
18 | | `devices` | array\<[`device`](#device)\> | Tracked connected devices |
19 | | `contexts` | array\<[`context`](#context)\> | Tracked buttons related to the plugin |
20 |
21 |
22 |
23 | ### Methods
24 |
25 | #### `streamdeck.on`
26 | Adds an event listener
27 |
28 | | Arguments | Type | Description |
29 | |-----------|:--------:|----------------------------------------------------------------|
30 | | `event` | string | The event to listen for |
31 | | `handler` | function | The callback to handle the event |
32 | | `once` | boolean | If true the handler will be removed after the event is emitted |
33 |
34 |
35 |
36 | #### `streamdeck.off`
37 | Adds an event listener to the streamdeck instance.
38 |
39 | | Arguments\* | Type | Description |
40 | |-------------|:--------:|----------------------------------------------------------------|
41 | | `event` | string | The event to listen for |
42 | | `handler` | function | The callback to handle the event |
43 | | `once` | boolean | If true the handler will be removed after the event is emitted |
44 |
45 | \*: Arguments must match those used to create the listener exactly
46 |
47 |
48 |
49 | #### `streamdeck.once`
50 | Alias for `streamdeck.on(event, handler, true)`
51 |
52 | | Arguments | Type | Description |
53 | |------------|:--------:|----------------------------------------------------------------|
54 | | `event` | string | The event to listen for |
55 | | `handler` | function | The callback to handle the event |
56 |
57 |
58 |
59 | #### `streamdeck.nonce`
60 | Alias for `streamdeck.off(event, handler, true)`
61 |
62 | | Arguments\* | Type | Description |
63 | |-------------|:--------:|----------------------------------------------------------------|
64 | | `event` | string | The event to listen for |
65 | | `handler` | function | The callback to handle the event |
66 |
67 | \*: Arguments must match those used to create the listener exactly
68 |
69 |
70 |
71 | #### `streamdeck.openUrl`
72 | Tell Stream Deck's software to open the specified url in the native default browser
73 |
74 | | Arguments | Type | Description |
75 | |------------|:------:|----------------------------|
76 | | `url` | string | The URL to open |
77 |
78 |
79 |
80 | #### `streamdeck.send`
81 | JSON stringify's the data and sends the result to Stream Deck's software
82 |
83 | | Arguments | Type | Description |
84 | |------------|:------:|----------------------------|
85 | | `data` | *any* | The data to send |
86 |
87 |
88 |
89 | #### `streamdeck.register`
90 | Registers a callback that an opposing layer can invoke.
91 |
92 | If the callback returns a `Promise`, easy-streamdeck will wait for the promise to resolve before responding with the result, otherwise the returned value is assumed to be the result.
93 |
94 | | Arguments | Type | Description |
95 | |-----------|:--------:|------------------------------------------------|
96 | | `method` | string | A unique name identifying the method |
97 | | `handler` | function | The callback function to handle the invocation |
98 |
99 |
100 |
101 | #### `streamdeck.unregister`
102 | Unregisters a callback that an opposing layer could invoke.
103 |
104 | | Arguments\* | Type | Description |
105 | |-------------|:--------:|------------------------------------------------|
106 | | `method` | string | A unique name identifying the method |
107 | | `handler` | function | The callback function to handle the invocation |
108 |
109 | \*: Arguments must exactly match those used when registering
110 |
111 |
112 |
113 | ### Events
114 | All events are emitted with a single [Event]() instance argument
115 |
116 | #### `websocket:ready`
117 | Emitted when the underlying websocket connection to the streamdeck software connects
118 |
119 | `this` refers to the streamdeck instance
120 |
121 |
122 |
123 | #### `websocket:message`
124 | Emitted when a message is received from the streamdeck software websocket connection.
125 | This event is NOT emitted if the message contains a streamdeck event
126 |
127 | `this` refers to the streamdeck instance
128 |
129 | | `` Property | Type | Description |
130 | |-------------------------|:------:|------------------|
131 | | | String | The message data |
132 |
133 |
134 |
135 | #### `websocket:close`
136 | Emitted when the underlying websocket connection to the streamdeck software connects
137 |
138 | `this` refers to the streamdeck instance
139 |
140 | | `` Property | Type | Description |
141 | |-------------------------|:------:|-------------------------------------------|
142 | | `code` | Number | The close code |
143 | | `reason` | String | A plain text decription of the close code |
144 |
145 |
146 |
147 | #### `websocket:error`
148 | Emitted when the underlying websocket connection suffers from either a protocol or connection error.
149 |
150 | `this` refers to the streamdeck instance
151 |
152 |
153 |
154 | #### `ready`
155 | Emitted when easy-streamdeck is ready
156 |
157 | `this` refers to the streamdeck instance
158 |
159 |
160 |
161 | ## Plugin/Background
162 | Members specific to the background instance
163 |
164 | ### `streamdeck.Context`
165 | [Context](#context) class used to create arbitrary context instances.
166 |
167 | | Arguments | Type | Description |
168 | |-----------|:------:|-------------------------------------------|
169 | | `id` | string | The context id identifying the context |
170 | | `action` | string | The action the context is associated with |
171 |
172 |
173 |
174 | ### Methods
175 |
176 | #### `streamdeck.switchToProfile`
177 | *`Background-Only`*
178 |
179 | Tell streamdeck to switch to a predefined profile
180 |
181 | | Argument | Type | Description |
182 | |---------------|:------:|-----------------------------------------------------------------------|
183 | | `profileName` | string | The exact profile name as it is defined in the plugin's manifest.json |
184 |
185 |
186 |
187 | ### Events
188 | All events are emitted with a single [`Event`](#event) instance argument.
189 |
190 | #### `application:launch`
191 | Emitted when a monitored application is launched
192 |
193 | `this` refers to the Stream Deck instance
194 |
195 | | `` Property | Type | Description |
196 | |-------------------------|:------:|--------------------------|
197 | | | String | The application launched |
198 |
199 |
200 |
201 | #### `application:terminate`
202 | Emitted when a monitored application is terminated
203 |
204 | `this` refers to the Stream Deck instance
205 |
206 | | `` Property | Type | Description |
207 | |-------------------------|:------:|--------------------------|
208 | | | String | The application launched |
209 |
210 |
211 |
212 | #### `application`
213 | Emitted when a monitored application is launched or terminated
214 |
215 | `this` refers to the Stream Deck instance
216 |
217 | | `` Property | Type | Description |
218 | |-------------------------|:------:|--------------------------------|
219 | | `event` | String | `"launched"` or `"terminated"` |
220 | | `application` | String | The monitor application |
221 |
222 |
223 |
224 | #### `device:connect`
225 | Emitted when a streamdeck device is connected
226 |
227 | `this` refers to the Stream Deck instance
228 |
229 | | `` Property | Type | Description |
230 | |-------------------------|:-------------------:|--------------------------|
231 | | | [`Device`](#device) | The application launched |
232 |
233 |
234 |
235 | #### `device:disconnect`
236 | Emitted when a streamdeck device is disconnected
237 |
238 | `this` refers to the Stream Deck instance
239 |
240 | | `` Property | Type | Description |
241 | |-------------------------|:-------------------:|--------------------------|
242 | | | [`Device`](#device) | The application launched |
243 |
244 |
245 |
246 | #### `device`
247 | Emitted when a monitored application is launched or terminated
248 |
249 | `this` refers to the Stream Deck instance
250 |
251 | | `` Property | Type | Description |
252 | |-------------------------|:-------------------:|-------------------------------|
253 | | `event` | String | `"connect"` or `"disconnect"` |
254 | | `device` | [`Device`](#device) | The device affected |
255 |
256 |
257 |
258 | #### `keypress:down`
259 | Emitted when a button is pressed on the Stream Deck hardware
260 |
261 | `this` refers to the [Context](#context) instance that caused the event
262 |
263 |
264 |
265 |
266 | #### `keypress:up`
267 | Emitted when a pressed button is released on the Stream Deck hardware
268 |
269 | `this` refers to the [Context](#context) instance that caused the event
270 |
271 |
272 |
273 |
274 | #### `keypress`
275 | Emitted when a button is either pressed or released
276 |
277 | `this` refers to the [Context](#context) instance
278 |
279 | | `` Property | Type | Description |
280 | |-------------------------|:------:|---------------------------------------------------|
281 | | `event` | String | The keypress event that took place |
282 |
283 |
284 |
285 | #### `context:appear`
286 | Emitted when a button related to the plugin will appear on the stream deck hardware
287 |
288 | `this` refers to the [Context](#context) instance that caused the event
289 |
290 |
291 |
292 | #### `context:titlechange`
293 | Emitted when a context's title parameters have changed
294 |
295 | `this` refers to the [Context](#context) instance
296 |
297 | | `` Property | Type | Description |
298 | |-------------------------|:-----------------:|---------------------------------------|
299 | | | [`Title`](#title) | The title before changes were applied |
300 |
301 |
302 |
303 | #### `context:disappear`
304 | Emitted when a context will not longer be displayed on the stream deck hardware
305 |
306 | `this` refers to the [Context](#context) instance
307 |
308 |
309 |
310 | #### `context`
311 | Emitted when an event happens on a context
312 |
313 | `this` refers to the [Context](#context) instance
314 |
315 | | `` Property | Type | Description |
316 | |-------------------------|:-----------------:|-------------------------------------------------------------------------------|
317 | | `event` | String | The event name |
318 | | `previousTitle` | [`Title`](#title) | The title before changes were applied (only included with titlechange events) |
319 |
320 |
321 |
322 | #### `notify:`
323 | Emitted when the foreground sends a notification
324 |
325 | `this` refers to the [Context](#context) instance that sent the notification.
326 | *Bugged: `this` current refers to streamdeck; will be fixed in a near future version*
327 |
328 | | `` Property | Type | Description |
329 | |-------------------------|:-----:|---------------------------------|
330 | | | *any* | Any data accompanying the event |
331 |
332 |
333 |
334 | #### `notify`
335 | Emitted when the foreground sends a notification
336 |
337 | `this` refers to the [Context](#context) instance that sent the notification.
338 | *Bugged: `this` current refers to streamdeck; will be fixed in a near future version*
339 |
340 | | `` Property | Type | Description |
341 | |-------------------------|:-------:|---------------------------------|
342 | | `event` | string | The name of the notify event |
343 | | `data` | *any* | The data accompanying the event |
344 |
345 |
346 |
347 | #### `message`
348 | Emitted when the foreground sends a message to the background via `sendToPlugin`
349 | This event is suppressed if its handled by the Cross-Layer Communication protocol
350 |
351 | `this` refers to the [context](#context) instance that sent the message
352 |
353 | | `` Property | Type | Description |
354 | |-------------------------|:-------:|---------------------------------|
355 | | | *any* | The data accompanying the event |
356 |
357 |
358 |
359 | ## PropertyInspector/Foreground
360 | Members specific to the PropertyInspector/Foreground instance
361 |
362 | ### Properties
363 | Properties are read-only
364 |
365 | | Property | Type | Description |
366 | |-------------|:------:|-------------------------------------------------|
367 | | `contextId` | string | Context id representing the background instance |
368 | | `actionId` | string | ActionId of the foreground |
369 |
370 |
371 |
372 | ### Methods
373 |
374 | #### `streamdeck.sendToPlugin`
375 | Uses `JSON.stringify` and then sends the data to the background layer
376 |
377 | | Arguments | Type | Description |
378 | |------------|:------:|----------------------------|
379 | | `data` | *any* | The data to send |
380 |
381 |
382 |
383 | #### `streamdeck.invoke`
384 | Invokes a method registered on the background layer.
385 |
386 | Returns a `Promise` that is fulfilled when the background layer responds with a result.
387 |
388 | | Arguments | Type | Description |
389 | |-----------|:------:|-----------------------------------------|
390 | | `method` | string | The registered method to invoke |
391 | | `...args` | *any* | Data to pass to the method's invocation |
392 |
393 |
394 |
395 | #### `streamdeck.notify`
396 | Sends a `notify` event to the background layer
397 |
398 | | Arguments | Type | Description |
399 | |-----------|:------:|-----------------------------------|
400 | | `event` | string | The name of the notify event |
401 | | `data` | *any* | Data to pass to the event emitter |
402 |
403 |
404 |
405 | #### `streamdeck.getTitle`
406 | Requests the foreground's title from the background layer.
407 |
408 | Returns a `Promise` that is fulfilled when the background layer responds
409 |
410 |
411 |
412 | #### `streamdeck.setTitle`
413 | Requests the background layer change the foreground's title
414 |
415 | Returns a `Promise` that is fulfilled when the background layer responds
416 |
417 | | Arguments | Type | Description |
418 | |-----------|:--------------:|--------------------------------------------------------------------|
419 | | `title` | string | Text to set the title to. Use an empty string to revert to default |
420 | | `target` | number\|string | (Optional; default: 0) 0: both, 1: hardware, 2: software |
421 |
422 |
423 |
424 | #### `streamdeck.getImage`
425 | Requests the foreground's image from the background layer.
426 |
427 | Returns a `Promise` that is fulfilled when the background layer responds.
428 | *Currently, always results in a rejection as getImage is not supported by Stream Deck's SDK*
429 |
430 |
431 |
432 | #### `streamdeck.setImage`
433 | Requests the background layer change the foreground's image
434 |
435 | Returns a `Promise` that is fulfilled when the background layer responds
436 |
437 | | Arguments | Type | Description |
438 | |-----------|:--------------:|----------------------------------------------------------|
439 | | `image` | string | base64 encoded data url to set as the image |
440 | | `target` | number\|string | (Optional; default: 0) 0: both, 1: hardware, 2: software |
441 |
442 |
443 |
444 | #### `streamdeck.setImageFromURL`
445 | Requests the background layer change the foreground's image
446 |
447 | Returns a `Promise` that is fulfilled when the background layer responds
448 |
449 | | Arguments | Type | Description |
450 | |-----------|:--------------:|----------------------------------------------------------|
451 | | `url` | string | url of image |
452 | | `target` | number\|string | (Optional; default: 0) 0: both, 1: hardware, 2: software |
453 |
454 |
455 |
456 | #### `streamdeck.getState`
457 | Requests the foreground's state from the background layer.
458 |
459 | Returns a `Promise` that is fulfilled when the background layer responds.
460 |
461 |
462 |
463 | #### `streamdeck.setState`
464 | Requests the background layer update the foreground's state
465 |
466 | Returns a `Promise` that is fulfilled when the background layer responds
467 |
468 |
469 |
470 | #### `streamdeck.getSettings`
471 | Requests the foreground's settings from the background layer.
472 |
473 | Returns a `Promise` that is fulfilled when the background layer responds.
474 |
475 | | Arguments | Type | Description |
476 | |-----------|:------:|-------------------------------------|
477 | | `state` | number | The state to set for the foreground |
478 |
479 |
480 |
481 | #### `streamdeck.setSettings`
482 | Requests the background layer update the foreground's settings.
483 |
484 | Returns a `Promise` that is fulfilled when the background layer responds.
485 |
486 | | Arguments | Type | Description |
487 | |------------|:------:|------------------------------------------------------------|
488 | | `settings` | *any* | Settings object to overwrite the currently stored settings |
489 |
490 |
491 |
492 | #### `streamdeck.showAlert`
493 | Requess the background layer show an alert on the foreground's context.
494 |
495 | Returns a `Promise` that is fulfilled when the background layer responds.
496 |
497 |
498 |
499 | #### `streamdeck.showOk`
500 | Requess the background layer show an Ok alert on the foreground's context.
501 |
502 | Returns a `Promise` that is fulfilled when the background layer responds.
503 |
504 |
505 |
506 | ### Events
507 |
508 | #### `notify:`
509 | Emitted when the foreground sends a notification
510 |
511 | `this` refers to the Stream Deck instance
512 |
513 | | `` Property | Type | Description |
514 | |-------------------------|:-----:|---------------------------------|
515 | | | *any* | Any data accompanying the event |
516 |
517 |
518 |
519 | #### `notify`
520 | Emitted when the foreground sends a notification
521 |
522 | `this` refers to the Stream Deck instance
523 |
524 | | `` Property | Type | Description |
525 | |-------------------------|:-------:|---------------------------------|
526 | | `event` | string | The name of the notify event |
527 | | `data` | *any* | The data accompanying the event |
528 |
529 |
530 |
531 | #### `message`
532 | Emitted when the background sends a message to the foreground via `sendToPropertyInspector`
533 | This event is suppressed if its handled by the Cross-Layer Communication protocol
534 |
535 | `this` refers to the Stream Deck instance
536 |
537 | | `` Property | Type | Description |
538 | |-------------------------|:-------:|---------------------------------|
539 | | | *any* | The data accompanying the event |
540 |
541 |
542 |
543 | # Structures
544 |
545 | ## Host
546 | Describes streamdeck's host enviornment
547 |
548 | | Property\* | Type | Description |
549 | |------------|:------:|------------------------------------------------------|
550 | | `language` | String | The current language Stream Deck's software is using |
551 | | `platform` | String | The platform; `"windows"` or `"mac"` |
552 | | `version` | String | Stream Deck's software version |
553 |
554 | \*: Properties are read-only
555 |
556 |
557 |
558 | ## Device
559 | Describes a streamdeck hardware device
560 |
561 | | Property\* | Type | Description |
562 | |------------|:------:|----------------------------------------------|
563 | | `id` | String | An opaque value used to reference the device |
564 | | `type` | Number | *unknown* |
565 | | `columns` | Number | The number of button columns the device has |
566 | | `rows` | Number | The number of button rows the device has |
567 |
568 | \*: Properties are read-only
569 |
570 |
571 |
572 | ## Title
573 | Describes a context's title
574 |
575 | | Property\* | Type | Description |
576 | |-------------|:-------:|-------------------------------------------------------------------------------------|
577 | | `shown` | Boolean | Indicates if the title is shown |
578 | | `text` | String | The title text |
579 | | `font` | String | The font used to display the title text |
580 | | `style` | String | *unknown* |
581 | | `underline` | Boolean | `true` if the text is to be underlined; `false` otherwise |
582 | | `color` | String | Color used to display the title as a hex color value |
583 | | `alignment` | String | `top`, `middle`, or `bottom` indicating how the title text is aligned on the button |
584 |
585 | \*: Properties are read-only
586 |
587 |
588 |
589 | ## Context
590 | Describes a context
591 |
592 | ### Properties
593 |
594 | | Property\* | Type | Description
595 | |-----------------|:-------------------:|-------------------------------------------------------------------|
596 | | `action` | string | Action id associated with the context |
597 | | `id` | string | An opaque value identifying the context |
598 | | `column` | number | The column the button/context resides |
599 | | `row` | number | The row the button/context resides |
600 | | `device` | [`device`](#deivce) | The device the context is assoicated with |
601 | | `title` | [`title`](#title) | The context's title |
602 | | `settings` | object | Settings stored for the context |
603 | | `state` | number | The current state of the button |
604 | | `inMultiAction` | boolean | `true` the the context is part of a multiaction otherwise `false` |
605 |
606 | \*: Properties are read-only
607 |
608 |
609 |
610 | ### Methods
611 |
612 | #### `.send`
613 | Uses `JSON.stringify` on the data then sends the data from the plugin layer to the property inspector layer
614 |
615 | | Arguments | Type | Description |
616 | |-----------|:------:|------------------|
617 | | `data` | *any* | The data to send |
618 |
619 |
620 |
621 | #### `.setTitle`
622 | Attempts to set the title text for the context
623 |
624 | | Arguments | Type | Description |
625 | |-----------|:------------:|-----------------------------------------------------------------|
626 | | `title` | string\|null | The title text to set; specify null to revert changes |
627 | | `target` | Number | 0(default): Both software and hardare, 1: hardware, 2: software |
628 |
629 |
630 |
631 | #### `.setImage`
632 | Attempts to set the context's image
633 |
634 | | Arguments | Type | Description |
635 | |-----------|:------:|-----------------------------------------------------------------|
636 | | `image` | string | The image as a base64 data URI to use |
637 | | `target` | number | 0(default): Both software and hardare, 1: hardware, 2: software |
638 |
639 |
640 |
641 | #### `.setImageFromUrl`
642 | Attempts to set the context's image
643 |
644 | | Arguments | Type | Description |
645 | |-----------|:------:|-----------------------------------------------------------------|
646 | | `url` | string | The image url to load |
647 | | `target` | number | 0(default): Both software and hardare, 1: hardware, 2: software |
648 |
649 |
650 |
651 | #### `.setState`
652 | Sets the context to a predefined state
653 |
654 | | Arguments | Type | Description |
655 | |-----------|:------:|-----------------------------------------------|
656 | | `state` | number | The 0-based state index to set the context to |
657 |
658 |
659 |
660 | #### `.setSettings`
661 | Stores a settings object for the context
662 |
663 | | Arguments | Type | Description |
664 | |------------|:------:|-------------------|
665 | | `settings` | object | Settings to store |
666 |
667 |
668 |
669 | #### `.showAlert`
670 | Shows the alert icon on the context for a few moments
671 |
672 |
673 |
674 | #### `.showOk`
675 | Shows the ok icon on the context for a few moments
676 |
677 |
678 |
679 | #### `.invoke`
680 | Invokes a registered method on the context.
681 |
682 | Returns a `Promise` that is fulfilled when the context responds
683 |
684 | | Arguments | Type | Description |
685 | |-----------|:------:|--------------------------------------|
686 | | `method` | string | The registered method name |
687 | | `...args` | *any* | The arguments to pass to the handler |
688 |
689 |
690 |
691 | #### `.notify`
692 | Raises a notify event on the context
693 |
694 | | Arguments | Type | Description |
695 | |-----------|:------:|---------------------------------|
696 | | `event` | string | The name of the event to emit |
697 | | `data` | *any* | The data to accompany the event |
698 |
699 |
700 |
701 | ## Event
702 | Passed as the only argument to event handlers when an event is emitted
703 |
704 | #### `.stop()`
705 | If called, no other event handlers will be called for the emitted event instance
706 |
707 | #### `.data`
708 | The data accompanying the event; the value varies dependant on the event being emitted
709 |
710 |
711 |
712 | # NodeJS
713 |
714 | #### `.start()
715 | Begins the start up process for the streamdeck instance
716 |
717 | | Arguments | Type | Description |
718 | |--------------|:-----------------------:|----------------------------------------------------------------------------|
719 | | `port` | number | Websocket port of which to connect |
720 | | `uuid` | sting | The instances UUID |
721 | | `registerAs` | string | The register event; either `registerPlugin` or `registerPropertyInspector` |
722 | | `hostInfo` | [`HostInfo`](https://developer.elgato.com/documentation/stream-deck/sdk/registration-procedure/#info-parameter) | The host info |
723 | | `selfInfo` | [`SelfInfo`](https://developer.elgato.com/documentation/stream-deck/sdk/registration-procedure/#inapplicationinfo-parameter) | Only specified if this is acting as a PropertyInspector instance |
724 |
725 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019, SReject
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4 |
5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
6 |
7 | Source: http://opensource.org/licenses/ISC
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # easy-streamdeck
2 | An abstraction layer for Elgato's Stream Deck plugin SDK
3 |
4 | # Help
5 | Have questions? ask on Stream Deck's [Community Ran Discord](https://discord.gg/4gYyuxy)
6 |
7 | # Usage
8 |
9 | ### Install
10 | ```
11 | npm install --save easy-streamdeck-sdk
12 | ```
13 |
14 | ### Build For Browser
15 | ```
16 | npm install -g browserify
17 | npm run build
18 | ```
19 |
20 | ### Use in NodeJs
21 | Simply require the package, then call the `streamdeck.start()` function as detailed in the api.md
22 |
23 | ### Include in Browser
24 |
25 | After building, include the easy-streamdeck.js file as the first resource to be loaded by your plugin
26 |
27 | ```html
28 |
29 |
30 | ```
31 |
32 | # API
33 | Documentation for the api can be found in **API.md**
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | This is a recreation of Elgato's Stream Deck Counter plugin wrote to make use of easy-streamdeck.js
--------------------------------------------------------------------------------
/example/action.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | easy-streamDeck - Action Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Change Value
15 |
16 | 0
17 | 1
18 | 2
19 | 3
20 | 4
21 | 5
22 | 6
23 | 7
24 | 8
25 | 9
26 |
27 |
28 |
29 |
Change Value
30 |
31 |
32 |
33 |
34 |
35 |
Background
36 |
37 | Original Background
38 | Red Background
39 | Blue Background
40 | Yellow Background
41 |
42 |
43 |
44 |
100 |
101 |
--------------------------------------------------------------------------------
/example/background.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | easy-streamdeck - Plugin Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
57 |
58 |
--------------------------------------------------------------------------------
/example/easy-streamdeck-v2.0.1.js:
--------------------------------------------------------------------------------
1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;iself.setImage(res,target),()=>{}).catch(()=>{})}setState(state){if(!util.isNumber(state,{while:true,min:0})){throw new TypeError("invalid state argument")}streamdeck.send({event:"setState",context:this.id,payload:{state:state}})}setSettings(settings){streamdeck.send({event:"setSettings",context:this.id,payload:settings})}showAlert(){streamdeck.send({event:"showAlert",context:this.id})}showOk(){streamdeck.sendJSON({event:"showAlert",context:this.id})}}return Context}module.exports=contextWrapper},{"../common/boilers.js":5,"../common/utils.js":9}],3:[function(require,module,exports){const util=require("../common/utils.js");const irnClient=require("../common/irn-client.js");const onmessage=require("./onmessage.js");const context=require("./context.js");function background(streamdeck,deviceList){const contextList={};const irn=irnClient(streamdeck);Object.defineProperties(streamdeck,{onMessage:{value:onmessage.call(streamdeck,deviceList,contextList)},contexts:{enumerable:true,get:function(){return Object.assign({},contextList)}},switchToProfile:{enumerable:true,value:function switchToProfile(profile,device){if(!util.isString(profile)){throw new Error("invalid profile argument")}this.send({event:"switchToProfile",context:this.id,device:device,payload:{profile:profile}})}},Context:{enumerable:true,value:context(streamdeck)}});Object.defineProperties(streamdeck.Context,{invoke:{enumerable:true,value:function invoke(method,...args){let res=irn.invoke(method,...args);this.send(res.result);return res.promise}},notify:{enumerable:true,value:function notify(event,...args){this.send(irn.notify(event,...args))}}});irn.register("$getTitle",function(){return this.title});irn.register("$setTitle",function(title,target){this.setTitle(title,target);return title});irn.register("$getImage",function(){throw new Error("not supported")});irn.register("$setImage",function(image,target){this.setImage(image,target)});irn.register("$setImageFromUrl",function(url,target){this.setImageFromUrl(url,target)});irn.register("$getState",function(){return this.state});irn.register("$setState",function(state){this.setState(state);this.state=state;return state});irn.register("$getSettings",function(){return this.settings});irn.register("$setSettings",function(settings){this.setSettings(settings);console.log(this,settings);return settings});irn.register(`$showAlert`,function(){this.showAlert()});irn.register(`$showOk`,function(){this.showOk()})}module.exports=background},{"../common/irn-client.js":8,"../common/utils.js":9,"./context.js":2,"./onmessage.js":4}],4:[function(require,module,exports){const util=require("../common/utils.js");function onMessageWrapper(deviceList,contextList){let streamdeck=this;return function onmessage(evt){let msg=evt.data;if(msg==null||!util.isString(msg,{match:/^\{[\s\S]+\}$/})){return this.emit("websocket:message",evt.data)}try{msg=JSON.parse(msg)}catch(ignore){return this.emit("websocket:message",evt.data)}let eventName,info;switch(msg.event){case"applicationDidLaunch":case"applicationDidTerminate":if(msg.payload==null||!util.isString(msg.payload.application,{notEmpty:true})){return this.emit("websocket:message",evt.data)}eventName=msg.event==="applicationDidLaunch"?"launch":"terminate";this.emit(`application:${eventName}`,msg.payload.application);this.emit(`application`,{event:eventName,application:msg.payload.application});return;case"deviceDidConnect":case"deviceDidDisconnect":if(!util.isString(msg.device,{notEmpty:true})||msg.deviceInfo.size==null||msg.deviceInfo.size.columns==null||msg.deviceInfo.size.rows==null||!util.isNumber(msg.deviceInfo.type,{whole:true,min:0})||!util.isNumber(msg.deviceInfo.size.columns,{whole:true,min:0})||!util.isNumber(msg.deviceInfo.size.rows,{whole:true,min:0})){return this.emit("websocket:message",evt.data)}info={id:msg.device,type:msg.deviceInfo.type,columns:msg.deviceInfo.size.rows,rows:msg.deviceInfo.size.rows};if(msg.event==="deviceDidConnect"){deviceList[info.id]=Object.assign({},info);eventName="connect"}else{delete deviceList[info.id];eventName="disconnect"}this.emit(`device:${eventName}`,info);this.emit("device",{event:eventName,device:info});return;case"keyUp":case"keyDown":case"willAppear":case"willDisappear":case"titleParametersDidChange":case"sendToPlugin":if(!util.isString(msg.context,{match:/^[A-F\d]{32}$/})||!util.isString(msg.action,{match:/^[^\\\/;%@:]+$/})||msg.payload==null){return this.emit("websocket:message",evt.data)}break;default:return this.emit("websocket:message",evt.data)}let device;if(deviceList[msg.device]!=null){device=Object.assign({},deviceList[msg.device])}else{device={id:msg.device}}let context;if(contextList[msg.context]!=null){context=contextList[msg.context]}else{context=new streamdeck.Context(msg.action,msg.context)}context.action=msg.action;if(msg.event==="sendToPlugin"){return this.emit("message",msg.payload,{self:context})}let params=msg.payload.titleParameters;if(msg.payload.settings==null||msg.payload.coordinates==null||!util.isNumber(msg.payload.coordinates.row,{whole:true,min:0})||!util.isNumber(msg.payload.coordinates.column,{whole:true,min:0})||msg.payload.state!=null&&!util.isNumber(msg.payload.state,{whole:true,min:0})||msg.payload.isInMultiAction!=null&&!util.isBoolean(msg.payload.isInMultiAction)||msg.event==="titleParametersDidChange"&&(!util.isString(msg.payload.title)||params==null||!util.isString(params.fontFamily)||!util.isNumber(params.fontSize,{whole:true,min:6})||!util.isString(params.fontStyle)||!util.isBoolean(params.fontUnderline)||!util.isBoolean(params.showTitle)||!util.isString(params.titleAlignment,{match:/^(?:top|middle|bottom)$/})||!util.isString(params.titleColor,{match:/^#(?:[a-f\d]{1,8})$/}))){return this.emit("websocket:message",evt.data)}context.row=msg.payload.coordinates.row;context.column=msg.payload.coordinates.column;context.device=device;context.settings=msg.payload.settings;if(msg.payload.isInMultiAction!=null){context.isInMultiAction=msg.payload.isInMultiAction}if(msg.payload.state!=null){context.state=msg.payload.state}switch(msg.event){case"keyUp":case"keyDown":eventName=msg.event==="keyUp"?"up":"down";this.emit(`keypress:${eventName}`,null,{self:context});this.emit("keypress",{event:eventName},{self:context});return;case"willAppear":case"willDisappear":if(msg.event==="willAppear"){contextList[context.id]=context;eventName="appear"}else{delete contextList[context.id];eventName="disappear"}this.emit(`context:${eventName}`,null,{self:context});this.emit(`context`,{event:eventName},{self:context});return;case"titleParametersDidChange":info=context.title;context.title={text:msg.payload.title,font:params.fontFamily,style:params.fontStyle,underline:params.fontUnderline,shown:params.showTitle,alignment:params.titleAlignment,color:params.titleColor};this.emit("context:titlechange",info,{self:context});this.emit("context",{event:"titlechange",previousTitle:info},{self:context});return}}}module.exports=onMessageWrapper},{"../common/utils.js":9}],5:[function(require,module,exports){if(typeof WebSocket!=="function"){exports.WebSocket=require("ws")}else{exports.WebSocket=WebSocket}if(typeof HTMLCanvasElement!=="function"){exports.imageToDataUrl=require("image-data-uri")}else{exports.imageToDataUrl=function(url){return new Promise((resolve,reject)=>{let image=new Image;image.onload=function(){let canvas=document.createElement("canvas");canvas.width=image.naturalWidth;canvas.height=image.naturalHeight;let ctx=canvas.getContext("2d");ctx.drawImage(image,0,0);image.onload=null;image.onerror=null;image=null;resolve(canvas.toDataURL("image/png"))};image.onerror=function(){image.onload=null;image.onerror=null;image=null;reject(new Error("image failed to load"))};image.src=url})}}},{"image-data-uri":undefined,ws:undefined}],6:[function(require,module,exports){const Emitter=require("./emitter.js");const{WebSocket:WebSocket}=require("./boilers.js");const $websock=Symbol("ws connection");const $readyState=Symbol("ws readyState");const $spooledMessages=Symbol("ws spooled messages");const $reconnectTimeout=Symbol("ws reconnect timeout");const $reconnectDelay=Symbol("ws reconnect delay");const $addressKey=Symbol("ws address key");let onConnect=false;function cleanup(self){if(self[$websock]!=null){if(self[$websock].readyState<2){self[$websock].close()}self[$websock].onopen=null;self[$websock].onmessage=null;self[$websock].onclose=null;self[$websock].onerror=null;self[$websock]=null;self[$readyState]=0}if(self[$reconnectTimeout]){clearTimeout(self[$reconnectTimeout])}}function reconnect(self){self[$readyState]=1;self[$reconnectTimeout]=setTimeout(self.connect.bind(self),self[$reconnectDelay]);self[$reconnectDelay]*=1.5;if(self[$reconnectDelay]>3e4){self[$reconnectDelay]=3e4}}class Connection extends Emitter{constructor(){super();Object.defineProperty(this,$websock,{writable:true,value:null});Object.defineProperty(this,$readyState,{writable:true,value:0});Object.defineProperty(this,$reconnectDelay,{writable:true,value:1e3});Object.defineProperty(this,$spooledMessages,{writable:true,value:[]})}onOpen(){if(this[$reconnectTimeout]){clearTimeout(this[$reconnectTimeout]);this[$reconnectTimeout]=null;this[$reconnectDelay]=1e3}this[$readyState]=2;onConnect=true;this.emit("websocket:connect");onConnect=false;if(this[$spooledMessages].length){this[$spooledMessages].forEach(msg=>this[$websock].send(msg));this[$spooledMessages]=[]}this[$readyState]=3;this.emit("websocket:ready")}onMessage(evt){this.emit("websocket:message",evt.data)}onClose(evt){let reason;switch(evt.code){case 1e3:reason="Normal Closure. The purpose for which the connection was established has been fulfilled.";break;case 1001:reason='Going Away. An endpoint is "going away", such as a server going down or a browser having navigated away from a page.';break;case 1002:reason="Protocol error. An endpoint is terminating the connection due to a protocol error";break;case 1003:reason="Unsupported Data. An endpoint received a type of data it doesn't support.";break;case 1004:reason="--Reserved--. The specific meaning might be defined in the future.";break;case 1005:reason="No Status. No status code was actually present.";break;case 1006:reason="Abnormal Closure. The connection was closed abnormally, e.g., without sending or receiving a Close control frame";break;case 1007:reason="Invalid frame payload data. The connection was closed, because the received data was not consistent with the type of the message (e.g., non-UTF-8 [http://tools.ietf.org/html/rfc3629]).";break;case 1008:reason='Policy Violation. The connection was closed, because current message data "violates its policy". This reason is given either if there is no other suitable reason, or if there is a need to hide specific details about the policy.';break;case 1009:reason="Message Too Big. Connection closed because the message is too big for it to process.";break;case 1010:reason="Mandatory Ext. Connection is terminated the connection because the server didn't negotiate one or more extensions in the WebSocket handshake. Mandatory extensions were: "+evt.reason;break;case 1011:reason="Internl Server Error. Connection closed because it encountered an unexpected condition that prevented it from fulfilling the request.";break;case 1015:reason="TLS Handshake. The connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified).";break;default:reason="Unknown reason";break}cleanup(this);this.emit(`websocket:close`,{code:evt.code,reason:reason});reconnect(this)}onError(){cleanup(this);this.emit("websocket:error");reconnect(this)}connect(address){if(this[$websock]){return this}if(address!=null){if(this[$addressKey]==null){Object.defineProperty(this,$addressKey,{value:address})}else{this[$addressKey]=address}}this[$readyState]=1;this[$websock]=new WebSocket(this[$addressKey]);this[$websock].onopen=this.onOpen.bind(this);this[$websock].onmessage=this.onMessage.bind(this);this[$websock].onerror=this.onError.bind(this);this[$websock].onclose=this.onClose.bind(this);return this}send(data){data=JSON.stringify(data);if(onConnect===true||this[$readyState]===3&&!this[$spooledMessages].length){this[$websock].send(data)}else{this[$spooledMessages].push(data)}return this}}module.exports=Connection},{"./boilers.js":5,"./emitter.js":7}],7:[function(require,module,exports){const util=require("./utils.js");const $eventListenersKey=Symbol("event listeners");class Emitter{constructor(){Object.defineProperty(this,$eventListenersKey,{value:{}})}on(event,handler,isOnce){if(!util.isString(event,{notEmpty:true})){throw new TypeError("invalid name argument")}if(!util.isCallable(handler)){throw new TypeError("invalid handler argument")}if(isOnce!=null&&!util.isBoolean(isOnce)){throw new TypeError("invalid isOnce argument")}if(this[$eventListenersKey][event]==null){this[$eventListenersKey][event]=[]}this[$eventListenersKey][event].push({handler:handler,once:isOnce==null?false:isOnce});return this}off(event,handler,isOnce){if(!util.isString(event,{notEmpty:true})){throw new TypeError("invalid name argument")}if(!util.isCallable(handler)){throw new TypeError("invalid handler argument")}if(isOnce!=null&&!util.isBoolean(isOnce)){throw new TypeError("invalid isOneTimeHandler argument")}let listeners=self[$eventListenersKey][event];if(listeners==null||!listeners.length){return}let idx=listeners.length;do{idx-=1;let listener=listeners[idx];if(listener.handler===handler&&listener.once===isOnce){listeners.splice(idx,1);break}}while(idx>0);return this}once(event,handler){return this.on(event,handler,true)}nonce(event,handler){return this.off(event,handler,true)}emit(event,data,options){if(!util.isString(event,{notEmpty:true})){throw new TypeError("invalid event name")}if(this[$eventListenersKey]==null||this[$eventListenersKey][event]==null||this[$eventListenersKey][event].length===0){return this}options=options==null?{}:options;let self=this,listeners=this[$eventListenersKey][event],stopped=false,evt=Object.create(null),idx=0;Object.defineProperties(evt,{stop:{enumerable:true,value:function stop(){stopped=true}},data:{enumerable:true,value:data}});while(idx{this[sendProp](format(data.id,"response","ok",res))},err=>{this[sendProp](format(data.id,"response","error",err instanceof Error?err.message:String(err)===err?err:"unknown error"))}).catch(err=>{this[sendProp](format(data.id,"response","error",err instanceof Error?err.message:String(err)===err?err:"unknown error"))})}catch(err){this[sendProp](format(data.id,"response","error",err.message))}}break}evt.stop()});return{invoke:function(method,...args){let id=genId();return{promise:new Promise((resolve,reject)=>{$pending[id]={resolve:resolve,reject:reject,timeout:setTimeout(function(){delete $pending[id];reject(new Error("invoke timed out"))},3e4)}}),result:format(id,"invoke",method,args)}},notify:function(event,data){return format(reserved,"notify",event,data)},register:registerMethod}}module.exports=irnClient},{"./utils.js":9}],9:[function(require,module,exports){"use strict";const hasOwnProperty=Object.prototype.hasOwnProperty;function isBoolean(subject){return subject===true||subject===false}function isNumber(subject,opts={}){if(typeof subject!=="number"||Number(subject)!==subject){return false}if(!opts.allowNaN&&isNaN(subject)){return false}if(!opts.allowInfinity&&!isFinite(subject)){return false}if(opts.min&&subjectopts.max){return false}if(opts.whole&&subject%1>0){return false}return true}function isString(subject,opts={}){if(typeof subject!=="string"||String(subject)!==subject){return false}if(opts.notEmpty&&subject===""){return false}if(opts.match&&!opts.match.test(subject)){return false}return true}function isBase64(subject,options={}){if(!isString(subject,{notEmpty:true})){return false}let char62=options["62"]!=null?options["62"]:"+",char63=options["63"]!=null?options["63"]:"/";if(!isString(char62,{notEmpty:true,matches:/^[+._~-]$/i})){throw new TypeError("specified 62nd character invalid")}if(!isString(char63,{notEmpty:true,matches:/^[^\/_,:-]$/i})){throw new TypeError("specified 63rd character invalid")}switch(char62+char63){case"+/":case"+,":case"._":case".-":case"_:":case"_-":case"~-":case"-_":break;default:throw new TypeError("invalid 62nd and 63rd character pair")}char62="\\"+char62;char63="\\"+char63;let match=new RegExp(`^(?:[a-z\\d${char62}${char63}]{4})*(?:[a-z\\d${char62}${char63}]{2}(?:[a-z\\d${char62}${char63}]|=)=)?$`,"i");return match.test(subject)}function isArray(subject){return Array.isArray(subject)&&subject instanceof Array}function isKey(subject,key){return hasOwnProperty.call(subject,key)}const isCallable=function(){let fnToStr=Function.prototype.toString,fnClass="[object Function]",toStr=Object.prototype.toString,genClass="[object GeneratorFunction]",hasToStringTag=typeof Symbol==="function"&&typeof Symbol.toStringTag==="symbol",constructorRegex=/^\s*class\b/;function isES6ClassFn(value){try{let fnStr=fnToStr.call(value);return constructorRegex.test(fnStr)}catch(e){return false}}function tryFunctionObject(value){try{if(isES6ClassFn(value)){return false}fnToStr.call(value);return true}catch(e){return false}}return function isCallable(value){if(!value){return false}if(typeof value!=="function"&&typeof value!=="object"){return false}if(typeof value==="function"&&!value.prototype){return true}if(hasToStringTag){return tryFunctionObject(value)}if(isES6ClassFn(value)){return false}let strClass=toStr.call(value);return strClass===fnClass||strClass===genClass}}();const deepFreeze=function(){function freeze(obj,freezing){Object.keys(obj).forEach(key=>{let desc=Object.getOwnPropertyDescriptor(obj,key);if(!isKey(desc,"value")){return}let value=obj[key];if(value!=null&&!Object.isFrozen(value)&&value instanceof Object&&freezing.findIndex(item=>item===value)===-1){freezing.push(value);obj[key]=freeze(value,freezing);freezing.pop(value)}});return Object.freeze(obj)}return function deepFreeze(subject){return freeze(subject,[subject])}}();module.exports=Object.freeze({isBoolean:isBoolean,isNumber:isNumber,isString:isString,isBase64:isBase64,isArray:isArray,isKey:isKey,isCallable:isCallable,deepFreeze:deepFreeze})},{}],10:[function(require,module,exports){const onmessage=require("./onmessage.js");const irnClient=require("../common/irn-client.js");function foreground(streamdeck,selfinfo){let irn=irnClient(streamdeck);Object.defineProperties(streamdeck,{onMessage:{enumerable:true,value:onmessage},contextId:{enumerable:true,value:selfinfo.context},actionId:{enumerable:true,value:selfinfo.action},sendToPlugin:{enumerable:true,value:function sendToPlugin(data){streamdeck.send({event:"sendToPlugin",action:streamdeck.actionId,context:streamdeck.id,payload:data})}},invoke:{enumerable:true,value:function invoke(method,...args){let res=irn.invoke(method,...args);this.sendToPlugin(res.result);return res.promise}},notify:{enumerable:true,value:function notify(event,...args){this.sendToPlugin(irn.notify(event,...args))}},getTitle:{enumerable:true,value:function getTitle(){return this.invoke("$getTitle")}},setTitle:{enumerable:true,value:function setTitle(title,target){return this.invoke("$setTitle",title,target)}},getImage:{enumerable:true,value:function getImage(){return Promise.reject(new Error("not supported"))}},setImage:{enumerable:true,value:function setImage(image,target){return this.invoke("$setImage",image,target)}},setImageFromUrl:{enumerable:true,value:function setImageFromUrl(url,target){return this.invoke("$setImageToUrl",url,target)}},getState:{enumerable:true,value:function getState(){return this.invoke("$getState")}},setState:{enumerable:true,value:function setState(state){return this.invoke("$setState",state)}},getSettings:{enumerable:true,value:function getSettings(){return this.invoke("$getSettings")}},setSettings:{enumerable:true,value:function setSettings(settings){return this.invoke("$setSettings",settings)}},showAlert:{enumerable:true,value:function showAlert(){return this.invoke("$showAlert")}},showOk:{enumerable:true,value:function showOk(){return this.invoke("$showOk")}}})}module.exports=foreground},{"../common/irn-client.js":8,"./onmessage.js":11}],11:[function(require,module,exports){const util=require("../common/utils.js");function onmessage(evt){let msg=evt.data;if(msg==null||!util.isString(msg,{match:/^\{[\s\S]+\}$/})){return this.emit("websocket:message",evt.data)}try{msg=JSON.parse(msg)}catch(ignore){return this.emit("websocket:message",evt.data)}if(!util.isString(msg.event,{match:/^sendToPropertyInspector$/})){return this.emit("websocket:message",evt.data)}this.emit("message",msg.payload)}module.exports=onmessage},{"../common/utils.js":9}],12:[function(require,module,exports){const util=require("./common/utils.js");const Connection=require("./common/connection.js");const background=require("./background");const foreground=require("./foreground");const $ready=Symbol("ready");const $port=Symbol("port");const $id=Symbol("instance identifier");const $register=Symbol("registerEvent");const $layer=Symbol("layer");const $host=Symbol("host");const $deviceList=Symbol("device list");class StreamDeck extends Connection{on(event,handler,once){if(event==="ready"){if(this.ready){handler.call(this);return}once=true}return super.on(event,handler,once)}off(event,handler,once){if(event==="ready"){once=true}return super.off(event,handler,once)}constructor(){super();Object.defineProperty(this,$ready,{writable:true,value:false});Object.defineProperties(this,{ready:{enumerable:true,get:function(){return this[$ready]}},port:{enumerable:true,get:function(){return this[$id]}},id:{enumerable:true,get:function(){return this[$id]}},layer:{enumerable:true,get:function(){return this[$layer]}},host:{enumerable:true,get:function(){return Object.assign({},this[$host])}},devices:{enumerable:true,get:function(){return JSON.parse(JSON.stringify(this[$deviceList]))}}})}openUrl(url){if(!util.isString(url,{notEmpty:true})){throw new TypeError("invalid url")}this.send({event:"openUrl",payload:{url:url}})}start(port,id,register,hostinfo,selfinfo){console.log("start called");if(this[$ready]!==false){throw new Error("start() function already called")}let readyDesc=Object.getOwnPropertyDescriptor(this,$ready);readyDesc.value=true;readyDesc.writable=false;if(util.isString(port,{match:/^\d+$/i})){port=Number(port)}if(!util.isNumber(port,{whole:true,min:0,max:65535})){throw new TypeError("invalid port argument")}if(!util.isString(id,{match:/^(?:(?:[A-F\d]+-){4}[A-F\d]+)$/})){throw new TypeError("invalid uuid argument")}if(!util.isString(register,{match:/^register(?:Plugin|PropertyInspector)$/})){throw new TypeError("invalid registerEvent argument")}if(util.isString(hostinfo)){try{hostinfo=JSON.parse(hostinfo)}catch(e){throw new TypeError("invalid hostInfo argument")}}if(hostinfo==null||!util.isKey(hostinfo,"application")||!util.isKey(hostinfo.application,"language")||!util.isString(hostinfo.application.language)||!util.isKey(hostinfo.application,"platform")||!util.isString(hostinfo.application.platform)||!util.isKey(hostinfo.application,"version")||!util.isString(hostinfo.application.version)||!util.isKey(hostinfo,"devices")||!util.isArray(hostinfo.devices)){throw new TypeError("invalid environment argument")}let deviceList={};hostinfo.devices.forEach(device=>{if(device==null||!util.isString(device.id,{match:/^[A-F\d]{32}$/})||device.size==null||!util.isNumber(device.size.rows,{whole:true,min:1})||!util.isNumber(device.size.columns,{whole:true,min:1})||device.type!=null&&!util.isNumber(device.type,{whole:true,min:0})){throw new TypeError("invalid device list")}deviceList[device.id]={id:device.id,rows:device.size.rows,columns:device.size.columns,type:device.type}});if(register==="registerPropertyInspector"){if(util.isString(selfinfo)){try{selfinfo=JSON.parse(selfinfo)}catch(e){throw new TypeError("invalid selfInfo argument")}}if(selfinfo==null||!util.isString(selfinfo.context,{match:/^[A-F\d]{32}$/})||!util.isString(selfinfo.action,{notEmpty:true})){throw new TypeError("invalid selfInfo argument")}}else if(selfinfo!=null){throw new TypeError("selfinfo specified for plugin")}Object.defineProperty(this,$port,{value:port});Object.defineProperty(this,$id,{value:id});Object.defineProperty(this,$register,{value:register});Object.defineProperty(this,$layer,{value:register==="registerPlugin"?"plugin":"propertyinspector"});Object.defineProperty(this,$host,{value:hostinfo.application});Object.defineProperty(this,$deviceList,{value:deviceList});if(this[$layer]==="plugin"){background(this,deviceList)}else{foreground(this,selfinfo)}let self=this;this.connect(`ws://localhost:${port}`);this.on("websocket:connect",function(evt){evt.stop();self.send({event:register,uuid:id})});this.emit("ready")}}module.exports=StreamDeck},{"./background":3,"./common/connection.js":6,"./common/utils.js":9,"./foreground":10}]},{},[1]);
2 |
--------------------------------------------------------------------------------
/example/icons/actionDefaultImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/actionDefaultImage.png
--------------------------------------------------------------------------------
/example/icons/actionDefaultImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/actionDefaultImage@2x.png
--------------------------------------------------------------------------------
/example/icons/actionDefaultImage_blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/actionDefaultImage_blue.png
--------------------------------------------------------------------------------
/example/icons/actionDefaultImage_blue@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/actionDefaultImage_blue@2x.png
--------------------------------------------------------------------------------
/example/icons/actionDefaultImage_red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/actionDefaultImage_red.png
--------------------------------------------------------------------------------
/example/icons/actionDefaultImage_red@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/actionDefaultImage_red@2x.png
--------------------------------------------------------------------------------
/example/icons/actionDefaultImage_yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/actionDefaultImage_yellow.png
--------------------------------------------------------------------------------
/example/icons/actionDefaultImage_yellow@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/actionDefaultImage_yellow@2x.png
--------------------------------------------------------------------------------
/example/icons/actionIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/actionIcon.png
--------------------------------------------------------------------------------
/example/icons/actionIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/actionIcon@2x.png
--------------------------------------------------------------------------------
/example/icons/caret.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/example/icons/pluginIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/pluginIcon.png
--------------------------------------------------------------------------------
/example/icons/pluginIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SReject/easy-streamdeck/7521755b9ca5d0add5df679fb07f15ed44fc2a6a/example/icons/pluginIcon@2x.png
--------------------------------------------------------------------------------
/example/sdpi.css:
--------------------------------------------------------------------------------
1 | html {
2 | --sdpi-bgcolor: #2D2D2D;
3 | --sdpi-background: #3D3D3D;
4 | --sdpi-color: #d8d8d8;
5 | --sdpi-bordercolor: #3a3a3a;
6 | --sdpi-borderradius: 0px;
7 | --sdpi-width: 224px;
8 | --sdpi-fontweight: 600;
9 | --sdpi-letterspacing: -0.25pt;
10 | height: 100%;
11 | width: 100%;
12 | overflow: hidden;
13 | }
14 |
15 | html, body {
16 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
17 | font-size: 9pt;
18 | background-color: var(--sdpi-bgcolor);
19 | color: #9a9a9a;
20 | }
21 |
22 | body {
23 | height: 100%;
24 | padding: 0;
25 | overflow-x: hidden;
26 | overflow-y: auto;
27 | margin: 0;
28 | -webkit-overflow-scrolling: touch;
29 | -webkit-text-size-adjust: 100%;
30 | -webkit-font-smoothing: antialiased;
31 | }
32 |
33 | mark {
34 | background-color: var(--sdpi-bgcolor);
35 | color: var(--sdpi-color);
36 | }
37 |
38 | .hidden {
39 | display: none;
40 | }
41 |
42 | hr, hr2 {
43 | -webkit-margin-before: 1em;
44 | -webkit-margin-after: 1em;
45 | border-style: none;
46 | background: var(--sdpi-background);
47 | height: 1px;
48 | }
49 |
50 | hr2,
51 | .sdpi-heading {
52 | display: flex;
53 | flex-basis: 100%;
54 | align-items: center;
55 | color: inherit;
56 | font-size: 9pt;
57 | margin: 8px 0px;
58 | }
59 |
60 | .sdpi-heading::before,
61 | .sdpi-heading::after {
62 | content: "";
63 | flex-grow: 1;
64 | background: var(--sdpi-background);
65 | height: 1px;
66 | font-size: 0px;
67 | line-height: 0px;
68 | margin: 0px 16px;
69 | }
70 |
71 | hr2 {
72 | height: 2px;
73 | }
74 |
75 | hr, hr2 {
76 | margin-left:16px;
77 | margin-right:16px;
78 | }
79 |
80 | .sdpi-item-value,
81 | option,
82 | input,
83 | select,
84 | button {
85 | font-size: 10pt;
86 | font-weight: var(--sdpi-fontweight);
87 | letter-spacing: var(--sdpi-letterspacing);
88 | }
89 |
90 |
91 |
92 | .win .sdpi-item-value,
93 | .win option,
94 | .win input,
95 | .win select,
96 | .win button {
97 | font-size: 11px;
98 | font-style: normal;
99 | letter-spacing: inherit;
100 | font-weight: 100;
101 | }
102 |
103 | .win button {
104 | font-size: 12px;
105 | }
106 |
107 | ::-webkit-progress-value,
108 | meter::-webkit-meter-optimum-value {
109 | border-radius: 2px;
110 | /* background: linear-gradient(#ccf, #99f 20%, #77f 45%, #77f 55%, #cdf); */
111 | }
112 |
113 | ::-webkit-progress-bar,
114 | meter::-webkit-meter-bar {
115 | border-radius: 3px;
116 | background: var(--sdpi-background);
117 | }
118 |
119 | ::-webkit-progress-bar:active,
120 | meter::-webkit-meter-bar:active {
121 | border-radius: 3px;
122 | background: #222222;
123 | }
124 | ::-webkit-progress-value:active,
125 | meter::-webkit-meter-optimum-value:active {
126 | background: #99f;
127 | }
128 |
129 | progress,
130 | progress.sdpi-item-value {
131 | min-height: 5px !important;
132 | height: 5px;
133 | background-color: #303030;
134 | }
135 |
136 | progress {
137 | margin-top: 8px !important;
138 | margin-bottom: 8px !important;
139 | }
140 |
141 | .full progress,
142 | progress.full {
143 | margin-top: 3px !important;
144 | }
145 |
146 | ::-webkit-progress-inner-element {
147 | background-color: transparent;
148 | }
149 |
150 |
151 | .sdpi-item[type="progress"] {
152 | margin-top: 4px !important;
153 | margin-bottom: 12px;
154 | min-height: 15px;
155 | }
156 |
157 | .sdpi-item-child.full:last-child {
158 | margin-bottom: 4px;
159 | }
160 |
161 | .tabs {
162 | /**
163 | * Setting display to flex makes this container lay
164 | * out its children using flexbox, the exact same
165 | * as in the above "Stepper input" example.
166 | */
167 | display: flex;
168 |
169 | border-bottom: 1px solid #D7DBDD;
170 | }
171 |
172 | .tab {
173 | cursor: pointer;
174 | padding: 5px 30px;
175 | color: #16a2d7;
176 | font-size: 9pt;
177 | border-bottom: 2px solid transparent;
178 | }
179 |
180 | .tab.is-tab-selected {
181 | border-bottom-color: #4ebbe4;
182 | }
183 |
184 | select {
185 | -webkit-appearance: none;
186 | -moz-appearance: none;
187 | -o-appearance: none;
188 | appearance: none;
189 | background: url(./icons/caret.svg) no-repeat 97% center;
190 | }
191 |
192 | label.sdpi-file-label,
193 | input[type="button"],
194 | input[type="submit"],
195 | input[type="reset"],
196 | input[type="file"],
197 | input[type=file]::-webkit-file-upload-button,
198 | button,
199 | select {
200 | color: var(--sdpi-color);
201 | border: 1pt solid #303030;
202 | font-size: 8pt;
203 | background-color: var(--sdpi-background);
204 | border-radius: var(--sdpi-borderradius);
205 | }
206 |
207 | label.sdpi-file-label,
208 | input[type="button"],
209 | input[type="submit"],
210 | input[type="reset"],
211 | input[type="file"],
212 | input[type=file]::-webkit-file-upload-button,
213 | button {
214 | border: 1pt solid var(--sdpi-color);
215 | border-radius: var(--sdpi-borderradius);
216 | min-height: 23px !important;
217 | height: 23px !important;
218 | margin-right: 8px;
219 | }
220 |
221 | input[type=number]::-webkit-inner-spin-button,
222 | input[type=number]::-webkit-outer-spin-button {
223 | -webkit-appearance: none;
224 | margin: 0;
225 | }
226 |
227 | input[type="file"] {
228 | border-radius: var(--sdpi-borderradius);
229 | max-width: 220px;
230 | }
231 |
232 | option {
233 | height: 1.5em;
234 | padding: 4px;
235 | }
236 |
237 | /* SDPI */
238 |
239 | .sdpi-wrapper {
240 | overflow-x: hidden;
241 | }
242 |
243 | .sdpi-item {
244 | display: flex;
245 | flex-direction: row;
246 | min-height: 32px;
247 | align-items: center;
248 | margin-top: 2px;
249 | max-width: 344px;
250 | }
251 |
252 | .sdpi-item:first-child {
253 | margin-top:1px;
254 | }
255 |
256 | .sdpi-item:last-child {
257 | margin-bottom: 0px;
258 | }
259 |
260 | .sdpi-item > *:not(.sdpi-item-label):not(meter):not(details) {
261 | min-height: 26px;
262 | padding: 0px 4px 0px 4px;
263 | }
264 |
265 | .sdpi-item > *:not(.sdpi-item-label.empty):not(meter) {
266 | min-height: 26px;
267 | padding: 0px 4px 0px 4px;
268 | }
269 |
270 |
271 | .sdpi-item-group {
272 | padding: 0 !important;
273 | }
274 |
275 | meter.sdpi-item-value {
276 | margin-left: 6px;
277 | }
278 |
279 | .sdpi-item[type="group"] {
280 | display: block;
281 | margin-top: 12px;
282 | margin-bottom: 12px;
283 | /* border: 1px solid white; */
284 | flex-direction: unset;
285 | text-align: left;
286 | }
287 |
288 | .sdpi-item[type="group"] > .sdpi-item-label,
289 | .sdpi-item[type="group"].sdpi-item-label {
290 | width: 96%;
291 | text-align: left;
292 | font-weight: 700;
293 | margin-bottom: 4px;
294 | padding-left: 4px;
295 | }
296 |
297 | dl,
298 | ul,
299 | ol {
300 | -webkit-margin-before: 0px;
301 | -webkit-margin-after: 4px;
302 | -webkit-padding-start: 1em;
303 | max-height: 90px;
304 | overflow-y: scroll;
305 | cursor: pointer;
306 | user-select: none;
307 | }
308 |
309 | table.sdpi-item-value,
310 | dl.sdpi-item-value,
311 | ul.sdpi-item-value,
312 | ol.sdpi-item-value {
313 | -webkit-margin-before: 4px;
314 | -webkit-margin-after: 8px;
315 | -webkit-padding-start: 1em;
316 | width: var(--sdpi-width);
317 | text-align: center;
318 | }
319 |
320 | table > caption {
321 | margin: 2px;
322 | }
323 |
324 | .list,
325 | .sdpi-item[type="list"] {
326 | align-items: baseline;
327 | }
328 |
329 | .sdpi-item-label {
330 | text-align: right;
331 | flex: none;
332 | width: 94px;
333 | padding-right: 4px;
334 | font-weight: 600;
335 | -webkit-user-select: none;
336 | }
337 |
338 | .win .sdpi-item-label,
339 | .sdpi-item-label > small{
340 | font-weight: normal;
341 | }
342 |
343 | .sdpi-item-label:after {
344 | content: ": ";
345 | }
346 |
347 | .sdpi-item-label.empty:after {
348 | content: "";
349 | }
350 |
351 | .sdpi-test,
352 | .sdpi-item-value {
353 | flex: 1 0 0;
354 | /* flex-grow: 1;
355 | flex-shrink: 0; */
356 | margin-right: 14px;
357 | margin-left: 4px;
358 | justify-content: space-evenly;
359 | }
360 |
361 | canvas.sdpi-item-value {
362 | max-width: 144px;
363 | max-height: 144px;
364 | width: 144px;
365 | height: 144px;
366 | margin: 0 auto;
367 | cursor: pointer;
368 | }
369 |
370 | input.sdpi-item-value {
371 | margin-left: 5px;
372 | }
373 |
374 | .sdpi-item-value button,
375 | button.sdpi-item-value {
376 | margin-left: 7px;
377 | margin-right: 19px;
378 | }
379 |
380 | .sdpi-item-value.range {
381 | margin-left: 0px;
382 | }
383 |
384 | table,
385 | dl.sdpi-item-value,
386 | ul.sdpi-item-value,
387 | ol.sdpi-item-value,
388 | .sdpi-item-value > dl,
389 | .sdpi-item-value > ul,
390 | .sdpi-item-value > ol
391 | {
392 | list-style-type: none;
393 | list-style-position: outside;
394 | margin-left: -4px;
395 | margin-right: -4px;
396 | padding: 4px;
397 | border: 1px solid var(--sdpi-bordercolor);
398 | }
399 |
400 | dl.sdpi-item-value,
401 | ul.sdpi-item-value,
402 | ol.sdpi-item-value,
403 | .sdpi-item-value > ol {
404 | list-style-type: none;
405 | list-style-position: inside;
406 | margin-left: 5px;
407 | margin-right: 12px;
408 | padding: 4px !important;
409 | }
410 |
411 | ol.sdpi-item-value,
412 | .sdpi-item-value > ol[listtype="none"] {
413 | list-style-type: none;
414 | }
415 | ol.sdpi-item-value[type="decimal"],
416 | .sdpi-item-value > ol[type="decimal"] {
417 | list-style-type: decimal;
418 | }
419 |
420 | ol.sdpi-item-value[type="decimal-leading-zero"],
421 | .sdpi-item-value > ol[type="decimal-leading-zero"] {
422 | list-style-type: decimal-leading-zero;
423 | }
424 |
425 | ol.sdpi-item-value[type="lower-alpha"],
426 | .sdpi-item-value > ol[type="lower-alpha"] {
427 | list-style-type: lower-alpha;
428 | }
429 |
430 | ol.sdpi-item-value[type="upper-alpha"],
431 | .sdpi-item-value > ol[type="upper-alpha"] {
432 | list-style-type: upper-alpha;
433 | }
434 |
435 | ol.sdpi-item-value[type="upper-roman"],
436 | .sdpi-item-value > ol[type="upper-roman"] {
437 | list-style-type: upper-roman;
438 | }
439 |
440 | ol.sdpi-item-value[type="lower-roman"],
441 | .sdpi-item-value > ol[type="lower-roman"] {
442 | list-style-type: upper-roman;
443 | }
444 |
445 | tr:nth-child(even),
446 | .sdpi-item-value > ul > li:nth-child(even),
447 | .sdpi-item-value > ol > li:nth-child(even),
448 | li:nth-child(even) {
449 | background-color: rgba(0,0,0,.2)
450 | }
451 |
452 | td:hover,
453 | .sdpi-item-value > ul > li:hover:nth-child(even),
454 | .sdpi-item-value > ol > li:hover:nth-child(even),
455 | li:hover:nth-child(even),
456 | li:hover {
457 | background-color: rgba(255,255,255,.1);
458 | }
459 |
460 | td.selected,
461 | td.selected:hover,
462 | li.selected:hover,
463 | li.selected {
464 | color: white;
465 | background-color: #77f;
466 | }
467 |
468 | tr {
469 | border: 1px solid var(--sdpi-bordercolor);
470 | }
471 |
472 | td {
473 | border-right: 1px solid var(--sdpi-bordercolor);
474 | -webkit-user-select: none;
475 | }
476 |
477 | tr:last-child,
478 | td:last-child {
479 | border: none;
480 | }
481 |
482 | .sdpi-item-value.select,
483 | .sdpi-item-value > select {
484 | margin-right: 13px;
485 | margin-left: 4px;
486 | }
487 |
488 | .sdpi-item-child,
489 | .sdpi-item-group > .sdpi-item > input[type="color"] {
490 | margin-top: 0.4em;
491 | margin-right: 4px;
492 | }
493 |
494 | .full,
495 | .full *,
496 | .sdpi-item-value.full,
497 | .sdpi-item-child > full > *,
498 | .sdpi-item-child.full,
499 | .sdpi-item-child.full > *,
500 | .full > .sdpi-item-child,
501 | .full > .sdpi-item-child > *{
502 | display: flex;
503 | flex: 1 1 0;
504 | margin-bottom: 4px;
505 | margin-left: 0px;
506 | width: 100%;
507 |
508 | justify-content: space-evenly;
509 | }
510 |
511 | .sdpi-item-group > .sdpi-item > input[type="color"] {
512 | margin-top: 0px;
513 | }
514 |
515 | ::-webkit-calendar-picker-indicator:focus,
516 | input[type=file]::-webkit-file-upload-button:focus,
517 | button:focus,
518 | textarea:focus,
519 | input:focus,
520 | select:focus,
521 | option:focus,
522 | details:focus,
523 | summary:focus,
524 | .custom-select select {
525 | outline: none;
526 | }
527 |
528 | summary {
529 | cursor: default;
530 | -webkit-user-select: none;
531 | }
532 |
533 | .pointer,
534 | summary .pointer {
535 | cursor: pointer;
536 | }
537 |
538 | details.message {
539 | padding: 4px 18px 4px 12px;
540 | }
541 |
542 | details.message summary {
543 | font-size: 10pt;
544 | font-weight: 600;
545 | min-height: 18px;
546 | }
547 |
548 | details.message:first-child {
549 | margin-top: 4px;
550 | margin-left: 0;
551 | padding-left: 106px;
552 | }
553 |
554 | details.message h1 {
555 | text-align: left;
556 | }
557 |
558 | .message > summary::-webkit-details-marker {
559 | display: none;
560 | }
561 |
562 | .info20,
563 | .question,
564 | .caution,
565 | .info {
566 | background-repeat: no-repeat;
567 | background-position: 70px center;
568 | }
569 |
570 | .info20 {
571 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,20 C4.4771525,20 0,15.5228475 0,10 C0,4.4771525 4.4771525,0 10,0 C15.5228475,0 20,4.4771525 20,10 C20,15.5228475 15.5228475,20 10,20 Z M10,8 C8.8954305,8 8,8.84275812 8,9.88235294 L8,16.1176471 C8,17.1572419 8.8954305,18 10,18 C11.1045695,18 12,17.1572419 12,16.1176471 L12,9.88235294 C12,8.84275812 11.1045695,8 10,8 Z M10,3 C8.8954305,3 8,3.88165465 8,4.96923077 L8,5.03076923 C8,6.11834535 8.8954305,7 10,7 C11.1045695,7 12,6.11834535 12,5.03076923 L12,4.96923077 C12,3.88165465 11.1045695,3 10,3 Z'/%3E%3C/svg%3E%0A");
572 | }
573 |
574 | .info {
575 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M10,8 C9.44771525,8 9,8.42137906 9,8.94117647 L9,14.0588235 C9,14.5786209 9.44771525,15 10,15 C10.5522847,15 11,14.5786209 11,14.0588235 L11,8.94117647 C11,8.42137906 10.5522847,8 10,8 Z M10,5 C9.44771525,5 9,5.44082732 9,5.98461538 L9,6.01538462 C9,6.55917268 9.44771525,7 10,7 C10.5522847,7 11,6.55917268 11,6.01538462 L11,5.98461538 C11,5.44082732 10.5522847,5 10,5 Z'/%3E%3C/svg%3E%0A");
576 | }
577 |
578 | .info2 {
579 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 15 15'%3E%3Cpath fill='%23999' d='M7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,11.6421356 11.6421356,15 7.5,15 Z M7.5,2 C6.67157287,2 6,2.66124098 6,3.47692307 L6,3.52307693 C6,4.33875902 6.67157287,5 7.5,5 C8.32842705,5 9,4.33875902 9,3.52307693 L9,3.47692307 C9,2.66124098 8.32842705,2 7.5,2 Z M5,6 L5,7.02155172 L6,7 L6,12 L5,12.0076778 L5,13 L10,13 L10,12 L9,12.0076778 L9,6 L5,6 Z'/%3E%3C/svg%3E%0A");
580 | }
581 |
582 | .sdpi-more-info {
583 | background-image: linear-gradient(to right, #00000000 0%,#00000040 80%), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpolygon fill='%23999' points='4 7 8 7 8 5 12 8 8 11 8 9 4 9'/%3E%3C/svg%3E%0A");
584 | }
585 | .caution {
586 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' fill-rule='evenodd' d='M9.03952676,0.746646542 C9.57068894,-0.245797319 10.4285735,-0.25196227 10.9630352,0.746646542 L19.7705903,17.2030214 C20.3017525,18.1954653 19.8777595,19 18.8371387,19 L1.16542323,19 C0.118729947,19 -0.302490098,18.2016302 0.231971607,17.2030214 L9.03952676,0.746646542 Z M10,2.25584053 L1.9601405,17.3478261 L18.04099,17.3478261 L10,2.25584053 Z M10,5.9375 C10.531043,5.9375 10.9615385,6.37373537 10.9615385,6.91185897 L10.9615385,11.6923077 C10.9615385,12.2304313 10.531043,12.6666667 10,12.6666667 C9.46895697,12.6666667 9.03846154,12.2304313 9.03846154,11.6923077 L9.03846154,6.91185897 C9.03846154,6.37373537 9.46895697,5.9375 10,5.9375 Z M10,13.4583333 C10.6372516,13.4583333 11.1538462,13.9818158 11.1538462,14.6275641 L11.1538462,14.6641026 C11.1538462,15.3098509 10.6372516,15.8333333 10,15.8333333 C9.36274837,15.8333333 8.84615385,15.3098509 8.84615385,14.6641026 L8.84615385,14.6275641 C8.84615385,13.9818158 9.36274837,13.4583333 10,13.4583333 Z'/%3E%3C/svg%3E%0A");
587 | }
588 |
589 | .question {
590 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M6.77783203,7.65332031 C6.77783203,7.84798274 6.85929281,8.02888914 7.0222168,8.19604492 C7.18514079,8.36320071 7.38508996,8.44677734 7.62207031,8.44677734 C8.02409055,8.44677734 8.29703704,8.20768468 8.44091797,7.72949219 C8.59326248,7.27245865 8.77945854,6.92651485 8.99951172,6.69165039 C9.2195649,6.45678594 9.56233491,6.33935547 10.027832,6.33935547 C10.4256205,6.33935547 10.7006836,6.37695313 11.0021973,6.68847656 C11.652832,7.53271484 10.942627,8.472229 10.3750916,9.1321106 C9.80755615,9.79199219 8.29492188,11.9897461 10.027832,12.1347656 C10.4498423,12.1700818 10.7027991,11.9147157 10.7832031,11.4746094 C11.0021973,9.59857178 13.1254883,8.82415771 13.1254883,7.53271484 C13.1254883,7.07568131 12.9974785,6.65250846 12.7414551,6.26318359 C12.4854317,5.87385873 12.1225609,5.56600048 11.652832,5.33959961 C11.1831031,5.11319874 10.6414419,5 10.027832,5 C9.36767248,5 8.79004154,5.13541531 8.29492187,5.40625 C7.79980221,5.67708469 7.42317837,6.01879677 7.16503906,6.43139648 C6.90689975,6.8439962 6.77783203,7.25130007 6.77783203,7.65332031 Z M10.0099668,15 C10.2713191,15 10.5016601,14.9108147 10.7009967,14.7324415 C10.9003332,14.5540682 11,14.3088087 11,13.9966555 C11,13.7157177 10.9047629,13.4793767 10.7142857,13.2876254 C10.5238086,13.0958742 10.2890379,13 10.0099668,13 C9.72646591,13 9.48726565,13.0958742 9.2923588,13.2876254 C9.09745196,13.4793767 9,13.7157177 9,13.9966555 C9,14.313268 9.10077419,14.5596424 9.30232558,14.735786 C9.50387698,14.9119295 9.73975502,15 10.0099668,15 Z'/%3E%3C/svg%3E%0A");
591 | }
592 |
593 |
594 | .sdpi-more-info {
595 | position: fixed;
596 | left: 0px;
597 | right: 0px;
598 | bottom: 0px;
599 | min-height:16px;
600 | padding-right: 16px;
601 | text-align: right;
602 | -webkit-touch-callout: none;
603 | cursor: pointer;
604 | user-select: none;
605 | background-position: right center;
606 | background-repeat: no-repeat;
607 | border-radius: var(--sdpi-borderradius);
608 | text-decoration: none;
609 | color: var(--sdpi-color);
610 | }
611 |
612 | .sdpi-more-info-button {
613 | display: flex;
614 | align-self: right;
615 | margin-left: auto;
616 | position: fixed;
617 | right: 17px;
618 | bottom: 0px;
619 | }
620 |
621 | details a {
622 | background-position: right !important;
623 | min-height: 24px;
624 | display: inline-block;
625 | line-height: 24px;
626 | padding-right: 28px;
627 | }
628 | input:not([type="range"]),
629 | textarea {
630 | -webkit-appearance: none;
631 | background: var(--sdpi-background);
632 | color: var(--sdpi-color);
633 | font-weight: normal;
634 | font-size: 9pt;
635 | border: none;
636 | margin-top: 2px;
637 | margin-bottom: 2px;
638 | }
639 |
640 | textarea + label {
641 | display: flex;
642 | justify-content: flex-end
643 | }
644 | input[type="radio"],
645 | input[type="checkbox"] {
646 | display: none;
647 | }
648 | input[type="radio"] + label,
649 | input[type="checkbox"] + label {
650 | font-size: 9pt;
651 | color: var(--sdpi-color);
652 | font-weight: normal;
653 | margin-right: 8px;
654 | -webkit-user-select: none;
655 | }
656 |
657 | input[type="radio"] + label:after,
658 | input[type="checkbox"] + label:after {
659 | content: " " !important;
660 | }
661 |
662 | .sdpi-item[type="radio"] > .sdpi-item-value,
663 | .sdpi-item[type="checkbox"] > .sdpi-item-value {
664 | padding-top: 2px;
665 | }
666 |
667 | .sdpi-item[type="checkbox"] > .sdpi-item-value > * {
668 | margin-top: 4px;
669 | }
670 |
671 | .sdpi-item[type="checkbox"] .sdpi-item-child,
672 | .sdpi-item[type="radio"] .sdpi-item-child {
673 | display: inline-block;
674 | }
675 |
676 | .sdpi-item[type="range"] .sdpi-item-value,
677 | .sdpi-item[type="meter"] .sdpi-item-child,
678 | .sdpi-item[type="progress"] .sdpi-item-child {
679 | display: flex;
680 | }
681 |
682 | .sdpi-item[type="range"] .sdpi-item-value {
683 | min-height: 26px;
684 | }
685 |
686 | .sdpi-item[type="range"] .sdpi-item-value span,
687 | .sdpi-item[type="meter"] .sdpi-item-child span,
688 | .sdpi-item[type="progress"] .sdpi-item-child span {
689 | margin-top: -2px;
690 | min-width: 8px;
691 | text-align: right;
692 | user-select: none;
693 | cursor: pointer;
694 | }
695 |
696 | .sdpi-item[type="range"] .sdpi-item-value span {
697 | margin-top: 7px;
698 | text-align: right;
699 | }
700 |
701 | span + input[type="range"] {
702 | display: flex;
703 | max-width: 168px;
704 |
705 | }
706 |
707 | .sdpi-item[type="range"] .sdpi-item-value span:first-child,
708 | .sdpi-item[type="meter"] .sdpi-item-child span:first-child,
709 | .sdpi-item[type="progress"] .sdpi-item-child span:first-child {
710 | margin-right: 4px;
711 | }
712 |
713 | .sdpi-item[type="range"] .sdpi-item-value span:last-child,
714 | .sdpi-item[type="meter"] .sdpi-item-child span:last-child,
715 | .sdpi-item[type="progress"] .sdpi-item-child span:last-child {
716 | margin-left: 4px;
717 | }
718 |
719 | .reverse {
720 | transform: rotate(180deg);
721 | }
722 |
723 | .sdpi-item[type="meter"] .sdpi-item-child meter + span:last-child {
724 | margin-left: -10px;
725 | }
726 |
727 | .sdpi-item[type="progress"] .sdpi-item-child meter + span:last-child {
728 | margin-left: -14px;
729 | }
730 |
731 | .sdpi-item[type="radio"] > .sdpi-item-value > * {
732 | margin-top: 2px;
733 | }
734 |
735 | details {
736 | padding: 8px 18px 8px 12px;
737 | min-width: 86px;
738 | }
739 |
740 | details > h4 {
741 | border-bottom: 1px solid var(--sdpi-bordercolor);
742 | }
743 |
744 | legend {
745 | display: none;
746 | }
747 | .sdpi-item-value > textarea {
748 | padding: 0px;
749 | width: 227px;
750 | margin-left: 1px;
751 | }
752 |
753 | input[type="radio"] + label span,
754 | input[type="checkbox"] + label span {
755 | display: inline-block;
756 | width: 16px;
757 | height: 16px;
758 | margin: 2px 4px 2px 0;
759 | border-radius: 3px;
760 | vertical-align: middle;
761 | background: var(--sdpi-background);
762 | cursor: pointer;
763 | border: 1px solid rgb(0,0,0,.2);
764 | }
765 |
766 | input[type="radio"] + label span {
767 | border-radius: 100%;
768 | }
769 |
770 | input[type="radio"]:checked + label span,
771 | input[type="checkbox"]:checked + label span {
772 | background-color: #77f;
773 | background-image: url(check.svg);
774 | background-repeat: no-repeat;
775 | background-position: center center;
776 | border: 1px solid rgb(0,0,0,.4);
777 | }
778 |
779 | input[type="radio"]:active:checked + label span,
780 | input[type="radio"]:active + label span,
781 | input[type="checkbox"]:active:checked + label span,
782 | input[type="checkbox"]:active + label span {
783 | background-color: #303030;
784 | }
785 |
786 | input[type="radio"]:checked + label span {
787 | background-image: url(rcheck.svg);
788 | }
789 |
790 |
791 | /*
792 | input[type="radio"] + label span {
793 | background: url(buttons.png) -38px top no-repeat;
794 | }
795 |
796 | input[type="radio"]:checked + label span {
797 | background: url(buttons.png) -57px top no-repeat;
798 | }
799 | */
800 |
801 | input[type="range"] {
802 | width: var(--sdpi-width);
803 | height: 30px;
804 | overflow: hidden;
805 | cursor: pointer;
806 | background: transparent !important;
807 | }
808 |
809 | .sdpi-item > input[type="range"] {
810 | margin-left: 8px;
811 | max-width: var(--sdpi-width);
812 | width: var(--sdpi-width);
813 | padding: 0px;
814 | }
815 |
816 | /*
817 | input[type="range"],
818 | input[type="range"]::-webkit-slider-runnable-track,
819 | input[type="range"]::-webkit-slider-thumb {
820 | -webkit-appearance: none;
821 | }
822 | */
823 |
824 | input[type="range"]::-webkit-slider-runnable-track {
825 | height: 5px;
826 | background: #979797;
827 | border-radius: 3px;
828 | padding:0px !important;
829 | border: 1px solid var(--sdpi-background);
830 | }
831 |
832 | input[type="range"]::-webkit-slider-thumb {
833 | position: relative;
834 | -webkit-appearance: none;
835 | background-color: var(--sdpi-color);
836 | width: 12px;
837 | height: 12px;
838 | border-radius: 20px;
839 | margin-top: -5px;
840 | border: none;
841 |
842 | }
843 | input[type="range" i]{
844 | margin: 0;
845 | }
846 |
847 | input[type="range"]::-webkit-slider-thumb::before {
848 | position: absolute;
849 | content: "";
850 | height: 5px; /* equal to height of runnable track or 1 less */
851 | width: 500px; /* make this bigger than the widest range input element */
852 | left: -502px; /* this should be -2px - width */
853 | top: 8px; /* don't change this */
854 | background: #77f;
855 | }
856 |
857 | input[type="color"] {
858 | min-width: 32px;
859 | min-height: 32px;
860 | width: 32px;
861 | height: 32px;
862 | padding: 0;
863 | background-color: var(--sdpi-bgcolor);
864 | flex: none;
865 | }
866 |
867 | ::-webkit-color-swatch {
868 | min-width: 24px;
869 | }
870 |
871 | textarea {
872 | height: 3em;
873 | word-break: break-word;
874 | line-height: 1.5em;
875 | }
876 |
877 | .textarea {
878 | padding: 0px !important;
879 | }
880 |
881 | textarea {
882 | width: 221px; /*98%;*/
883 | height: 96%;
884 | min-height: 6em;
885 | resize: none;
886 | border-radius: var(--sdpi-borderradius);
887 | }
888 |
889 | /* CAROUSEL */
890 |
891 | .sdpi-item[type="carousel"]{
892 |
893 | }
894 |
895 | .sdpi-item.card-carousel-wrapper,
896 | .sdpi-item > .card-carousel-wrapper {
897 | padding: 0;
898 | }
899 |
900 |
901 | .card-carousel-wrapper {
902 | display: flex;
903 | align-items: center;
904 | justify-content: center;
905 | margin: 12px auto;
906 | color: #666a73;
907 | }
908 |
909 | .card-carousel {
910 | display: flex;
911 | justify-content: center;
912 | width: 278px;
913 | }
914 | .card-carousel--overflow-container {
915 | overflow: hidden;
916 | }
917 | .card-carousel--nav__left,
918 | .card-carousel--nav__right {
919 | /* display: inline-block; */
920 | width: 12px;
921 | height: 12px;
922 | border-top: 2px solid #42b883;
923 | border-right: 2px solid #42b883;
924 | cursor: pointer;
925 | margin: 0 4px;
926 | transition: transform 150ms linear;
927 | }
928 | .card-carousel--nav__left[disabled],
929 | .card-carousel--nav__right[disabled] {
930 | opacity: 0.2;
931 | border-color: black;
932 | }
933 | .card-carousel--nav__left {
934 | transform: rotate(-135deg);
935 | }
936 | .card-carousel--nav__left:active {
937 | transform: rotate(-135deg) scale(0.85);
938 | }
939 | .card-carousel--nav__right {
940 | transform: rotate(45deg);
941 | }
942 | .card-carousel--nav__right:active {
943 | transform: rotate(45deg) scale(0.85);
944 | }
945 | .card-carousel-cards {
946 | display: flex;
947 | transition: transform 150ms ease-out;
948 | transform: translatex(0px);
949 | }
950 | .card-carousel-cards .card-carousel--card {
951 | margin: 0 5px;
952 | cursor: pointer;
953 | /* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */
954 | background-color: #fff;
955 | border-radius: 4px;
956 | z-index: 3;
957 | }
958 | .xxcard-carousel-cards .card-carousel--card:first-child {
959 | margin-left: 0;
960 | }
961 | .xxcard-carousel-cards .card-carousel--card:last-child {
962 | margin-right: 0;
963 | }
964 | .card-carousel-cards .card-carousel--card img {
965 | vertical-align: bottom;
966 | border-top-left-radius: 4px;
967 | border-top-right-radius: 4px;
968 | transition: opacity 150ms linear;
969 | width: 60px;
970 | }
971 | .card-carousel-cards .card-carousel--card img:hover {
972 | opacity: 0.5;
973 | }
974 | .card-carousel-cards .card-carousel--card--footer {
975 | border-top: 0;
976 | max-width: 80px;
977 | overflow: hidden;
978 | display: flex;
979 | height: 100%;
980 | flex-direction: column;
981 | }
982 | .card-carousel-cards .card-carousel--card--footer p {
983 | padding: 3px 0;
984 | margin: 0;
985 | margin-bottom: 2px;
986 | font-size: 15px;
987 | font-weight: 500;
988 | color: #2c3e50;
989 | }
990 | .card-carousel-cards .card-carousel--card--footer p:nth-of-type(2) {
991 | font-size: 12px;
992 | font-weight: 300;
993 | padding: 6px;
994 | color: #666a73;
995 | }
996 |
997 |
998 | h1 {
999 | font-size: 1.3em;
1000 | font-weight: 500;
1001 | text-align: center;
1002 | margin-bottom: 12px;
1003 | }
1004 |
1005 | ::-webkit-datetime-edit {
1006 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
1007 | background: url(elg_calendar_inv.svg) no-repeat left center;
1008 | padding-right: 1em;
1009 | padding-left: 25px;
1010 | background-position: 4px 0px;
1011 | }
1012 | ::-webkit-datetime-edit-fields-wrapper {
1013 |
1014 | }
1015 | ::-webkit-datetime-edit-text { padding: 0 0.3em; }
1016 | ::-webkit-datetime-edit-month-field { }
1017 | ::-webkit-datetime-edit-day-field {}
1018 | ::-webkit-datetime-edit-year-field {}
1019 | ::-webkit-inner-spin-button {
1020 |
1021 | /* display: none; */
1022 | }
1023 | ::-webkit-calendar-picker-indicator {
1024 | background: transparent;
1025 | font-size: 17px;
1026 | }
1027 |
1028 | ::-webkit-calendar-picker-indicator:focus {
1029 | background-color: rgba(0,0,0,0.2);
1030 | }
1031 |
1032 | input[type="date"] {
1033 | -webkit-align-items: center;
1034 | display: -webkit-inline-flex;
1035 | font-family: monospace;
1036 | overflow: hidden;
1037 | padding: 0;
1038 | -webkit-padding-start: 1px;
1039 | }
1040 |
1041 | input::-webkit-datetime-edit {
1042 | -webkit-flex: 1;
1043 | -webkit-user-modify: read-only !important;
1044 | display: inline-block;
1045 | min-width: 0;
1046 | overflow: hidden;
1047 | }
1048 |
1049 | /*
1050 | input::-webkit-datetime-edit-fields-wrapper {
1051 | -webkit-user-modify: read-only !important;
1052 | display: inline-block;
1053 | padding: 1px 0;
1054 | white-space: pre;
1055 |
1056 | }
1057 | */
1058 |
1059 | /*
1060 | input[type="date"] {
1061 | background-color: red;
1062 | outline: none;
1063 | }
1064 |
1065 | input[type="date"]::-webkit-clear-button {
1066 | font-size: 18px;
1067 | height: 30px;
1068 | position: relative;
1069 | }
1070 |
1071 | input[type="date"]::-webkit-inner-spin-button {
1072 | height: 28px;
1073 | }
1074 |
1075 | input[type="date"]::-webkit-calendar-picker-indicator {
1076 | font-size: 15px;
1077 | } */
1078 |
1079 | input[type="file"] {
1080 | opacity: 0;
1081 | display: none;
1082 | }
1083 |
1084 | .sdpi-item > input[type="file"] {
1085 | opacity: 1;
1086 | display: flex;
1087 | }
1088 |
1089 | input[type="file"] + span {
1090 | display: flex;
1091 | flex: 0 1 auto;
1092 | background-color: #0000ff50;
1093 | }
1094 |
1095 | label.sdpi-file-label {
1096 | cursor: pointer;
1097 | user-select: none;
1098 | display: inline-block;
1099 | min-height: 21px !important;
1100 | height: 21px !important;
1101 | line-height: 20px;
1102 | padding: 0px 4px;
1103 | margin: auto;
1104 | margin-right: 0px;
1105 | float:right;
1106 | }
1107 |
1108 | .sdpi-file-label > label:active,
1109 | .sdpi-file-label.file:active,
1110 | label.sdpi-file-label:active,
1111 | label.sdpi-file-info:active,
1112 | input[type="file"]::-webkit-file-upload-button:active,
1113 | button:active {
1114 | background-color: var(--sdpi-color);
1115 | color:#303030;
1116 | }
1117 |
1118 |
1119 | input:required:invalid, input:focus:invalid {
1120 | background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPgogICAgPHBhdGggZmlsbD0iI0Q4RDhEOCIgZD0iTTQuNSwwIEM2Ljk4NTI4MTM3LC00LjU2NTM4NzgyZS0xNiA5LDIuMDE0NzE4NjMgOSw0LjUgQzksNi45ODUyODEzNyA2Ljk4NTI4MTM3LDkgNC41LDkgQzIuMDE0NzE4NjMsOSAzLjA0MzU5MTg4ZS0xNiw2Ljk4NTI4MTM3IDAsNC41IEMtMy4wNDM1OTE4OGUtMTYsMi4wMTQ3MTg2MyAyLjAxNDcxODYzLDQuNTY1Mzg3ODJlLTE2IDQuNSwwIFogTTQsMSBMNCw2IEw1LDYgTDUsMSBMNCwxIFogTTQuNSw4IEM0Ljc3NjE0MjM3LDggNSw3Ljc3NjE0MjM3IDUsNy41IEM1LDcuMjIzODU3NjMgNC43NzYxNDIzNyw3IDQuNSw3IEM0LjIyMzg1NzYzLDcgNCw3LjIyMzg1NzYzIDQsNy41IEM0LDcuNzc2MTQyMzcgNC4yMjM4NTc2Myw4IDQuNSw4IFoiLz4KICA8L3N2Zz4) no-repeat 98% center;
1121 | }
1122 |
1123 | input:required:valid {
1124 | background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPjxwb2x5Z29uIGZpbGw9IiNEOEQ4RDgiIHBvaW50cz0iNS4yIDEgNi4yIDEgNi4yIDcgMy4yIDcgMy4yIDYgNS4yIDYiIHRyYW5zZm9ybT0icm90YXRlKDQwIDQuNjc3IDQpIi8+PC9zdmc+) no-repeat 98% center;
1125 | }
1126 |
1127 | .tooltip,
1128 | :tooltip,
1129 | :title {
1130 | color: yellow;
1131 | }
1132 |
1133 | [title]:hover {
1134 | display: flex;
1135 | align-items: center;
1136 | justify-content: center;
1137 | }
1138 |
1139 | [title]:hover::after {
1140 | content: '';
1141 | position: absolute;
1142 | bottom: -1000px;
1143 | left: 8px;
1144 | display: none;
1145 | color: #fff;
1146 | border: 8px solid transparent;
1147 | border-bottom: 8px solid #000;
1148 | }
1149 | [title]:hover::before {
1150 | content: attr(title);
1151 | display: flex;
1152 | justify-content: center;
1153 | align-self: center;
1154 | padding: 6px 12px;
1155 | border-radius: 5px;
1156 | background: rgba(0,0,0,0.8);
1157 | color: var(--sdpi-color);
1158 | font-size: 9pt;
1159 | font-family: sans-serif;
1160 | opacity: 1;
1161 | position: absolute;
1162 | height: auto;
1163 | /* width: 50%;
1164 | left: 35%; */
1165 | text-align: center;
1166 | bottom: 2px;
1167 | z-index: 100;
1168 | box-shadow: 0px 3px 6px rgba(0, 0, 0, .5);
1169 | /* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); */
1170 | }
1171 |
1172 | .sdpi-item-group.file {
1173 | width: 232px;
1174 | display: flex;
1175 | align-items: center;
1176 | }
1177 |
1178 | .sdpi-file-info {
1179 | overflow-wrap: break-word;
1180 | word-wrap: break-word;
1181 | hyphens: auto;
1182 |
1183 | min-width: 132px;
1184 | max-width: 144px;
1185 | max-height: 32px;
1186 | margin-top: 0px;
1187 | margin-left: 5px;
1188 | display: inline-block;
1189 | overflow: hidden;
1190 | padding: 6px 4px;
1191 | background-color: var(--sdpi-background);
1192 | }
1193 |
1194 |
1195 | ::-webkit-scrollbar {
1196 | width: 8px;
1197 | }
1198 |
1199 | ::-webkit-scrollbar-track {
1200 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
1201 | }
1202 |
1203 | ::-webkit-scrollbar-thumb {
1204 | background-color: #999999;
1205 | outline: 1px solid slategrey;
1206 | border-radius: 8px;
1207 | }
1208 |
1209 | a {
1210 | color: #7397d2;
1211 | }
1212 |
1213 | .testcontainer {
1214 | display: flex;
1215 | background-color: #0000ff20;
1216 | max-width: 400px;
1217 | height: 200px;
1218 | align-content: space-evenly;
1219 | }
1220 |
1221 | input[type=range] {
1222 | -webkit-appearance: none;
1223 | /* background-color: green; */
1224 | height:6px;
1225 | margin-top: 12px;
1226 | z-index: 0;
1227 | overflow: visible;
1228 | }
1229 |
1230 | /*
1231 | input[type="range"]::-webkit-slider-thumb {
1232 | -webkit-appearance: none;
1233 | background-color: var(--sdpi-color);
1234 | width: 12px;
1235 | height: 12px;
1236 | border-radius: 20px;
1237 | margin-top: -6px;
1238 | border: none;
1239 | } */
1240 |
1241 | :-webkit-slider-thumb {
1242 | -webkit-appearance: none;
1243 | background-color: var(--sdpi-color);
1244 | width: 16px;
1245 | height: 16px;
1246 | border-radius: 20px;
1247 | margin-top: -6px;
1248 | border: 1px solid #999999;
1249 | }
1250 |
1251 | .sdpi-item[type="range"] .sdpi-item-group {
1252 | display: flex;
1253 | flex-direction: column;
1254 | }
1255 |
1256 | .xxsdpi-item[type="range"] .sdpi-item-group input {
1257 | max-width: 204px;
1258 | }
1259 |
1260 | .sdpi-item[type="range"] .sdpi-item-group span {
1261 | margin-left: 0px !important;
1262 | }
1263 |
1264 | .sdpi-item[type="range"] .sdpi-item-group > .sdpi-item-child {
1265 | display: flex;
1266 | flex-direction: row;
1267 | }
1268 |
1269 | :disabled {
1270 | color: #993333;
1271 | }
1272 |
1273 | select,
1274 | select option {
1275 | color: var(--sdpi-color);
1276 | }
1277 |
1278 | select.disabled,
1279 | select option:disabled {
1280 | color: #fd9494;
1281 | font-style: italic;
1282 | }
1283 |
1284 | .runningAppsContainer {
1285 | display: none;
1286 | }
1287 |
1288 | /* debug
1289 | div {
1290 | background-color: rgba(64,128,255,0.2);
1291 | }
1292 | */
1293 |
1294 | .min80 > .sdpi-item-child {
1295 | min-width: 80px;
1296 | }
1297 |
1298 | .min100 > .sdpi-item-child {
1299 | min-width: 100px;
1300 | }
1301 |
1302 | .min120 > .sdpi-item-child {
1303 | min-width: 120px;
1304 | }
1305 |
1306 | .min140 > .sdpi-item-child {
1307 | min-width: 140px;
1308 | }
1309 |
1310 | .min160 > .sdpi-item-child {
1311 | min-width: 160px;
1312 | }
1313 |
1314 | .min200 > .sdpi-item-child {
1315 | min-width: 200px;
1316 | }
1317 |
1318 | .max40 {
1319 | flex-basis: 40%;
1320 | flex-grow: 0;
1321 | }
1322 |
1323 | .max30 {
1324 | flex-basis: 30%;
1325 | flex-grow: 0;
1326 | }
1327 |
1328 | .max20 {
1329 | flex-basis: 20%;
1330 | flex-grow: 0;
1331 | }
1332 |
1333 | .up20 {
1334 | margin-top: -20px;
1335 | }
1336 |
1337 | .alignCenter {
1338 | align-items: center;
1339 | }
1340 |
1341 | .alignTop {
1342 | align-items: flex-start;
1343 | }
1344 |
1345 | .alignBaseline {
1346 | align-items: baseline;
1347 | }
1348 |
1349 | .noMargins,
1350 | .noMargins *,
1351 | .noInnerMargins * {
1352 | margin: 0;
1353 | padding: 0;
1354 | }
1355 |
1356 |
1357 | /**
1358 | input[type=range].vVertical {
1359 | -webkit-appearance: none;
1360 | background-color: green;
1361 | margin-left: -60px;
1362 | width: 100px;
1363 | height:6px;
1364 | margin-top: 0px;
1365 | transform:rotate(90deg);
1366 | z-index: 0;
1367 | overflow: visible;
1368 | }
1369 |
1370 | input[type=range].vHorizon {
1371 | -webkit-appearance: none;
1372 | background-color: pink;
1373 | height: 10px;
1374 | width:200px;
1375 |
1376 | }
1377 |
1378 | .test2 {
1379 | background-color: #00ff0020;
1380 | display: flex;
1381 | }
1382 |
1383 |
1384 | .vertical.sdpi-item[type="range"] .sdpi-item-value {
1385 | display: block;
1386 | }
1387 |
1388 |
1389 | .vertical.sdpi-item:first-child,
1390 | .vertical {
1391 | margin-top: 12px;
1392 | margin-bottom: 16px;
1393 | }
1394 | .vertical > .sdpi-item-value {
1395 | margin-right: 16px;
1396 | }
1397 |
1398 | .vertical .sdpi-item-group {
1399 | width: 100%;
1400 | display: flex;
1401 | justify-content: space-evenly;
1402 | }
1403 |
1404 | .vertical input[type=range] {
1405 | height: 100px;
1406 | width: 21px;
1407 | -webkit-appearance: slider-vertical;
1408 | display: flex;
1409 | flex-flow: column;
1410 | }
1411 |
1412 | .vertical input[type="range"]::-webkit-slider-runnable-track {
1413 | height: auto;
1414 | width: 5px;
1415 | }
1416 |
1417 | .vertical input[type="range"]::-webkit-slider-thumb {
1418 | margin-top: 0px;
1419 | margin-left: -6px;
1420 | }
1421 |
1422 | .vertical .sdpi-item-value {
1423 | flex-flow: column;
1424 | align-items: flex-start;
1425 | }
1426 |
1427 | .vertical.sdpi-item[type="range"] .sdpi-item-value {
1428 | align-items: center;
1429 | margin-right: 16px;
1430 | text-align: center;
1431 | }
1432 |
1433 | .vertical.sdpi-item[type="range"] .sdpi-item-value span,
1434 | .vertical input[type="range"] .sdpi-item-value span {
1435 | text-align: center;
1436 | margin: 4px 0px;
1437 | }
1438 | */
1439 |
1440 | /*
1441 | .file {
1442 | box-sizing: border-box;
1443 | display: block;
1444 | overflow: hidden;
1445 | padding: 10px;
1446 | position: relative;
1447 | text-indent: 100%;
1448 | white-space: nowrap;
1449 | height: 190px;
1450 | width: 160px;
1451 | }
1452 | .file::before {
1453 | content: "";
1454 | display: block;
1455 | position: absolute;
1456 | top: 10px;
1457 | left: 10px;
1458 | height: 170px;
1459 | width: 140px;
1460 | }
1461 | .file::after {
1462 | content: "";
1463 | height: 90px;
1464 | width: 90px;
1465 | position: absolute;
1466 | right: 0;
1467 | bottom: 0;
1468 | overflow: visible;
1469 | }
1470 |
1471 | .list--files {
1472 | display: flex;
1473 | flex-wrap: wrap;
1474 | justify-content: center;
1475 | margin: auto;
1476 | padding: 30px 0;
1477 | width: 630px;
1478 | }
1479 | .list--files > li {
1480 | margin: 0;
1481 | padding: 15px;
1482 | }
1483 |
1484 | .type-document::before {
1485 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIxNDBweCIgaGVpZ2h0PSIxNzBweCIgdmlld0JveD0iMCAwIDE0MCAxNzAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0E3QTlBQyIgZD0iTTAsMHYxNzBoMTQwVjBIMHogTTEzMCwxNjBIMTBWMTBoMTIwVjE2MHogTTExMCw0MEgzMFYzMGg4MFY0MHogTTExMCw2MEgzMFY1MGg4MFY2MHogTTExMCw4MEgzMFY3MGg4MFY4MHoNCiAgIE0xMTAsMTAwSDMwVjkwaDgwVjEwMHogTTExMCwxMjBIMzB2LTEwaDgwVjEyMHogTTkwLDE0MEgzMHYtMTBoNjBWMTQweiIvPg0KPC9zdmc+);
1486 | }
1487 |
1488 | .type-image {
1489 | height: 160px;
1490 | }
1491 | .type-image::before {
1492 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIxNDBweCIgaGVpZ2h0PSIxNDBweCIgdmlld0JveD0iMCAwIDE0MCAxNDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE0MCAxNDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxwYXRoIGZpbGw9IiNBN0E5QUMiIGQ9Ik0wLDB2MTQwaDE0MFYwSDB6IE0xMzAsMTMwSDEwVjEwaDEyMFYxMzB6Ii8+DQogIDxwb2x5Z29uIGZpbGw9IiNFNkU3RTgiIHBvaW50cz0iOTAsMTEwIDQwLDQwIDEwLDgwIDEwLDEzMCA5MCwxMzAgICIvPg0KICA8cG9seWdvbiBmaWxsPSIjRDFEM0Q0IiBwb2ludHM9IjEwLDEzMCA1MCw5MCA2MCwxMDAgMTAwLDYwIDEzMCwxMzAgICIvPg0KPC9nPg0KPC9zdmc+);
1493 | height: 140px;
1494 | }
1495 |
1496 | .state-synced::after {
1497 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iIzAwQTY1MSIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0yMCw0NUwyMCw0NWMtMi44LDIuOC0yLjgsNy4yLDAsMTBsMTAuMSwxMC4xYzIuNywyLjcsNy4yLDIuNyw5LjksMEw3MCwzNWMyLjgtMi44LDIuOC03LjIsMC0xMGwwLDANCiAgICBjLTIuOC0yLjgtNy4yLTIuOC0xMCwwTDM1LDUwbC01LTVDMjcuMiw0Mi4yLDIyLjgsNDIuMiwyMCw0NXoiLz4NCjwvZz4NCjwvc3ZnPg==);
1498 | }
1499 |
1500 | .state-deleted::after {
1501 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iI0VEMUMyNCIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik02NSwyNUw2NSwyNWMtMi44LTIuOC03LjItMi44LTEwLDBMNDUsMzVMMzUsMjVjLTIuOC0yLjgtNy4yLTIuOC0xMCwwbDAsMGMtMi44LDIuOC0yLjgsNy4yLDAsMTBsMTAsMTANCiAgICBMMjUsNTVjLTIuOCwyLjgtMi44LDcuMiwwLDEwbDAsMGMyLjgsMi44LDcuMiwyLjgsMTAsMGwxMC0xMGwxMCwxMGMyLjgsMi44LDcuMiwyLjgsMTAsMGwwLDBjMi44LTIuOCwyLjgtNy4yLDAtMTBMNTUsNDVsMTAtMTANCiAgICBDNjcuOCwzMi4yLDY3LjgsMjcuOCw2NSwyNXoiLz4NCjwvZz4NCjwvc3ZnPg==);
1502 | }
1503 | .state-deleted::before {
1504 | opacity: .25;
1505 | }
1506 |
1507 | .state-locked::after {
1508 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iIzU4NTk1QiIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxyZWN0IHg9IjIwIiB5PSI0MCIgZmlsbD0iI0ZGRkZGRiIgd2lkdGg9IjUwIiBoZWlnaHQ9IjMwIi8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0zMi41LDQ2LjVjLTIuOCwwLTUtMi4yLTUtNVYyOWMwLTkuNiw3LjktMTcuNSwxNy41LTE3LjVTNjIuNSwxOS40LDYyLjUsMjljMCwyLjgtMi4yLDUtNSw1cy01LTIuMi01LTUNCiAgICBjMC00LjEtMy40LTcuNS03LjUtNy41cy03LjUsMy40LTcuNSw3LjV2MTIuNUMzNy41LDQ0LjMsMzUuMyw0Ni41LDMyLjUsNDYuNXoiLz4NCjwvZz4NCjwvc3ZnPg==);
1509 | }
1510 |
1511 |
1512 |
1513 | html {
1514 | --fheight: 95px;
1515 | --fwidth: 80px;
1516 | --fspacing: 5px;
1517 | --ftotalwidth: 315px;
1518 | --bgsize: 50%;
1519 | --bgsize2: cover;
1520 | --bgsize3: contain;
1521 | }
1522 |
1523 | ul {
1524 | list-style: none;
1525 | }
1526 |
1527 |
1528 | .file {
1529 | height: var(--fheight);
1530 | width: var(--fwidth);
1531 | }
1532 | .file::before {
1533 | content: "";
1534 | display: block;
1535 | position: absolute;
1536 | top: var(--fspacing);
1537 | left: var(--fspacing);
1538 | height: calc(var(--fheight) - var(--fspacing)*2);
1539 | width: calc(var(--fwidth) - var(--fspacing)*2);
1540 | }
1541 | .file::after {
1542 | content: "";
1543 | height: calc(var(--fheight)/2);
1544 | width: calc(var(--fheight)/2);
1545 | position: absolute;
1546 | right: 0;
1547 | bottom: 0;
1548 | overflow: visible;
1549 | }
1550 |
1551 | .list--files {
1552 | display: flex;
1553 | flex-wrap: wrap;
1554 | justify-content: center;
1555 | margin: auto;
1556 | padding: calc(var(--fspacing)*3) 0;
1557 | width: var(--ftotalwidth);
1558 | }
1559 | .list--files > li {
1560 | margin: 0;
1561 | padding: var(--fspacing);
1562 | }
1563 |
1564 | .type-document::before {
1565 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIxNDBweCIgaGVpZ2h0PSIxNzBweCIgdmlld0JveD0iMCAwIDE0MCAxNzAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0E3QTlBQyIgZD0iTTAsMHYxNzBoMTQwVjBIMHogTTEzMCwxNjBIMTBWMTBoMTIwVjE2MHogTTExMCw0MEgzMFYzMGg4MFY0MHogTTExMCw2MEgzMFY1MGg4MFY2MHogTTExMCw4MEgzMFY3MGg4MFY4MHoNCiAgIE0xMTAsMTAwSDMwVjkwaDgwVjEwMHogTTExMCwxMjBIMzB2LTEwaDgwVjEyMHogTTkwLDE0MEgzMHYtMTBoNjBWMTQweiIvPg0KPC9zdmc+);
1566 | height: calc(var(--fheight) - var(--fspacing)*2);
1567 | background-size: var(--bgsize2);
1568 | background-repeat: no-repeat;
1569 | }
1570 |
1571 | .type-image {
1572 | height: var(--fwidth);
1573 | height: calc(var(--fheight) - var(--fspacing)*2);
1574 | }
1575 | .type-image::before {
1576 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIxNDBweCIgaGVpZ2h0PSIxNDBweCIgdmlld0JveD0iMCAwIDE0MCAxNDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE0MCAxNDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxwYXRoIGZpbGw9IiNBN0E5QUMiIGQ9Ik0wLDB2MTQwaDE0MFYwSDB6IE0xMzAsMTMwSDEwVjEwaDEyMFYxMzB6Ii8+DQogIDxwb2x5Z29uIGZpbGw9IiNFNkU3RTgiIHBvaW50cz0iOTAsMTEwIDQwLDQwIDEwLDgwIDEwLDEzMCA5MCwxMzAgICIvPg0KICA8cG9seWdvbiBmaWxsPSIjRDFEM0Q0IiBwb2ludHM9IjEwLDEzMCA1MCw5MCA2MCwxMDAgMTAwLDYwIDEzMCwxMzAgICIvPg0KPC9nPg0KPC9zdmc+);
1577 | height: calc(var(--fheight) - var(--fspacing)*2);
1578 | background-size: var(--bgsize3);
1579 | background-repeat: no-repeat;
1580 | }
1581 |
1582 | .state-synced::after {
1583 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iIzAwQTY1MSIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0yMCw0NUwyMCw0NWMtMi44LDIuOC0yLjgsNy4yLDAsMTBsMTAuMSwxMC4xYzIuNywyLjcsNy4yLDIuNyw5LjksMEw3MCwzNWMyLjgtMi44LDIuOC03LjIsMC0xMGwwLDANCiAgICBjLTIuOC0yLjgtNy4yLTIuOC0xMCwwTDM1LDUwbC01LTVDMjcuMiw0Mi4yLDIyLjgsNDIuMiwyMCw0NXoiLz4NCjwvZz4NCjwvc3ZnPg==);
1584 | background-size: var(--bgsize);
1585 | background-repeat: no-repeat;
1586 | background-position: bottom right;
1587 | }
1588 |
1589 | .state-deleted::after {
1590 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iI0VEMUMyNCIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik02NSwyNUw2NSwyNWMtMi44LTIuOC03LjItMi44LTEwLDBMNDUsMzVMMzUsMjVjLTIuOC0yLjgtNy4yLTIuOC0xMCwwbDAsMGMtMi44LDIuOC0yLjgsNy4yLDAsMTBsMTAsMTANCiAgICBMMjUsNTVjLTIuOCwyLjgtMi44LDcuMiwwLDEwbDAsMGMyLjgsMi44LDcuMiwyLjgsMTAsMGwxMC0xMGwxMCwxMGMyLjgsMi44LDcuMiwyLjgsMTAsMGwwLDBjMi44LTIuOCwyLjgtNy4yLDAtMTBMNTUsNDVsMTAtMTANCiAgICBDNjcuOCwzMi4yLDY3LjgsMjcuOCw2NSwyNXoiLz4NCjwvZz4NCjwvc3ZnPg==);
1591 | background-size: var(--bgsize);
1592 | background-repeat: no-repeat;
1593 | background-position: bottom right;
1594 | }
1595 | .state-deleted::before {
1596 | opacity: .25;
1597 | }
1598 |
1599 | .state-locked::after {
1600 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iIzU4NTk1QiIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxyZWN0IHg9IjIwIiB5PSI0MCIgZmlsbD0iI0ZGRkZGRiIgd2lkdGg9IjUwIiBoZWlnaHQ9IjMwIi8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0zMi41LDQ2LjVjLTIuOCwwLTUtMi4yLTUtNVYyOWMwLTkuNiw3LjktMTcuNSwxNy41LTE3LjVTNjIuNSwxOS40LDYyLjUsMjljMCwyLjgtMi4yLDUtNSw1cy01LTIuMi01LTUNCiAgICBjMC00LjEtMy40LTcuNS03LjUtNy41cy03LjUsMy40LTcuNSw3LjV2MTIuNUMzNy41LDQ0LjMsMzUuMyw0Ni41LDMyLjUsNDYuNXoiLz4NCjwvZz4NCjwvc3ZnPg==);
1601 | background-size: var(--bgsize);
1602 | background-repeat: no-repeat;
1603 | background-position: bottom right;
1604 | }
1605 | */
1606 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const Streamdeck = require('./src/');
2 |
3 | if (typeof window === 'object' && typeof document === 'object') {
4 | window.streamdeck = new Streamdeck();
5 | window.connectSocket = window.streamdeck.start.bind(window.streamdeck);
6 |
7 | } else {
8 | module.exports = Streamdeck;
9 | }
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "Name": "easy-streamdeck Example: Counter",
3 | "Author": "SReject",
4 | "Description": "Recreation of Elgato's Counter plugin using easy-streamdeck",
5 | "URL": "https://github.com/SReject/easy-streamdeck",
6 | "Icon": "./example/icons/pluginIcon",
7 | "Version": "2.0.0",
8 | "OS": [
9 | {
10 | "Platform": "mac",
11 | "MinimumVersion" : "10.11"
12 | },
13 | {
14 | "Platform": "windows",
15 | "MinimumVersion" : "10"
16 | }
17 | ],
18 |
19 | "Actions": [
20 | {
21 | "Icon": "./example/icons/actionIcon",
22 | "Name": "Counter",
23 | "States": [
24 | {
25 | "Image": "./example/icons/actionDefaultImage",
26 | "TitleAlignment": "middle",
27 | "FontSize": "16"
28 | }
29 | ],
30 | "SupportedInMultiActions": false,
31 | "Tooltip": "Counts how many times the button was pressed",
32 | "UUID": "com.sreject.easystreamdeck.counter"
33 | }
34 | ],
35 |
36 | "CodePath": "./example/background.html",
37 | "PropertyInspectorPath": "./example/action.html"
38 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "easy-streamdeck-sdk",
3 | "version": "2.0.1",
4 | "description": "Abstraction layer for streamdeck's SDK",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "browserify --node --no-bundle-external --no-bf index.js | terser > easy-streamdeck-v2.0.1.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/sreject/easy-streamdeck.git"
12 | },
13 | "keywords": [
14 | "streamdeck",
15 | "stream",
16 | "deck",
17 | "sdk",
18 | "abstraction"
19 | ],
20 | "author": "SReject",
21 | "license": "ISC",
22 | "bugs": {
23 | "url": "https://github.com/sreject/easy-streamdeck/issues"
24 | },
25 | "homepage": "https://github.com/sreject/easy-streamdeck#readme",
26 | "dependencies": {
27 | "image-data-uri": "^2.0.0",
28 | "ws": "^6.1.3"
29 | },
30 | "devDependencies": {
31 | "browserify": "^16.2.3",
32 | "jsdoc": "^3.5.5"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/background/context.js:
--------------------------------------------------------------------------------
1 | const util = require('../common/utils.js');
2 | const {imageToDataURL} = require('../common/boilers.js');
3 |
4 | function validateTarget(target) {
5 | target = target == null ? 0 : target;
6 |
7 | if (String(target) === target) {
8 | target = target.toLowerCase();
9 | }
10 |
11 | switch (target) {
12 | case 0:
13 | case 'both':
14 | return 0;
15 |
16 | case 1:
17 | case 'hardware':
18 | return 1;
19 |
20 | case 2:
21 | case 'software':
22 | return 2;
23 |
24 | default:
25 | throw new TypeError('invalid target argument');
26 | }
27 | }
28 |
29 | function contextWrapper(streamdeck) {
30 |
31 | class Context {
32 | constructor(action, id) {
33 |
34 | // todo: validate action and uuid
35 |
36 | this.action = action;
37 | this.id = id;
38 | }
39 |
40 | send(data) {
41 | streamdeck.send({
42 | event: "sendToPropertyInspector",
43 | context: this.id,
44 | action: this.action,
45 | payload: data
46 | });
47 | }
48 |
49 | setTitle(title, target) {
50 | if (title != null && !util.isString(title)) {
51 | throw new TypeError('invalid title argument');
52 | }
53 |
54 | streamdeck.send({
55 | event: "setTitle",
56 | context: this.id,
57 | payload: {
58 | title: title == null ? null : title,
59 | target: validateTarget(target)
60 | }
61 | });
62 | }
63 | setImage(image, target) {
64 |
65 | // TODO: validate image
66 |
67 | streamdeck.send({
68 | event: "setImage",
69 | context: this.id,
70 | payload: {
71 | image: image == null ? null : image,
72 | target: validateTarget(target)
73 | }
74 | });
75 | }
76 | setImageFromUrl(url, target) {
77 | if (!util.isString(url, {notEmpty: true})) {
78 | throw new TypeError('invalid url');
79 | }
80 | target = validateTarget(target);
81 | let self = this;
82 | imageToDataURL(url)
83 | .then(res => self.setImage(res, target), () => {})
84 | .catch(() => {});
85 | }
86 | setState(state) {
87 | if (!util.isNumber(state, {while: true, min: 0})) {
88 | throw new TypeError('invalid state argument');
89 | }
90 |
91 | streamdeck.send({
92 | event: "setState",
93 | context: this.id,
94 | payload: {state: state}
95 | });
96 | }
97 | setSettings(settings) {
98 | streamdeck.send({
99 | event: "setSettings",
100 | context: this.id,
101 | payload: settings
102 | });
103 | }
104 | showAlert() {
105 | streamdeck.send({
106 | event: "showAlert",
107 | context: this.id
108 | });
109 | }
110 | showOk() {
111 | streamdeck.sendJSON({
112 | event: "showAlert",
113 | context: this.id
114 | });
115 | }
116 | }
117 |
118 |
119 | return Context;
120 | }
121 |
122 | module.exports = contextWrapper;
--------------------------------------------------------------------------------
/src/background/index.js:
--------------------------------------------------------------------------------
1 | const util = require('../common/utils.js');
2 | const irnClient = require('../common/irn-client.js');
3 | const onmessage = require('./onmessage.js');
4 | const context = require('./context.js');
5 |
6 | function background(streamdeck, deviceList) {
7 |
8 | const contextList = {};
9 | const irn = irnClient(streamdeck);
10 |
11 | // Add background-related properties to streamdeck
12 | Object.defineProperties(streamdeck, {
13 | onMessage: {
14 | value: onmessage.call(streamdeck, deviceList, contextList)
15 | },
16 | contexts: {
17 | enumerable: true,
18 | get: function () {
19 | return Object.assign({}, contextList);
20 | }
21 | },
22 | switchToProfile: {
23 | enumerable: true,
24 | value: function switchToProfile(profile, device) {
25 | if (!util.isString(profile)) {
26 | throw new Error('invalid profile argument');
27 | }
28 | this.send({
29 | event: "switchToProfile",
30 | context: this.id,
31 | device: device,
32 | payload: {
33 | profile: profile
34 | }
35 | });
36 | }
37 | },
38 | Context: {
39 | enumerable: true,
40 | value: context(streamdeck)
41 | }
42 | });
43 |
44 | // Add IRN client related properties to the Context class
45 | Object.defineProperties(streamdeck.Context, {
46 | invoke: {
47 | enumerable: true,
48 | value: function invoke(method, ...args) {
49 | let res = irn.invoke(method, ...args);
50 | this.send(res.result);
51 | return res.promise;
52 | }
53 | },
54 | notify: {
55 | enumerable: true,
56 | value: function notify(event, ...args) {
57 | this.send(irn.notify(event, ...args));
58 | }
59 | }
60 | });
61 |
62 | // register foreground-invokable methods
63 | irn.register('$getTitle', function () {
64 | return this.title;
65 | });
66 | irn.register('$setTitle', function (title, target) {
67 | this.setTitle(title, target);
68 | return title;
69 | });
70 | irn.register('$getImage', function () {
71 | throw new Error('not supported');
72 | });
73 | irn.register('$setImage', function (image, target) {
74 | this.setImage(image, target);
75 | });
76 | irn.register('$setImageFromUrl', function (url, target) {
77 | this.setImageFromUrl(url, target);
78 | });
79 | irn.register('$getState', function () {
80 | return this.state;
81 | });
82 | irn.register('$setState', function (state) {
83 | this.setState(state);
84 | this.state = state;
85 | return state;
86 | });
87 | irn.register('$getSettings', function () {
88 | return this.settings;
89 | });
90 | irn.register('$setSettings', function (settings) {
91 | this.setSettings(settings);
92 | return settings;
93 | });
94 | irn.register(`$showAlert`, function () {
95 | this.showAlert();
96 | });
97 | irn.register(`$showOk`, function () {
98 | this.showOk();
99 | });
100 | }
101 |
102 | module.exports = background;
--------------------------------------------------------------------------------
/src/background/onmessage.js:
--------------------------------------------------------------------------------
1 | const util = require('../common/utils.js');
2 | // const Context = require('./context.js');
3 |
4 | // Wrapper function
5 | function onMessageWrapper(deviceList, contextList) {
6 |
7 | let streamdeck = this;
8 |
9 | // Returns the function that will handle the onmessage event
10 | return function onmessage(evt) {
11 |
12 | // Retrieve message data
13 | let msg = evt.data;
14 |
15 | // Message null or doesn't appear to be a JSON object string
16 | if (msg == null || !util.isString(msg, {match: /^\{[\s\S]+\}$/})) {
17 | return this.emit('websocket:message', evt.data);
18 | }
19 |
20 | // Attempt to parse the msg
21 | try {
22 | msg = JSON.parse(msg);
23 | } catch (ignore) {
24 | return this.emit('websocket:message', evt.data);
25 | }
26 |
27 | let eventName,
28 | info;
29 |
30 | // Do basic validation of event-specific msg data
31 | switch (msg.event) {
32 |
33 | case 'applicationDidLaunch':
34 | case 'applicationDidTerminate':
35 |
36 | // Application related messages will always have a payload.application property that is a non-empty string
37 | if (msg.payload == null || !util.isString(msg.payload.application, {notEmpty: true})) {
38 | return this.emit('websocket:message', evt.data);
39 | }
40 |
41 | // Emit events
42 | eventName = msg.event === 'applicationDidLaunch' ? 'launch' : 'terminate';
43 | this.emit(`application:${eventName}`, msg.payload.application);
44 | this.emit(`application`, {event: eventName, application: msg.payload.application});
45 | return;
46 |
47 |
48 | case 'deviceDidConnect':
49 | case 'deviceDidDisconnect':
50 |
51 | // Validate device data
52 | if (
53 | !util.isString(msg.device, {notEmpty: true}) ||
54 | msg.deviceInfo.size == null ||
55 | msg.deviceInfo.size.columns == null ||
56 | msg.deviceInfo.size.rows == null ||
57 | !util.isNumber(msg.deviceInfo.type, {whole: true, min: 0}) ||
58 | !util.isNumber(msg.deviceInfo.size.columns, {whole: true, min: 0}) ||
59 | !util.isNumber(msg.deviceInfo.size.rows, {whole: true, min: 0})
60 | ) {
61 | return this.emit('websocket:message', evt.data);
62 | }
63 |
64 | // Build device details object
65 | info = {
66 | id: msg.device,
67 | type: msg.deviceInfo.type,
68 | columns: msg.deviceInfo.size.rows,
69 | rows: msg.deviceInfo.size.rows
70 | };
71 |
72 | // Device connected: store a copy of the details in stream deck's device list
73 | if (msg.event === 'deviceDidConnect') {
74 | deviceList[info.id] = Object.assign({}, info);
75 | eventName = 'connect';
76 |
77 | // Device disconnected: remove it from stream deck's device list
78 | } else {
79 | delete deviceList[info.id];
80 | eventName = 'disconnect';
81 | }
82 |
83 | // Emit events
84 | this.emit(`device:${eventName}`, info);
85 | this.emit('device', {event: eventName, device: info});
86 | return;
87 |
88 |
89 | case 'keyUp':
90 | case 'keyDown':
91 | case 'willAppear':
92 | case 'willDisappear':
93 | case 'titleParametersDidChange':
94 | case 'sendToPlugin':
95 |
96 | // Valid the event's .context .action and .payload properties
97 | if (
98 | !util.isString(msg.context, {match: /^[A-F\d]{32}$/}) ||
99 | !util.isString(msg.action, {match: /^[^\\/;%@:]+$/}) ||
100 | msg.payload == null
101 | ) {
102 | return this.emit('websocket:message', evt.data);
103 | }
104 | break;
105 |
106 |
107 | default:
108 | return this.emit('websocket:message', evt.data);
109 | }
110 |
111 | // Build device info
112 | let device;
113 | if (deviceList[msg.device] != null) {
114 | device = Object.assign({}, deviceList[msg.device]);
115 |
116 | } else {
117 | device = {id: msg.device};
118 | }
119 |
120 | // Deduce Context instance
121 | let context;
122 | if (contextList[msg.context] != null) {
123 | context = contextList[msg.context];
124 |
125 | } else {
126 | context = new streamdeck.Context(msg.action, msg.context);
127 | }
128 | context.action = msg.action;
129 |
130 | // Event: sendToPlugin
131 | if (msg.event === 'sendToPlugin') {
132 | return this.emit('message', msg.payload, {self: context});
133 | }
134 |
135 | // Ease accessing the title parameters for validation
136 | let params = msg.payload.titleParameters;
137 |
138 | // Validate msg.payload
139 | if (
140 | msg.payload.settings == null ||
141 | msg.payload.coordinates == null ||
142 | !util.isNumber(msg.payload.coordinates.row, {whole: true, min: 0}) ||
143 | !util.isNumber(msg.payload.coordinates.column, {whole: true, min: 0}) ||
144 | (msg.payload.state != null && !util.isNumber(msg.payload.state, {whole: true, min: 0})) ||
145 | (msg.payload.isInMultiAction != null && !util.isBoolean(msg.payload.isInMultiAction)) ||
146 | (
147 | // validate payload.titleParameters for title change event
148 | msg.event === 'titleParametersDidChange' &&
149 | (
150 | !util.isString(msg.payload.title) ||
151 | params == null ||
152 | !util.isString(params.fontFamily) ||
153 | !util.isNumber(params.fontSize, {whole: true, min: 6}) ||
154 | !util.isString(params.fontStyle) ||
155 | !util.isBoolean(params.fontUnderline) ||
156 | !util.isBoolean(params.showTitle) ||
157 | !util.isString(params.titleAlignment, {match: /^(?:top|middle|bottom)$/}) ||
158 | !util.isString(params.titleColor, {match: /^#(?:[a-f\d]{1,8})$/})
159 | )
160 | )
161 | ) {
162 | return this.emit('websocket:message', evt.data);
163 | }
164 |
165 | // update context info
166 | context.row = msg.payload.coordinates.row;
167 | context.column = msg.payload.coordinates.column;
168 | context.device = device;
169 | context.settings = msg.payload.settings;
170 | if (msg.payload.isInMultiAction != null) {
171 | context.isInMultiAction = msg.payload.isInMultiAction;
172 | }
173 | if (msg.payload.state != null) {
174 | context.state = msg.payload.state;
175 | }
176 |
177 | switch (msg.event) {
178 | case 'keyUp':
179 | case 'keyDown':
180 | eventName = msg.event === 'keyUp' ? 'up' : 'down';
181 | this.emit(`keypress:${eventName}`, null, {self: context});
182 | this.emit('keypress', {event: eventName}, {self: context});
183 | return;
184 |
185 | case 'willAppear':
186 | case 'willDisappear':
187 | if (msg.event === 'willAppear') {
188 | contextList[context.id] = context;
189 | eventName = 'appear';
190 |
191 | } else {
192 | delete contextList[context.id];
193 | eventName = 'disappear';
194 | }
195 |
196 | this.emit(`context:${eventName}`, null, {self: context});
197 | this.emit(`context`, {event: eventName}, {self: context});
198 | return;
199 |
200 | case 'titleParametersDidChange':
201 |
202 | // store previous title, and update context with new title info
203 | info = context.title;
204 | context.title = {
205 | text: msg.payload.title,
206 | font: params.fontFamily,
207 | style: params.fontStyle,
208 | underline: params.fontUnderline,
209 | shown: params.showTitle,
210 | alignment: params.titleAlignment,
211 | color: params.titleColor
212 | };
213 |
214 | // emit events
215 | this.emit('context:titlechange', info, {self: context});
216 | this.emit('context', {event: 'titlechange', previousTitle: info}, {self: context});
217 | return;
218 | }
219 | };
220 | }
221 |
222 | module.exports = onMessageWrapper;
--------------------------------------------------------------------------------
/src/common/boilers.js:
--------------------------------------------------------------------------------
1 | // websocket class missing, use ws package
2 | if (typeof WebSocket !== 'function') {
3 | exports.WebSocket = require('ws');
4 | } else {
5 | exports.WebSocket = WebSocket;
6 | }
7 |
8 | // canvas missing, use image-data-uri package
9 | if (typeof HTMLCanvasElement !== 'function') {
10 | exports.imageToDataUrl = require('image-data-uri');
11 |
12 | } else {
13 | exports.imageToDataUrl = function (url) {
14 | return new Promise((resolve, reject) => {
15 | let image = new Image();
16 |
17 | image.onload = function () {
18 | let canvas = document.createElement('canvas');
19 | canvas.width = image.naturalWidth;
20 | canvas.height = image.naturalHeight;
21 |
22 | // draw image on canvas
23 | let ctx = canvas.getContext("2d");
24 | ctx.drawImage(image, 0, 0);
25 |
26 | image.onload = null;
27 | image.onerror = null;
28 | image = null;
29 |
30 | resolve(canvas.toDataURL('image/png'));
31 | };
32 | image.onerror = function () {
33 |
34 | image.onload = null;
35 | image.onerror = null;
36 | image = null;
37 |
38 | reject(new Error('image failed to load'));
39 | };
40 | image.src = url;
41 | });
42 | };
43 | }
--------------------------------------------------------------------------------
/src/common/connection.js:
--------------------------------------------------------------------------------
1 | const Emitter = require('./emitter.js');
2 | const {WebSocket} = require('./boilers.js');
3 |
4 | const $websock = Symbol('ws connection');
5 | const $readyState = Symbol('ws readyState');
6 | const $spooledMessages = Symbol('ws spooled messages');
7 | const $reconnectTimeout = Symbol('ws reconnect timeout');
8 | const $reconnectDelay = Symbol('ws reconnect delay');
9 | const $addressKey = Symbol('ws address key');
10 |
11 | let onConnect = false;
12 |
13 | function cleanup(self) {
14 | if (self[$websock] != null) {
15 | if (self[$websock].readyState < 2) {
16 | self[$websock].close();
17 | }
18 | self[$websock].onopen = null;
19 | self[$websock].onmessage = null;
20 | self[$websock].onclose = null;
21 | self[$websock].onerror = null;
22 | self[$websock] = null;
23 | self[$readyState] = 0;
24 | }
25 | if (self[$reconnectTimeout]) {
26 | clearTimeout(self[$reconnectTimeout]);
27 | }
28 | }
29 | function reconnect(self) {
30 | self[$readyState] = 1;
31 |
32 | // Start a timeout that will attempt to connect when it elapses
33 | self[$reconnectTimeout] = setTimeout(self.connect.bind(self), self[$reconnectDelay]);
34 |
35 | // Decay the timeout delay
36 | self[$reconnectDelay] *= 1.5;
37 | if (self[$reconnectDelay] > 30000) {
38 | self[$reconnectDelay] = 30000;
39 | }
40 | }
41 |
42 | class Connection extends Emitter {
43 |
44 | constructor() {
45 | super();
46 |
47 | Object.defineProperty(this, $websock, {writable: true, value: null});
48 | Object.defineProperty(this, $readyState, {writable: true, value: 0});
49 | Object.defineProperty(this, $reconnectDelay, {writable: true, value: 1000});
50 | Object.defineProperty(this, $spooledMessages, {writable: true, value: []});
51 | }
52 |
53 | // Overridable websocket on-open event handler
54 | onOpen() {
55 |
56 | // Reset reconnect timeout
57 | if (this[$reconnectTimeout]) {
58 | clearTimeout(this[$reconnectTimeout]);
59 | this[$reconnectTimeout] = null;
60 | this[$reconnectDelay] = 1000;
61 | }
62 |
63 | // emit connect event
64 | this[$readyState] = 2;
65 | onConnect = true;
66 | this.emit('websocket:connect');
67 | onConnect = false;
68 |
69 | // send spooled messages
70 | if (this[$spooledMessages].length) {
71 | this[$spooledMessages].forEach(msg => this[$websock].send(msg));
72 | this[$spooledMessages] = [];
73 | }
74 |
75 | // emit ready event
76 | this[$readyState] = 3;
77 | this.emit('websocket:ready');
78 | }
79 |
80 | // Overridable websocket on-message event handler
81 | onMessage(evt) {
82 | this.emit('websocket:message', evt.data);
83 | }
84 |
85 | // Overridable websocket on-close event handler
86 | onClose(evt) {
87 |
88 | // deduce close reason and emit event
89 | let reason;
90 | switch (evt.code) {
91 | case 1000:
92 | reason = 'Normal Closure. The purpose for which the connection was established has been fulfilled.';
93 | break;
94 | case 1001:
95 | reason = 'Going Away. An endpoint is "going away", such as a server going down or a browser having navigated away from a page.';
96 | break;
97 | case 1002:
98 | reason = 'Protocol error. An endpoint is terminating the connection due to a protocol error';
99 | break;
100 | case 1003:
101 | reason = "Unsupported Data. An endpoint received a type of data it doesn't support.";
102 | break;
103 | case 1004:
104 | reason = '--Reserved--. The specific meaning might be defined in the future.';
105 | break;
106 | case 1005:
107 | reason = 'No Status. No status code was actually present.';
108 | break;
109 | case 1006:
110 | reason = 'Abnormal Closure. The connection was closed abnormally, e.g., without sending or receiving a Close control frame';
111 | break;
112 | case 1007:
113 | reason = 'Invalid frame payload data. The connection was closed, because the received data was not consistent with the type of the message (e.g., non-UTF-8 [http://tools.ietf.org/html/rfc3629]).';
114 | break;
115 | case 1008:
116 | reason = 'Policy Violation. The connection was closed, because current message data "violates its policy". This reason is given either if there is no other suitable reason, or if there is a need to hide specific details about the policy.';
117 | break;
118 | case 1009:
119 | reason = 'Message Too Big. Connection closed because the message is too big for it to process.';
120 | break;
121 | case 1010:
122 | reason = "Mandatory Ext. Connection is terminated the connection because the server didn't negotiate one or more extensions in the WebSocket handshake. Mandatory extensions were: " + evt.reason;
123 | break;
124 | case 1011:
125 | reason = 'Internl Server Error. Connection closed because it encountered an unexpected condition that prevented it from fulfilling the request.';
126 | break;
127 | case 1015:
128 | reason = "TLS Handshake. The connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified).";
129 | break;
130 | default:
131 | reason = 'Unknown reason';
132 | break;
133 | }
134 |
135 | // cleanup connection
136 | cleanup(this);
137 |
138 | // emit close event
139 | this.emit(`websocket:close`, {code: evt.code, reason: reason});
140 |
141 | // Start reconnection
142 | reconnect(this);
143 | }
144 |
145 | // Override able websocket on-error event handler
146 | onError() {
147 |
148 | // cleanup
149 | cleanup(this);
150 |
151 | // emit error event
152 | this.emit('websocket:error');
153 |
154 | // Start delayed reconnect
155 | reconnect(this);
156 | }
157 |
158 |
159 | // starts connection to address
160 | connect(address) {
161 | if (this[$websock]) {
162 | return this;
163 | }
164 |
165 | if (address != null) {
166 | if (this[$addressKey] == null) {
167 | Object.defineProperty(this, $addressKey, {value: address});
168 | } else {
169 | this[$addressKey] = address;
170 | }
171 | }
172 |
173 | this[$readyState] = 1;
174 | this[$websock] = new WebSocket(this[$addressKey]);
175 | this[$websock].onopen = this.onOpen.bind(this);
176 | this[$websock].onmessage = this.onMessage.bind(this);
177 | this[$websock].onerror = this.onError.bind(this);
178 | this[$websock].onclose = this.onClose.bind(this);
179 |
180 | return this;
181 | }
182 |
183 | // All data sent should be JSON strings
184 | send(data) {
185 | data = JSON.stringify(data);
186 |
187 | if (
188 | onConnect === true ||
189 | (this[$readyState] === 3 && !this[$spooledMessages].length)) {
190 | this[$websock].send(data);
191 | } else {
192 | this[$spooledMessages].push(data);
193 | }
194 | return this;
195 | }
196 | }
197 |
198 | module.exports = Connection;
--------------------------------------------------------------------------------
/src/common/emitter.js:
--------------------------------------------------------------------------------
1 | const util = require('./utils.js');
2 |
3 | const $eventListenersKey = Symbol('event listeners');
4 |
5 | class Emitter {
6 | constructor() {
7 | Object.defineProperty(this, $eventListenersKey, {value: {}});
8 | }
9 |
10 | on(event, handler, isOnce) {
11 |
12 | // Validate event
13 | if (!util.isString(event, {notEmpty: true})) {
14 | throw new TypeError('invalid name argument');
15 | }
16 |
17 | // Validate handler
18 | if (!util.isCallable(handler)) {
19 | throw new TypeError('invalid handler argument');
20 | }
21 |
22 | // Validate isOneTimeHandler
23 | if (isOnce != null && !util.isBoolean(isOnce)) {
24 | throw new TypeError('invalid isOnce argument');
25 | }
26 |
27 | // Create a list of event handlers for the event if one does not exist
28 | if (this[$eventListenersKey][event] == null) {
29 | this[$eventListenersKey][event] = [];
30 | }
31 |
32 | // Store the handler
33 | this[$eventListenersKey][event].push({
34 | handler: handler,
35 | once: isOnce == null ? false : isOnce
36 | });
37 |
38 | // Return instance to enable chaining
39 | return this;
40 | }
41 |
42 | off(event, handler, isOnce) {
43 |
44 | // validate event
45 | if (!util.isString(event, {notEmpty: true})) {
46 | throw new TypeError('invalid name argument');
47 | }
48 |
49 | // validate handler
50 | if (!util.isCallable(handler)) {
51 | throw new TypeError('invalid handler argument');
52 | }
53 |
54 | // validate isOneTimeHandler
55 | if (isOnce != null && !util.isBoolean(isOnce)) {
56 | throw new TypeError('invalid isOneTimeHandler argument');
57 | }
58 |
59 | let listeners = self[$eventListenersKey][event];
60 |
61 | // event does not have registered listeners so nothing left to do
62 | if (listeners == null || !listeners.length) {
63 | return;
64 | }
65 |
66 | // find
67 | let idx = listeners.length;
68 | do {
69 | idx -= 1;
70 |
71 | // get listener instance
72 | let listener = listeners[idx];
73 |
74 | // Check: listener instance matches the inputs
75 | if (listener.handler === handler && listener.once === isOnce) {
76 |
77 | // remove the listener and exit looping
78 | listeners.splice(idx, 1);
79 | break;
80 | }
81 | } while (idx > 0);
82 |
83 | // Return instance to enable chaining
84 | return this;
85 | }
86 |
87 | once(event, handler) {
88 | return this.on(event, handler, true);
89 | }
90 |
91 | nonce(event, handler) {
92 | return this.off(event, handler, true);
93 | }
94 |
95 | emit(event, data, options) {
96 |
97 | // Validate inputs
98 | if (!util.isString(event, {notEmpty: true})) {
99 | throw new TypeError('invalid event name');
100 | }
101 |
102 | // No listeners for event
103 | if (
104 | this[$eventListenersKey] == null ||
105 | this[$eventListenersKey][event] == null ||
106 | this[$eventListenersKey][event].length === 0
107 | ) {
108 | return this;
109 | }
110 |
111 | options = options == null ? {} : options;
112 |
113 | let self = this,
114 | listeners = this[$eventListenersKey][event],
115 | stopped = false,
116 | evt = Object.create(null),
117 | idx = 0;
118 |
119 | Object.defineProperties(evt, {
120 | stop: {
121 | enumerable: true,
122 | value: function stop() {
123 | stopped = true;
124 | }
125 | },
126 | data: {
127 | enumerable: true,
128 | value: data
129 | }
130 | });
131 |
132 | while (idx < listeners.length) {
133 |
134 | // Retrieve next listener for the event
135 | let listener = listeners[idx];
136 |
137 | // Listener is a one-time handler
138 | if (listener.once) {
139 |
140 | // Remove the handler from the event's listeners list
141 | listeners.splice(idx, 1);
142 |
143 | } else {
144 | idx += 1;
145 | }
146 |
147 | // Attempt to call handler
148 | listener.handler.call(options.self != null ? options.self : self, evt);
149 |
150 | // Listener called .stop() - exit processing
151 | if (stopped && options.stoppable !== false) {
152 | break;
153 | }
154 | }
155 |
156 | // return instance to enable chaining
157 | return this;
158 | }
159 | }
160 |
161 | module.exports = Emitter;
--------------------------------------------------------------------------------
/src/common/irn-client.js:
--------------------------------------------------------------------------------
1 | const util = require('./utils.js');
2 |
3 | const idChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
4 | const reserved = "0".repeat(32);
5 |
6 | function format(id, type, meta, data) {
7 | return {
8 | irn: {
9 | id: id,
10 | type: type,
11 | meta: meta,
12 | data: data == null ? null : data
13 | }
14 | };
15 | }
16 |
17 | function irnClient(streamdeck) {
18 | let $pending = {},
19 | $methods = {};
20 |
21 | const genId = function () {
22 | let result = "";
23 | do {
24 | let i = 32;
25 | while (i--) {
26 | result += idChars[Math.floor(Math.random() * 62)];
27 | }
28 | } while (result !== reserved && $pending[result] != null);
29 | return result;
30 | };
31 |
32 | const registerMethod = function register(method, handler) {
33 | if (!util.isString(method, {notEmpty: true})) {
34 | throw new TypeError('invalid method argument');
35 | }
36 | if (!util.isCallable(handler)) {
37 | throw new TypeError('invalid handler argument');
38 | }
39 | if (util.isKey($methods, method) && $methods[method] != null) {
40 | throw new TypeError('method already registered');
41 | }
42 | $methods[method] = handler;
43 | };
44 |
45 | Object.defineProperties(streamdeck, {
46 | register: {
47 | enumerable: true,
48 | value: function register(...args) {
49 | if (util.isString(args[0], {match: /^\$/})) {
50 | throw new TypeError('invalid method argument');
51 | }
52 | registerMethod(...args);
53 | }
54 | },
55 | unregister: {
56 | enumerable: true,
57 | value: function unregister(method, handler) {
58 | if (!util.isString(method, {notEmpty: true, matches: /^[^$]/})) {
59 | throw new TypeError('invalid method argument');
60 | }
61 | if ($methods[method] == null) {
62 | return;
63 | }
64 | if (!util.isCallable(handler)) {
65 | throw new TypeError('invalid handler argument');
66 | }
67 | if ($methods[method] !== handler) {
68 | throw new TypeError('handler does not match registered handler');
69 | }
70 |
71 | delete $methods[method];
72 | }
73 | }
74 | });
75 |
76 | streamdeck.on('message', function (evt) {
77 |
78 | let data = evt.data,
79 | info;
80 |
81 | // basic validation
82 | if (
83 | data == null ||
84 | data.irn == null ||
85 | !util.isString(data.irn.id, {match: /^(?:[a-z\d]{32})/i}) ||
86 | !util.isString(data.irn.type, {match: /^(?:invoke|response|notify)$/}) ||
87 | !util.isString(data.irn.meta, {notEmpty: true}) ||
88 | !util.isKey(data.irn, 'data')
89 | ) {
90 | return;
91 | }
92 |
93 | data = evt.data.irn;
94 |
95 | const sendProp = streamdeck.layer === 'plugin' ? 'send' : 'sendToPlugin';
96 | switch (data.type) {
97 |
98 | case 'notify':
99 | if (data.id !== reserved) {
100 | return;
101 | }
102 | streamdeck.emit(`notify:${data.meta}`, data.data);
103 | streamdeck.emit(`notify`, {event: data.meta, data: data.data});
104 | break;
105 |
106 | case 'response':
107 | if ($pending[data.id] == null) {
108 | return;
109 | }
110 |
111 | info = $pending[data.id];
112 | delete $pending[data.id];
113 |
114 | clearTimeout(info.timeout);
115 |
116 | if (data.meta === 'ok') {
117 | info.resolve(data.data);
118 | } else if (data.meta === 'error') {
119 | info.reject(new Error(data.data));
120 | } else {
121 | info.reject(new Error('invalid state received'));
122 | }
123 | break;
124 |
125 | case 'invoke':
126 | if ($methods[data.meta] == null) {
127 | this[sendProp](format(data.id, 'response', 'error', 'method not registered'));
128 |
129 | } else if (!util.isArray(data.data)) {
130 | this[sendProp](format(data.id, 'response', 'error', 'invalid arguments'));
131 |
132 | } else {
133 | try {
134 | info = $methods[data.meta].call(this, ...data.data);
135 | if (!(info instanceof Promise)) {
136 | info = Promise.resolve(info);
137 | }
138 |
139 | info
140 | .then(
141 | res => {
142 | this[sendProp](format(data.id, 'response', 'ok', res));
143 | },
144 | err => {
145 | this[sendProp](format(
146 | data.id,
147 | 'response',
148 | 'error',
149 | err instanceof Error ? err.message : String(err) === err ? err : 'unknown error'
150 | ));
151 | }
152 | )
153 | .catch(err => {
154 | this[sendProp](format(
155 | data.id,
156 | 'response',
157 | 'error',
158 | err instanceof Error ? err.message : String(err) === err ? err : 'unknown error'
159 | ));
160 | });
161 |
162 | } catch (err) {
163 | this[sendProp](format(data.id, 'response', 'error', err.message));
164 | }
165 | }
166 | break;
167 | }
168 | evt.stop();
169 | });
170 |
171 |
172 | return {
173 | invoke: function (method, ...args) {
174 | let id = genId();
175 |
176 | return {
177 | promise: new Promise((resolve, reject) => {
178 | $pending[id] = {
179 | resolve: resolve,
180 | reject: reject,
181 | timeout: setTimeout(function () {
182 | delete $pending[id];
183 | reject(new Error('invoke timed out'));
184 | }, 30000)
185 | };
186 | }),
187 | result: format(id, 'invoke', method, args)
188 | };
189 | },
190 | notify: function (event, data) {
191 | return format(reserved, 'notify', event, data);
192 | },
193 | register: registerMethod
194 | };
195 | }
196 |
197 | module.exports = irnClient;
--------------------------------------------------------------------------------
/src/common/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const hasOwnProperty = Object.prototype.hasOwnProperty;
4 |
5 | function isBoolean(subject) {
6 | return subject === true || subject === false;
7 | }
8 | function isNumber(subject, opts = {}) {
9 |
10 | // not a primitive number
11 | if (typeof subject !== 'number' || Number(subject) !== subject) {
12 | return false;
13 | }
14 |
15 | // NaN not allowed
16 | if (!opts.allowNaN && isNaN(subject)) {
17 | return false;
18 | }
19 |
20 | // infinity not allowed
21 | if (!opts.allowInfinity && !isFinite(subject)) {
22 | return false;
23 | }
24 |
25 | // above specified min
26 | if (opts.min && subject < opts.min) {
27 | return false;
28 | }
29 |
30 | // above specified max
31 | if (opts.max && subject > opts.max) {
32 | return false;
33 | }
34 |
35 | // not a whole number
36 | if (opts.whole && subject % 1 > 0) {
37 | return false;
38 | }
39 |
40 | // is valid
41 | return true;
42 | }
43 | function isString(subject, opts = {}) {
44 |
45 | // not a primitive string
46 | if (typeof subject !== 'string' || String(subject) !== subject) {
47 | return false;
48 | }
49 |
50 | // Empty string not allowed
51 | if (opts.notEmpty && subject === '') {
52 | return false;
53 | }
54 |
55 | // string didn't match specified regex
56 | if (opts.match && !opts.match.test(subject)) {
57 | return false;
58 | }
59 |
60 | return true;
61 | }
62 | function isBase64(subject, options = {}) {
63 |
64 | // Is either not a string or an empty string
65 | if (!isString(subject, {notEmpty: true})) {
66 | return false;
67 | }
68 |
69 | let char62 = options['62'] != null ? options['62'] : '+',
70 | char63 = options['63'] != null ? options['63'] : '/';
71 |
72 | // validate 62nd and then escape it for the regex pattern
73 | if (!isString(char62, {notEmpty: true, matches: /^[+._~-]$/i})) {
74 | throw new TypeError('specified 62nd character invalid');
75 | }
76 |
77 | // validate 62nd and then escape it for the regex pattern
78 | if (!isString(char63, {notEmpty: true, matches: /^[^/_,:-]$/i})) {
79 | throw new TypeError('specified 63rd character invalid');
80 | }
81 |
82 | // validate 62nd and 63rd pairing
83 | switch (char62 + char63) {
84 | case '+/': // RFC 1421, 2045, 3548, 4880, 1642
85 | case '+,': // RFC 3501
86 | case '._': // YUI, Program identifier variant 2
87 | case '.-': // XML name tokens
88 | case '_:': // RFC 4648
89 | case '_-': // XML identifiers, Program Identifier variant 1
90 | case '~-': // Freenet URL-safe
91 | case '-_': // RFC 4648
92 | break;
93 | default:
94 | throw new TypeError('invalid 62nd and 63rd character pair');
95 | }
96 |
97 | // escape for regex
98 | char62 = '\\' + char62;
99 | char63 = '\\' + char63;
100 |
101 | // create regex
102 | let match = new RegExp(`^(?:[a-z\\d${char62}${char63}]{4})*(?:[a-z\\d${char62}${char63}]{2}(?:[a-z\\d${char62}${char63}]|=)=)?$`, 'i');
103 |
104 | // test the input
105 | return match.test(subject);
106 | }
107 |
108 | function isArray(subject) {
109 | return Array.isArray(subject) && subject instanceof Array;
110 | }
111 |
112 | function isKey(subject, key) {
113 | return hasOwnProperty.call(subject, key);
114 | }
115 |
116 | const isCallable = (function() {
117 |
118 | // https://github.com/ljharb/is-callable
119 | let fnToStr = Function.prototype.toString,
120 | fnClass = '[object Function]',
121 | toStr = Object.prototype.toString,
122 | genClass = '[object GeneratorFunction]',
123 | hasToStringTag = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol',
124 | constructorRegex = /^\s*class\b/;
125 |
126 | function isES6ClassFn(value) {
127 | try {
128 | let fnStr = fnToStr.call(value);
129 | return constructorRegex.test(fnStr);
130 | } catch (e) {
131 | return false; // not a function
132 | }
133 | }
134 |
135 | function tryFunctionObject(value) {
136 | try {
137 | if (isES6ClassFn(value)) {
138 | return false;
139 | }
140 | fnToStr.call(value);
141 | return true;
142 | } catch (e) {
143 | return false;
144 | }
145 | }
146 | return function isCallable(value) {
147 | if (!value) {
148 | return false;
149 | }
150 | if (typeof value !== 'function' && typeof value !== 'object') {
151 | return false;
152 | }
153 | if (typeof value === 'function' && !value.prototype) {
154 | return true;
155 | }
156 | if (hasToStringTag) {
157 | return tryFunctionObject(value);
158 | }
159 | if (isES6ClassFn(value)) {
160 | return false;
161 | }
162 | let strClass = toStr.call(value);
163 | return strClass === fnClass || strClass === genClass;
164 | };
165 | }());
166 |
167 | const deepFreeze = (function() {
168 | function freeze(obj, freezing) {
169 |
170 | // Loop over properties of the input object
171 | // Done before freezing the initial object
172 | Object.keys(obj).forEach(key => {
173 |
174 | // ignore properties that have setter/getter descriptors
175 | let desc = Object.getOwnPropertyDescriptor(obj, key);
176 | if (!isKey(desc, 'value')) {
177 | return;
178 | }
179 |
180 | // get property's value
181 | let value = obj[key];
182 |
183 | if (
184 | // value isn't null or undefined
185 | value != null &&
186 |
187 | // value isn't frozen
188 | !Object.isFrozen(value) &&
189 |
190 | // value is freezable
191 | value instanceof Object &&
192 |
193 | // value isn't already in the process of being frozen
194 | freezing.findIndex(item => item === value) === -1
195 | ) {
196 |
197 | // store a reference to the value - used to prevent circular reference loops
198 | freezing.push(value);
199 |
200 | // freeze the property
201 | obj[key] = freeze(value, freezing);
202 |
203 | // remove the reference
204 | freezing.pop(value);
205 | }
206 | });
207 |
208 | // freeze the base object
209 | return Object.freeze(obj);
210 | }
211 | return function deepFreeze(subject) {
212 | return freeze(subject, [subject]);
213 | };
214 | }());
215 |
216 | module.exports = Object.freeze({
217 | isBoolean: isBoolean,
218 | isNumber: isNumber,
219 | isString: isString,
220 | isBase64: isBase64,
221 | isArray: isArray,
222 | isKey: isKey,
223 | isCallable: isCallable,
224 | deepFreeze: deepFreeze
225 | });
--------------------------------------------------------------------------------
/src/foreground/index.js:
--------------------------------------------------------------------------------
1 | const onmessage = require('./onmessage.js');
2 | const irnClient = require('../common/irn-client.js');
3 |
4 | function foreground(streamdeck, selfinfo) {
5 |
6 | // Setup Foreground Invoke-Respond-Notify client
7 | let irn = irnClient(streamdeck);
8 |
9 | // Define foreground-specific properties to the streamdeck isntance
10 | Object.defineProperties(streamdeck, {
11 |
12 | // Override default on-message handler
13 | onMessage: {
14 | enumerable: true,
15 | value: onmessage
16 | },
17 |
18 | // Context and Action ids
19 | contextId: {
20 | enumerable: true,
21 | value: selfinfo.context
22 | },
23 | actionId: {
24 | enumerable: true,
25 | value: selfinfo.action
26 | },
27 |
28 | // Function to send data to background
29 | sendToPlugin: {
30 | enumerable: true,
31 | value: function sendToPlugin(data) {
32 | streamdeck.send({
33 | event: "sendToPlugin",
34 | action: streamdeck.actionId,
35 | context: streamdeck.id,
36 | payload: data
37 | });
38 | }
39 | },
40 |
41 | // IRN client related invoke and notify
42 | invoke: {
43 | enumerable: true,
44 | value: function invoke(method, ...args) {
45 | let res = irn.invoke(method, ...args);
46 | this.sendToPlugin(res.result);
47 | return res.promise;
48 | }
49 | },
50 | notify: {
51 | enumerable: true,
52 | value: function notify(event, ...args) {
53 | this.sendToPlugin(irn.notify(event, ...args));
54 | }
55 | },
56 |
57 | // get/setTitle functions
58 | getTitle: {
59 | enumerable: true,
60 | value: function getTitle() {
61 | return this.invoke('$getTitle');
62 | }
63 | },
64 | setTitle: {
65 | enumerable: true,
66 | value: function setTitle(title, target) {
67 | return this.invoke('$setTitle', title, target);
68 | }
69 | },
70 |
71 | // get/setImage functions
72 | getImage: {
73 | enumerable: true,
74 | value: function getImage() {
75 | return Promise.reject(new Error('not supported'));
76 | }
77 | },
78 | setImage: {
79 | enumerable: true,
80 | value: function setImage(image, target) {
81 | return this.invoke('$setImage', image, target);
82 | }
83 | },
84 | setImageFromUrl: {
85 | enumerable: true,
86 | value: function setImageFromUrl(url, target) {
87 | return this.invoke('$setImageToUrl', url, target);
88 | }
89 | },
90 |
91 | // get/setState functions
92 | getState: {
93 | enumerable: true,
94 | value: function getState() {
95 | return this.invoke('$getState');
96 | }
97 | },
98 | setState: {
99 | enumerable: true,
100 | value: function setState(state) {
101 | return this.invoke('$setState', state);
102 | }
103 | },
104 |
105 | // get/setSettings functions
106 | getSettings: {
107 | enumerable: true,
108 | value: function getSettings() {
109 | return this.invoke('$getSettings');
110 | }
111 | },
112 | setSettings: {
113 | enumerable: true,
114 | value: function setSettings(settings) {
115 | return this.invoke('$setSettings', settings);
116 | }
117 | },
118 |
119 | // show alerts
120 | showAlert: {
121 | enumerable: true,
122 | value: function showAlert() {
123 | return this.invoke('$showAlert');
124 | }
125 | },
126 | showOk: {
127 | enumerable: true,
128 | value: function showOk() {
129 | return this.invoke('$showOk');
130 | }
131 | }
132 | });
133 | }
134 |
135 | module.exports = foreground;
--------------------------------------------------------------------------------
/src/foreground/onmessage.js:
--------------------------------------------------------------------------------
1 | const util = require('../common/utils.js');
2 |
3 | function onmessage(evt) {
4 |
5 | // Retrieve message data
6 | let msg = evt.data;
7 |
8 | // Message null or doesn't appear to be a JSON object string
9 | if (msg == null || !util.isString(msg, {match: /^\{[\s\S]+\}$/})) {
10 | return this.emit('websocket:message', evt.data);
11 | }
12 |
13 | // Attempt to parse the msg
14 | try {
15 | msg = JSON.parse(msg);
16 | } catch (ignore) {
17 | return this.emit('websocket:message', evt.data);
18 | }
19 |
20 | // Streamdeck messages sent to the foreground will always have an event property of 'sendToPropertyInspector'
21 | if (!util.isString(msg.event, {match: /^sendToPropertyInspector$/})) {
22 | return this.emit('websocket:message', evt.data);
23 | }
24 |
25 | // emit the received event as a 'message' event
26 | this.emit('message', msg.payload);
27 | }
28 |
29 | module.exports = onmessage;
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const util = require('./common/utils.js');
2 | const Connection = require('./common/connection.js');
3 |
4 | const background = require('./background');
5 | const foreground = require('./foreground');
6 |
7 | const $ready = Symbol('ready');
8 | const $port = Symbol('port');
9 | const $id = Symbol('instance identifier');
10 | const $register = Symbol('registerEvent');
11 | const $layer = Symbol('layer');
12 | const $host = Symbol('host');
13 | const $deviceList = Symbol('device list');
14 |
15 | /**
16 | * @class StreamDeck
17 | * @classdesc StreamDeck API handler
18 | * @extends {Connection}
19 | */
20 | class StreamDeck extends Connection {
21 |
22 | /**
23 | * @desc Adds an event listener
24 | * @param {string} event The event name to attach to
25 | * @param {function} handler The callback function to call when the event occurs
26 | * @param {boolean} [once=false] If true, after the event is emitted the handler will be removed
27 | * @memberof StreamDeck
28 | * @instance
29 | * @return {this}
30 | */
31 | on(event, handler, once) {
32 | if (event === 'ready') {
33 | if (this.ready) {
34 | handler.call(this);
35 | return;
36 | }
37 | once = true;
38 | }
39 | return super.on(event, handler, once);
40 | }
41 |
42 | /**
43 | * @desc Removes the event listener. All parameters must match those used to create the listener
44 | * @memberof StreamDeck
45 | * @instance
46 | * @param {string} event The event name to attach to
47 | * @param {function} handler The callback function to call when the event occurs
48 | * @param {boolean} [once=false] If true, after the event is emitted the handler will be removed
49 | * @return {this}
50 | */
51 | off(event, handler, once) {
52 | if (event === 'ready') {
53 | once = true;
54 | }
55 | return super.off(event, handler, once);
56 | }
57 |
58 | constructor() {
59 | super();
60 | Object.defineProperty(this, $ready, {writable: true, value: false});
61 |
62 | Object.defineProperties(this, {
63 | /**
64 | * The ready state of the StreamDeck instance.
65 | *
66 | * true if ready, false if not
67 | * @name StreamDeck#ready
68 | * @instance
69 | * @type {boolean}
70 | * @readonly
71 | */
72 | ready: {
73 | enumerable: true,
74 | get: function () {
75 | return this[$ready];
76 | }
77 | },
78 |
79 | /**
80 | * The port to use to connect to Stream Deck's software
81 | * @name StreamDeck#port
82 | * @instance
83 | * @type {boolean}
84 | * @readonly
85 | */
86 | port: {
87 | enumerable: true,
88 | get: function () {
89 | return this[$id];
90 | }
91 | },
92 | id: {
93 | enumerable: true,
94 | get: function () {
95 | return this[$id];
96 | }
97 | },
98 | layer: {
99 | enumerable: true,
100 | get: function () {
101 | return this[$layer];
102 | }
103 | },
104 | host: {
105 | enumerable: true,
106 | get: function () {
107 | return Object.assign({}, this[$host]);
108 | }
109 | },
110 | devices: {
111 | enumerable: true,
112 | get: function () {
113 | return JSON.parse(JSON.stringify(this[$deviceList]));
114 | }
115 | }
116 | });
117 | }
118 |
119 | openUrl(url) {
120 | if (!util.isString(url, {notEmpty: true})) {
121 | throw new TypeError('invalid url');
122 | }
123 |
124 | this.send({
125 | event: "openUrl",
126 | payload: { url: url }
127 | });
128 | }
129 |
130 | start(port, id, register, hostinfo, selfinfo) {
131 |
132 | if (this[$ready] !== false) {
133 | throw new Error('start() function already called');
134 | }
135 | let readyDesc = Object.getOwnPropertyDescriptor(this, $ready);
136 | readyDesc.value = true;
137 | readyDesc.writable = false;
138 |
139 | /*
140 | ** ARGUMENT VALIDATION
141 | */
142 |
143 | // Validate port
144 | if (util.isString(port, {match: /^\d+$/i})) {
145 | port = Number(port);
146 | }
147 | if (!util.isNumber(port, {whole: true, min: 0, max: 65535})) {
148 | throw new TypeError('invalid port argument');
149 | }
150 |
151 | // Validate uuid
152 | if (!util.isString(id, {match: /^(?:(?:[A-F\d]+-){4}[A-F\d]+)$/})) {
153 | throw new TypeError('invalid uuid argument');
154 | }
155 |
156 | // Validate registerEvent
157 | if (!util.isString(register, {match: /^register(?:Plugin|PropertyInspector)$/})) {
158 | throw new TypeError('invalid registerEvent argument');
159 | }
160 |
161 | // Process host as JSON if its a string
162 | if (util.isString(hostinfo)) {
163 | try {
164 | hostinfo = JSON.parse(hostinfo);
165 | } catch (e) {
166 | throw new TypeError('invalid hostInfo argument');
167 | }
168 | }
169 |
170 | // Validate hostinfo
171 | if (
172 | hostinfo == null ||
173 | !util.isKey(hostinfo, 'application') ||
174 | !util.isKey(hostinfo.application, 'language') ||
175 | !util.isString(hostinfo.application.language) ||
176 | !util.isKey(hostinfo.application, 'platform') ||
177 | !util.isString(hostinfo.application.platform) ||
178 | !util.isKey(hostinfo.application, 'version') ||
179 | !util.isString(hostinfo.application.version) ||
180 | !util.isKey(hostinfo, 'devices') ||
181 | !util.isArray(hostinfo.devices)
182 | ) {
183 | throw new TypeError('invalid environment argument');
184 | }
185 |
186 | let deviceList = {};
187 | hostinfo.devices.forEach(device => {
188 | if (
189 | device == null ||
190 | !util.isString(device.id, {match: /^[A-F\d]{32}$/}) ||
191 | device.size == null ||
192 | !util.isNumber(device.size.rows, {whole: true, min: 1}) ||
193 | !util.isNumber(device.size.columns, {whole: true, min: 1}) ||
194 | (device.type != null && !util.isNumber(device.type, {whole: true, min: 0}))
195 | ) {
196 | throw new TypeError('invalid device list');
197 | }
198 |
199 | // add the validated device to the deviceList
200 | deviceList[device.id] = {
201 | id: device.id,
202 | rows: device.size.rows,
203 | columns: device.size.columns,
204 | type: device.type
205 | };
206 | });
207 |
208 | // If foreground, validate selfinfo
209 | if (register === 'registerPropertyInspector') {
210 |
211 | // If string, convert to object
212 | if (util.isString(selfinfo)) {
213 | try {
214 | selfinfo = JSON.parse(selfinfo);
215 | } catch (e) {
216 | throw new TypeError('invalid selfInfo argument');
217 | }
218 | }
219 |
220 | // Validate selfinfo
221 | if (
222 | selfinfo == null ||
223 | !util.isString(selfinfo.context, {match: /^[A-F\d]{32}$/}) ||
224 | !util.isString(selfinfo.action, {notEmpty: true})
225 | ) {
226 | throw new TypeError('invalid selfInfo argument');
227 | }
228 |
229 | // If background, selfinfo should be null
230 | } else if (selfinfo != null) {
231 | throw new TypeError('selfinfo specified for plugin');
232 | }
233 | /*
234 | ** VALIDATION COMPLETE
235 | */
236 | Object.defineProperty(this, $port, {value: port});
237 | Object.defineProperty(this, $id, {value: id});
238 | Object.defineProperty(this, $register, {value: register});
239 | Object.defineProperty(this, $layer, {value: register === 'registerPlugin' ? 'plugin' : 'propertyinspector'});
240 | Object.defineProperty(this, $host, {value: hostinfo.application});
241 | Object.defineProperty(this, $deviceList, {value: deviceList});
242 |
243 | // Start based on register value
244 | if (this[$layer] === 'plugin') {
245 | background(this, deviceList);
246 |
247 | } else {
248 | foreground(this, selfinfo);
249 | }
250 |
251 | let self = this;
252 |
253 | // start connection to Stream Deck
254 | this.connect(`ws://localhost:${port}`);
255 | this.on('websocket:connect', function (evt) {
256 | evt.stop();
257 | self.send({
258 | event: register,
259 | uuid: id
260 | });
261 | });
262 |
263 | // emit ready event
264 | this.emit('ready');
265 | }
266 | }
267 |
268 | module.exports = StreamDeck;
--------------------------------------------------------------------------------