From 739f58320aeea2d4f229344d7435b6dcdb0459af Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sun, 2 Feb 2025 19:03:06 +0000 Subject: [PATCH] feat: support websocket connection --- openapi.yaml | 24 +- satellite/package.json | 4 +- satellite/src/apiTypes.ts | 4 + satellite/src/client.ts | 180 +++++--- satellite/src/clientImplementations.ts | 91 ++++ satellite/src/config.ts | 52 ++- satellite/src/device-types/api.ts | 2 +- .../src/device-types/blackmagic-panel.ts | 2 +- satellite/src/device-types/infinitton.ts | 2 +- .../src/device-types/loupedeck-live-s.ts | 2 +- satellite/src/device-types/loupedeck-live.ts | 2 +- .../device-types/razer-stream-controller-x.ts | 2 +- satellite/src/device-types/streamdeck.ts | 2 +- .../src/device-types/xencelabs-quick-keys.ts | 2 +- satellite/src/electron.ts | 21 +- satellite/src/generated/openapi.ts | 22 +- satellite/src/lib.ts | 3 +- satellite/src/main.ts | 8 +- satellite/src/rest.ts | 20 + satellite/src/surface-manager.ts | 6 +- webui/package.json | 1 + webui/src/app/ConnectionConfig.tsx | 58 ++- webui/src/components/ui/select.tsx | 142 +++++++ yarn.lock | 398 +++++++++++++++++- 24 files changed, 937 insertions(+), 113 deletions(-) create mode 100644 satellite/src/clientImplementations.ts create mode 100644 webui/src/components/ui/select.tsx diff --git a/openapi.yaml b/openapi.yaml index 4d43bd5..3386b01 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -196,12 +196,19 @@ components: ConfigData: type: object properties: + protocol: + type: string + enum: ['tcp', 'ws'] + description: Protocol to use for the connection host: type: string - description: Address of the Companion server to connect to + description: Address of the Companion server to connect to, for TCP protocol port: type: number - description: Port number of the Companion server to connect to + description: Port number of the Companion server to connect to, for TCP protocol + wsAddress: + type: string + description: Address of the Companion server to connect to, for WS protocol installationName: type: string @@ -217,8 +224,10 @@ components: type: number description: Port number of the HTTP api. This is readonly in the HTTP api required: + - protocol - host - port + - wsAddress - installationName - mdnsEnabled - httpEnabled @@ -227,12 +236,19 @@ components: ConfigDataUpdate: type: object properties: + protocol: + type: string + enum: ['tcp', 'ws'] + description: Protocol to use for the connection host: type: string - description: Address of the Companion server to connect to + description: Address of the Companion server to connect to, for TCP protocol port: type: number - description: Port number of the Companion server to connect to + description: Port number of the Companion server to connect to, for TCP protocol + wsAddress: + type: string + description: Address of the Companion server to connect to, for WS protocol installationName: type: string diff --git a/satellite/package.json b/satellite/package.json index 4767c3d..85f5ad8 100644 --- a/satellite/package.json +++ b/satellite/package.json @@ -26,6 +26,7 @@ "@types/koa-static": "^4.0.4", "@types/node": "^20.17.16", "@types/semver": "^7.5.8", + "@types/ws": "^8.5.12", "cross-env": "^7.0.3", "electron": "34.0.2", "electron-builder": "^26.0.1", @@ -58,7 +59,8 @@ "node-hid": "^3.1.2", "semver": "^7.7.0", "tslib": "^2.8.1", - "usb": "^2.14.0" + "usb": "^2.14.0", + "ws": "^8.18.0" }, "lint-staged": { "*.{css,json,md,scss}": [ diff --git a/satellite/src/apiTypes.ts b/satellite/src/apiTypes.ts index 2818a58..56666ff 100644 --- a/satellite/src/apiTypes.ts +++ b/satellite/src/apiTypes.ts @@ -35,8 +35,10 @@ export function compileStatus(client: CompanionSatelliteClient): ApiStatusRespon export function compileConfig(appConfig: Conf): ApiConfigData { return { + protocol: appConfig.get('remoteProtocol'), host: appConfig.get('remoteIp'), port: appConfig.get('remotePort'), + wsAddress: appConfig.get('remoteWsAddress'), installationName: appConfig.get('installationName'), @@ -48,8 +50,10 @@ export function compileConfig(appConfig: Conf): ApiConfigData { } export function updateConfig(appConfig: SatelliteConfigInstance, newConfig: ApiConfigDataUpdateElectron): void { + if (newConfig.protocol !== undefined) appConfig.set('remoteProtocol', newConfig.protocol) if (newConfig.host !== undefined) appConfig.set('remoteIp', newConfig.host) if (newConfig.port !== undefined) appConfig.set('remotePort', newConfig.port) + if (newConfig.wsAddress !== undefined) appConfig.set('remoteWsAddress', newConfig.wsAddress) if (newConfig.httpEnabled !== undefined) appConfig.set('restEnabled', newConfig.httpEnabled) if (newConfig.httpPort !== undefined) appConfig.set('restPort', newConfig.httpPort) diff --git a/satellite/src/client.ts b/satellite/src/client.ts index 041e9ef..997369b 100644 --- a/satellite/src/client.ts +++ b/satellite/src/client.ts @@ -1,8 +1,15 @@ import { EventEmitter } from 'events' -import { Socket } from 'net' import { ClientCapabilities, CompanionClient, DeviceDrawProps, DeviceRegisterProps } from './device-types/api.js' -import { DEFAULT_PORT } from './lib.js' +import { assertNever, DEFAULT_TCP_PORT } from './lib.js' import * as semver from 'semver' +import { + CompanionSatelliteTcpClient, + CompanionSatelliteWsClient, + formatConnectionUrl, + ICompanionSatelliteClient, + ICompanionSatelliteClientOptions, + SomeConnectionDetails, +} from './clientImplementations.js' const PING_UNACKED_LIMIT = 15 // Arbitrary number const PING_IDLE_TIMEOUT = 1000 // Pings are allowed to be late if another packet has been received recently @@ -93,7 +100,7 @@ export type CompanionSatelliteClientEvents = { export class CompanionSatelliteClient extends EventEmitter implements CompanionClient { private readonly debug: boolean - private socket: Socket | undefined + private socket: ICompanionSatelliteClient | undefined private receiveBuffer = '' @@ -103,8 +110,7 @@ export class CompanionSatelliteClient extends EventEmitter() private _pendingDevices = new Map() // Time submitted @@ -113,11 +119,8 @@ export class CompanionSatelliteClient extends EventEmitter { - this.emit('error', e) - }) - this.socket.on('close', () => { - if (this.debug) { - this.emit('log', 'Connection closed') - } + if (this.socket) { + this.socket.destroy() + this.socket = undefined + } - this._registeredDevices.clear() - this._pendingDevices.clear() + if (!this._connectionDetails) { + this.emit('log', `Missing connection details`) + return + } - if (this._connected) { - this.emit('disconnected') - } else { - this._companionUnsupported = false - } - this._connected = false - this.receiveBuffer = '' + const socketOptions: ICompanionSatelliteClientOptions = { + onError: (e) => { + this.emit('error', e) + }, + onClose: () => { + if (this.debug) { + this.emit('log', 'Connection closed') + } - if (this._pingInterval) { - clearInterval(this._pingInterval) - this._pingInterval = undefined - } + this._registeredDevices.clear() + this._pendingDevices.clear() - if (!this._retryConnectTimeout && this.socket === socket) { - this._retryConnectTimeout = setTimeout( - () => { - this._retryConnectTimeout = undefined - this.emit('log', 'Trying reconnect') - this.initSocket() - }, - this._companionUnsupported ? RECONNECT_DELAY_UNSUPPORTED : RECONNECT_DELAY, - ) - } - }) + if (this._connected) { + this.emit('disconnected') + } else { + this._companionUnsupported = false + } + this._connected = false + this.receiveBuffer = '' - this.socket.on('data', (d) => this._handleReceivedData(d)) + if (this._pingInterval) { + clearInterval(this._pingInterval) + this._pingInterval = undefined + } - this.socket.on('connect', () => { - if (this.debug) { - this.emit('log', 'Connected') - } + if (!this._retryConnectTimeout && this.socket === socket) { + this._retryConnectTimeout = setTimeout( + () => { + this._retryConnectTimeout = undefined + this.emit('log', 'Trying reconnect') + this.initSocket() + }, + this._companionUnsupported ? RECONNECT_DELAY_UNSUPPORTED : RECONNECT_DELAY, + ) + } + }, + onData: (d) => this._handleReceivedData(d), + onConnect: () => { + if (this.debug) { + this.emit('log', 'Connected') + } - this._registeredDevices.clear() - this._pendingDevices.clear() + this._registeredDevices.clear() + this._pendingDevices.clear() - this._connected = true - this._pingUnackedCount = 0 - this.receiveBuffer = '' + this._connected = true + this._pingUnackedCount = 0 + this.receiveBuffer = '' - if (!this._pingInterval) { - this._pingInterval = setInterval(() => this.sendPing(), PING_INTERVAL) - } + if (!this._pingInterval) { + this._pingInterval = setInterval(() => this.sendPing(), PING_INTERVAL) + } - if (!this.socket) { - // should never hit, but just in case - this.disconnect() - return - } + if (!this.socket) { + // should never hit, but just in case + this.disconnect() + return + } - // 'connected' gets emitted once we receive 'Begin' - }) + // 'connected' gets emitted once we receive 'Begin' + }, + } - if (this._host) { - this.emit('log', `Connecting to ${this._host}:${this._port}`) - this.socket.connect(this._port, this._host) + let socket: ICompanionSatelliteClient + switch (this._connectionDetails.mode) { + case 'tcp': + socket = new CompanionSatelliteTcpClient(socketOptions, this._connectionDetails) + break + case 'ws': + socket = new CompanionSatelliteWsClient(socketOptions, this._connectionDetails) + break + default: + assertNever(this._connectionDetails) + this.socket = undefined + throw new Error('Invalid connection mode') } + this.socket = socket + + this.emit('log', `Connecting to ${formatConnectionUrl(this._connectionDetails)}`) } private sendPing(): void { @@ -235,7 +276,7 @@ export class CompanionSatelliteClient extends EventEmitter { + public async connect(details: SomeConnectionDetails): Promise { if (this._connected || this._connectionActive) { this.disconnect() } @@ -245,8 +286,7 @@ export class CompanionSatelliteClient extends EventEmitter void + onClose: () => void + onData: (data: string) => void + onConnect: () => void +} + +export interface ICompanionSatelliteClient { + write(data: string): void + end(): void + destroy(): void +} + +export interface TcpConnectionDetails { + mode: 'tcp' + host: string + port: number +} +export interface WsConnectionDetails { + mode: 'ws' + url: string +} +export type SomeConnectionDetails = TcpConnectionDetails | WsConnectionDetails + +export class CompanionSatelliteTcpClient implements ICompanionSatelliteClient { + #socket: Socket + + constructor(options: ICompanionSatelliteClientOptions, details: TcpConnectionDetails) { + this.#socket = new Socket() + + this.#socket.on('error', (err) => options.onError(err)) + this.#socket.on('close', () => options.onClose()) + this.#socket.on('data', (data) => options.onData(data.toString())) + this.#socket.on('connect', () => options.onConnect()) + + this.#socket.connect(details.port, details.host) + } + + write(data: string): void { + this.#socket.write(data) + } + end(): void { + this.#socket.end() + } + destroy(): void { + this.#socket.destroy() + } +} + +export class CompanionSatelliteWsClient implements ICompanionSatelliteClient { + #socket: WebSocket + + constructor(options: ICompanionSatelliteClientOptions, details: WsConnectionDetails) { + this.#socket = new WebSocket(details.url, { + timeout: 5000, + }) + + this.#socket.on('error', (err) => options.onError(err)) + this.#socket.on('close', () => options.onClose()) + this.#socket.on('message', (data) => options.onData(data.toString())) + this.#socket.on('open', () => options.onConnect()) + } + + write(data: string): void { + this.#socket.send(data) + } + end(): void { + this.#socket.terminate() + } + destroy(): void { + this.#socket.close() + } +} + +export function formatConnectionUrl(details: SomeConnectionDetails): string { + if (details.mode === 'tcp') { + return `tcp://${details.host}:${details.port}` + } else { + return details.url + } +} diff --git a/satellite/src/config.ts b/satellite/src/config.ts index 7b0fe59..328cdd2 100644 --- a/satellite/src/config.ts +++ b/satellite/src/config.ts @@ -2,14 +2,19 @@ import Conf, { Schema } from 'conf' import path from 'path' import os from 'os' import { customAlphabet } from 'nanoid' +import { SomeConnectionDetails } from './clientImplementations.js' +import { assertNever, DEFAULT_TCP_PORT, DEFAULT_WS_PORT } from './lib.js' +import debounceFn from 'debounce-fn' const nanoidHex = customAlphabet('0123456789abcdef') export type SatelliteConfigInstance = Conf export interface SatelliteConfig { + remoteProtocol: 'tcp' | 'ws' remoteIp: string remotePort: number + remoteWsAddress: string installationName: string @@ -22,6 +27,12 @@ export interface SatelliteConfig { } export const satelliteConfigSchema: Schema = { + remoteProtocol: { + type: 'string', + enum: ['tcp', 'ws'], + description: 'Protocol to use for connecting to Companion installation', + default: 'tcp', + }, remoteIp: { type: 'string', description: 'Address of Companion installation', @@ -32,7 +43,12 @@ export const satelliteConfigSchema: Schema = { description: 'Port number of Companion installation', minimum: 1, maximum: 65535, - default: 16622, + default: DEFAULT_TCP_PORT, + }, + remoteWsAddress: { + type: 'string', + description: 'Websocket address of Companion installation', + default: `ws://127.0.0.1:${DEFAULT_WS_PORT}`, }, installationName: { @@ -101,3 +117,37 @@ export function openHeadlessConfig(rawConfigPath: string): Conf ensureFieldsPopulated(appConfig) return appConfig } + +export function getConnectionDetailsFromConfig(config: SatelliteConfigInstance): SomeConnectionDetails { + const protocol = config.get('remoteProtocol') + switch (protocol) { + case 'tcp': + return { + mode: 'tcp', + host: config.get('remoteIp') || '127.0.0.1', + port: config.get('remotePort') || DEFAULT_TCP_PORT, + } + case 'ws': + console.log('get', config.get('remoteWsAddress')) + return { + mode: 'ws', + url: config.get('remoteWsAddress') || `ws://127.0.0.1:${DEFAULT_WS_PORT}`, + } + default: + assertNever(protocol) + return { + mode: 'tcp', + host: config.get('remoteIp'), + port: config.get('remotePort'), + } + } +} + +export function listenToConnectionConfigChanges(config: SatelliteConfigInstance, tryConnect: () => void): void { + const debounceConnect = debounceFn(tryConnect, { wait: 50, after: true, before: false }) + + config.onDidChange('remoteProtocol', debounceConnect) + config.onDidChange('remoteIp', debounceConnect) + config.onDidChange('remotePort', debounceConnect) + config.onDidChange('remoteWsAddress', debounceConnect) +} diff --git a/satellite/src/device-types/api.ts b/satellite/src/device-types/api.ts index 618bec8..d0206f6 100644 --- a/satellite/src/device-types/api.ts +++ b/satellite/src/device-types/api.ts @@ -145,7 +145,7 @@ export interface ClientCapabilities { } export interface CompanionClient { - get host(): string + get displayHost(): string keyDown(deviceId: string, keyIndex: number): void keyUp(deviceId: string, keyIndex: number): void diff --git a/satellite/src/device-types/blackmagic-panel.ts b/satellite/src/device-types/blackmagic-panel.ts index a3d1929..7d2c1a4 100644 --- a/satellite/src/device-types/blackmagic-panel.ts +++ b/satellite/src/device-types/blackmagic-panel.ts @@ -157,7 +157,7 @@ export class BlackmagicControllerWrapper extends EventEmitter implem // Start with blanking it await this.blankDevice() - this.showStatus(client.host, status) + this.showStatus(client.displayHost, status) } updateCapabilities(_capabilities: ClientCapabilities): void { diff --git a/satellite/src/device-types/loupedeck-live-s.ts b/satellite/src/device-types/loupedeck-live-s.ts index ece2f16..7d20c9b 100644 --- a/satellite/src/device-types/loupedeck-live-s.ts +++ b/satellite/src/device-types/loupedeck-live-s.ts @@ -153,7 +153,7 @@ export class LoupedeckLiveSWrapper extends EventEmitter im // Start with blanking it await this.blankDevice() - this.showStatus(client.host, status) + this.showStatus(client.displayHost, status) } updateCapabilities(_capabilities: ClientCapabilities): void { diff --git a/satellite/src/device-types/loupedeck-live.ts b/satellite/src/device-types/loupedeck-live.ts index 4b73159..84363f4 100644 --- a/satellite/src/device-types/loupedeck-live.ts +++ b/satellite/src/device-types/loupedeck-live.ts @@ -155,7 +155,7 @@ export class LoupedeckLiveWrapper extends EventEmitter imp // Start with blanking it await this.blankDevice() - this.showStatus(client.host, status) + this.showStatus(client.displayHost, status) } updateCapabilities(_capabilities: ClientCapabilities): void { diff --git a/satellite/src/device-types/razer-stream-controller-x.ts b/satellite/src/device-types/razer-stream-controller-x.ts index 6ca6dc4..1260e49 100644 --- a/satellite/src/device-types/razer-stream-controller-x.ts +++ b/satellite/src/device-types/razer-stream-controller-x.ts @@ -99,7 +99,7 @@ export class RazerStreamControllerXWrapper extends EventEmitter implem // Start with blanking it await this.blankDevice() - this.showStatus(client.host, status) + this.showStatus(client.displayHost, status) } updateCapabilities(_capabilities: ClientCapabilities): void { diff --git a/satellite/src/device-types/xencelabs-quick-keys.ts b/satellite/src/device-types/xencelabs-quick-keys.ts index 616b8da..bd7354a 100644 --- a/satellite/src/device-types/xencelabs-quick-keys.ts +++ b/satellite/src/device-types/xencelabs-quick-keys.ts @@ -185,7 +185,7 @@ export class QuickKeysWrapper extends EventEmitter impleme // Start with blanking it await this.blankDevice() - this.showStatus(client.host, status) + this.showStatus(client.displayHost, status) } updateCapabilities(_capabilities: ClientCapabilities): void { diff --git a/satellite/src/electron.ts b/satellite/src/electron.ts index dfea294..b4bf8b3 100644 --- a/satellite/src/electron.ts +++ b/satellite/src/electron.ts @@ -5,9 +5,13 @@ import * as path from 'path' import electronStore from 'electron-store' import { SurfaceManager } from './surface-manager.js' import { CompanionSatelliteClient } from './client.js' -import { DEFAULT_PORT } from './lib.js' import { RestServer } from './rest.js' -import { SatelliteConfig, ensureFieldsPopulated } from './config.js' +import { + SatelliteConfig, + ensureFieldsPopulated, + getConnectionDetailsFromConfig, + listenToConnectionConfigChanges, +} from './config.js' import { ApiConfigData, ApiConfigDataUpdateElectron, @@ -47,8 +51,7 @@ const surfaceManager = await SurfaceManager.create(client, appConfig.get('surfac const server = new RestServer(webRoot, appConfig, client, surfaceManager) const mdnsAnnouncer = new MdnsAnnouncer(appConfig) -appConfig.onDidChange('remoteIp', () => tryConnect()) -appConfig.onDidChange('remotePort', () => tryConnect()) +listenToConnectionConfigChanges(appConfig, tryConnect) appConfig.onDidChange( 'surfacePluginsEnabled', debounceFn(() => surfaceManager.updatePluginsEnabled(appConfig.get('surfacePluginsEnabled')), { @@ -62,13 +65,9 @@ client.on('log', (l) => console.log(l)) client.on('error', (e) => console.error(e)) function tryConnect() { - const ip = appConfig.get('remoteIp') - const port = appConfig.get('remotePort') ?? DEFAULT_PORT - if (ip) { - client.connect(ip, port).catch((e) => { - console.log('Failed to update connection: ', e) - }) - } + client.connect(getConnectionDetailsFromConfig(appConfig)).catch((e) => { + console.log('Failed to update connection: ', e) + }) } function restartRestApi() { server.open() diff --git a/satellite/src/generated/openapi.ts b/satellite/src/generated/openapi.ts index ef7fc74..7f210f1 100644 --- a/satellite/src/generated/openapi.ts +++ b/satellite/src/generated/openapi.ts @@ -126,10 +126,17 @@ export interface components { companionUnsupportedApi: boolean; }; ConfigData: { - /** @description Address of the Companion server to connect to */ + /** + * @description Protocol to use for the connection + * @enum {string} + */ + protocol: "tcp" | "ws"; + /** @description Address of the Companion server to connect to, for TCP protocol */ host: string; - /** @description Port number of the Companion server to connect to */ + /** @description Port number of the Companion server to connect to, for TCP protocol */ port: number; + /** @description Address of the Companion server to connect to, for WS protocol */ + wsAddress: string; /** @description Name of the installation, reported in the mDNS announcement */ installationName: string; /** @description Enable mDNS announcement to allow automatic discovery of the Companion Satellite */ @@ -140,10 +147,17 @@ export interface components { httpPort: number; }; ConfigDataUpdate: { - /** @description Address of the Companion server to connect to */ + /** + * @description Protocol to use for the connection + * @enum {string} + */ + protocol?: "tcp" | "ws"; + /** @description Address of the Companion server to connect to, for TCP protocol */ host?: string; - /** @description Port number of the Companion server to connect to */ + /** @description Port number of the Companion server to connect to, for TCP protocol */ port?: number; + /** @description Address of the Companion server to connect to, for WS protocol */ + wsAddress?: string; /** @description Name of the installation, reported in the mDNS announcement */ installationName?: string; /** @description Enable mDNS announcement to allow automatic discovery of the Companion Satellite */ diff --git a/satellite/src/lib.ts b/satellite/src/lib.ts index 6e661ca..b4b9510 100644 --- a/satellite/src/lib.ts +++ b/satellite/src/lib.ts @@ -1,4 +1,5 @@ -export const DEFAULT_PORT = 16622 +export const DEFAULT_TCP_PORT = 16622 +export const DEFAULT_WS_PORT = 16623 export function assertNever(_v: never): void { // Nothing to do diff --git a/satellite/src/main.ts b/satellite/src/main.ts index 3b81b06..282567e 100644 --- a/satellite/src/main.ts +++ b/satellite/src/main.ts @@ -4,9 +4,8 @@ import '@julusian/segfault-raub' import exitHook from 'exit-hook' import { CompanionSatelliteClient } from './client.js' import { SurfaceManager } from './surface-manager.js' -import { DEFAULT_PORT } from './lib.js' import { RestServer } from './rest.js' -import { openHeadlessConfig } from './config.js' +import { getConnectionDetailsFromConfig, listenToConnectionConfigChanges, openHeadlessConfig } from './config.js' import { fileURLToPath } from 'url' import { MdnsAnnouncer } from './mdnsAnnouncer.js' import debounceFn from 'debounce-fn' @@ -47,13 +46,12 @@ exitHook(() => { }) const tryConnect = () => { - client.connect(appConfig.get('remoteIp') || '127.0.0.1', appConfig.get('remotePort') || DEFAULT_PORT).catch((e) => { + client.connect(getConnectionDetailsFromConfig(appConfig)).catch((e) => { console.log(`Failed to connect`, e) }) } -appConfig.onDidChange('remoteIp', () => tryConnect()) -appConfig.onDidChange('remotePort', () => tryConnect()) +listenToConnectionConfigChanges(appConfig, tryConnect) appConfig.onDidChange( 'surfacePluginsEnabled', debounceFn(() => surfaceManager.updatePluginsEnabled(appConfig.get('surfacePluginsEnabled')), { diff --git a/satellite/src/rest.ts b/satellite/src/rest.ts index 3524068..2965cbd 100644 --- a/satellite/src/rest.ts +++ b/satellite/src/rest.ts @@ -105,6 +105,16 @@ export class RestServer { const partialConfig: ApiConfigDataUpdate = {} + const protocol = body.protocol + if (protocol !== undefined) { + if (typeof protocol === 'string') { + partialConfig.protocol = protocol + } else { + ctx.status = 400 + ctx.body = 'Invalid protocol' + } + } + const host = body.host if (host !== undefined) { if (typeof host === 'string') { @@ -123,6 +133,16 @@ export class RestServer { partialConfig.port = port } + const wsAddress = body.wsAddress + if (wsAddress !== undefined) { + if (typeof wsAddress === 'string') { + partialConfig.wsAddress = wsAddress + } else { + ctx.status = 400 + ctx.body = 'Invalid wsAddress' + } + } + const installationName = body.installationName if (installationName !== undefined) { if (typeof installationName === 'string') { diff --git a/satellite/src/surface-manager.ts b/satellite/src/surface-manager.ts index 236efb7..673b3eb 100644 --- a/satellite/src/surface-manager.ts +++ b/satellite/src/surface-manager.ts @@ -178,7 +178,7 @@ export class SurfaceManager { async (msg) => { const surface = this.#getWrappedSurface(msg.deviceId) - surface.showStatus(this.#client.host, msg.message) + surface.showStatus(this.#client.displayHost, msg.message) // Try again to add the device, in case we can recover this.#delayRetryAddOfDevice(msg.deviceId) @@ -268,7 +268,7 @@ export class SurfaceManager { if (this.#pendingSurfaces.has(device.surfaceId)) continue // Indicate on device - device.showStatus(this.#client.host, this.#statusString) + device.showStatus(this.#client.displayHost, this.#statusString) // Make sure device knows what the client is capable of device.updateCapabilities(this.#client.capabilities) @@ -480,7 +480,7 @@ export class SurfaceManager { #doDrawStatusCard(message: string) { for (const dev of this.#surfaces.values()) { - dev.showStatus(this.#client.host, message) + dev.showStatus(this.#client.displayHost, message) } } } diff --git a/webui/package.json b/webui/package.json index 1d14772..919a5fc 100644 --- a/webui/package.json +++ b/webui/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-select": "^2.1.5", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", diff --git a/webui/src/app/ConnectionConfig.tsx b/webui/src/app/ConnectionConfig.tsx index 4039352..1372296 100644 --- a/webui/src/app/ConnectionConfig.tsx +++ b/webui/src/app/ConnectionConfig.tsx @@ -9,6 +9,8 @@ import { Switch } from '@/components/ui/switch.js' import React from 'react' import { CONNECTION_CONFIG_QUERY_KEY, CONNECTION_STATUS_QUERY_KEY } from './constants.js' import { BarLoader } from 'react-spinners' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js' +import { cn } from '@/lib/utils.js' export function ConnectionConfig(): JSX.Element { const api = useSatelliteApi() @@ -55,10 +57,29 @@ function ConnectionConfigContent({ config }: { config: ApiConfigData }): JSX.Ele
Companion Connection + ( + + + + )} + /> ( - +