├── .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 | 27 |
28 |
29 |
Change Value
30 |
31 | 32 |
33 |
34 |
35 |
Background
36 | 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; --------------------------------------------------------------------------------