diff --git a/package.json b/package.json index 4a19c1f..5fb8f17 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@typescript-eslint/eslint-plugin": "^5.10.2", "@typescript-eslint/parser": "^5.10.2", "babel-loader": "^8.2.3", - "babel-plugin-module-resolver": "^4.1.0", + "babel-plugin-module-resolver": "^5.0.0", "eslint": "^8.8.0", "jest": "^27.4.7", "jest-filename-transform": "^0.1.0", diff --git a/src/Action.ts b/src/Action.ts index ae701c8..234c299 100644 --- a/src/Action.ts +++ b/src/Action.ts @@ -2,6 +2,7 @@ import { ActionEvents } from "./events"; import { State, StateProps } from "./State"; import { Target } from "./Target"; import type { Plugin } from "./Plugin"; +import { Encoder } from "./Encoder"; /** * The Action must match a few files: @@ -90,6 +91,44 @@ export class Action< */ public device = ""; + /** + * Defines how the action interacts with Dial buttons on the SD+ + * By default this is not set and actions don't list Encoder as a Controller. + * @type {Encoder} + */ + public encoder: Encoder = undefined as unknown as Encoder; + + /** + * Defines if the Action is allowed on a KeyPad button. + * `true` by default for backwards compatibility. + * + * @type {boolean} + */ + public keyPad = true; + + /** + * Boolean to disable the title field for users in the property inspector. + * True by default. + * + * @type {boolean} + */ + public enableUserTitle = true; + + /** + * Boolean to hide the action in the actions list. This can be used for a + * plugin that only works with a specific profile. True by default. + * + * @type {boolean} + */ + public isVisibleInActionsList = true; + + /** + * Boolean to disable image caching. False by default. + * + * @type {boolean} + */ + public disableCachingImages = false; + constructor(params: { name: string; inspectorName?: string; @@ -117,6 +156,18 @@ export class Action< States: this.states.map((s) => s.toManifest()), }; + const controllers: string[] = []; + + if (this.encoder !== undefined) { + controllers.push("Encoder"); + } + + if (this.keyPad === true) { + controllers.push("KeyPad"); + } + + snippet.Controllers = controllers as unknown as string[]; + const optionals: [string, unknown, unknown][] = [ [ "PropertyInspectorPath", @@ -129,6 +180,19 @@ export class Action< this.hasMultiActionSupport === false, this.hasMultiActionSupport, ], + // ["Controllers", controllers], + ["Encoder", this.encoder !== undefined, this.encoder.toManifest()], + [ + "UserTitleEnabled", + this.enableUserTitle === false, + this.enableUserTitle, + ], + [ + "VisibleInActionsList", + this.isVisibleInActionsList === false, + this.isVisibleInActionsList, + ], + ["DisableCaching", this.disableCachingImages, this.disableCachingImages], ]; optionals.forEach(([prop, condition, value]) => { @@ -406,4 +470,34 @@ export class Action< payload: { profile }, }); } + + /** + * Send event to dynamically change properties of the SD+ touch display + * @param {Record} payload Key/Value pairs of properties to + * change + * @return {void} + */ + public setFeedback(payload: Record) { + this.send({ + event: "setFeedback", + context: this.context, + payload, + }); + } + + /** + * Send an event to dynamically change the layout of a SD+ touch display + * @param {string} layout Internal `id` of built-in layout or path to json + * file that contains a custom layout + * @return {void} + */ + public setFeedbackLayout(layout: string) { + this.send({ + event: "setFeedbackLayout", + context: this.context, + payload: { + layout, + }, + }); + } } diff --git a/src/Encoder.ts b/src/Encoder.ts new file mode 100644 index 0000000..0fd09a5 --- /dev/null +++ b/src/Encoder.ts @@ -0,0 +1,122 @@ +/** + * The encoder is used to configure the dial and display segment on an SD+. + * It is completely optional. + */ +export class Encoder { + /** + * Default background for the touch display slot + * @type {string} + */ + public background?: string; + + /** + * Default icon for the Property Inspector, dial stack and layout. Falls back + * to the Action List Icon + * + * @type {string} + */ + public icon?: string; + + /** + * Either a built-in layout (string) or a path to a JSON file that describes + * a custom layout. Can be changed with `setFeedbackLayout` event. + * @see Layout + * @type {string} + */ + public layout?: string; + + /** + * The color used as the background of the Dial Stack + * @type {string} + */ + public stackColor?: string; + + /** + * Part of TriggerDescription. Describes the action performed on rotation of + * the dial + * @type {string} + */ + public onRotateDescription?: string; + + /** + * Part of TriggerDescription. Describes the action performed on pressing of + * the dial + * @type {string} + */ + public onPushDescription?: string; + + /** + * Part of TriggerDescription. Describes the action performed on touching the + * display pad + * @type {string} + */ + public onTouchDescription?: string; + + /** + * Part of TriggerDescription. Describes the action performed on long pressing + * the display pad + * @type {string} + */ + public onLongTouchDescription?: string; + + public toManifest(): Record { + const snippet: Record = { + background: this.background, + Icon: this.icon, + layout: this.layout, + StackColor: this.stackColor, + TriggerDescription: { + Rotate: this.onRotateDescription, + Push: this.onPushDescription, + Touch: this.onTouchDescription, + LongTouch: this.onLongTouchDescription, + }, + }; + + const optionals: [string, unknown, unknown][] = [ + ["background", this.background, this.background], + ["Icon", this.icon, this.icon], + ["layout", this.layout, this.layout], + ["StackColor", this.stackColor, this.stackColor], + ["StackColor", this.stackColor, this.stackColor], + [ + "TriggerDescription", + this.onRotateDescription || + this.onPushDescription || + this.onTouchDescription || + this.onLongTouchDescription, + this.buildTriggerDescription(), + ], + ]; + + this.evaluateOptionalValues(optionals, snippet); + return snippet; + } + + private buildTriggerDescription() { + const snippet: Record = {}; + + const optionals: [string, unknown, unknown][] = [ + ["Rotate", this.onRotateDescription, this.onRotateDescription], + ["Push", this.onPushDescription, this.onPushDescription], + ["Touch", this.onTouchDescription, this.onTouchDescription], + ["LongTouch", this.onLongTouchDescription, this.onLongTouchDescription], + ]; + + this.evaluateOptionalValues(optionals, snippet); + return snippet; + } + + private evaluateOptionalValues( + optionals: [string, unknown, unknown][], + object: Record, + ): Record { + optionals.forEach(([prop, condition, value]) => { + if (condition) { + object[prop] = value; + } + }); + + return object; + } +} diff --git a/src/Layout.ts b/src/Layout.ts new file mode 100644 index 0000000..eb11054 --- /dev/null +++ b/src/Layout.ts @@ -0,0 +1,245 @@ +/** + * Describes available layouts for the SD+ Touch Display + * + * ID for built-in layouts are available as CONSTANTS + * Object can be used to create a custom layout. + * + * @see https://developer.elgato.com/documentation/stream-deck/sdk/layouts/ + */ +export class Layout { + /** + * Default layout, Title + Icon + * @type {string} + */ + static readonly ICON_LAYOUT: string = "$X1"; + + /** + * Custom Image with a title, canvas is more flexible + * @type {string} + */ + static readonly CANVAS_LAYOUT: string = "$A0"; + + /** + * Title, Icon and Text + * @type {string} + */ + static readonly VALUE_LAYOUT: string = "$A1"; + + /** + * Title, Icon,Value and Indicator. Better to represent a range. + * @type {string} + */ + static readonly INDICATOR_LAYOUT: string = "$B1"; + + /** + * Title, Icon, Value and Indicator with color. Better to represent a range + * that can be explained better with colors + * @type {string} + */ + static readonly GRADIENT_INDICATOR_LAYOUT: string = "$B2"; + + /** + * Title, 2 icons and 2 indicators. + * @type {string} + */ + static readonly DOUBLE_INDICATOR_LAYOUT: string = "$C1"; + + /** + * Unique ID to identify a Custom Layout. + * @type {string} + */ + public id: string; + + /** + * Array of Layout Items to compose a layout + * @type {[LayoutItem]} + */ + public items: [LayoutItem?] = []; + + constructor(id: string) { + this.id = id; + } + + public insertPlaccard(item: PlaccardItem): void { + this.items.push({ ...item, type: "placcard" }); + } + + public insertPixmap(item: PixmapItem): void { + this.items.push({ ...item, type: "pixmap" }); + } + + public insertBar(item: BarItem): void { + this.items.push({ ...item, type: "bar" }); + } + + public insertGbar(item: GbarItem): void { + this.items.push({ ...item, type: "gbar" }); + } + + public insertText(item: TextItem): void { + this.items.push({ ...item, type: "text" }); + } +} + +type LayoutItem = { + /** + * Name of the defined Item, used to identify it in `setFeedback` + */ + key: string; + + /** + * Pre-defined type of each Item + */ + type: string; + + /** + * The array holding the rectangle coordinates (x, y, w, h) of the defined + * item. Items with the same zOrder must NOT overlap. The rectangle must be + * inside of slot coordinates - (0, 0) x (200, 100). + */ + rect: [number, number, number, number]; + + /** + * The non-negative integer in a range [0, 700) defining the z-order of the + * item. + */ + zOrder?: number; + + /** + * Defines is the item is enabled + */ + enabled?: boolean; + + /** + * A real number in a range [0.0, 1.0] determining the opacity level of the + * item + */ + opacity?: number; + + /** + * The string used to define the item background fill color. + */ + background?: string; +}; + +type PlaccardItem = LayoutItem; + +type PixmapItem = LayoutItem & { + /** + * Image path ot base64 encoded image itself + */ + value: string; +}; + +type BarItem = LayoutItem & { + /** + * An integer value in the range [0, 100] to display an indicator. + */ + value: string; + + /** + * An integer value to represent shape: + * 0 - rectangle, + * 1 - double rectangle, + * 2 - trapezoid, + * 3 - double trapezoid, + * 4 - groove + * (groove is recommended design for SD+) + */ + subtype?: 0 | 1 | 2 | 3 | 4; + + /** + * An integer value for border width. Defaulted to 2 + */ + border_w?: number; + + /** + * A string value to determine bar color or gradient. Defaulted to darkGray + */ + bar_bg_c?: string; + + /** + * A string value for bar border color. Defaulted to white + */ + bar_border_c?: string; + + /** + * A string value for bar indicator fill color. Defaulted to white + */ + bar_fill_c?: string; +}; + +type GbarItem = LayoutItem & { + /** + * An integer value in the range [0, 100] to display an indicator. + */ + value: string; + + /** + * An integer value to represent shape: + * 0 - rectangle, + * 1 - double rectangle, + * 2 - trapezoid, + * 3 - double trapezoid, + * 4 - groove + * (groove is recommended design for SD+) + */ + subtype?: 0 | 1 | 2 | 3 | 4; + + /** + * An integer value for border width. Defaulted to 2 + */ + border_w?: number; + + /** + * A string value to determine bar color or gradient. Defaulted to darkGray + */ + bar_bg_c?: string; + + /** + * A string value for bar border color. Defaulted to white + */ + bar_border_c?: string; + + /** + * An integer value for the indicator's groove height. The indicator height + * will be adjusted to fit in the items height. Defaulted to 10. + */ + bar_h?: number; +}; + +type TextItem = LayoutItem & { + /** + * A string value to display + */ + value: string; + + /** + * Describes the text visually + */ + font?: TextItemFont; + + /** + * A string describing the color of text. Defaulted to white. + */ + color?: string; + + /** + * A string describing the text alignment in the rectangle. + * Values include: left, center, or right. Defaulted to center. + */ + alignment?: "left" | "center" | "right"; +}; + +type TextItemFont = { + /** + * An integer font pixel size + */ + size?: number; + + /** + * Weight of the font (an integer value between 100 and 1000 or the string + * with a name of typographical weight). + */ + weight?: number; +}; diff --git a/src/cli/bundle.ts b/src/cli/bundle.ts index cc7821e..ae1837b 100644 --- a/src/cli/bundle.ts +++ b/src/cli/bundle.ts @@ -2,7 +2,7 @@ import path from "path"; import fs from "fs"; import imageSize from "image-size"; -import glob from "glob"; +import * as glob from "glob"; import { build } from "esbuild"; import zipdir from "zip-dir"; import { Action, Plugin, State } from ".."; diff --git a/src/cli/images.ts b/src/cli/images.ts index 04221d3..c2a0ea3 100644 --- a/src/cli/images.ts +++ b/src/cli/images.ts @@ -1,6 +1,6 @@ import fs from "fs"; import path from "path"; -import glob from "glob"; +import * as glob from "glob"; const sourceDir = path.join(process.cwd(), "src"); diff --git a/src/events.ts b/src/events.ts index d8cdbb9..5ec3aaa 100644 --- a/src/events.ts +++ b/src/events.ts @@ -14,6 +14,12 @@ export interface ActionEvent extends StreamDeckEvent { userDesiredState: number; } +export interface DialActionEvent extends StreamDeckEvent { + context: string; + settings: T; + coordinates: { column: number; row: number }; +} + export interface DidReceiveGlobalSettingsEvent { event: string; settings: T; @@ -33,6 +39,17 @@ export type KeyDownEvent = ActionEvent; export type KeyUpEvent = ActionEvent; export type WillAppearEvent = ActionEvent; export type WillDisappearEvent = ActionEvent; +export type TouchTapEvent = DialActionEvent & { + tapPos: number[]; + hold: boolean; +}; +export type DialPressEvent = DialActionEvent & { + pressed: boolean; +}; +export type DialRotateEvent = DialActionEvent & { + pressed: boolean; + ticks: number; +}; export interface TitleParametersDidChangeEvent extends StreamDeckEvent { @@ -245,6 +262,33 @@ export class ActionEvents extends Events { handleSendToPlugin(event: SendToPluginEvent): void { this.debug("Received sendToPlugin event:", event); } + + /** + * Handle the touchTap event. + * @param {TouchTapEvent} event The event data. + * @return {void} + */ + handleTouchTap(event: TouchTapEvent): void { + this.debug("Received touchTap event:", event); + } + + /** + * Handle the dialPress event. + * @param {DialPressEvent} event The event data. + * @return {void} + */ + handleDialPress(event: DialPressEvent): void { + this.debug("Received dialPress event:", event); + } + + /** + * Handle the dialRotate event. + * @param {DialRotateEvent} event The event data. + * @return {void} + */ + handleDialRotate(event: DialRotateEvent): void { + this.debug("Received dialRotate event:", event); + } } export class InspectorEvents< diff --git a/src/index.ts b/src/index.ts index e2904c7..f98bd03 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,3 +18,5 @@ export * from "./Action"; export * from "./State"; export * from "./Inspector"; export * from "./events"; +export * from "./Encoder"; +export * from "./Layout";