diff --git a/nodecg-io-core/dashboard/crypto.ts b/nodecg-io-core/dashboard/crypto.ts
index 4ead7cf98..883ab7225 100644
--- a/nodecg-io-core/dashboard/crypto.ts
+++ b/nodecg-io-core/dashboard/crypto.ts
@@ -39,11 +39,11 @@ class Config extends EventEmitter {
export const config = new Config();
// Update the decrypted copy of the data once the encrypted version changes (if pw available).
-// This ensures that the decrypted data is kept uptodate.
+// This ensures that the decrypted data is kept up-to-date.
encryptedData.on("change", updateDecryptedData);
/**
- * Sets the passed passwort to be used by the crypto module.
+ * Sets the passed password to be used by the crypto module.
* Will try to decrypt decrypted data to tell whether the password is correct,
* if it is wrong the internal password will be set to undefined.
* Returns whether the password is correct.
diff --git a/nodecg-io-core/dashboard/main.ts b/nodecg-io-core/dashboard/main.ts
index e00bde1ea..a878b1756 100644
--- a/nodecg-io-core/dashboard/main.ts
+++ b/nodecg-io-core/dashboard/main.ts
@@ -6,6 +6,7 @@ export {
createInstance,
saveInstanceConfig,
deleteInstance,
+ selectInstanceConfigPreset,
} from "./serviceInstance";
export {
renderBundleDeps,
diff --git a/nodecg-io-core/dashboard/panel.html b/nodecg-io-core/dashboard/panel.html
index 621fb7098..b7507fc51 100644
--- a/nodecg-io-core/dashboard/panel.html
+++ b/nodecg-io-core/dashboard/panel.html
@@ -42,6 +42,11 @@
+
+
+
+
+
diff --git a/nodecg-io-core/dashboard/serviceInstance.ts b/nodecg-io-core/dashboard/serviceInstance.ts
index e302f4852..9630f5095 100644
--- a/nodecg-io-core/dashboard/serviceInstance.ts
+++ b/nodecg-io-core/dashboard/serviceInstance.ts
@@ -7,6 +7,7 @@ import {
import { updateOptionsArr, updateOptionsMap } from "./utils/selectUtils";
import { objectDeepCopy } from "./utils/deepCopy";
import { config, sendAuthenticatedMessage } from "./crypto";
+import { ObjectMap } from "../extension/service";
const editorDefaultText = "<---- Select a service instance to start editing it in here";
const editorCreateText = "<---- Create a new service instance on the left and then you can edit it in here";
@@ -23,10 +24,12 @@ document.addEventListener("DOMContentLoaded", () => {
// Inputs
const selectInstance = document.getElementById("selectInstance") as HTMLSelectElement;
const selectService = document.getElementById("selectService") as HTMLSelectElement;
+const selectPreset = document.getElementById("selectPreset") as HTMLSelectElement;
const inputInstanceName = document.getElementById("inputInstanceName") as HTMLInputElement;
// Website areas
const instanceServiceSelector = document.getElementById("instanceServiceSelector");
+const instancePreset = document.getElementById("instancePreset");
const instanceNameField = document.getElementById("instanceNameField");
const instanceEditButtons = document.getElementById("instanceEditButtons");
const instanceCreateButton = document.getElementById("instanceCreateButton");
@@ -62,33 +65,59 @@ export function onInstanceSelectChange(value: string): void {
showNotice(undefined);
switch (value) {
case "new":
- showInMonaco("text", true, editorCreateText);
- setCreateInputs(true, false, true);
+ showInMonaco(true, editorCreateText);
+ setCreateInputs(true, false, true, false);
inputInstanceName.value = "";
break;
case "select":
- showInMonaco("text", true, editorDefaultText);
- setCreateInputs(false, false, true);
+ showInMonaco(true, editorDefaultText);
+ setCreateInputs(false, false, true, false);
break;
default:
showConfig(value);
}
}
-function showConfig(value: string) {
- const inst = config.data?.instances[value];
+function showConfig(instName: string) {
+ const inst = config.data?.instances[instName];
const service = config.data?.services.find((svc) => svc.serviceType === inst?.serviceType);
if (!service) {
- showInMonaco("text", true, editorInvalidServiceText);
+ showInMonaco(true, editorInvalidServiceText);
} else if (service.requiresNoConfig) {
- showInMonaco("text", true, editorNotConfigurableText);
+ showInMonaco(true, editorNotConfigurableText);
} else {
- const jsonString = JSON.stringify(inst?.config || {}, null, 4);
- showInMonaco("json", false, jsonString, service?.schema);
+ showInMonaco(false, inst?.config ?? {}, service?.schema);
}
- setCreateInputs(false, true, !(service?.requiresNoConfig ?? false));
+ setCreateInputs(false, true, !(service?.requiresNoConfig ?? false), service?.presets !== undefined);
+
+ if (service?.presets) {
+ renderPresets(service.presets);
+ }
+}
+
+// Preset drop-down
+export function selectInstanceConfigPreset(): void {
+ const selectedPresetName = selectPreset.options[selectPreset.selectedIndex]?.value;
+ if (!selectedPresetName) {
+ return;
+ }
+
+ const instName = selectInstance.options[selectInstance.selectedIndex]?.value;
+ if (!instName) {
+ return;
+ }
+
+ const instance = config.data?.instances[instName];
+ if (!instance) {
+ return;
+ }
+
+ const service = config.data?.services.find((svc) => svc.serviceType === instance.serviceType);
+ const presetValue = service?.presets?.[selectedPresetName] ?? {};
+
+ showInMonaco(false, presetValue, service?.schema);
}
// Save button
@@ -191,6 +220,17 @@ function renderInstances() {
selectServiceInstance(previousSelected);
}
+function renderPresets(presets: ObjectMap) {
+ updateOptionsMap(selectPreset, presets);
+
+ // Add "Select..." element that hints the user that he can use this select box
+ // to choose a preset
+ const selectHintOption = document.createElement("option");
+ selectHintOption.innerText = "Select...";
+ selectPreset.prepend(selectHintOption);
+ selectPreset.selectedIndex = 0; // Select newly added hint
+}
+
// Util functions
function selectServiceInstance(instanceName: string) {
@@ -208,7 +248,12 @@ function selectServiceInstance(instanceName: string) {
}
// Hides/unhides parts of the website based on the passed parameters
-function setCreateInputs(createMode: boolean, instanceSelected: boolean, showSave: boolean) {
+function setCreateInputs(
+ createMode: boolean,
+ instanceSelected: boolean,
+ showSave: boolean,
+ serviceHasPresets: boolean,
+) {
function setVisible(node: HTMLElement | null, visible: boolean) {
if (visible && node?.classList.contains("hidden")) {
node?.classList.remove("hidden");
@@ -218,6 +263,7 @@ function setCreateInputs(createMode: boolean, instanceSelected: boolean, showSav
}
setVisible(instanceEditButtons, !createMode && instanceSelected);
+ setVisible(instancePreset, !createMode && instanceSelected && serviceHasPresets);
setVisible(instanceCreateButton, createMode);
setVisible(instanceNameField, createMode);
setVisible(instanceServiceSelector, createMode);
@@ -231,12 +277,14 @@ export function showNotice(msg: string | undefined): void {
}
function showInMonaco(
- type: "text" | "json",
readOnly: boolean,
- content: string,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ content: any,
schema?: Record,
): void {
editor?.updateOptions({ readOnly });
+ const type = typeof content === "object" ? "json" : "text";
+ const contentStr = typeof content === "object" ? JSON.stringify(content, null, 4) : content;
// JSON Schema stuff
// Get rid of old models, as they have to be unique and we may add the same again
@@ -263,5 +311,5 @@ function showInMonaco(
},
);
- editor?.setModel(monaco.editor.createModel(content, type, schema ? modelUri : undefined));
+ editor?.setModel(monaco.editor.createModel(contentStr, type, schema ? modelUri : undefined));
}
diff --git a/nodecg-io-core/dashboard/styles.css b/nodecg-io-core/dashboard/styles.css
index cc92aeb8e..744c789a7 100644
--- a/nodecg-io-core/dashboard/styles.css
+++ b/nodecg-io-core/dashboard/styles.css
@@ -6,6 +6,7 @@
#bundleControlDiv {
display: grid;
grid-template-columns: auto 1fr;
+ width: 96.5%;
}
.flex {
@@ -14,6 +15,7 @@
.flex-fill {
flex: 1;
+ width: 100%;
}
.flex-column {
@@ -32,3 +34,7 @@
display: none;
visibility: hidden;
}
+
+select {
+ text-overflow: ellipsis;
+}
diff --git a/nodecg-io-core/extension/service.ts b/nodecg-io-core/extension/service.ts
index 416899b79..25ff2253b 100644
--- a/nodecg-io-core/extension/service.ts
+++ b/nodecg-io-core/extension/service.ts
@@ -37,6 +37,13 @@ export interface Service {
*/
readonly defaultConfig?: R;
+ /**
+ * Config presets that the user can choose to load as their config.
+ * Useful for e.g. detected devices with everything already filled in for that specific device.
+ * Can also be used to show the user multiple different authentication methods or similar.
+ */
+ presets?: ObjectMap;
+
/**
* This function validates the passed config after it has been validated against the json schema (if applicable).
* Should make deeper checks like checking validity of auth tokens.
diff --git a/nodecg-io-core/extension/serviceBundle.ts b/nodecg-io-core/extension/serviceBundle.ts
index dc6331eb9..b8a599e95 100644
--- a/nodecg-io-core/extension/serviceBundle.ts
+++ b/nodecg-io-core/extension/serviceBundle.ts
@@ -25,6 +25,13 @@ export abstract class ServiceBundle implements Service {
*/
public defaultConfig?: R;
+ /**
+ * Config presets that the user can choose to load as their config.
+ * Useful for e.g. detected devices with everything already filled in for that specific device.
+ * Can also be used to show the user multiple different authentication methods or similar.
+ */
+ public presets?: ObjectMap;
+
/**
* This constructor creates the service and gets the nodecg-io-core
* @param nodecg the current NodeCG instance
diff --git a/nodecg-io-midi-input/extension/index.ts b/nodecg-io-midi-input/extension/index.ts
index b79842976..526c0045d 100644
--- a/nodecg-io-midi-input/extension/index.ts
+++ b/nodecg-io-midi-input/extension/index.ts
@@ -13,6 +13,8 @@ module.exports = (nodecg: NodeCG) => {
};
class MidiService extends ServiceBundle {
+ presets = Object.fromEntries(easymidi.getInputs().map((device) => [device, { device }]));
+
async validateConfig(config: MidiInputServiceConfig): Promise> {
const devices: Array = new Array();
diff --git a/nodecg-io-midi-output/extension/index.ts b/nodecg-io-midi-output/extension/index.ts
index 8d322a1aa..381386d88 100644
--- a/nodecg-io-midi-output/extension/index.ts
+++ b/nodecg-io-midi-output/extension/index.ts
@@ -13,6 +13,8 @@ module.exports = (nodecg: NodeCG) => {
};
class MidiService extends ServiceBundle {
+ presets = Object.fromEntries(easymidi.getOutputs().map((device) => [device, { device }]));
+
async validateConfig(config: MidiOutputServiceConfig): Promise> {
const devices: Array = new Array();
diff --git a/nodecg-io-philipshue/extension/index.ts b/nodecg-io-philipshue/extension/index.ts
index 2469377ac..024a0ce4e 100644
--- a/nodecg-io-philipshue/extension/index.ts
+++ b/nodecg-io-philipshue/extension/index.ts
@@ -1,5 +1,5 @@
import { NodeCG } from "nodecg-types/types/server";
-import { Result, emptySuccess, success, error, ServiceBundle } from "nodecg-io-core";
+import { Result, emptySuccess, success, error, ServiceBundle, ObjectMap } from "nodecg-io-core";
import { v4 as ipv4 } from "is-ip";
import { v3 } from "node-hue-api";
// Only needed for type because of that it is "unused" but still needed
@@ -11,9 +11,8 @@ const deviceName = "nodecg-io";
const name = "philipshue";
interface PhilipsHueServiceConfig {
- discover: boolean;
ipAddr: string;
- port: number;
+ port?: number;
username?: string;
apiKey?: string;
}
@@ -25,20 +24,20 @@ module.exports = (nodecg: NodeCG) => {
};
class PhilipsHueService extends ServiceBundle {
+ presets = {};
+
+ constructor(nodecg: NodeCG, name: string, ...pathSegments: string[]) {
+ super(nodecg, name, ...pathSegments);
+ this.discoverBridges()
+ .then((bridgePresets) => (this.presets = bridgePresets))
+ .catch((err) => this.nodecg.log.error(`Failed to discover local bridges: ${err}`));
+ }
+
async validateConfig(config: PhilipsHueServiceConfig): Promise> {
- const { discover, port, ipAddr } = config;
-
- if (!config) {
- // config could not be found
- return error("No config found!");
- } else if (!discover) {
- // check the ip address if its there
- if (ipAddr && !ipv4(ipAddr)) {
- return error("Invalid IP address, can handle only IPv4 at the moment!");
- }
+ const { port, ipAddr } = config;
- // discover is not set but there is no ip address
- return error("Discover isn't true there is no IP address!");
+ if (!ipv4(ipAddr)) {
+ return error("Invalid IP address, can handle only IPv4 at the moment!");
} else if (port && !(0 <= port && port <= 65535)) {
// the port is there but the port is wrong
return error("Your port is not between 0 and 65535!");
@@ -49,16 +48,6 @@ class PhilipsHueService extends ServiceBundle> {
- if (config.discover) {
- const discIP = await this.discoverBridge();
- if (discIP) {
- config.ipAddr = discIP;
- config.discover = false;
- } else {
- return error("Could not discover your Hue Bridge, maybe try specifying a specific IP!");
- }
- }
-
const { port, username, apiKey, ipAddr } = config;
// check if there is one thing missing
@@ -97,15 +86,15 @@ class PhilipsHueService extends ServiceBundle> {
+ const results: { ipaddress: string }[] = await discovery.nupnpSearch();
- if (discoveryResults.length === 0) {
- this.nodecg.log.error("Failed to resolve any Hue Bridges");
- return null;
- } else {
- // Ignoring that you could have more than one Hue Bridge on a network as this is unlikely in 99.9% of users situations
- return discoveryResults[0].ipaddress as string;
- }
+ return Object.fromEntries(
+ results.map((bridge) => {
+ const ipAddr = bridge.ipaddress;
+ const config: PhilipsHueServiceConfig = { ipAddr };
+ return [ipAddr, config];
+ }),
+ );
}
}
diff --git a/nodecg-io-philipshue/philipshue-schema.json b/nodecg-io-philipshue/philipshue-schema.json
index a27872d61..ffebadc6b 100644
--- a/nodecg-io-philipshue/philipshue-schema.json
+++ b/nodecg-io-philipshue/philipshue-schema.json
@@ -3,17 +3,13 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "discover": {
- "type": "boolean",
- "description": "If the extension should try to discover the Philips Hue Bridge. **IMPORTANT** ignores the specified IP address and errors if it could not find a bridge, will only be used on first run"
- },
"ipAddr": {
"type": "string",
- "description": "The IP address of the bridge that you want to connect to, not required/used if you specify discover as true"
+ "description": "The IP address of the bridge that you want to connect to"
},
"port": {
"type": "number",
- "description": "The port that you want to use for connecting to your Hue bridge, not required/used if you specify discover as true"
+ "description": "The port that you want to use for connecting to your Hue bridge"
},
"apiKey": {
"type": "string",
@@ -24,5 +20,5 @@
"description": "The username that you want to use/that will be generated on first startup"
}
},
- "required": ["discover"]
+ "required": ["ipAddr"]
}
diff --git a/nodecg-io-serial/extension/SerialClient.ts b/nodecg-io-serial/extension/SerialClient.ts
index 2252e21eb..254d8f1eb 100644
--- a/nodecg-io-serial/extension/SerialClient.ts
+++ b/nodecg-io-serial/extension/SerialClient.ts
@@ -1,16 +1,16 @@
import { success, error, Result, emptySuccess } from "nodecg-io-core";
-import * as SerialPort from "serialport"; // This is neccesary, because serialport only likes require!
+import * as SerialPort from "serialport";
export interface DeviceInfo {
- port: string;
- manucaturer: string;
- serialNumber: string;
- pnpId: string;
+ port?: string;
+ manufacturer?: string;
+ serialNumber?: string;
+ pnpId?: string;
}
interface Protocol {
delimiter: "\n\r" | "\n";
- encoding: "ascii" | "utf8" | "utf16le" | "ucs2" | "base64" | "binary" | "hex" | undefined;
+ encoding?: "ascii" | "utf8" | "utf16le" | "ucs2" | "base64" | "binary" | "hex";
}
export interface SerialServiceConfig {
@@ -55,9 +55,9 @@ export class SerialServiceClient extends SerialPort {
if (deviceInfo.pnpId && element.pnpId && element.pnpId === deviceInfo.pnpId) {
result.push(element.path);
} else if (
- deviceInfo.manucaturer &&
+ deviceInfo.manufacturer &&
deviceInfo.serialNumber &&
- element.manufacturer === deviceInfo.manucaturer &&
+ element.manufacturer === deviceInfo.manufacturer &&
element.serialNumber === deviceInfo.serialNumber
) {
result.push(element.path);
@@ -75,6 +75,25 @@ export class SerialServiceClient extends SerialPort {
}
}
+ static async getConnectedDevices(): Promise> {
+ const list = await SerialPort.list();
+ return list.map((dev) => {
+ return {
+ device: {
+ // If we know the manufacturer and serial number we prefer them over the port
+ // because reboots or replugging devices may change the port number.
+ // Only use the raw port number if we have.
+ port: dev.manufacturer && dev.serialNumber ? undefined : dev.path,
+ manufacturer: dev.manufacturer,
+ serialNumber: dev.serialNumber,
+ pnpId: dev.pnpId,
+ },
+ connection: {},
+ protocol: { delimiter: "\n" },
+ };
+ });
+ }
+
async send(payload: string): Promise> {
const err: Error | undefined | null = await new Promise((resolve) => {
this.write(payload, (err) => {
diff --git a/nodecg-io-serial/extension/index.ts b/nodecg-io-serial/extension/index.ts
index 175fb9632..4feec8a00 100644
--- a/nodecg-io-serial/extension/index.ts
+++ b/nodecg-io-serial/extension/index.ts
@@ -9,6 +9,28 @@ module.exports = (nodecg: NodeCG) => {
export { SerialServiceClient } from "./SerialClient";
class SerialService extends ServiceBundle {
+ presets = {};
+
+ constructor(nodecg: NodeCG, serviceName: string, ...pathSegments: string[]) {
+ super(nodecg, serviceName, ...pathSegments);
+
+ SerialServiceClient.getConnectedDevices()
+ .then((devices) => {
+ this.presets = Object.fromEntries(
+ devices.map((dev) => [
+ // If we have the manufacturer and serial we can use a human friendly name, otherwise we need to fallback to the OS serial port
+ dev.device.manufacturer && dev.device.serialNumber
+ ? `${dev.device.manufacturer}:${dev.device.serialNumber}`
+ : dev.device.port,
+ dev,
+ ]),
+ );
+ })
+ .catch((err) => {
+ this.nodecg.log.error(`Failed to get connected devices for presets: ${err}`);
+ });
+ }
+
async validateConfig(config: SerialServiceConfig): Promise> {
const result = await SerialServiceClient.inferPort(config.device);
return result.failed ? error(result.errorMessage) : emptySuccess();
diff --git a/nodecg-io-serial/serial-schema.json b/nodecg-io-serial/serial-schema.json
index 6fb6bfe1c..6157fe71c 100644
--- a/nodecg-io-serial/serial-schema.json
+++ b/nodecg-io-serial/serial-schema.json
@@ -12,20 +12,20 @@
"title": "The name of the serial port you want to connect to.",
"description": "This is a specific name for a serial port. The device connected to this port may change on reboot, if multiple devices are connected to the system. It is the simplest option, so it should be prefeered if only one device is connected."
},
- "maufacturer": {
+ "manufacturer": {
"type": "string",
"title": "Name of the manufacturer of the desired device.",
- "description": "This name sould in most cases be the name of the manufacturer of the device. This may change and is mainly for human reference."
+ "description": "This name should in most cases be the name of the manufacturer of the device. This may change and is mainly for human reference."
},
"serialNumber": {
"type": "string",
"pattern": "[0-9A-F]+",
"title": "The serial number of your device.",
- "description": "This is a Hexadecimal number that should be unique for every device of this make and model. These originate from the usb serial controller and may not be programmed. You can spot if this is the case if it is only 4 - 5 characters lonng and has many zeros."
+ "description": "This is a Hexadecimal number that should be unique for every device of this make and model. These originate from the usb serial controller and may not be programmed. You can spot if this is the case if it is only 4 - 5 characters long and has many zeros."
},
"pnpId": {
"type": "string",
- "title": "The Pug'N'Play Id of your device.",
+ "title": "The Plug'N'Play Id of your device.",
"description": "This originates from pre usb times and was for Plug and Play driver support. This will work with usb serial devices as well as with original serial ports."
}
},
@@ -56,15 +56,15 @@
"type": "integer",
"default": 9600,
"enum": [110, 300, 1200, 2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200],
- "title": "Speed of transmition",
- "description": "This signifies how many bits are transmitted per second. Beware that there are dedicated values that are commonly used. Non-standard baud rates have to be supported by devices, converters and operatig systems so please avoid them if possible."
+ "title": "Speed of transmission",
+ "description": "This signifies how many bits are transmitted per second. Beware that there are dedicated values that are commonly used. Non-standard baud rates have to be supported by devices, converters and operating systems so please avoid them if possible."
},
"dataBits": {
"type": "integer",
"default": 8,
"enum": [5, 6, 7, 8],
"title": "The number of bits carrying data per packet.",
- "description": "As Stated this signifies how many bits fo data are transported per packet. Most microcontroller based devices use 8 bits so 8 might a reasonable guess. 5 was standard in teletypes for a long time and also was commonly used to transpot data via punched tape."
+ "description": "As Stated this signifies how many bits fo data are transported per packet. Most microcontroller based devices use 8 bits so 8 might a reasonable guess. 5 was standard in teletypes for a long time and also was commonly used to transport data via punched tape."
},
"lock": {
"type": "boolean",
@@ -84,7 +84,7 @@
"default": "none",
"enum": ["none", "even", "odd", "mark", "space"],
"title": "Mode of parity checking",
- "description": "Parity checking is a way to detect simple transmittion errors. There are multiple modes of operation. This will most likely be given in the manual but most microcontroller boards dont use ist, so it defaults to 'none'"
+ "description": "Parity checking is a way to detect simple transmission errors. There are multiple modes of operation. This will most likely be given in the manual but most microcontroller boards don't use it, so it defaults to 'none'"
},
"rtscts": {
"type": "boolean",
@@ -102,25 +102,25 @@
"type": "boolean",
"default": false,
"title": "Use XOFF software flow control.",
- "description": "This enables software flow control on the side going to the computer. This is most often used in conjuction with xon. The Sender transmits a special character after the tranmission ended."
+ "description": "This enables software flow control on the side going to the computer. This is most often used in conjunction with xon. The Sender transmits a special character after the transmission ended."
},
"xany": {
"type": "boolean",
"default": false,
"title": "Use XANY software flow control",
- "description": "A special form of Software flow control. It uses XON/XOFF but if transmition is stopped every character will resume operation. On the other ones will only resume on a special character."
+ "description": "A special form of Software flow control. It uses XON/XOFF but if transmission is stopped every character will resume operation. On the other ones will only resume on a special character."
}
},
"title": "Information about the serial communication.",
- "description": "You can specify how the data should be transfered. With exception of the baud rate you will most likely not have to change any settings in this section if you only are using common microcontroller boards."
+ "description": "You can specify how the data should be transferred. With exception of the baud rate you will most likely not have to change any settings in this section if you only are using common microcontroller boards."
},
"protocol": {
"type": "object",
"properties": {
"delimiter": {
"type": "string",
- "default": "\r\n",
+ "default": "\n",
"enum": ["\r\n", "\n"],
"title": "Line ending delimiter",
"description": "This sequence is placed to signify the end of a line."
@@ -130,10 +130,10 @@
"default": "utf8",
"enum": ["ascii", "utf8", "utf16le", "ucs2", "base64", "binary", "hex"],
"title": "Character encoding used to transmit or receive",
- "description": "The characterencoding specified which bit sequence results in what character."
+ "description": "The character encoding specified which bit sequence results in what character."
}
},
- "title": "Information about the protocoll",
+ "title": "Information about the protocol",
"description": "At the moment only complete lines will be interpreted as data. This might expand in the future."
}
},
diff --git a/nodecg-io-streamdeck/extension/index.ts b/nodecg-io-streamdeck/extension/index.ts
index f868f8e74..0f3e4a7d5 100644
--- a/nodecg-io-streamdeck/extension/index.ts
+++ b/nodecg-io-streamdeck/extension/index.ts
@@ -14,6 +14,17 @@ module.exports = (nodecg: NodeCG) => {
};
class StreamdeckServiceBundle extends ServiceBundle {
+ presets = Object.fromEntries(this.buildPresets());
+
+ private buildPresets(): Array<[string, StreamdeckServiceConfig]> {
+ const decks = streamdeck.listStreamDecks();
+ return decks.map((deck) => {
+ const presetName = `${deck.model}@${deck.path}`;
+ const presetConfig = { device: deck.path };
+ return [presetName, presetConfig];
+ });
+ }
+
async validateConfig(config: StreamdeckServiceConfig): Promise> {
try {
let device: string | undefined = config.device;
diff --git a/tsconfig.common.json b/tsconfig.common.json
index f5078ddd2..36af0d9b4 100644
--- a/tsconfig.common.json
+++ b/tsconfig.common.json
@@ -1,7 +1,7 @@
{
"compilerOptions": {
// Output related
- "target": "es2017",
+ "target": "es2019",
"importHelpers": true,
"sourceMap": true,
"declaration": true,
@@ -10,7 +10,7 @@
"moduleResolution": "node",
// Type checking
- "lib": ["es2017"],
+ "lib": ["es2019"],
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,