diff --git a/api/client.js b/api/client.js index 6a2e2ac..1f743af 100644 --- a/api/client.js +++ b/api/client.js @@ -4,7 +4,7 @@ const client = (function () { if (module.parent) { - return undefined + return require('./node/client') } if (typeof window !== 'undefined') { return require('./browser/client') diff --git a/api/node/client.js b/api/node/client.js new file mode 100644 index 0000000..d860482 --- /dev/null +++ b/api/node/client.js @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2024 Sveriges Television AB +// +// SPDX-License-Identifier: MIT + +const MissingArgumentError = require('../error/MissingArgumentError') +const InvalidArgumentError = require('../error/InvalidArgumentError') +const state = require('../state') + +/** + * @typedef {{ + * id: String, + * role: Number, + * heartbeat: Number, + * isPersistent: Boolean, + * isEditingLayout: Boolean + * }} Connection + */ + +/** + * Roles that a + * client can assume + */ +const ROLES = { + satellite: 0, + main: 1 +} + +/** + * Get all clients + * from the state + * @returns { Promise. } + */ +async function getAllConnections () { + return Object.entries((await state.get('_connections')) || {}) + .map(([id, connection]) => ({ + id, + ...connection, + role: (connection.role == null ? ROLES.satellite : connection.role) + })) +} + +/** + * Set the role of a + * client by its id + * @param { String } id + * @param { Number } role + */ +async function setRole (id, role) { + if (!id || typeof id !== 'string') { + throw new InvalidArgumentError('Invalid argument \'id\', must be a string') + } + + if (!Object.values(ROLES).includes(role)) { + throw new InvalidArgumentError('Invalid argument \'role\', must be a valid role') + } + + const set = { + _connections: { + [id]: { + role + } + } + } + + /* + There can only be one client with the main role, + if set, demote all other mains to satellite + */ + if (role === ROLES.main) { + (await getConnectionsByRole(ROLES.main)) + /* + Don't reset the role of the + connection we're currently setting + */ + .filter(connection => connection.id !== id) + .forEach(connection => { set._connections[connection.id] = { role: ROLES.satellite } }) + } + + state.apply(set) +} + +/** + * Get an array of all clients that + * have assumed a certain role + * @param { Number } role A valid role + * @returns { Promise. } + */ +async function getConnectionsByRole (role) { + if (!Object.values(ROLES).includes(role)) { + throw new InvalidArgumentError('Invalid argument \'role\', must be a valid role') + } + + return (await getAllConnections()) + .filter(connection => connection.role === role) +} + +/** + * Get the current selection + * of a connection by its id + * @param { String } connectionId + * @returns { Promise. } + */ +async function getSelection (connectionId) { + if (!connectionId) { + throw new MissingArgumentError('Missing required argument \'connectionId\'') + } + return (await state.get(`_connections.${connectionId}.selection`)) || [] +} + +module.exports = { + roles: ROLES, + setRole, + getSelection, + getAllConnections, + getConnectionsByRole +} diff --git a/docs/api/README.md b/docs/api/README.md index f535bc5..d65f86d 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -332,20 +332,53 @@ Play an item and set its state to 'playing'. Stop an item and set its state to 'stopped'. ## Client -**The client api is only available within renderer processes** -Control aspects of the current client +**The client api between the renerer and main processes** +Control aspects of clients + +### `bridge.client.awaitIdentity(): Promise` +**Only available within the render process** +Await the current identity to be set, will return as soon as the identity is set or immediately if it's already set ### `bridge.client.getIdentity(): String?` +**Only available within the render process** Get the client's identity as set by the host app. This may be undefined if it has not yet been set. It's useful for manually getting client parameters if optimizing queries to the state. -### `bridge.client.setSelection(items)` -Select one or multiple items, will clear the current selection +### `bridge.client.setSelection(itemIds)` +**Only available within the render process** +Select one or multiple items, will clear the current selection. + +### `bridge.client.addSelection(itemId|itemIds)` +**Only available within the render process** +Add one or more items to the selecton by their ids. + +### `bridge.client.subtractSelection(itemId|itemIds)` +**Only available within the render process** +Subtract one or more items to the selecton by their ids. + +### `bridge.client.isSelected(itemId): Boolean` +**Only available within the render process** +Check whether or not an item is selected by the current client. ### `bridge.client.clearSelection()` +**Only available within the render process** Clear the current selection ### `bridge.client.getSelection(): Promise` -Get the current selection +**Only available within the render process** +Get the current selection + +### `bridge.client.getSelection(connectionId): Promise` +**Only available within main processes** +Get the current selection of a connection by its id + +### `bridge.client.setRole(id, role)` +Set the role of a specified connection, if assigning the main role to a connection, any other main connection will be denoted to a satellite + +### `bridge.client.getAllConnections(): Promise` +Get an array of all current connections + +### `bridge.client.getAllConnectionsByRole(role): Promise` +Get an array of all current connections with a specific role ## Keyboard shortcuts Keyboard shortcuts SHOULD be registered with the API to give the user an index of which commands are available. diff --git a/plugins/osc/README.md b/plugins/osc/README.md index d8cc766..93f679a 100644 --- a/plugins/osc/README.md +++ b/plugins/osc/README.md @@ -35,3 +35,9 @@ Stop an item | Index | Type | Description | | --- | --- | --- | | 0 | String | The id of the item to stop | + +### `/api/client/selection/play` +Play the main client's current selection + +### `/api/client/selection/stop` +Stop the main client's current selection \ No newline at end of file diff --git a/plugins/osc/lib/handlers.js b/plugins/osc/lib/handlers.js index c91409d..4fbc265 100644 --- a/plugins/osc/lib/handlers.js +++ b/plugins/osc/lib/handlers.js @@ -4,6 +4,25 @@ const bridge = require('bridge') +async function getMainSelection () { + const connections = await bridge.client.getConnectionsByRole(bridge.client.roles.main) + return connections[0]?.selection || [] +} + +async function playSelection () { + const selection = await getMainSelection() + for (const id of selection) { + bridge.items.playItem(id) + } +} + +async function stopSelection () { + const selection = await getMainSelection() + for (const id of selection) { + bridge.items.stopItem(id) + } +} + /* Define any available osc paths */ @@ -15,6 +34,12 @@ module.exports = { '/items': { '/playItem': message => bridge.items.playItem(message?.args?.[0].value), '/stopItem': message => bridge.items.stopItem(message?.args?.[0].value) + }, + '/client': { + '/selection': { + '/play': message => playSelection(), + '/stop': message => stopSelection() + } } } }