Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: action tracking #49

Merged
merged 30 commits into from
Sep 25, 2024
Merged
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4c35a7b
feat: add Enumerable class
GeekyEggo Apr 30, 2024
07d2f3e
Merge branch 'main' into action-tracking
GeekyEggo Sep 14, 2024
9dd8a7c
refactor: split Action into KeyAction, DialAction, and KeyInMultiAction
GeekyEggo Sep 14, 2024
683a163
refactor: devices to use store, add initial tracking of actions
GeekyEggo Sep 14, 2024
c062b08
refactor: update Enumerable to be inheritable
GeekyEggo Sep 14, 2024
5041dc3
feat: allow Enumerable to be constructed from another Enumerable
GeekyEggo Sep 14, 2024
23fe625
feat: update action to include device and coordinates
GeekyEggo Sep 14, 2024
62756b8
refactor: update devices to inherit Enumerable
GeekyEggo Sep 14, 2024
7609ea9
style: fix linting
GeekyEggo Sep 14, 2024
b5951f8
feat: track visible actions on devices
GeekyEggo Sep 14, 2024
180bfa0
feat: update events to use Action instance
GeekyEggo Sep 15, 2024
9d4d306
fix: action type
GeekyEggo Sep 15, 2024
e93680a
feat: simplify action store
GeekyEggo Sep 15, 2024
6185074
feat: add type-checking helpers
GeekyEggo Sep 15, 2024
597cd54
test: fix tests
GeekyEggo Sep 15, 2024
20e9210
test: fix tests
GeekyEggo Sep 15, 2024
f5008b4
test: fix tests
GeekyEggo Sep 15, 2024
bf4cd7e
test: fix tests
GeekyEggo Sep 15, 2024
72406b2
style: linting
GeekyEggo Sep 15, 2024
a32e8ba
refactor: update actions to be a service, allowing for it to be itera…
GeekyEggo Sep 15, 2024
995ca5c
feat: add visible actions to SingletonAction
GeekyEggo Sep 18, 2024
0a9fff3
refactor: merge MultiActionKey in KeyAction
GeekyEggo Sep 18, 2024
8a94788
test: mock ActionStore (WIP)
GeekyEggo Sep 22, 2024
d43ed59
refactor: action and device store
GeekyEggo Sep 22, 2024
e3f3ec2
refactor: improve exports
GeekyEggo Sep 22, 2024
625a396
refactor: decouple stores
GeekyEggo Sep 22, 2024
597b89d
chore: fix linting
GeekyEggo Sep 22, 2024
a36d43d
refactor: remove deprecation notice for v1
GeekyEggo Sep 24, 2024
4d0f781
refactor: remove deprecation notice for v1
GeekyEggo Sep 24, 2024
75ec8c2
refactor!: remove deviceId from events, export types over classes
GeekyEggo Sep 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: merge MultiActionKey in KeyAction
GeekyEggo committed Sep 18, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 0a9fff34f9954d7729d94881c93026f4479c73a8
8 changes: 4 additions & 4 deletions src/plugin/actions/__tests__/action.test.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import { Settings } from "../../../api/__mocks__/events";
import { connection } from "../../connection";
import { Device } from "../../devices/device";
import { Action, type ActionContext } from "../action";
import { MultiActionKey } from "../multi";
import { KeyAction } from "../key";

jest.mock("../../logging");
jest.mock("../../manifest");
@@ -36,7 +36,7 @@ describe("Action", () => {
};

// Act.
const action = new MultiActionKey(context);
const action = new KeyAction(context);

// Assert.
expect(action).toBeInstanceOf(Action);
@@ -50,7 +50,7 @@ describe("Action", () => {
*/
it("getSettings", async () => {
// Arrange.
const action = new MultiActionKey<Settings>({
const action = new KeyAction<Settings>({
device,
id: "ABC123",
manifestId: "com.elgato.test.one"
@@ -114,7 +114,7 @@ describe("Action", () => {
});

describe("sending", () => {
const action = new MultiActionKey({
const action = new KeyAction({
device,
id: "ABC123",
manifestId: "com.elgato.test.one"
86 changes: 0 additions & 86 deletions src/plugin/actions/__tests__/multi.test.ts

This file was deleted.

86 changes: 17 additions & 69 deletions src/plugin/actions/action.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type streamDeck from "../";
import type { Coordinates, DidReceiveSettings, SetImage, SetTitle } from "../../api";

import type { DidReceiveSettings } from "../../api";
import type { JsonObject, JsonValue } from "../../common/json";
import type { KeyOf } from "../../common/utils";
import { connection } from "../connection";
import type { Device } from "../devices";
import type { DialAction } from "./dial";
import type { KeyAction } from "./key";
import type { MultiActionKey } from "./multi";
import type { SingletonAction } from "./singleton-action";
import type { ActionContext } from "./store";

/**
* Provides a contextualized instance of an {@link Action}, allowing for direct communication with the Stream Deck.
* @template T The type of settings associated with the action.
*/
export abstract class Action<T extends JsonObject = JsonObject> implements ActionContext {
export class Action<T extends JsonObject = JsonObject> {
/**
* The action context.
*/
@@ -48,12 +48,6 @@ export abstract class Action<T extends JsonObject = JsonObject> implements Actio
return this.#context.manifestId;
}

/**
* Underlying type of the action.
* @returns The type.
*/
protected abstract get type(): ActionType;

/**
* Gets the settings associated this action instance.
* @template U The type of settings associated with the action.
@@ -77,27 +71,19 @@ export abstract class Action<T extends JsonObject = JsonObject> implements Actio
}

/**
* Determines whether this instance is a dial action.
* Determines whether this instance is a dial.
* @returns `true` when this instance is a dial; otherwise `false`.
*/
public isDial(): this is DialAction {
return this.type === "Dial";
return this.#context.controller === "Encoder";
}

/**
* Determines whether this instance is a key action.
* Determines whether this instance is a key.
* @returns `true` when this instance is a key; otherwise `false`.
*/
public isKey(): this is KeyAction {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isKey/isDial are methods rather than properties, which is inconsistent with existing checks, e.g. isInMultiAction. Is this intentional?

return this.type === "Key";
}

/**
* Determines whether this instance is a multi-action key.
* @returns `true` when this instance is a multi-action key; otherwise `false`.
*/
public isMultiActionKey(): this is MultiActionKey {
return this.type === "MultiActionKey";
return this.#context.controller === "Keypad";
}

/**
@@ -127,53 +113,15 @@ export abstract class Action<T extends JsonObject = JsonObject> implements Actio
payload: settings
});
}
}

/**
* Options that define how to render an image associated with an action.
*/
export type ImageOptions = Omit<KeyOf<SetImage, "payload">, "image">;

/**
* Options that define how to render a title associated with an action.
*/
export type TitleOptions = Omit<KeyOf<SetTitle, "payload">, "title">;

/**
* Action type, for example dial or key.
*/
export type ActionType = "Dial" | "Key" | "MultiActionKey";

/**
* Provides context information for an instance of an action.
*/
export type ActionContext = {
/**
* Stream Deck device the action is positioned on.
* @returns Stream Deck device.
*/
get device(): Device;

/**
* Action instance identifier.
* @returns Identifier.
* Temporarily shows an alert (i.e. warning), in the form of an exclamation mark in a yellow triangle, on this action instance. Used to provide visual feedback when an action failed.
* @returns `Promise` resolved when the request to show an alert has been sent to Stream Deck.
*/
get id(): string;

/**
* Manifest identifier (UUID) for this action type.
* @returns Manifest identifier.
*/
get manifestId(): string;
};

/**
* Provides context information for an instance of an action, with coordinates.
*/
export type CoordinatedActionContext = ActionContext & {
/**
* Coordinates of the action, on the Stream Deck device.
* @returns Coordinates.
*/
get coordinates(): Readonly<Coordinates>;
};
public showAlert(): Promise<void> {
return connection.send({
event: "showAlert",
context: this.id
});
}
}
95 changes: 32 additions & 63 deletions src/plugin/actions/dial.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,59 @@
import type { Coordinates, FeedbackPayload, SetTriggerDescription } from "../../api";
import type { Coordinates, FeedbackPayload, SetTriggerDescription, WillAppear } from "../../api";
import type { JsonObject } from "../../common/json";
import type { KeyOf } from "../../common/utils";
import { connection } from "../connection";
import { Action, type ActionType, type CoordinatedActionContext, type ImageOptions, type TitleOptions } from "./action";
import { Action } from "./action";
import type { ActionContext } from "./store";

/**
* Provides a contextualized instance of a dial action.
* @template T The type of settings associated with the action.
*/
export class DialAction<T extends JsonObject = JsonObject> extends Action<T> implements CoordinatedActionContext {
export class DialAction<T extends JsonObject = JsonObject> extends Action<T> {
/**
* The action context.
* Private backing field for {@link coordinates}.
*/
readonly #context: CoordinatedActionContext;
readonly #coordinates: Readonly<Coordinates>;

/**
* Initializes a new instance of the {@see DialAction} class.
* @param context Action context.
* @param source Source of the action.
*/
constructor(context: CoordinatedActionContext) {
constructor(context: ActionContext, source: WillAppear<JsonObject>) {
super(context);
this.#context = context;

if (source.payload.controller === "Keypad") {
throw new Error("Unable to create DialAction from Keypad");
}

this.#coordinates = Object.freeze(source.payload.coordinates);
}

/**
* @inheritdoc
* Coordinates of the dial.
* @returns The coordinates.
*/
public get coordinates(): Coordinates {
return this.#context.coordinates;
public get coordinates(): Readonly<Coordinates> {
return this.#coordinates;
}

/**
* @inheritdoc
* Sets the {@link image} to be display for this action instance within Stream Deck app.
*
* NB: The image can only be set by the plugin when the the user has not specified a custom image.
* @param image Image to display; this can be either a path to a local file within the plugin's folder, a base64 encoded `string` with the mime type declared (e.g. PNG, JPEG, etc.),
* or an SVG `string`. When `undefined`, the image from the manifest will be used.
* @returns `Promise` resolved when the request to set the {@link image} has been sent to Stream Deck.
*/
protected override get type(): ActionType {
return "Dial";
public setImage(image?: string): Promise<void> {
return connection.send({
event: "setImage",
context: this.id,
payload: {
image
}
});
}

/**
@@ -70,45 +89,6 @@ export class DialAction<T extends JsonObject = JsonObject> extends Action<T> imp
});
}

/**
* Sets the {@link image} to be display for this action instance.
*
* NB: The image can only be set by the plugin when the the user has not specified a custom image.
* @param image Image to display; this can be either a path to a local file within the plugin's folder, a base64 encoded `string` with the mime type declared (e.g. PNG, JPEG, etc.),
* or an SVG `string`. When `undefined`, the image from the manifest will be used.
* @param options Additional options that define where and how the image should be rendered.
* @returns `Promise` resolved when the request to set the {@link image} has been sent to Stream Deck.
*/
public setImage(image?: string, options?: ImageOptions): Promise<void> {
return connection.send({
event: "setImage",
context: this.id,
payload: {
image,
...options
}
});
}

/**
* Sets the {@link title} displayed for this action instance.
*
* NB: The title can only be set by the plugin when the the user has not specified a custom title.
* @param title Title to display; when `undefined` the title within the manifest will be used.
* @param options Additional options that define where and how the title should be rendered.
* @returns `Promise` resolved when the request to set the {@link title} has been sent to Stream Deck.
*/
public setTitle(title?: string, options?: TitleOptions): Promise<void> {
return connection.send({
event: "setTitle",
context: this.id,
payload: {
title,
...options
}
});
}

/**
* Sets the trigger (interaction) {@link descriptions} associated with this action instance. Descriptions are shown within the Stream Deck application, and informs the user what
* will happen when they interact with the action, e.g. rotate, touch, etc. When {@link descriptions} is `undefined`, the descriptions will be reset to the values provided as part
@@ -125,17 +105,6 @@ export class DialAction<T extends JsonObject = JsonObject> extends Action<T> imp
payload: descriptions || {}
});
}

/**
* Temporarily shows an alert (i.e. warning), in the form of an exclamation mark in a yellow triangle, on this action instance. Used to provide visual feedback when an action failed.
* @returns `Promise` resolved when the request to show an alert has been sent to Stream Deck.
*/
public showAlert(): Promise<void> {
return connection.send({
event: "showAlert",
context: this.id
});
}
}

/**
Loading