diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ba16bcb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - run: npm ci + + - run: npm run typecheck + + - run: npm test diff --git a/.gitignore b/.gitignore index f2d741f..68e32e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,12 @@ .DS_Store -*.streamDeckPlugin \ No newline at end of file +*.streamDeckPlugin + +# Node.js +node_modules/ + +# Stream Deck build output +*.sdPlugin/bin +*.sdPlugin/logs + +# SDK log output when run from the repo root +/logs/ diff --git a/README.md b/README.md index 39f9e57..a34e79a 100644 --- a/README.md +++ b/README.md @@ -6,39 +6,46 @@ To download the current official release, visit our page on the [Elgato marketpl If you're interested in developing a software intergration with Meld Studio, you can find our API documentation [here](./WebChannelAPI.md). If you have any questions, please join our [discord](https://discord.gg/ZHpeXEw4) and reach out in [`#support`](https://discord.com/channels/1019227275897614337/1019584582913364101). +## Architecture + +The plugin is built on Elgato's official [Node.js Stream Deck SDK](https://docs.elgato.com/streamdeck/sdk/introduction/getting-started/) (`@elgato/streamdeck`). It runs as a single long-lived Node process, so there is exactly **one** connection to Meld Studio for the whole plugin, shared by every action and every property inspector. + +- `src/plugin.ts` — entry point; registers the 14 actions and starts the Meld connection. +- `src/meld/` — the Meld service layer: a single QWebChannel-over-WebSocket connection (`connection.ts`) with automatic reconnect and a track-observer registry, a typed session model (`session.ts` / `types.ts`), and typed events (`events.ts`). +- `src/actions/` — one file per action, all extending `MeldAction` (`base.ts`), which turns Meld/session/reconnect events into a single declarative `render()` call. +- `src/ui-bridge.ts` — answers property-inspector requests for session data from the one connection; the inspectors under `co.meldstudio.streamdeck.sdPlugin/ui/` never talk to Meld directly. + +The build (`rollup`) bundles `src/plugin.ts` into `co.meldstudio.streamdeck.sdPlugin/bin/plugin.js`. + +Requires Stream Deck **7.1+** and bundles the **Node 24** runtime (see `manifest.json`). + ## Development We primarily develop on macOS, so these instructions are tailored to macOS. Feel free to submit a PR to update them for Windows if that's your flavor of OS. -1) Check out the repository. +1) Check out the repository and run `npm install`. 2) Run `scripts/install.sh` (optionally, `--fresh` to remove and reinstall). - This will update the plugin in `~/Library/Application\ Support/com.elgato.StreamDeck/Plugins/` if it exists. + This builds the plugin (`npm run build`) and syncs it into + `~/Library/Application\ Support/com.elgato.StreamDeck/Plugins/`. -3) Code away. -4) Run `scripts/install.sh` again to update the install. +3) Code away. Type-check with `npm run typecheck`. +4) Run `scripts/install.sh` again to rebuild and update the install, or use + `npm run watch` to rebuild on save and restart the plugin automatically. See below for debugging information. ## Debugging: -1. To enable debugging on macOS, run: `defaults write com.elgato.StreamDeck html_remote_debugging_enabled -bool YES` and restart Stream Deck app. -2. Navigation to `http://localhost:23654/` in Chrome. -3. Click on one of the two entries: +The plugin runs in Node, so it is debugged as a Node process, while property inspectors remain web pages. -![](images/screen-list.png) +- **Plugin (Node):** the manifest enables the Node inspector (`"Nodejs": { "Debug": "enabled" }`). Attach a debugger / open `chrome://inspect` in Chrome to connect to the plugin process. Logs are written to `co.meldstudio.streamdeck.sdPlugin/logs/`. +- **Property inspectors (web):** run `defaults write com.elgato.StreamDeck html_remote_debugging_enabled -bool YES` and restart the Stream Deck app, then navigate to `http://localhost:23654/` in Chrome to inspect the inspector pages. -* The blue link shows the Property Inspector page and console log. -* The red link shows the CodePath button press page and console. - -Which should yield you: +![](images/screen-list.png) ![](images/debug.png) -From here you can refresh the plugin source from the debug console UI. - -For more details: https://streamdecklabs.com/debugging-your-javascript-plugin/ - ## Packaging Elgato provides a CLI packaging tool. @@ -46,9 +53,11 @@ Elgato provides a CLI packaging tool. [https://docs.elgato.com/sdk/plugins/packaging] ``` -$ npm -g install @elgato/cli -$ streamdeck validate co.meldstudio.streamdeck.sdPlugin -$ streamdeck pack co.meldstudio.streamdeck.sdPlugin +$ npm install +$ npm run build +$ npx streamdeck validate co.meldstudio.streamdeck.sdPlugin +$ npx streamdeck pack co.meldstudio.streamdeck.sdPlugin ``` -This outputs a file `co.meldstudio.streamdeck.streamDeckPlugin` which can be installed directly. +Or simply `npm run pack`, which builds and packs in one step. + diff --git a/WebChannelAPI.md b/WebChannelAPI.md index 032546a..d81ba62 100644 --- a/WebChannelAPI.md +++ b/WebChannelAPI.md @@ -503,7 +503,7 @@ _Side Effects_: Once streaming starts, the `channel`'s `isStreaming` property wi meld.toggleStream(); ``` -#### **`registerTrackObserver(trackId: string, context: string)`** +#### **`registerTrackObserver(context: string, trackId: string)`** Registers an observer for changes to a specific track. @@ -519,7 +519,7 @@ const trackId = /* desired track id */; meld.registerTrackObserver("documentation-context", trackId); ``` -#### **`unregisterTrackObserver(trackId: string, context: string)`** +#### **`unregisterTrackObserver(context: string, trackId: string)`** Unregisters an observer for a specific track. @@ -616,6 +616,25 @@ meld.sendCommand("meld.screenshot"); | `meld.replay.show` | Show replay layer for last clip. | | `meld.replay.dismiss` | Dismiss all active replay layers. | +#### **`sendCommandWithArgs(commandName: string, args: any[])`** + +Triggers a custom command with positional arguments. + +**Parameters**: + +| property | type | description | +| -------- | ---------- | ----------------------------------- | +| commandName | `string` | Command to trigger in Meld. | +| args | `any[]` | Positional arguments for the command. | + +```js +meld.sendCommandWithArgs("meld.recordClip", [30]); +``` + +`meld.recordClip` accepts at most one argument: a finite, non-negative clip +duration in seconds. Its advertised range is 0–90 seconds; omitting the +argument, or passing `0`, records the maximum available duration. + #### **`sendStreamEvent(type: string, data?: any)`** Dispatches a widget-specific event to Meld Studio (e.g., stopwatch, countdown, confetti, subathon timer, wheel spin). diff --git a/co.meldstudio.streamdeck.sdPlugin/README.md b/co.meldstudio.streamdeck.sdPlugin/README.md index 7836293..c771c64 100644 --- a/co.meldstudio.streamdeck.sdPlugin/README.md +++ b/co.meldstudio.streamdeck.sdPlugin/README.md @@ -12,9 +12,10 @@ And we're not done yet! We are continually working to bring you even more capabi ## What's New -Version 0.6.2.1 -- Added Widget control actions for controlling the counter widget in Meld Studio. -- Added Mute and Cue controls to the Replay Clip action. +Version 0.6.4.1 +- Added configurable durations to the Record Clip action. Choose a 10, 15, 30, 60, or 90 second preset, or enter a custom duration from 0 to 90 seconds. +- Improved action state synchronization and recovery after Meld Studio reconnects and session restores. +- Fixed reversed active/inactive icon states on the Cue, Layer Visibility, Effect Visibility, Record, Go Live, and Change Scene actions so thirdparty icon packs indicate correctly with Meld Studio actions. ## Helpful Links @@ -22,7 +23,7 @@ https://meldstudio.co/ ## Information -Stream Deck 6.0 and later, macOS 12.3 and later. +Stream Deck 7.1 and later, macOS 12.3 and later. Languages: English, German ## Support Links @@ -34,6 +35,22 @@ hi@meldstudio.co # Change History: ------------ +## 2026-08-12 (v0.6.4.1) +### Changes: +----------- +- Refactored the plugin onto Elgato's official Node.js Stream Deck SDK. +- Optimized Meld Studio interactions for all actions. +- Added configurable durations to the Record Clip action. Choose a 10, 15, 30, 60, or 90 second preset, or enter a custom duration from 0 to 90 seconds. +- Improved action state synchronization and recovery after Meld Studio reconnects and session restores. +- Fixed reversed active/inactive icon states on the Cue, Layer Visibility, Effect Visibility, Record, Go Live, and Change Scene actions so thirdparty icon packs indicate correctly with Meld Studio actions. + + +## 2026-05-13 (v0.6.3.2) +### Changes: +----------- +- Fixed a bug in how dials and scene change actions store state. + + ## 2026-01-22 (v0.6.2.1) ### Changes: ----------- @@ -91,6 +108,3 @@ hi@meldstudio.co ### Changes: ----------- - Initial implementation of 6 core features (Scene Selection, Layer Visibility, Effect Visibility, Mute Audio, Stream, and Record). - - - diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/clip/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/clip/inspector.html deleted file mode 100644 index 70017f5..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/clip/inspector.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - co.meldstudio.streamdeck.clip Property Inspector - - - - -
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/clip/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/clip/plugin.js deleted file mode 100644 index cccade0..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/clip/plugin.js +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class Clip extends MeldStudioPlugin { - constructor() { - super("co.meldstudio.streamdeck.clip"); - - this.action.onKeyUp(({ action, context, device, event, payload }) => { - if ($MS.meld?.sendCommand) - $MS.meld.sendCommand("meld.recordClip"); - }); - } -} - -const clip = new Clip(); diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/media/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/media/inspector.html deleted file mode 100644 index cc3b97b..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/media/inspector.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - co.meldstudio.streamdeck.media Property Inspector - - - - -
-
-
Scene
- -
-
-
Layer
- -
- -
-
Action
- -
- - -
- - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/media/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/media/plugin.js deleted file mode 100644 index 6b1b6ef..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/media/plugin.js +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class Media extends MeldStudioPlugin { - mediaState = {} - - setLocalState (context, action, isPlaying) { - let path = isPlaying - ? 'assets/Media/Key Icon/media-controls-pause' - : 'assets/Media/Key Icon/media-controls-play' - - if (action === 'restart') { - path = 'assets/Media/Key Icon/media-controls-skip-back' - } else if (action === 'play') { - path = 'assets/Media/Key Icon/media-controls-play' - } else if (action === 'pause') { - path = 'assets/Media/Key Icon/media-controls-pause' - } - - $SD.setImage(context, path, 0) - } - - findMediaLayer (context, session) { - if (!session?.items) return null - - const settings = this.getSettings(context) - const { scene: sceneId, layer: layerId } = settings - - // If no scene/layer selected, return null - if (!sceneId || !layerId) return null - - const layerItem = session.items[layerId] - - // Verify the layer exists, belongs to the selected scene, and has mediaSource - if ( - !layerItem || - layerItem.type !== 'layer' || - layerItem.parent !== sceneId || - !layerItem.mediaSource - ) { - return null - } - - return { - layerId, - layerItem - } - } - - mediaPlay (context, mediaAction, mediaInfo) { - this.setLocalState(context, mediaAction, true) - if (!mediaInfo?.layerId) return - if ($MS.meld?.callFunction) { - $MS.meld.callFunction(mediaInfo.layerId, 'play') - } - } - - mediaPause (context, mediaAction, mediaInfo) { - this.setLocalState(context, mediaAction, false) - if (!mediaInfo?.layerId) return - if ($MS.meld?.callFunction) { - $MS.meld.callFunction(mediaInfo.layerId, 'pause') - } - } - - mediaRestart (context, mediaAction, mediaInfo) { - this.setLocalState(context, mediaAction, false) - if (!mediaInfo?.layerId) return - if ($MS.meld?.callFunctionWithArgs) { - $MS.meld.callFunctionWithArgs(mediaInfo.layerId, 'seekTo', [0]) - } - } - - mediaToggle (context, mediaAction, mediaInfo) { - if (!mediaInfo?.layerId) return - const isPlaying = mediaInfo.layerItem?.isPlaying - if (isPlaying) { - this.mediaPause(context, mediaAction, mediaInfo) - } else { - this.mediaPlay(context, mediaAction, mediaInfo) - } - } - - constructor () { - super('co.meldstudio.streamdeck.media') - - const actionHandlers = { - play: (context, mediaAction, mediaInfo) => - this.mediaPlay(context, mediaAction, mediaInfo), - pause: (context, mediaAction, mediaInfo) => - this.mediaPause(context, mediaAction, mediaInfo), - restart: (context, mediaAction, mediaInfo) => - this.mediaRestart(context, mediaAction, mediaInfo), - toggle: (context, mediaAction, mediaInfo) => - this.mediaToggle(context, mediaAction, mediaInfo) - } - - this.action.onKeyUp(({ context }) => { - const { action: mediaAction } = this.getSettings(context) - const mediaInfo = this.mediaState[context] - - const action = mediaAction || 'toggle' - const handler = actionHandlers[action] - if (handler) { - handler(context, mediaAction, mediaInfo) - } - }) - - this.action.onKeyDown(() => {}) - } - - onSessionChanged (session) { - this.forAllContexts(context => { - const mediaLayer = this.findMediaLayer(context, session) - const { action: mediaAction } = this.getSettings(context) - - if (!mediaLayer) { - this.mediaState[context] = null - return this.setLocalState(context, mediaAction, false) - } - - const { layerId, layerItem } = mediaLayer - this.mediaState[context] = { - layerId, - layerItem - } - - this.setLocalState(context, mediaAction, layerItem.isPlaying) - }) - } - - onReceivedSettings (context, newSettings, oldSettings) { - const { action: mediaAction } = newSettings - if (mediaAction !== oldSettings.action) { - this.setLocalState( - context, - mediaAction, - this.mediaState[context]?.layerItem?.isPlaying ?? false - ) - } - } -} - -const media = new Media() diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/replay/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/replay/inspector.html deleted file mode 100644 index 66bff64..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/replay/inspector.html +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - co.meldstudio.streamdeck.replay Property Inspector - - - - -
-
-
Action
- -
- - -
- - - - - - - - - - - - - - - diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/replay/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/replay/plugin.js deleted file mode 100644 index 70226f2..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/replay/plugin.js +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -function toDb (gain) { - let dB = 20 * Math.log10(gain) - if (!isFinite(dB)) { - dB = -60 - } - return dB -} - -function toGain (dB) { - if (!isFinite(dB) || dB < -60) dB = -60 - let gain = Math.pow(10, dB / 20) - - gain = gain <= 0.001 ? 0 : gain - gain = gain > 1 ? 1 : gain - return gain -} - -class Replay extends MeldStudioPlugin { - replayState = {} - muted = true - monitoring = false - - setLocalState (context, action) { - let path - - if (['toggle', 'mute', 'unmute'].includes(action)) { - const muted = this.replayState?.muted ?? true - if (action === 'mute') { - path = 'assets/ReplayMute/replay-clip-muted' - } else if (action === 'unmute') { - path = 'assets/ReplayMute/replay-clip-unmuted' - } else { - // toggle action shows current state - path = muted - ? 'assets/ReplayMute/replay-clip-muted' - : 'assets/ReplayMute/replay-clip-unmuted' - } - } else if (['toggle_cue', 'cue', 'uncue'].includes(action)) { - const monitoring = this.replayState?.monitoring ?? false - if (action === 'cue') { - path = 'assets/ReplayCUE/on' - } else if (action === 'uncue') { - path = 'assets/ReplayCUE/off' - } else { - // toggle_cue action shows current state - path = monitoring - ? 'assets/ReplayCUE/on' - : 'assets/ReplayCUE/off' - } - } else if (action === 'volume_up') { - path = 'assets/ReplayVolume/replay-clip-volumeUp' - } else if (action === 'volume_down') { - path = 'assets/ReplayVolume/replay-clip-volumeDown' - } else if (action === 'dismiss') { - path = 'assets/Replay/Key Icon/dismiss-clip-controls' - } else { - path = 'assets/Replay/Key Icon/replay-clip-controls' - } - - $SD.setImage(context, path, 0) - } - - findReplayTrack (session) { - if (!session?.items) return null - - // Convert items object to array with id property - const replayTracks = Object.entries(session.items) - .filter( - ([, item]) => item.name === 'Replay Clip' && item.type === 'track' - ) - .map(([id, item]) => ({ - id, - ...item - })) - - if (replayTracks.length === 0) return null - - const replayTrack = replayTracks[0] - - return replayTrack - ? { - layerId: replayTrack.parent, - trackId: replayTrack.id, - trackItem: replayTrack - } - : null - } - - replayShow () { - if (this.replayState?.layerId) return - if ($MS.meld?.sendCommand) $MS.meld.sendCommand('meld.replay.show') - } - - replayDismiss () { - if ($MS.meld?.sendCommand) $MS.meld.sendCommand('meld.replay.dismiss') - } - - replayToggleMute () { - if (!this.replayState?.trackId) return - if ($MS.meld?.toggleMute) $MS.meld.toggleMute(this.replayState.trackId) - } - - replayMute () { - if (!this.replayState?.trackId) return - if ($MS.meld?.setMuted) { - $MS.meld.setMuted(this.replayState.trackId, true) - this.replayState.muted = true - } - } - - replayUnmute () { - if (!this.replayState?.trackId) return - if ($MS.meld?.setMuted) { - $MS.meld.setMuted(this.replayState.trackId, false) - this.replayState.muted = false - } - } - - replayToggleCue () { - if (!this.replayState?.trackId) return - if ($MS.meld?.toggleMonitor) - $MS.meld.toggleMonitor(this.replayState.trackId) - } - - replayCue () { - if (!this.replayState?.trackId) return - if ($MS.meld?.setProperty) { - $MS.meld.setProperty(this.replayState.trackId, 'monitoring', true) - this.replayState.monitoring = false - } - } - - replayUncue () { - if (!this.replayState?.trackId) return - if ($MS.meld?.setProperty) { - $MS.meld.setProperty(this.replayState.trackId, 'monitoring', false) - this.replayState.monitoring = false - } - } - - replayVolumeUp () { - if (!this.replayState?.trackId) return - const stepsize = 3.0 // 3dB step - let gain = this.replayState.gain ?? 0.0 - - let dB = toDb(gain) - dB += stepsize - gain = toGain(dB) - - if ($MS.meld?.setGain) $MS.meld.setGain(this.replayState.trackId, gain) - } - - replayVolumeDown () { - if (!this.replayState?.trackId) return - const stepsize = -3.0 // -3dB step - let gain = this.replayState.gain ?? 0.0 - - let dB = toDb(gain) - dB += stepsize - gain = toGain(dB) - - if ($MS.meld?.setGain) $MS.meld.setGain(this.replayState.trackId, gain) - } - - constructor () { - super('co.meldstudio.streamdeck.replay') - - const actionHandlers = { - replay: () => this.replayShow(), - dismiss: () => this.replayDismiss(), - toggle: () => this.replayToggleMute(), - mute: () => this.replayMute(), - unmute: () => this.replayUnmute(), - toggle_cue: () => this.replayToggleCue(), - cue: () => this.replayCue(), - uncue: () => this.replayUncue(), - volume_up: () => this.replayVolumeUp(), - volume_down: () => this.replayVolumeDown() - } - - this.action.onKeyUp(({ context }) => { - const { action: replay_action } = this.getSettings(context) - - const action = replay_action || 'replay' - const handler = actionHandlers[action] - - if (handler) handler() - }) - } - - onReceivedSettings (context, newSettings, oldSettings) { - const { action: replay_action } = newSettings - this.setLocalState(context, replay_action) - } - - onSessionChanged (session) { - const replayTrack = this.findReplayTrack(session) - if (!replayTrack) { - this.replayState = { - muted: true, - monitoring: false - } - return - } - - const { layerId, trackId, trackItem } = replayTrack - this.replayState = { - layerId, - trackId, - muted: trackItem.muted, - monitoring: trackItem.monitoring, - gain: trackItem.gain ?? 0.0 - } - - this.forAllContexts((context, settings) => { - this.setLocalState(context, settings.action) - }) - } -} - -const replay = new Replay() diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/screenshot/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/screenshot/inspector.html deleted file mode 100644 index 9fe3b0c..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/screenshot/inspector.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - co.meldstudio.streamdeck.screenshot Property Inspector - - - - -
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/screenshot/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/screenshot/plugin.js deleted file mode 100644 index 8f383d1..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/screenshot/plugin.js +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class Screenshot extends MeldStudioPlugin { - constructor() { - super("co.meldstudio.streamdeck.screenshot"); - - this.action.onKeyUp(({ action, context, device, event, payload }) => { - if ($MS.meld?.sendCommand) - $MS.meld.sendCommand("meld.screenshot"); - // Keep backword compatibility for now. - else if ($MS.meld?.sendEvent) - $MS.meld.sendEvent("co.meldstudio.events.screenshot"); - }); - } -} - -const screenshot = new Screenshot(); diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/show-scene/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/show-scene/inspector.html deleted file mode 100644 index 88e539f..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/show-scene/inspector.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - co.meldstudio.streamdeck.show-scene Property Inspector - - - - -
-
-
Scene
- -
- - -
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/show-scene/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/show-scene/plugin.js deleted file mode 100644 index ca4f08c..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/show-scene/plugin.js +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class ShowScene extends MeldStudioPlugin { - sceneRequested = null - - constructor () { - super('co.meldstudio.streamdeck.show-scene') - - this.action.onKeyUp(({ action, context, device, event, payload }) => { - const { scene } = this.getSettings(context) - if (!scene) return - - this.sceneRequested = scene - this.updateState(context) - - if ($MS.meld?.showScene) $MS.meld.showScene(scene) - }) - } - - onSessionChanged (session) { - this.sceneRequested = null - - this.forAllContexts((context, settings) => { - this.updateState(context) - }) - } - - updateState (context) { - const { scene } = this.getSettings(context) - const session = $MS?.meld?.session - - const state = (() => { - if (!scene) return 0 - if (!session || !session?.items) return 0 - - const item = session.items[scene] - - if (!item) return 0 - if (scene == this.sceneRequested) return 1 - return item.current ? 1 : 0 - })() - - $SD.setState(context, state) - } -} - -const showScene = new ShowScene() diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-effect/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-effect/inspector.html deleted file mode 100644 index 5f5b347..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-effect/inspector.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - co.meldstudio.streamdeck.toggle-effect Property Inspector - - - - -
-
-
Scene
- -
-
-
Layer
- -
-
-
Effect
- -
-
-
Action
- -
- - -
- - - - - - - - - - - - - - - - diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-effect/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-effect/plugin.js deleted file mode 100644 index 0f6697c..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-effect/plugin.js +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class ToggleEffect extends MeldStudioPlugin { - effectState = {}; - - setLocalState(context, shown) { - const state = shown ? 1 : 0; - $SD.setState(context, state); - } - - constructor() { - super("co.meldstudio.streamdeck.toggle-effect"); - - this.action.onKeyUp(({ action, context, device, event, payload }) => { - const { scene, layer, effect, action: toggle_action } = this.getSettings( - context - ); - if (!scene || !layer || !effect) return; - - if (!toggle_action || toggle_action === "toggle") { - if ($MS.meld?.toggleEffect) $MS.meld.toggleEffect(scene, layer, effect); - } else { - const action_show = toggle_action === "show"; - const state_show = this.effectState[context]; - - // show | state | action - // 1 | 0 | 1 - // 0 | 1 | 1 - // 1 | 1 | 0 - // 0 | 0 | 0 - - const shouldToggle = action_show ^ state_show; - if (!shouldToggle) this.setLocalState(context, state_show); - if (shouldToggle && $MS.meld?.toggleEffect) - $MS.meld.toggleEffect(scene, layer, effect); - } - }); - - $MS.on("sessionChanged", (session) => { - this.forAllContexts((context, settings) => { - const { effect } = settings; - if (!effect) return; - if (!session.items[effect]) return $SD.setState(context, 0); - - const enabled = session.items[effect].enabled; - this.effectState[context] = enabled; - - const state = enabled ? 1 : 0; - $SD.setState(context, state); - }); - }); - } -} - -const toggleEffect = new ToggleEffect(); diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-layer/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-layer/inspector.html deleted file mode 100644 index 29d1629..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-layer/inspector.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - co.meldstudio.streamdeck.toggle-layer Property Inspector - - - - -
-
-
Scene
- -
-
-
Layer
- -
- - - -
-
Action
- -
-
- - - - - - - - - - - - - - - diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-layer/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-layer/plugin.js deleted file mode 100644 index 4a9e164..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-layer/plugin.js +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class ToggleLayer extends MeldStudioPlugin { - layerState = {}; - - setLocalState(context, shown) { - const state = shown ? 1 : 0; - $SD.setState(context, state); - } - - constructor() { - super("co.meldstudio.streamdeck.toggle-layer"); - - this.action.onKeyUp(({ action, context, device, event, payload }) => { - const { scene, layer, action: toggle_action } = this.getSettings(context); - if (!scene || !layer) return; - - if (!toggle_action || toggle_action === "toggle") { - if ($MS.meld?.toggleLayer) $MS.meld.toggleLayer(scene, layer); - } else { - const action_show = toggle_action === "show"; - const state_show = this.layerState[context]; - - // show | state | action - // 1 | 0 | 1 - // 0 | 1 | 1 - // 1 | 1 | 0 - // 0 | 0 | 0 - - const shouldToggle = action_show ^ state_show; - if (!shouldToggle) this.setLocalState(context, state_show); - if (shouldToggle && $MS.meld?.toggleLayer) - $MS.meld.toggleLayer(scene, layer); - } - }); - - $MS.on("sessionChanged", (session) => { - this.forAllContexts((context, settings) => { - const { layer } = settings; - if (!layer) return; - if (!session?.items || !session?.items[layer]) - return $SD.setState(context, 0); - - const visible = session.items[layer].visible; - this.layerState[context] = visible; - - this.setLocalState(context, visible); - }); - }); - } -} - -const toggleLayer = new ToggleLayer(); diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-monitor/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-monitor/inspector.html deleted file mode 100644 index ed7007c..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-monitor/inspector.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - co.meldstudio.streamdeck.toggle-monitor Property Inspector - - - - -
-
-
Track
- -
-
-
Action
- -
- - -
- - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-monitor/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-monitor/plugin.js deleted file mode 100644 index b2352fc..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-monitor/plugin.js +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class ToggleMonitor extends MeldStudioPlugin { - cueState = {}; - - setLocalState(context, cued) { - const state = cued ? 1 : 0; - $SD.setState(context, state); - } - - constructor() { - super("co.meldstudio.streamdeck.toggle-monitor"); - - this.action.onKeyUp(({ action, context, device, event, payload }) => { - const { track, action: toggle_action } = this.getSettings(context); - if (!track) return; - - if (!toggle_action || toggle_action === "toggle") { - if ($MS.meld?.toggleMonitor) $MS.meld.toggleMonitor(track); - } else { - const action_cue = toggle_action === "cue"; - const state_cue = this.cueState[context]; - - // cue | state | action - // 1 | 0 | 1 - // 0 | 1 | 1 - // 1 | 1 | 0 - // 0 | 0 | 0 - - if ($MS.meld?.setProperty) { - $MS.meld.setProperty(track, "monitoring", action_cue); - this.setLocalState(context, action_cue); - } else { - const shouldToggle = action_cue ^ state_cue; - if (!shouldToggle) this.setLocalState(context, state_cue); - if (shouldToggle && $MS.meld?.toggleMonitor) - $MS.meld.toggleMonitor(track); - } - } - }); - - $MS.on("sessionChanged", (session) => { - this.forAllContexts((context, settings) => { - const { track, action } = settings; - if (!track) return; - if (!session.items[track]) return $SD.setState(context, 0); - - const cued = session.items[track].monitoring; - this.cueState[context] = cued; - - this.setLocalState(context, cued); - }); - }); - } -} - -const toggleMonitor = new ToggleMonitor(); diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-mute/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-mute/inspector.html deleted file mode 100644 index e2e61e1..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-mute/inspector.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - co.meldstudio.streamdeck.toggle-mute Property Inspector - - - - -
-
-
Track
- -
-
-
Action
- -
- - -
- - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-mute/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-mute/plugin.js deleted file mode 100644 index 28cb768..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-mute/plugin.js +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class ToggleMute extends MeldStudioPlugin { - muteState = {} - - setLocalState(context, muted) { - const state = muted ? 0 : 1; - $SD.setState(context, state); - } - - constructor() { - super("co.meldstudio.streamdeck.toggle-mute"); - - this.action.onKeyUp(({ action, context, device, event, payload }) => { - const { track, action: toggle_action } = this.getSettings(context); - if (!track) return; - - if (!toggle_action || toggle_action === "toggle") { - if ($MS.meld?.toggleMute) $MS.meld.toggleMute(track); - } else { - const action_mute = toggle_action === "mute"; - const state_mute = this.muteState[context]; - - // mute | state | action - // 1 | 0 | 1 - // 0 | 1 | 1 - // 1 | 1 | 0 - // 0 | 0 | 0 - - if ($MS.meld?.setMuted) { - $MS.meld.setMuted(track, action_mute); - this.setLocalState(context, action_mute); - } else if ($MS.meld?.setProperty) { - $MS.meld.setProperty(track, "muted", action_mute); - this.setLocalState(context, action_mute); - } else { - const shouldToggle = action_mute ^ state_mute; - if (!shouldToggle) this.setLocalState(context, state_mute); - if (shouldToggle && $MS.meld?.toggleMute) $MS.meld.toggleMute(track); - } - } - }); - - $MS.on("sessionChanged", (session) => { - this.forAllContexts((context, settings) => { - const { track, action } = settings; - if (!track) return; - if (!session.items[track]) return $SD.setState(context, 1); - - const muted = session.items[track].muted; - this.muteState[context] = muted; - - this.setLocalState(context, muted); - }); - }); - } -} - -const toggleMute = new ToggleMute(); diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-record/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-record/inspector.html deleted file mode 100644 index cd7413d..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-record/inspector.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - co.meldstudio.streamdeck.toggle-record Property Inspector - - - - -
- -
- - - - - - - - - - - - - \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-record/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-record/plugin.js deleted file mode 100644 index 4848304..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-record/plugin.js +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class ToggleRecording extends MeldStudioPlugin { - constructor() { - super("co.meldstudio.streamdeck.toggle-record"); - - this.action.onKeyUp(({ action, context, device, event, payload }) => { - if ($MS.meld?.toggleRecord) $MS.meld.toggleRecord(); - }); - - $MS.on("isRecordingChanged", (recording) => { - const state = recording ? 1 : 0; - this.forAllContexts((context) => { - $SD.setState(context, state); - }); - }); - } -} - -const toggleRecord = new ToggleRecording(); diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-stream/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-stream/inspector.html deleted file mode 100644 index dee9d5c..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-stream/inspector.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - co.meldstudio.streamdeck.toggle-stream Property Inspector - - - - -
- -
- - - - - - - - - - - - - \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-stream/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/toggle-stream/plugin.js deleted file mode 100644 index f014235..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/toggle-stream/plugin.js +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class ToggleStreaming extends MeldStudioPlugin { - constructor() { - super("co.meldstudio.streamdeck.toggle-stream"); - - this.action.onKeyUp(({ action, context, device, event, payload }) => { - if ($MS.meld?.toggleStream) $MS.meld.toggleStream(); - }); - - $MS.on("isStreamingChanged", (streaming) => { - const state = streaming ? 1 : 0; - this.forAllContexts((context) => { - $SD.setState(context, state); - }); - }); - } -} - -const toggleStreaming = new ToggleStreaming(); diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/transition-stepper/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/transition-stepper/inspector.html deleted file mode 100644 index 2efedf8..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/transition-stepper/inspector.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - co.meldstudio.streamdeck.transition-stepper Property Inspector - - - - -
-
TransitionStepper
-
- - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/transition-stepper/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/transition-stepper/plugin.js deleted file mode 100644 index f96954a..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/transition-stepper/plugin.js +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class TransitionStepper extends MeldStudioPlugin { - stagedScene = -1; - availableScenes = []; - - constructor() { - super("co.meldstudio.streamdeck.transition-stepper"); - - this.action.onDialRotate(({ context, payload }) => { - if (this.availableScenes.length === 0) - return; - - const nextScene = () => { - this.stagedScene = (this.stagedScene + payload.ticks) % this.availableScenes.length; - if (this.stagedScene < 0) this.stagedScene += this.availableScenes.length; - return this.availableScenes[this.stagedScene]; - }; - - let item = nextScene(); - if (!item) - item = nextScene(); - - if ($MS.meld?.setStagedScene) $MS.meld?.setStagedScene(item.id); - }); - - this.action.onDialDown(() => { - if (this.stagedScene === -1) return; - if ($MS.meld?.showStagedScene) $MS.meld.showStagedScene(); - }); - - this.action.onTouchTap(({ payload }) => { - if (!payload.hold) return; - if (this.stagedScene === -1) return; - if ($MS.meld?.showStagedScene) $MS.meld.showStagedScene(); - }); - - $MS.on("sessionChanged", (session) => { - this.availableScenes = []; - - for(let key in session?.items) { - const item = session?.items[key]; - - if (item?.type !== "scene") continue; - if (item?.name.indexOf("(linked)") !== -1) continue; - - if (item?.staged) - this.stagedScene = item.index; - - if (!item?.current) - this.availableScenes[item.index] = { name: item.name, id: key }; - } - - if (this.stagedScene !== -1) { - const item = this.availableScenes[this.stagedScene]; - this.forAllContexts((context) => this.setSceneName(context, item?.name)); - } - }); - } - - setSceneName(context, name) { - $SD.setFeedback(context, { - icon: "assets/Scene Transition Control/Action Icon/sceneTransition", - title: name ?? "Nothing Staged", - }); - } - - getNameForScene(sceneId) { - const defaultName = "No Selection"; - if (!$MS?.meld?.session?.items) return defaultName; - - const name = $MS.meld.session.items[sceneId]?.name; - return name ? name : defaultName; - } -} - -const transitionStepper = new TransitionStepper(); diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/inspector.html deleted file mode 100644 index 0fa10ec..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/inspector.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - co.meldstudio.streamdeck.volume-stepper Property Inspector - - - - -
-
-
Track
- -
-
-
StepSize
- -
- -
-
Meter
- -
- - -
- - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/plugin.js deleted file mode 100644 index ed7c535..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/volume-stepper/plugin.js +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -function toDb(gain) { - let dB = 20 * Math.log10(gain); - if (!isFinite(dB)) { - dB = -60; - } - return dB; -} - -function toGain(dB) { - if (!isFinite(dB) || dB < -60) dB = -60; - let gain = Math.pow(10, dB / 20); - - gain = gain <= 0.001 ? 0 : gain; - gain = gain > 1 ? 1 : gain; - return gain; -} - -class VolumeStepper extends MeldStudioPlugin { - trackInfo = {}; - unregisterCallbacks = {}; - - constructor() { - super("co.meldstudio.streamdeck.volume-stepper"); - - this.action.onKeyUp(({ action, context, device, event, payload }) => { - const { track } = this.getSettings(context); - if (!track) return; - - if ($MS.meld?.toggleMute) $MS.meld.toggleMute(track); - }); - - this.action.onDialRotate(({ context, payload }) => { - const { track, stepsize: stepString, metertype } = this.getSettings( - context - ); - let use_percent = metertype === "percent"; - - const stepsize = parseFloat(stepString); - const info = this.trackInfo[track]; - - if (!track) return; - - let offset = +stepsize * payload.ticks; - let gain = info?.gain ?? 0.0; - - if (use_percent) { - gain += 0.01 * offset; // scale to percent... - gain = Math.min(gain, 1.0); - gain = Math.max(gain, 0.0); - } else { - let dB = toDb(gain); - dB += offset; - gain = toGain(dB); - } - - // Store the new gain until the callback fires. - this.trackInfo[track] = { ...info, gain }; - - if ($MS.meld?.setGain) $MS.meld?.setGain(track, gain); - }); - - this.action.onDialDown(({ context }) => { - const { track } = this.getSettings(context); - if (!track) return; - - if ($MS.meld?.toggleMute) $MS.meld.toggleMute(track); - }); - - this.action.onTouchTap(({ context, payload}) => { - if (payload.hold) return; - - const { tapPos } = payload; - const { track } = this.getSettings(context); - if (!track) return; - - // Indicator starts at x=76 and is 108 pixels wide. - if (tapPos[0] > 80) { - if (tapPos[0] > 180) return; - - const info = this.trackInfo[track]; - const gain = (tapPos[0] - 80) / 100; - - // Store the new gain until the callback fires. - this.trackInfo[track] = { ...info, gain }; - - if ($MS.meld?.setGain) $MS.meld?.setGain(track, gain); - } else { - if ($MS.meld?.toggleMonitor) $MS.meld.toggleMonitor(track); - } - }); - - this.action.onWillAppear(({ context, payload }) => { - $SD.setFeedback(context, { - icon: - "assets/Audio Track/Mute_Unmute Audio Track/Action Icons/audioTrack", - title: "", - value: "", - indicator: { - value: 0, - enabled: true, - bar_bg_c: "0:#666666,1:#666666", - }, - }); - }); - - this.action.onDidReceiveSettings(({ context, payload }) => { - this.setSettings(context, payload?.settings ?? {}); - - if ($MS.ready) { - this.onReady(context, payload?.settings); - } else { - // If we lose connection, we may need to reinitialize. - $MS.on("ready", () => { - this.onReady(context, payload?.settings); - }); - - this.setGainAndMute(context, payload.settings.metertype, { - gain: 0.0, - muted: true, - monitoring: false, - }); - } - }); - - $MS.on("sessionChanged", (session) => { - this.forAllContexts((context, { track, metertype }) => { - if (!track) return; - if (!session?.items) return; - if (!session?.items[track]) return; - - const { name, muted, monitoring } = session.items[track]; - - this.trackInfo[track] = { - ...this.trackInfo[track], - name, - muted, - monitoring, - }; - - this.setGainAndMute(context, metertype, this.trackInfo[track]); - }); - }); - - $MS.on("gainChanged", ({ trackId, gain, muted }) => { - let info = this.trackInfo[trackId]; - info = { ...info, gain, muted }; - this.trackInfo[trackId] = info; - - this.forAllContexts((context, { track, metertype }) => { - if (!track || trackId != track) return; - this.setGainAndMute(context, metertype, info); - }); - }); - - $MS.on("closed", () => { - // reset all registrations. - this.unregisterCallbacks = {}; - }); - } - - setGainAndMute(context, metertype, { gain, muted, name, monitoring }) { - // meter colors - - // green: #6DDE92 - // orange: #FB923C - // red: #F04A4A - - let use_percent = metertype === "percent"; - let volume; - let bar_colors; - let bar_value; - - if (use_percent) { - volume = `${parseInt(gain * 100)}%`; - bar_value = gain; - } else { - const dB = toDb(gain); - volume = dB === -60 ? "-∞ dB" : `${parseInt(Math.round(dB))} dB`; - bar_value = (dB + 60) / 60; - } - - const info = (() => { - if (!muted) { - if (gain > 0.4) - return { - icon: - "assets/Audio Track/Mute_Unmute Audio Track/Action Icons/audioTrack", - }; - if (gain > 0.0) - return { - icon: - "assets/Audio Track/Mute_Unmute Audio Track/Action Icons/audioUnmuted40", - }; - } - return { - icon: - "assets/Audio Track/Mute_Unmute Audio Track/Action Icons/audioMute", - }; - })(); - - $SD.setFeedback(context, { - ...info, - title: name ?? "Adjust Volume", - value: volume, - indicator: { - value: bar_value * 100, - enabled: true, - bar_bg_c: muted ? "0:#666666,1:#666666" : "0:#6DDE92,1:#6DDE92", - }, - }); - } - - register(context, track) { - console.assert($MS.meld); - - const callbackInfo = this.unregisterCallbacks[context]; - // Only register once. - if (callbackInfo?.track === track) return; - - this.maybeUnregister(context, track); - $MS.meld.registerTrackObserver(context, track); - - this.unregisterCallbacks[context] = { - callback: () => { - if ($MS.meld) $MS.meld.unregisterTrackObserver(context, track); - }, - track, - }; - } - - maybeUnregister(context) { - const callbackInfo = this.unregisterCallbacks[context]; - if (!callbackInfo) return; - - callbackInfo.callback(); - this.unregisterCallbacks[context] = undefined; - } - - getNameForTrack(track) { - const defaultName = "Adjust Volume"; - if (!$MS?.meld?.session?.items) return defaultName; - - const name = $MS.meld.session.items[track]?.name; - return name ? name : defaultName; - } - - connectGain(context, metertype, track) { - this.trackInfo[track] = { - gain: 0.0, - muted: false, - name: this.getNameForTrack(track), - }; - - this.setGainAndMute(context, metertype, this.trackInfo[track]); - this.register(context, track); - - this.action.onWillDisappear(({ context }) => { - this.maybeUnregister(context); - }); - } - - onReady(context, { track, metertype }) { - console.assert($MS.ready); - if (this.isEncoder(context)) this.connectGain(context, metertype, track); - } -} - -const volumeStepper = new VolumeStepper(); diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/widget-control/inspector.html b/co.meldstudio.streamdeck.sdPlugin/actions/widget-control/inspector.html deleted file mode 100644 index 55725d8..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/widget-control/inspector.html +++ /dev/null @@ -1,236 +0,0 @@ - - - - - - - co.meldstudio.streamdeck.widget-control Property Inspector - - - - - -
-
-
Widget
- -
- -
-
Action
- -
- - - - -
- - - - - - - - - - - - - - - - diff --git a/co.meldstudio.streamdeck.sdPlugin/actions/widget-control/plugin.js b/co.meldstudio.streamdeck.sdPlugin/actions/widget-control/plugin.js deleted file mode 100644 index 30a6d80..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/actions/widget-control/plugin.js +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -const ADD_TIME_EVENT = 'SUBATHONTIMER_ADDTIME' -const DEFAULT_EVENT = 'STOPWATCH_RESET' - -const WIDGET_EVENT_ICONS = { - STOPWATCH_RESET: 'assets/Widget/Key Icon/widget-stopwatch-reset', - STOPWATCH_PAUSE: 'assets/Widget/Key Icon/widget-stopwatch-pause', - STOPWATCH_RESUME: 'assets/Widget/Key Icon/widget-stopwatch-resume', - COUNTDOWN_RESET: 'assets/Widget/Key Icon/widget-countdown-reset', - COUNTDOWN_PAUSE: 'assets/Widget/Key Icon/widget-countdown-pause', - COUNTDOWN_RESUME: 'assets/Widget/Key Icon/widget-countdown-resume', - CONFETTIFALL_TRIGGER: 'assets/Widget/Key Icon/widget-confettifall-trigger', - CONFETTIPOP_TRIGGER: 'assets/Widget/Key Icon/widget-confettipop-trigger', - SUBATHONTIMER_RESET: 'assets/Widget/Key Icon/widget-subathontimer-reset', - SUBATHONTIMER_PAUSE: 'assets/Widget/Key Icon/widget-subathontimer-pause', - SUBATHONTIMER_RESUME: 'assets/Widget/Key Icon/widget-subathontimer-resume', - SUBATHONTIMER_ADDTIME: 'assets/Widget/Key Icon/widget-subathontimer-addtime', - COUNTER_INCREMENT: 'assets/Widget/Key Icon/widget-counter-increment', - COUNTER_DECREMENT: 'assets/Widget/Key Icon/widget-counter-decrement' -} - -class WidgetControl extends MeldStudioPlugin { - constructor () { - super('co.meldstudio.streamdeck.widget-control') - - this.action.onWillAppear(({ context }) => { - const { event: eventType } = this.getSettings(context) - this.setKeyImage(context, eventType) - }) - - this.action.onKeyUp(({ context }) => { - const { event: eventTypeSetting, amount } = this.getSettings(context) - const eventType = eventTypeSetting || DEFAULT_EVENT - console.log(eventType) - - const payload = this.buildPayload(eventType, amount) - - if ($MS.meld?.sendStreamEvent) { - if (payload === undefined) { - $MS.meld.sendStreamEvent(eventType, false) - } else { - $MS.meld.sendStreamEvent(eventType, payload) - } - } - }) - } - - onReceivedSettings (context, newSettings) { - this.setKeyImage(context, newSettings?.event) - } - - setKeyImage (context, eventType) { - const selectedEvent = eventType || DEFAULT_EVENT - const imagePath = - WIDGET_EVENT_ICONS[selectedEvent] || - WIDGET_EVENT_ICONS[DEFAULT_EVENT] || - 'assets/Widget/Key Icon/widget-controls-pause' - - $SD.setImage(context, imagePath, 0) - } - - buildPayload (eventType, amountSetting) { - if (eventType !== ADD_TIME_EVENT) return undefined - - const parsedAmount = Number(amountSetting) - const amount = Number.isFinite(parsedAmount) - ? Math.max(0, Math.round(parsedAmount)) - : 0 - - return { amount } - } -} - -const widgetControl = new WidgetControl() diff --git a/co.meldstudio.streamdeck.sdPlugin/app.html b/co.meldstudio.streamdeck.sdPlugin/app.html deleted file mode 100644 index a3942b3..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/app.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - co.meldstudio.streamdeck - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/co.meldstudio.streamdeck.sdPlugin/app.js b/co.meldstudio.streamdeck.sdPlugin/app.js deleted file mode 100644 index 6573872..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/app.js +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -let settingsCache = {}; - -/** - * The first event fired when Stream Deck starts - */ -$SD.onConnected( - ({ actionInfo, appInfo, connection, messageType, port, uuid }) => { - console.log("Stream Deck connected!"); - } -); - -$MS.on("connected", () => { - console.log("Connect Callback."); -}); - -$MS.on("ready", () => { - console.log("Session Ready."); -}); - -$MS.on("closed", () => { - console.log("Closed Callback."); -}); \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/css/sdpi.css b/co.meldstudio.streamdeck.sdPlugin/libs/css/sdpi.css index 01802ef..56a19c0 100644 --- a/co.meldstudio.streamdeck.sdPlugin/libs/css/sdpi.css +++ b/co.meldstudio.streamdeck.sdPlugin/libs/css/sdpi.css @@ -1706,12 +1706,22 @@ input[type="range"].colortemperature::-webkit-slider-thumb { /* adding some styles here that override sdpi things so we can use this as notes for sdpi updates*/ select { min-width: 0px; - - /* this is a clunky fix for using background image as select arrow with long text options */ - -webkit-appearance: media-slider; + -webkit-appearance: none; + appearance: none; + background-image: url(../assets/caret.svg); + background-repeat: no-repeat; + background-position: right 6px center; + background-size: 12px 6px; + padding-right: 24px !important; text-overflow: ellipsis; } +select.no-selection, +select option.no-selection-option { + color: #8E8E92; + font-style: italic; +} + /*--------- context menu ----------*/ .context-menu { diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/action.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/action.js deleted file mode 100644 index 78fc1ed..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/action.js +++ /dev/null @@ -1,188 +0,0 @@ -/// - -/** - * @class Action - * A Stream Deck plugin action, where you can register callback functions for different events - */ -class ELGSDAction { - UUID; - on = EventEmitter.on; - emit = EventEmitter.emit; - - constructor(UUID) { - if (!UUID) { - console.error( - 'An action UUID matching the action UUID in your manifest is required when creating Actions.' - ); - } - - this.UUID = UUID; - } - - /** - * Registers a callback function for the didReceiveSettings event, which fires when calling getSettings - * @param {function} fn - */ - onDidReceiveSettings(fn) { - if (!fn) { - console.error( - 'A callback function for the didReceiveSettings event is required for onDidReceiveSettings.' - ); - } - - this.on(`${this.UUID}.${Events.didReceiveSettings}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the keyDown event, which fires when pressing a key down - * @param {function} fn - */ - onKeyDown(fn) { - if (!fn) { - console.error('A callback function for the keyDown event is required for onKeyDown.'); - } - - this.on(`${this.UUID}.${Events.keyDown}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the keyUp event, which fires when releasing a key - * @param {function} fn - */ - onKeyUp(fn) { - if (!fn) { - console.error('A callback function for the keyUp event is required for onKeyUp.'); - } - - this.on(`${this.UUID}.${Events.keyUp}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the willAppear event, which fires when an action appears on the canvas - * @param {function} fn - */ - onWillAppear(fn) { - if (!fn) { - console.error('A callback function for the willAppear event is required for onWillAppear.'); - } - - this.on(`${this.UUID}.${Events.willAppear}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the willAppear event, which fires when an action disappears on the canvas - * @param {function} fn - */ - onWillDisappear(fn) { - if (!fn) { - console.error( - 'A callback function for the willDisappear event is required for onWillDisappear.' - ); - } - - this.on(`${this.UUID}.${Events.willDisappear}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the titleParametersDidChange event, which fires when a user changes the key title - * @param {function} fn - */ - onTitleParametersDidChange(fn) { - if (!fn) { - console.error( - 'A callback function for the titleParametersDidChange event is required for onTitleParametersDidChange.' - ); - } - - this.on(`${this.UUID}.${Events.titleParametersDidChange}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the propertyInspectorDidAppear event, which fires when the property inspector is displayed - * @param {function} fn - */ - onPropertyInspectorDidAppear(fn) { - if (!fn) { - console.error( - 'A callback function for the propertyInspectorDidAppear event is required for onPropertyInspectorDidAppear.' - ); - } - - this.on(`${this.UUID}.${Events.propertyInspectorDidAppear}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the propertyInspectorDidDisappear event, which fires when the property inspector is closed - * @param {function} fn - */ - onPropertyInspectorDidDisappear(fn) { - if (!fn) { - console.error( - 'A callback function for the propertyInspectorDidDisappear event is required for onPropertyInspectorDidDisappear.' - ); - } - - this.on(`${this.UUID}.${Events.propertyInspectorDidDisappear}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the sendToPlugin event, which fires when the property inspector uses the sendToPlugin api - * @param {function} fn - */ - onSendToPlugin(fn) { - if (!fn) { - console.error( - 'A callback function for the sendToPlugin event is required for onSendToPlugin.' - ); - } - - this.on(`${this.UUID}.${Events.sendToPlugin}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the onDialRotate event, which fires when a SD+ dial was rotated - * @param {function} fn - */ - onDialRotate(fn) { - if (!fn) { - console.error('A callback function for the onDialRotate event is required for onDialRotate.'); - } - this.on(`${this.UUID}.${Events.dialRotate}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the onDialDown event, which fires when a SD+ dial was pressed or released - * @param {function} fn - */ - onDialDown(fn) { - if (!fn) { - console.error('A callback function for the onDialDown event is required for onDialDown.'); - } - this.on(`${this.UUID}.${Events.dialDown}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the touchTap event, which fires when a SD+ touch panel was touched quickly - * @param {function} fn - */ - onTouchTap(fn) { - if (!fn) { - console.error('A callback function for the onTouchTap event is required for onTouchTap.'); - } - this.on(`${this.UUID}.${Events.touchTap}`, (jsn) => fn(jsn)); - return this; - } -} - -const Action = ELGSDAction; diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/api.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/api.js deleted file mode 100644 index 9d5edad..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/api.js +++ /dev/null @@ -1,349 +0,0 @@ -/// - -class ELGSDPlugin { - #data = {}; - #language = 'en'; - #localization; - test = new Set(); - on = EventEmitter.on; - emit = EventEmitter.emit; - - localizationLoaded = false; - constructor () { - // super(); - if(ELGSDPlugin.__instance) { - return ELGSDPlugin.__instance; - } - - ELGSDPlugin.__instance = this; - const pathArr = location.pathname.split("/"); - const idx = pathArr.findIndex(f => f.endsWith('sdPlugin')); - this.#data.__filename = pathArr[pathArr.length - 1]; - this.#data.__dirname = pathArr[pathArr.length - 2]; - this.#data.__folderpath = `${pathArr.slice(0, idx + 1).join("/")}/`; - this.#data.__folderroot = `${pathArr.slice(0, idx).join("/")}/`; - this.#data.__parentdir = `${pathArr.slice(0, idx-1).join("/")}/`; - } - - set language(value) { - this.#language = value; - this.loadLocalization(this.#data.__folderpath).then(l => { - this.emit('languageChanged', value); - }); - } - - get language() { - return this.#language; - } - - set localization(value) { - this.#localization = value; - this.localizeUI(); - this.emit('localizationChanged', value); - } - - get localization() { - return this.#localization; - } - - get __filename() { - return this.#data.__filename; - } - - get __dirname() { - return this.#data.__dirname; - } - - get __folderpath() { - return this.#data.__folderpath; - } - get __folderroot() { - return this.#data.__folderroot; - } - get data() { - return this.#data; - } - - /** - * Finds the original key of the string value - * Note: This is used by the localization UI to find the original key (not used here) - * @param {string} str - * @returns {string} - */ - - localizedString(str) { - return Object.keys(this.localization).find(e => e == str); - } - - /** - * Returns the localized string or the original string if not found - * @param {string} str - * @returns {string} - */ - - localize(s) { - if(typeof s === 'undefined') return ''; - let str = String(s); - try { - str = this.localization[str] || str; - } catch(b) {} - return str; - }; - - /** - * Searches the document tree to find elements with data-localize attributes - * and replaces their values with the localized string - * @returns {} - */ - - localizeUI = () => { - const el = document.querySelector('.sdpi-wrapper'); - if(!el) return console.warn("No element found to localize"); - const selectorsList = '[data-localize]'; - // see if we have any data-localize attributes - // that means we can skip the rest of the DOM - el.querySelectorAll(selectorsList).forEach(e => { - const s = e.innerText.trim(); - e.innerHTML = e.innerHTML.replace(s, this.localize(s)); - if(e.placeholder && e.placeholder.length) { - e.placeholder = this.localize(e.placeholder); - } - if(e.title && e.title.length) { - e.title = this.localize(e.title); - } - }); - }; - /** - * Fetches the specified language json file - * @param {string} pathPrefix - * @returns {Promise} - */ -async loadLocalization(pathPrefix) { - if (!pathPrefix) { - pathPrefix = this.#data.__folderpath; - } - // here we save the promise to the JSON-reader result, - // which we can later re-use to see, if the strings are already loaded - this.localizationLoaded = this.readJson(`${pathPrefix}${this.language}.json`); - const manifest = await this.localizationLoaded; - this.localization = manifest['Localization'] ?? null; - window.$localizedStrings = this.localization; - this.emit('localizationLoaded', this.localization); - - return this.localization; -} - - /** - * - * @param {string} path - * @returns {Promise} json - */ - async readJson(path) { - if(!path) { - console.error('A path is required to readJson.'); - } - - return new Promise((resolve, reject) => { - const req = new XMLHttpRequest(); - req.onerror = reject; - req.overrideMimeType('application/json'); - req.open('GET', path, true); - req.onreadystatechange = (response) => { - if(req.readyState === 4) { - const jsonString = response?.target?.response; - if(jsonString) { - resolve(JSON.parse(response?.target?.response)); - } else { - reject(); - } - } - }; - - req.send(); - }); - } -} - -class ELGSDApi extends ELGSDPlugin { - port; - uuid; - messageType; - actionInfo; - websocket; - appInfo; - #data = {}; - - /** - * Connect to Stream Deck - * @param {string} port - * @param {string} uuid - * @param {string} messageType - * @param {string} appInfoString - * @param {string} actionString - */ - connect(port, uuid, messageType, appInfoString, actionString) { - this.port = port; - this.uuid = uuid; - this.messageType = messageType; - this.actionInfo = actionString ? JSON.parse(actionString) : null; - this.appInfo = JSON.parse(appInfoString); - this.language = this.appInfo?.application?.language ?? null; - - if(this.websocket) { - this.websocket.close(); - this.websocket = null; - } - - this.websocket = new WebSocket('ws://127.0.0.1:' + this.port); - - this.websocket.onopen = () => { - const json = { - event: this.messageType, - uuid: this.uuid, - }; - - this.websocket.send(JSON.stringify(json)); - - this.emit(Events.connected, { - connection: this.websocket, - port: this.port, - uuid: this.uuid, - actionInfo: this.actionInfo, - appInfo: this.appInfo, - messageType: this.messageType, - }); - }; - - this.websocket.onerror = (evt) => { - const error = `WEBSOCKET ERROR: ${evt}, ${evt.data}, ${SocketErrors[evt?.code]}`; - console.warn(error); - this.logMessage(error); - }; - - this.websocket.onclose = (evt) => { - console.warn('WEBSOCKET CLOSED:', SocketErrors[evt?.code]); - }; - - this.websocket.onmessage = (evt) => { - const data = evt?.data ? JSON.parse(evt.data) : null; - - const {action, event} = data; - const message = action ? `${action}.${event}` : event; - if(message && message !== '') this.emit(message, data); - }; - } - - /** - * Write to log file - * @param {string} message - */ - logMessage(message) { - if(!message) { - console.error('A message is required for logMessage.'); - } - - try { - if(this.websocket) { - const json = { - event: Events.logMessage, - payload: { - message: message, - }, - }; - this.websocket.send(JSON.stringify(json)); - } else { - console.error('Websocket not defined'); - } - } catch(e) { - console.error('Websocket not defined'); - } - } - - /** - * Send JSON payload to StreamDeck - * @param {string} context - * @param {string} event - * @param {object} [payload] - */ - send(context, event, payload = {}) { - this.websocket && this.websocket.send(JSON.stringify({context, event, ...payload})); - } - - /** - * Save the plugin's persistent data - * @param {object} payload - */ - setGlobalSettings(payload) { - this.send(this.uuid, Events.setGlobalSettings, { - payload: payload, - }); - } - - /** - * Request the plugin's persistent data. StreamDeck does not return the data, but trigger the plugin/property inspectors didReceiveGlobalSettings event - */ - getGlobalSettings() { - this.send(this.uuid, Events.getGlobalSettings); - } - - /** - * Opens a URL in the default web browser - * @param {string} url - */ - openUrl(url) { - if(!url) { - console.error('A url is required for openUrl.'); - } - - this.send(this.uuid, Events.openUrl, { - payload: { - url, - }, - }); - } - - /** - * Registers a callback function for when Stream Deck is connected - * @param {function} fn - * @returns ELGSDStreamDeck - */ - onConnected(fn) { - if(!fn) { - console.error('A callback function for the connected event is required for onConnected.'); - } - - this.on(Events.connected, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the didReceiveGlobalSettings event, which fires when calling getGlobalSettings - * @param {function} fn - */ - onDidReceiveGlobalSettings(fn) { - if(!fn) { - console.error( - 'A callback function for the didReceiveGlobalSettings event is required for onDidReceiveGlobalSettings.' - ); - } - - this.on(Events.didReceiveGlobalSettings, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the didReceiveSettings event, which fires when calling getSettings - * @param {string} action - * @param {function} fn - */ - onDidReceiveSettings(action, fn) { - if(!fn) { - console.error( - 'A callback function for the didReceiveSettings event is required for onDidReceiveSettings.' - ); - } - - this.on(`${action}.${Events.didReceiveSettings}`, (jsn) => fn(jsn)); - return this; - } -} diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/constants.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/constants.js deleted file mode 100644 index 59f0bec..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/constants.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Errors received from WebSocket - */ -const SocketErrors = { - 0: 'The connection has not yet been established', - 1: 'The connection is established and communication is possible', - 2: 'The connection is going through the closing handshake', - 3: 'The connection has been closed or could not be opened', - 1000: 'Normal Closure. The purpose for which the connection was established has been fulfilled.', - 1001: 'Going Away. An endpoint is "going away", such as a server going down or a browser having navigated away from a page.', - 1002: 'Protocol error. An endpoint is terminating the connection due to a protocol error', - 1003: "Unsupported Data. An endpoint received a type of data it doesn't support.", - 1004: '--Reserved--. The specific meaning might be defined in the future.', - 1005: 'No Status. No status code was actually present.', - 1006: 'Abnormal Closure. The connection was closed abnormally, e.g., without sending or receiving a Close control frame', - 1007: 'Invalid frame payload data. The connection was closed, because the received data was not consistent with the type of the message (e.g., non-UTF-8 [http://tools.ietf.org/html/rfc3629]).', - 1008: 'Policy Violation. The connection was closed, because current message data "violates its policy". This reason is given either if there is no other suitable reason, or if there is a need to hide specific details about the policy.', - 1009: 'Message Too Big. Connection closed because the message is too big for it to process.', - 1010: "Mandatory Extension. Connection is terminated the connection because the server didn't negotiate one or more extensions in the WebSocket handshake.", - 1011: 'Internl Server Error. Connection closed because it encountered an unexpected condition that prevented it from fulfilling the request.', - 1015: "TLS Handshake. The connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified).", -}; - -/** - * Events used for communicating with Stream Deck - */ -const Events = { - didReceiveSettings: 'didReceiveSettings', - didReceiveGlobalSettings: 'didReceiveGlobalSettings', - keyDown: 'keyDown', - keyUp: 'keyUp', - willAppear: 'willAppear', - willDisappear: 'willDisappear', - titleParametersDidChange: 'titleParametersDidChange', - deviceDidConnect: 'deviceDidConnect', - deviceDidDisconnect: 'deviceDidDisconnect', - applicationDidLaunch: 'applicationDidLaunch', - applicationDidTerminate: 'applicationDidTerminate', - systemDidWakeUp: 'systemDidWakeUp', - propertyInspectorDidAppear: 'propertyInspectorDidAppear', - propertyInspectorDidDisappear: 'propertyInspectorDidDisappear', - sendToPlugin: 'sendToPlugin', - sendToPropertyInspector: 'sendToPropertyInspector', - connected: 'connected', - setImage: 'setImage', - setXYWHImage: 'setXYWHImage', - setTitle: 'setTitle', - setState: 'setState', - showOk: 'showOk', - showAlert: 'showAlert', - openUrl: 'openUrl', - setGlobalSettings: 'setGlobalSettings', - getGlobalSettings: 'getGlobalSettings', - setSettings: 'setSettings', - getSettings: 'getSettings', - registerPropertyInspector: 'registerPropertyInspector', - registerPlugin: 'registerPlugin', - logMessage: 'logMessage', - switchToProfile: 'switchToProfile', - dialRotate: 'dialRotate', - dialDown: 'dialDown', - touchTap: 'touchTap', - setFeedback: 'setFeedback', - setFeedbackLayout: 'setFeedbackLayout', -}; - -/** - * Constants used for Stream Deck - */ -const Constants = { - dataLocalize: '[data-localize]', - hardwareAndSoftware: 0, - hardwareOnly: 1, - softwareOnly: 2, -}; - -const DestinationEnum = { - HARDWARE_AND_SOFTWARE: 0, - HARDWARE_ONLY: 1, - SOFTWARE_ONLY: 2, -}; diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/dynamic-styles.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/dynamic-styles.js deleted file mode 100644 index 901c4ce..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/dynamic-styles.js +++ /dev/null @@ -1,70 +0,0 @@ -const fadeColor = function (col, amt) { - const min = Math.min, - max = Math.max; - const num = parseInt(col.replace(/#/g, ''), 16); - const r = min(255, max((num >> 16) + amt, 0)); - const g = min(255, max((num & 0x0000ff) + amt, 0)); - const b = min(255, max(((num >> 8) & 0x00ff) + amt, 0)); - return '#' + (g | (b << 8) | (r << 16)).toString(16).padStart(6, 0); -}; - -$PI.onConnected(({appInfo}) => { - if (!appInfo?.colors) return; - const clrs = appInfo.colors; - const node = document.getElementById('#sdpi-dynamic-styles') || document.createElement('style'); - if (!clrs.mouseDownColor) clrs.mouseDownColor = fadeColor(clrs.highlightColor, -100); - const clr = clrs.highlightColor.slice(0, 7); - const clr1 = fadeColor(clr, 100); - const clr2 = fadeColor(clr, 60); - const metersActiveColor = fadeColor(clr, -60); - - node.setAttribute('id', 'sdpi-dynamic-styles'); - node.innerHTML = ` - - input[type="radio"]:checked + label span, - input[type="checkbox"]:checked + label span { - background-color: ${clrs.highlightColor}; - } - - input[type="radio"]:active:checked + label span, - input[type="checkbox"]:active:checked + label span { - background-color: ${clrs.mouseDownColor}; - } - - input[type="radio"]:active + label span, - input[type="checkbox"]:active + label span { - background-color: ${clrs.buttonPressedBorderColor}; - } - - td.selected, - td.selected:hover, - li.selected:hover, - li.selected { - color: white; - background-color: ${clrs.highlightColor}; - } - - .sdpi-file-label > label:active, - .sdpi-file-label.file:active, - label.sdpi-file-label:active, - label.sdpi-file-info:active, - input[type="file"]::-webkit-file-upload-button:active, - button:active { - border: 1pt solid ${clrs.buttonPressedBorderColor}; - background-color: ${clrs.buttonPressedBackgroundColor}; - color: ${clrs.buttonPressedTextColor}; - border-color: ${clrs.buttonPressedBorderColor}; - } - - ::-webkit-progress-value, - meter::-webkit-meter-optimum-value { - background: linear-gradient(${clr2}, ${clr1} 20%, ${clr} 45%, ${clr} 55%, ${clr2}) - } - - ::-webkit-progress-value:active, - meter::-webkit-meter-optimum-value:active { - background: linear-gradient(${clr}, ${clr2} 20%, ${metersActiveColor} 45%, ${metersActiveColor} 55%, ${clr}) - } - `; - document.body.appendChild(node); -}); diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/events.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/events.js deleted file mode 100644 index cc18825..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/events.js +++ /dev/null @@ -1,38 +0,0 @@ -/** ELGEvents - * Publish/Subscribe pattern to quickly signal events to - * the plugin, property inspector and data. - */ - -const ELGEvents = { - eventEmitter: function (name, fn) { - const eventList = new Map(); - - const on = (name, fn) => { - if (!eventList.has(name)) eventList.set(name, ELGEvents.pubSub()); - - return eventList.get(name).sub(fn); - }; - - const has = name => eventList.has(name); - - const emit = (name, data) => eventList.has(name) && eventList.get(name).pub(data); - - return Object.freeze({on, has, emit, eventList}); - }, - - pubSub: function pubSub() { - const subscribers = new Set(); - - const sub = fn => { - subscribers.add(fn); - return () => { - subscribers.delete(fn); - }; - }; - - const pub = data => subscribers.forEach(fn => fn(data)); - return Object.freeze({pub, sub}); - } -}; - -const EventEmitter = ELGEvents.eventEmitter(); \ No newline at end of file diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/inspector.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/inspector.js deleted file mode 100644 index 70703ca..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/inspector.js +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -class MeldStudioPropertyInspector { - settings = {} - callbacks = {} - - createOption (opt) { - const item = document.createElement('option') - - item.innerHTML = opt.text - item.value = opt.value - item.selected = opt.selected - - return item - } - - // Get all items of a specific type. - - getItemsByType (type, currentValue, parent, keep_filter) { - const items = $MS.meld?.session?.items ?? {} - - let selectedItems = [ - { - text: '[No Selection]', - value: '', - selected: currentValue == '' - } - ] - - for (let key in items) { - const item = items[key] - - if (keep_filter && !keep_filter(type, item)) continue - - if (item.type != type) continue - if (parent && item.parent != parent) continue - - selectedItems.push({ - text: item.name, - value: key, - selected: currentValue == key - }) - } - - return selectedItems - } - - getValue (key) { - return this.settings[key] ?? '' - } - - // Iterates over the items in the session and filters down - // each pass to the current type, filtered by the keep_filter function is provided. - - updateSelection (elements, keep_filter) { - if (!this.callbacks[elements]) { - this.callbacks[elements] = $MS.on('sessionChanged', session => { - this.updateSelection(elements) - }) - } - - if (!$MS.meld) { - $MS.on('ready', () => { - this.updateSelection(elements) - }) - return - } - - let last = '' - for (let elid of elements) { - const value = this.getValue(elid) - const element = document.getElementById(elid) - - let available = this.getItemsByType(elid, value, last, keep_filter) - - element.innerHTML = '' - element.disabled = false - - for (let opt of available) { - element.appendChild(this.createOption(opt)) - } - - last = value - } - } - - initializeSelection (action, elements, data_provided = true, keep_filter = null) { - for (let id of elements) { - const el = document.getElementById(id) - console.assert(el.id, 'Select element not found') - if (!el) continue - - if (this.settings[el.id] === undefined) this.settings[el.id] = '' - - el.onchange = () => { - if (!this.settings) return - this.settings = { ...this.settings, [id]: el.value } - $PI.setSettings(this.settings) - if (data_provided) this.updateSelection(elements, keep_filter) - } - } - - $PI.onDidReceiveSettings(action, ({ action, payload }) => { - const { settings } = payload - this.settings = settings - - for (let field of elements) { - const dom_field = document.getElementById(field) - - if (settings[field] === undefined) continue - - dom_field.value = settings[field] - } - - if (data_provided) this.updateSelection(elements, keep_filter) - }) - - $PI.getSettings() - } - - initializeText (action, elements) { - for (let id of elements) { - const el = document.getElementById(id) - console.assert(el.id, 'Input element not found') - if (!el) continue - - this.settings[el.id] = '' - - el.onchange = () => { - if (!this.settings) return - this.settings = { ...this.settings, [id]: el.value } - $PI.setSettings(this.settings) - } - } - - $PI.onDidReceiveSettings(action, ({ action, payload }) => { - const { settings } = payload - this.settings = settings - - for (let field of elements) { - const dom_field = document.getElementById(field) - if (settings[field] !== undefined) { - dom_field.value = settings[field] - } else { - // Use the default value specified by the field. - this.settings[field] = dom_field.value - } - } - }) - } - - watchConnections (id) { - $MS.on('connected', () => { - document.getElementById(id).style = 'display: none;' - }) - - $MS.on('closed', () => { - document.getElementById(id).style = '' - }) - - if ($MS.connected) document.getElementById(id).style = 'display: none;' - } -} - -const $MSPI = new MeldStudioPropertyInspector() diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/meldstudio.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/meldstudio.js deleted file mode 100644 index 2f2b627..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/meldstudio.js +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) 2024 Meld Studio, Inc. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -const MeldStudioEventEmitter = ELGEvents.eventEmitter() - -class MeldStudio { - on = MeldStudioEventEmitter.on - emit = MeldStudioEventEmitter.emit - - connected = false - ready = false - - constructor () { - this.connected = false - this.initConnection() - } - - initConnection () { - const address = '127.0.0.1' - const port = 13376 - - this.socket = new WebSocket(`ws://${address}:${port}`) - - this.socket.onopen = () => { - console.log('MeldStudio Connected!') - - this.connected = true - this.emit('connected') - - this.channel = new QWebChannel(this.socket, channel => { - this.meld = channel.objects.meld - - this.meld.sessionChanged.connect(() => { - console.log('Session Changed') - this.emit('sessionChanged', this.meld.session) - }) - - this.meld.isStreamingChanged.connect(() => { - console.log('Streaming Changed') - this.emit('isStreamingChanged', this.meld.isStreaming) - }) - - this.meld.isRecordingChanged.connect(() => { - console.log('Recording Changed') - this.emit('isRecordingChanged', this.meld.isRecording) - }) - - this.meld.gainUpdated.connect((trackId, gain, muted) => { - this.emit('gainChanged', { trackId, gain, muted }) - }) - - this.ready = true - this.emit('ready') - this.emit('sessionChanged', this.meld.session) - this.emit('isStreamingChanged', this.meld.isStreaming) - this.emit('isRecordingChanged', this.meld.isRecording) - }) - } - - this.socket.onclose = () => { - this.ready = false - this.connected = false - this.meld = {} - - this.emit('closed') - - setTimeout(() => { - this.initConnection() - }, 1000) - } - } -} - -const $MS = new MeldStudio() - -class MeldStudioPlugin { - action - contexts = {} - - constructor (UUID) { - this.action = new Action(UUID) - - this.action.onWillAppear(({ context, payload }) => { - this.contexts[context] = { - isEncoder: payload.controller == 'Encoder', - settings: {} - } - $SD.getSettings(context) - }) - - this.action.onDidReceiveSettings(({ context, payload }) => { - const oldSettings = this.getSettings(context) - this.setSettings(context, payload?.settings ?? {}) - - if ($MS.ready) { - this.onReady(context, payload?.settings, oldSettings) - } - - // If we lose connection, we may need to reinitialize. - $MS.on('ready', () => { - this.onReady(context, payload?.settings, oldSettings) - }) - - this.onReceivedSettings(context, payload?.settings, oldSettings) - }) - - $MS.on('sessionChanged', session => { - this.onSessionChanged(session) - }) - } - - onReceivedSettings (context, newSettings, oldSettings) {} - - onSessionChanged (session) {} - - onReady () { - if (!$MS.meld?.session) return - this.onSessionChanged($MS.meld.session) - } - - setSettings (context, settings) { - this.contexts[context].settings = settings - } - - isEncoder (context) { - return this.contexts[context].isEncoder - } - - getSettings (context) { - return this.contexts[context].settings - } - - updateState (context, state) { - $SD.setImage(context, state.image, 0) - $SD.setTitle(context, state.title) - } - - forAllContexts (fn) { - for (const context in this.contexts) { - fn(context, this.getSettings(context)) - } - } -} diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/property-inspector.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/property-inspector.js deleted file mode 100644 index d8b6705..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/property-inspector.js +++ /dev/null @@ -1,82 +0,0 @@ -/// -/// - -class ELGSDPropertyInspector extends ELGSDApi { - constructor() { - super(); - if (ELGSDPropertyInspector.__instance) { - return ELGSDPropertyInspector.__instance; - } - - ELGSDPropertyInspector.__instance = this; - } - - /** - * Registers a callback function for when Stream Deck sends data to the property inspector - * @param {string} actionUUID - * @param {function} fn - * @returns ELGSDStreamDeck - */ - onSendToPropertyInspector(actionUUID, fn) { - if (typeof actionUUID != 'string') { - console.error('An action UUID string is required for onSendToPropertyInspector.'); - } - - if (!fn) { - console.error( - 'A callback function for the sendToPropertyInspector event is required for onSendToPropertyInspector.' - ); - } - - this.on(`${actionUUID}.${Events.sendToPropertyInspector}`, (jsn) => fn(jsn)); - return this; - } - - /** - * Send payload from the property inspector to the plugin - * @param {object} payload - */ - sendToPlugin(payload) { - this.send(this.uuid, Events.sendToPlugin, { - action: this?.actionInfo?.action, - payload: payload || null, - }); - } - - /** - * Save the actions's persistent data. - * @param {object} payload - */ - setSettings(payload) { - this.send(this.uuid, Events.setSettings, { - action: this?.actionInfo?.action, - payload: payload || null, - }); - } - - /** - * Request the actions's persistent data. StreamDeck does not return the data, but trigger the actions's didReceiveSettings event - */ - getSettings() { - this.send(this.uuid, Events.getSettings); - } -} - -const $PI = new ELGSDPropertyInspector(); - -/** - * connectElgatoStreamDeckSocket - * This is the first function StreamDeck Software calls, when - * establishing the connection to the plugin or the Property Inspector - * @param {string} port - The socket's port to communicate with StreamDeck software. - * @param {string} uuid - A unique identifier, which StreamDeck uses to communicate with the plugin - * @param {string} messageType - Identifies, if the event is meant for the property inspector or the plugin. - * @param {string} appInfoString - Information about the host (StreamDeck) application - * @param {string} actionInfo - Context is an internal identifier used to communicate to the host application. - */ -function connectElgatoStreamDeckSocket(port, uuid, messageType, appInfoString, actionInfo) { - const delay = window?.initialConnectionDelay || 0; - setTimeout(() => { - $PI.connect(port, uuid, messageType, appInfoString, actionInfo); - }, delay); -} diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/prototypes.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/prototypes.js deleted file mode 100644 index e3b84fe..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/prototypes.js +++ /dev/null @@ -1,26 +0,0 @@ -/** reaches out for a magical global localization object, hopefully loaded by $SD and swaps the string **/ -String.prototype.lox = function () { - var a = String(this); - try { - a = $localizedStrings[a] || a; - } catch (b) { - } - return a; -}; - -String.prototype.sprintf = function (inArr) { - let i = 0; - const args = inArr && Array.isArray(inArr) ? inArr : arguments; - return this.replace(/%s/g, function () { - return args[i++]; - }); -}; - -WebSocket.prototype.sendJSON = function (jsn, log) { - if (log) { - console.log('SendJSON', this, jsn); - } - // if (this.readyState) { - this.send(JSON.stringify(jsn)); - // } -}; diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/qwebchannel.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/qwebchannel.js deleted file mode 100644 index 56fa66a..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/qwebchannel.js +++ /dev/null @@ -1,455 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -"use strict"; - -var QWebChannelMessageTypes = { - signal: 1, - propertyUpdate: 2, - init: 3, - idle: 4, - debug: 5, - invokeMethod: 6, - connectToSignal: 7, - disconnectFromSignal: 8, - setProperty: 9, - response: 10, -}; - -var QWebChannel = function(transport, initCallback, converters) -{ - if (typeof transport !== "object" || typeof transport.send !== "function") { - console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + - " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); - return; - } - - var channel = this; - this.transport = transport; - - var converterRegistry = - { - Date : function(response) { - if (typeof response === "string" - && response.match( - /^-?\d+-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d*)?([-+\u2212](\d{2}):(\d{2})|Z)?$/)) { - var date = new Date(response); - if (!isNaN(date)) - return date; - } - return undefined; // Return undefined if current converter is not applicable - } - }; - - this.usedConverters = []; - - this.addConverter = function(converter) - { - if (typeof converter === "string") { - if (converterRegistry.hasOwnProperty(converter)) - this.usedConverters.push(converterRegistry[converter]); - else - console.error("Converter '" + converter + "' not found"); - } else if (typeof converter === "function") { - this.usedConverters.push(converter); - } else { - console.error("Invalid converter object type " + typeof converter); - } - } - - if (Array.isArray(converters)) { - for (const converter of converters) - this.addConverter(converter); - } else if (converters !== undefined) { - this.addConverter(converters); - } - - this.send = function(data) - { - if (typeof(data) !== "string") { - data = JSON.stringify(data); - } - channel.transport.send(data); - } - - this.transport.onmessage = function(message) - { - var data = message.data; - if (typeof data === "string") { - data = JSON.parse(data); - } - switch (data.type) { - case QWebChannelMessageTypes.signal: - channel.handleSignal(data); - break; - case QWebChannelMessageTypes.response: - channel.handleResponse(data); - break; - case QWebChannelMessageTypes.propertyUpdate: - channel.handlePropertyUpdate(data); - break; - default: - console.error("invalid message received:", message.data); - break; - } - } - - this.execCallbacks = {}; - this.execId = 0; - this.exec = function(data, callback) - { - if (!callback) { - // if no callback is given, send directly - channel.send(data); - return; - } - if (channel.execId === Number.MAX_VALUE) { - // wrap - channel.execId = Number.MIN_VALUE; - } - if (data.hasOwnProperty("id")) { - console.error("Cannot exec message with property id: " + JSON.stringify(data)); - return; - } - data.id = channel.execId++; - channel.execCallbacks[data.id] = callback; - channel.send(data); - }; - - this.objects = {}; - - this.handleSignal = function(message) - { - var object = channel.objects[message.object]; - if (object) { - object.signalEmitted(message.signal, message.args); - } else { - console.warn("Unhandled signal: " + message.object + "::" + message.signal); - } - } - - this.handleResponse = function(message) - { - if (!message.hasOwnProperty("id")) { - console.error("Invalid response message received: ", JSON.stringify(message)); - return; - } - channel.execCallbacks[message.id](message.data); - delete channel.execCallbacks[message.id]; - } - - this.handlePropertyUpdate = function(message) - { - message.data.forEach(data => { - var object = channel.objects[data.object]; - if (object) { - object.propertyUpdate(data.signals, data.properties); - } else { - console.warn("Unhandled property update: " + data.object + "::" + data.signal); - } - }); - channel.exec({type: QWebChannelMessageTypes.idle}); - } - - this.debug = function(message) - { - channel.send({type: QWebChannelMessageTypes.debug, data: message}); - }; - - channel.exec({type: QWebChannelMessageTypes.init}, function(data) { - for (const objectName of Object.keys(data)) { - new QObject(objectName, data[objectName], channel); - } - - // now unwrap properties, which might reference other registered objects - for (const objectName of Object.keys(channel.objects)) { - channel.objects[objectName].unwrapProperties(); - } - - if (initCallback) { - initCallback(channel); - } - channel.exec({type: QWebChannelMessageTypes.idle}); - }); -}; - -function QObject(name, data, webChannel) -{ - this.__id__ = name; - webChannel.objects[name] = this; - - // List of callbacks that get invoked upon signal emission - this.__objectSignals__ = {}; - - // Cache of all properties, updated when a notify signal is emitted - this.__propertyCache__ = {}; - - var object = this; - - // ---------------------------------------------------------------------- - - this.unwrapQObject = function(response) - { - for (const converter of webChannel.usedConverters) { - var result = converter(response); - if (result !== undefined) - return result; - } - - if (response instanceof Array) { - // support list of objects - return response.map(qobj => object.unwrapQObject(qobj)) - } - if (!(response instanceof Object)) - return response; - - if (!response["__QObject*__"] || response.id === undefined) { - var jObj = {}; - for (const propName of Object.keys(response)) { - jObj[propName] = object.unwrapQObject(response[propName]); - } - return jObj; - } - - var objectId = response.id; - if (webChannel.objects[objectId]) - return webChannel.objects[objectId]; - - if (!response.data) { - console.error("Cannot unwrap unknown QObject " + objectId + " without data."); - return; - } - - var qObject = new QObject( objectId, response.data, webChannel ); - qObject.destroyed.connect(function() { - if (webChannel.objects[objectId] === qObject) { - delete webChannel.objects[objectId]; - // reset the now deleted QObject to an empty {} object - // just assigning {} though would not have the desired effect, but the - // below also ensures all external references will see the empty map - // NOTE: this detour is necessary to workaround QTBUG-40021 - Object.keys(qObject).forEach(name => delete qObject[name]); - } - }); - // here we are already initialized, and thus must directly unwrap the properties - qObject.unwrapProperties(); - return qObject; - } - - this.unwrapProperties = function() - { - for (const propertyIdx of Object.keys(object.__propertyCache__)) { - object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); - } - } - - function addSignal(signalData, isPropertyNotifySignal) - { - var signalName = signalData[0]; - var signalIndex = signalData[1]; - object[signalName] = { - connect: function(callback) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to connect to signal " + signalName); - return; - } - - object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; - object.__objectSignals__[signalIndex].push(callback); - - // only required for "pure" signals, handled separately for properties in propertyUpdate - if (isPropertyNotifySignal) - return; - - // also note that we always get notified about the destroyed signal - if (signalName === "destroyed" || signalName === "destroyed()" || signalName === "destroyed(QObject*)") - return; - - // and otherwise we only need to be connected only once - if (object.__objectSignals__[signalIndex].length == 1) { - webChannel.exec({ - type: QWebChannelMessageTypes.connectToSignal, - object: object.__id__, - signal: signalIndex - }); - } - }, - disconnect: function(callback) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to disconnect from signal " + signalName); - return; - } - // This makes a new list. This is important because it won't interfere with - // signal processing if a disconnection happens while emittig a signal - object.__objectSignals__[signalIndex] = (object.__objectSignals__[signalIndex] || []).filter(function(c) { - return c != callback; - }); - if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { - // only required for "pure" signals, handled separately for properties in propertyUpdate - webChannel.exec({ - type: QWebChannelMessageTypes.disconnectFromSignal, - object: object.__id__, - signal: signalIndex - }); - } - } - }; - } - - /** - * Invokes all callbacks for the given signalname. Also works for property notify callbacks. - */ - function invokeSignalCallbacks(signalName, signalArgs) - { - var connections = object.__objectSignals__[signalName]; - if (connections) { - connections.forEach(function(callback) { - callback.apply(callback, signalArgs); - }); - } - } - - this.propertyUpdate = function(signals, propertyMap) - { - // update property cache - for (const propertyIndex of Object.keys(propertyMap)) { - var propertyValue = propertyMap[propertyIndex]; - object.__propertyCache__[propertyIndex] = this.unwrapQObject(propertyValue); - } - - for (const signalName of Object.keys(signals)) { - // Invoke all callbacks, as signalEmitted() does not. This ensures the - // property cache is updated before the callbacks are invoked. - invokeSignalCallbacks(signalName, signals[signalName]); - } - } - - this.signalEmitted = function(signalName, signalArgs) - { - invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs)); - } - - function addMethod(methodData) - { - var methodName = methodData[0]; - var methodIdx = methodData[1]; - - // Fully specified methods are invoked by id, others by name for host-side overload resolution - var invokedMethod = methodName[methodName.length - 1] === ')' ? methodIdx : methodName - - object[methodName] = function() { - var args = []; - var callback; - var errCallback; - for (var i = 0; i < arguments.length; ++i) { - var argument = arguments[i]; - if (typeof argument === "function") - callback = argument; - else - args.push(argument); - } - - var result; - // during test, webChannel.exec synchronously calls the callback - // therefore, the promise must be constucted before calling - // webChannel.exec to ensure the callback is set up - if (!callback && (typeof(Promise) === 'function')) { - result = new Promise(function(resolve, reject) { - callback = resolve; - errCallback = reject; - }); - } - - webChannel.exec({ - "type": QWebChannelMessageTypes.invokeMethod, - "object": object.__id__, - "method": invokedMethod, - "args": args - }, function(response) { - if (response !== undefined) { - var result = object.unwrapQObject(response); - if (callback) { - (callback)(result); - } - } else if (errCallback) { - (errCallback)(); - } - }); - - return result; - }; - } - - function bindGetterSetter(propertyInfo) - { - var propertyIndex = propertyInfo[0]; - var propertyName = propertyInfo[1]; - var notifySignalData = propertyInfo[2]; - // initialize property cache with current value - // NOTE: if this is an object, it is not directly unwrapped as it might - // reference other QObject that we do not know yet - object.__propertyCache__[propertyIndex] = propertyInfo[3]; - - if (notifySignalData) { - if (notifySignalData[0] === 1) { - // signal name is optimized away, reconstruct the actual name - notifySignalData[0] = propertyName + "Changed"; - } - addSignal(notifySignalData, true); - } - - Object.defineProperty(object, propertyName, { - configurable: true, - get: function () { - var propertyValue = object.__propertyCache__[propertyIndex]; - if (propertyValue === undefined) { - // This shouldn't happen - console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); - } - - return propertyValue; - }, - set: function(value) { - if (value === undefined) { - console.warn("Property setter for " + propertyName + " called with undefined value!"); - return; - } - object.__propertyCache__[propertyIndex] = value; - var valueToSend = value; - webChannel.exec({ - "type": QWebChannelMessageTypes.setProperty, - "object": object.__id__, - "property": propertyIndex, - "value": valueToSend - }); - } - }); - - } - - // ---------------------------------------------------------------------- - - data.methods.forEach(addMethod); - - data.properties.forEach(bindGetterSetter); - - data.signals.forEach(function(signal) { addSignal(signal, false); }); - - Object.assign(object, data.enums); -} - -QObject.prototype.toJSON = function() { - if (this.__id__ === undefined) return {}; - return { - id: this.__id__, - "__QObject*__": true - }; -}; - -//required for use with nodejs -if (typeof module === 'object') { - module.exports = { - QWebChannel: QWebChannel - }; -} diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/stream-deck.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/stream-deck.js deleted file mode 100644 index 9c74c2b..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/stream-deck.js +++ /dev/null @@ -1,295 +0,0 @@ -/// -/// - -var $localizedStrings = $localizedStrings || {}; - -/** - * @class StreamDeck - * StreamDeck object containing all required code to establish - * communication with SD-Software and the Property Inspector - */ -class ELGSDStreamDeck extends ELGSDApi { - constructor() { - super(); - if (ELGSDStreamDeck.__instance) { - return ELGSDStreamDeck.__instance; - } - - ELGSDStreamDeck.__instance = this; - } - - /** - * Display alert triangle on actions key - * @param {string} context - */ - showAlert(context) { - if (!context) { - console.error('A context is required to showAlert on the key.'); - } - - this.send(context, Events.showAlert); - } - - /** - * Display ok check mark on actions key - * @param {string} context - */ - showOk(context) { - if (!context) { - console.error('A context is required to showOk on the key.'); - } - - this.send(context, Events.showOk); - } - - /** - * Save the actions's persistent data. - * @param context - * @param {object} payload - */ - setSettings(context, payload) { - this.send(context, Events.setSettings, { - action: this?.actionInfo?.action, - payload: payload || null, - targetContext: context, - }); - } - - /** - * Request the actions's persistent data. StreamDeck does not return the data, but trigger the actions's didReceiveSettings event - * @param {string} [context] - */ - getSettings(context) { - this.send(context, Events.getSettings); - } - - /** - * Set the state of the actions - * @param {string} context - * @param {number} [state] - */ - setState(context, state) { - if (!context) { - console.error('A context is required when using setState.'); - } - - this.send(context, Events.setState, { - payload: { - state: state || 0, - }, - }); - } - - /** - * Set the title of the action's key - * @param {string} context - * @param {string} title - * @param [target] - */ - setTitle(context, title = '', target = Constants.hardwareAndSoftware) { - if (!context) { - console.error('A key context is required for setTitle.'); - } - - this.send(context, Events.setTitle, { - payload: { - title: title ? `${title}` : '', - target, - }, - }); - } - - /** - * - * @param {string} context - * @param {number} [target] - */ - clearTitle(context, target) { - if (!context) { - console.error('A key context is required to clearTitle.'); - } - - this.setTitle(context, null, target); - } - - /** - * Send payload to property inspector - * @param {string} context - * @param {object} payload - * @param {string} [action] - */ - sendToPropertyInspector(context, payload = null, action = null) { - if (typeof context != 'string') { - console.error('A key context is required to sendToPropertyInspector.'); - } - - this.send(context, Events.sendToPropertyInspector, { - action, - payload, - }); - } - - /** - * Set the actions key image - * @param {string} context - * @param {string} [image] - * @param {number} [state] - * @param {number} [target] - */ - setImage(context, image, state, target = Constants.hardwareAndSoftware) { - if (!context) { - console.error('A key context is required for setImage.'); - } - - this.send(context, Events.setImage, { - payload: { - image, - target, - state, - }, - }); - } - - /** - * Set the properties of the layout on the Stream Deck + touch display - * @param {*} context - * @param {*} payload - */ - setFeedback(context, payload) { - if (!context) { - console.error('A context is required for setFeedback.'); - } - - this.send(context, Events.setFeedback, { - payload, - }); - } - - /** - * Set the active layout by ID or path for the Stream Deck + touch display - * @param {*} context - * @param {*} layout - */ - setFeedbackLayout(context, layout) { - if (!context) { - console.error('A context is required for setFeedbackLayout.'); - } - - this.send(context, Events.setFeedbackLayout, { - payload: { layout }, - }); - } - - /** - * Registers a callback function for the deviceDidConnect event, which fires when a device is plugged in - * @param {function} fn - * @returns ELGSDStreamDeck - */ - onDeviceDidConnect(fn) { - if (!fn) { - console.error( - 'A callback function for the deviceDidConnect event is required for onDeviceDidConnect.' - ); - } - - this.on(Events.deviceDidConnect, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the deviceDidDisconnect event, which fires when a device is unplugged - * @param {function} fn - * @returns ELGSDStreamDeck - */ - onDeviceDidDisconnect(fn) { - if (!fn) { - console.error( - 'A callback function for the deviceDidDisconnect event is required for onDeviceDidDisconnect.' - ); - } - - this.on(Events.deviceDidDisconnect, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the applicationDidLaunch event, which fires when the application starts - * @param {function} fn - * @returns ELGSDStreamDeck - */ - onApplicationDidLaunch(fn) { - if (!fn) { - console.error( - 'A callback function for the applicationDidLaunch event is required for onApplicationDidLaunch.' - ); - } - - this.on(Events.applicationDidLaunch, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the applicationDidTerminate event, which fires when the application exits - * @param {function} fn - * @returns ELGSDStreamDeck - */ - onApplicationDidTerminate(fn) { - if (!fn) { - console.error( - 'A callback function for the applicationDidTerminate event is required for onApplicationDidTerminate.' - ); - } - - this.on(Events.applicationDidTerminate, (jsn) => fn(jsn)); - return this; - } - - /** - * Registers a callback function for the systemDidWakeUp event, which fires when the computer wakes - * @param {function} fn - * @returns ELGSDStreamDeck - */ - onSystemDidWakeUp(fn) { - if (!fn) { - console.error( - 'A callback function for the systemDidWakeUp event is required for onSystemDidWakeUp.' - ); - } - - this.on(Events.systemDidWakeUp, (jsn) => fn(jsn)); - return this; - } - - /** - * Switches to a readonly profile or returns to previous profile - * @param {string} device - * @param {string} [profile] - */ - switchToProfile(device, profile) { - if (!device) { - console.error('A device id is required for switchToProfile.'); - } - - this.send(this.uuid, Events.switchToProfile, { device, payload: { profile } }); - } -} - -const $SD = new ELGSDStreamDeck(); - -/** - * connectElgatoStreamDeckSocket - * This is the first function StreamDeck Software calls, when - * establishing the connection to the plugin or the Property Inspector - * @param {string} port - The socket's port to communicate with StreamDeck software. - * @param {string} uuid - A unique identifier, which StreamDeck uses to communicate with the plugin - * @param {string} messageType - Identifies, if the event is meant for the property inspector or the plugin. - * @param {string} appInfoString - Information about the host (StreamDeck) application - * @param {string} actionInfo - Context is an internal identifier used to communicate to the host application. - */ -function connectElgatoStreamDeckSocket(port, uuid, messageType, appInfoString, actionInfo) { - const delay = window?.initialConnectionDelay || 0; - - setTimeout(() => { - $SD.connect(port, uuid, messageType, appInfoString, actionInfo); - }, delay); -} diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/timers.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/timers.js deleted file mode 100644 index 5d74e36..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/timers.js +++ /dev/null @@ -1,95 +0,0 @@ -/* global ESDTimerWorker */ -/*eslint no-unused-vars: "off"*/ -/*eslint-env es6*/ - -let ESDTimerWorker = new Worker(URL.createObjectURL( - new Blob([timerFn.toString().replace(/^[^{]*{\s*/, '').replace(/\s*}[^}]*$/, '')], {type: 'text/javascript'}) -)); -ESDTimerWorker.timerId = 1; -ESDTimerWorker.timers = {}; -const ESDDefaultTimeouts = { - timeout: 0, - interval: 10 -}; - -Object.freeze(ESDDefaultTimeouts); - -function _setTimer(callback, delay, type, params) { - const id = ESDTimerWorker.timerId++; - ESDTimerWorker.timers[id] = {callback, params}; - ESDTimerWorker.onmessage = (e) => { - if (ESDTimerWorker.timers[e.data.id]) { - if (e.data.type === 'clearTimer') { - delete ESDTimerWorker.timers[e.data.id]; - } else { - const cb = ESDTimerWorker.timers[e.data.id].callback; - if (cb && typeof cb === 'function') cb(...ESDTimerWorker.timers[e.data.id].params); - } - } - }; - ESDTimerWorker.postMessage({type, id, delay}); - return id; -} - -function _setTimeoutESD(...args) { - let [callback, delay = 0, ...params] = [...args]; - return _setTimer(callback, delay, 'setTimeout', params); -} - -function _setIntervalESD(...args) { - let [callback, delay = 0, ...params] = [...args]; - return _setTimer(callback, delay, 'setInterval', params); -} - -function _clearTimeoutESD(id) { - ESDTimerWorker.postMessage({type: 'clearTimeout', id}); // ESDTimerWorker.postMessage({type: 'clearInterval', id}); = same thing - delete ESDTimerWorker.timers[id]; -} - -window.setTimeout = _setTimeoutESD; -window.setInterval = _setIntervalESD; -window.clearTimeout = _clearTimeoutESD; //timeout and interval share the same timer-pool -window.clearInterval = _clearTimeoutESD; - -/** This is our worker-code - * It is executed in it's own (global) scope - * which is wrapped above @ `let ESDTimerWorker` - */ - -function timerFn() { - /*eslint indent: ["error", 4, { "SwitchCase": 1 }]*/ - - let timers = {}; - let debug = false; - let supportedCommands = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval']; - - function log(e) { - console.log('Worker-Info::Timers', timers); - } - - function clearTimerAndRemove(id) { - if (timers[id]) { - if (debug) console.log('clearTimerAndRemove', id, timers[id], timers); - clearTimeout(timers[id]); - delete timers[id]; - postMessage({type: 'clearTimer', id: id}); - if (debug) log(); - } - } - - onmessage = function (e) { - // first see, if we have a timer with this id and remove it - // this automatically fulfils clearTimeout and clearInterval - supportedCommands.includes(e.data.type) && timers[e.data.id] && clearTimerAndRemove(e.data.id); - if (e.data.type === 'setTimeout') { - timers[e.data.id] = setTimeout(() => { - postMessage({id: e.data.id}); - clearTimerAndRemove(e.data.id); //cleaning up - }, Math.max(e.data.delay || 0)); - } else if (e.data.type === 'setInterval') { - timers[e.data.id] = setInterval(() => { - postMessage({id: e.data.id}); - }, Math.max(e.data.delay || ESDDefaultTimeouts.interval)); - } - }; -} diff --git a/co.meldstudio.streamdeck.sdPlugin/libs/js/utils.js b/co.meldstudio.streamdeck.sdPlugin/libs/js/utils.js deleted file mode 100644 index a7b84cc..0000000 --- a/co.meldstudio.streamdeck.sdPlugin/libs/js/utils.js +++ /dev/null @@ -1,94 +0,0 @@ -class Utils { - /** - * Returns the value from a form using the form controls name property - * @param {Element | string} form - * @returns - */ - static getFormValue(form) { - if (typeof form === 'string') { - form = document.querySelector(form); - } - - const elements = form?.elements; - - if (!elements) { - console.error('Could not find form!'); - } - - const formData = new FormData(form); - let formValue = {}; - - formData.forEach((value, key) => { - if (!Reflect.has(formValue, key)) { - formValue[key] = value; - return; - } - if (!Array.isArray(formValue[key])) { - formValue[key] = [formValue[key]]; - } - formValue[key].push(value); - }); - - return formValue; - } - - /** - * Sets the value of form controls using their name attribute and the jsn object key - * @param {*} jsn - * @param {Element | string} form - */ - static setFormValue(jsn, form) { - if (!jsn) { - return; - } - - if (typeof form === 'string') { - form = document.querySelector(form); - } - - const elements = form?.elements; - - if (!elements) { - console.error('Could not find form!'); - } - - Array.from(elements) - .filter((element) => element?.name) - .forEach((element) => { - const { name, type } = element; - const value = name in jsn ? jsn[name] : null; - const isCheckOrRadio = type === 'checkbox' || type === 'radio'; - - if (value === null) return; - - if (isCheckOrRadio) { - const isSingle = value === element.value; - if (isSingle || (Array.isArray(value) && value.includes(element.value))) { - element.checked = true; - } - } else { - element.value = value ?? ''; - } - }); - } - - /** - * This provides a slight delay before processing rapid events - * @param {number} wait - delay before processing function (recommended time 150ms) - * @param {function} fn - * @returns - */ - static debounce(wait, fn) { - let timeoutId = null; - return (...args) => { - window.clearTimeout(timeoutId); - timeoutId = window.setTimeout(() => { - fn.apply(null, args); - }, wait); - }; - } - - static delay(wait) { - return new Promise((fn) => setTimeout(fn, wait)); - } -} diff --git a/co.meldstudio.streamdeck.sdPlugin/manifest.json b/co.meldstudio.streamdeck.sdPlugin/manifest.json index 8abe653..3738e9f 100644 --- a/co.meldstudio.streamdeck.sdPlugin/manifest.json +++ b/co.meldstudio.streamdeck.sdPlugin/manifest.json @@ -1,14 +1,17 @@ { - "SDKVersion": 2, + "SDKVersion": 3, "Category": "Meld Studio", "CategoryIcon": "assets/Category Icon/Meld Studio", "Name": "Meld Studio", "Icon": "assets/AppIcon/meldstudio", "Author": "MeldStudio", - "CodePath": "app.html", + "CodePath": "bin/plugin.js", + "Nodejs": { + "Version": "24" + }, "Description": "Meld Studio 💙 Stream Deck. Take your stream to new heights with powerful new capabilities at your fingertips. Trigger transitions, manage audio tracks, toggle filter effects on and off, and much more.", "URL": "https://meldstudio.co", - "Version": "0.6.2.1", + "Version": "0.6.4.1", "UUID": "co.meldstudio.streamdeck", "OS": [ { @@ -21,7 +24,7 @@ } ], "Software": { - "MinimumVersion": "6.4" + "MinimumVersion": "7.1" }, "Actions": [ { @@ -29,16 +32,17 @@ "Name": "Change Scene", "States": [ { - "Image": "assets/Scenes/Change to Scene/Key Icon/sceneOff" + "Image": "assets/Scenes/Change to Scene/Key Icon/sceneOn" }, { - "Image": "assets/Scenes/Change to Scene/Key Icon/sceneOn" + "Image": "assets/Scenes/Change to Scene/Key Icon/sceneOff" } ], "SupportedInMultiActions": true, + "DisableAutomaticStates": true, "Tooltip": "Change scenes!", "UUID": "co.meldstudio.streamdeck.show-scene", - "PropertyInspectorPath": "actions/show-scene/inspector.html" + "PropertyInspectorPath": "ui/show-scene.html" }, { "Icon": "assets/Screenshot/iconScreenshot", @@ -51,7 +55,7 @@ "SupportedInMultiActions": true, "Tooltip": "Take a screenshot!", "UUID": "co.meldstudio.streamdeck.screenshot", - "PropertyInspectorPath": "actions/screenshot/inspector.html" + "PropertyInspectorPath": "ui/screenshot.html" }, { "Icon": "assets/Clip/action icon/clip-action", @@ -62,41 +66,43 @@ } ], "SupportedInMultiActions": true, - "Tooltip": "Record a 90 second clip to disk!", + "Tooltip": "Record a configurable-duration clip to disk!", "UUID": "co.meldstudio.streamdeck.clip", - "PropertyInspectorPath": "actions/clip/inspector.html" + "PropertyInspectorPath": "ui/clip.html" }, { "Icon": "assets/Audio Track/Mute_Unmute Audio Track/Action Icons/audioTrack", "Name": "Mute Track", "States": [ { - "Image": "assets/Audio Track/Mute_Unmute Audio Track/Key Icons/mutedTrack" + "Image": "assets/Audio Track/Mute_Unmute Audio Track/Key Icons/audio-unmuted" }, { - "Image": "assets/Audio Track/Mute_Unmute Audio Track/Key Icons/audio-unmuted" + "Image": "assets/Audio Track/Mute_Unmute Audio Track/Key Icons/mutedTrack" } ], "SupportedInMultiActions": true, + "DisableAutomaticStates": true, "Tooltip": "Mute an audio track!", "UUID": "co.meldstudio.streamdeck.toggle-mute", - "PropertyInspectorPath": "actions/toggle-mute/inspector.html" + "PropertyInspectorPath": "ui/toggle-mute.html" }, { "Icon": "assets/Cue/action icon/cueAudio", "Name": "Cue Track", "States": [ { - "Image": "assets/Cue/key icon/cueOff" + "Image": "assets/Cue/key icon/cueOn" }, { - "Image": "assets/Cue/key icon/cueOn" + "Image": "assets/Cue/key icon/cueOff" } ], "SupportedInMultiActions": true, + "DisableAutomaticStates": true, "Tooltip": "Monitor an audio track!", "UUID": "co.meldstudio.streamdeck.toggle-monitor", - "PropertyInspectorPath": "actions/toggle-monitor/inspector.html" + "PropertyInspectorPath": "ui/toggle-monitor.html" }, { "Icon": "assets/Audio Track/Mute_Unmute Audio Track/Action Icons/audioTrack", @@ -120,7 +126,7 @@ }, "Tooltip": "Adjust the volume of a specified audio track.", "UUID": "co.meldstudio.streamdeck.volume-stepper", - "PropertyInspectorPath": "actions/volume-stepper/inspector.html" + "PropertyInspectorPath": "ui/volume-stepper.html" }, { "Icon": "assets/Scene Transition Control/Action Icon/sceneTransition", @@ -144,71 +150,75 @@ }, "Tooltip": "Select a staging scene and trigger a scene transition", "UUID": "co.meldstudio.streamdeck.transition-stepper", - "PropertyInspectorPath": "actions/transition-stepper/inspector.html" + "PropertyInspectorPath": "ui/transition-stepper.html" }, { "Icon": "assets/Layers/Layer Visibility/Action Icon/iconLayerVisibility", "Name": "Layer Visibility", "States": [ { - "Image": "assets/Layers/Layer Visibility/Key Icon/layerHide" + "Image": "assets/Layers/Layer Visibility/Key Icon/layerShow" }, { - "Image": "assets/Layers/Layer Visibility/Key Icon/layerShow" + "Image": "assets/Layers/Layer Visibility/Key Icon/layerHide" } ], "SupportedInMultiActions": true, + "DisableAutomaticStates": true, "Tooltip": "Toggle layer visibility!", "UUID": "co.meldstudio.streamdeck.toggle-layer", - "PropertyInspectorPath": "actions/toggle-layer/inspector.html" + "PropertyInspectorPath": "ui/toggle-layer.html" }, { "Icon": "assets/Effects/Action Icon/iconEffectVisibility", "Name": "Effect Visibility", "States": [ { - "Image": "assets/Effects/Key Icons/hideEffect" + "Image": "assets/Effects/Key Icons/showEffect" }, { - "Image": "assets/Effects/Key Icons/showEffect" + "Image": "assets/Effects/Key Icons/hideEffect" } ], "SupportedInMultiActions": true, + "DisableAutomaticStates": true, "Tooltip": "Toggle effect visibility!", "UUID": "co.meldstudio.streamdeck.toggle-effect", - "PropertyInspectorPath": "actions/toggle-effect/inspector.html" + "PropertyInspectorPath": "ui/toggle-effect.html" }, { "Icon": "assets/Record/Action Icon/iconRecord", "Name": "Record", "States": [ { - "Image": "assets/Record/Key Icon/startRecord" + "Image": "assets/Record/Key Icon/stopRecord" }, { - "Image": "assets/Record/Key Icon/stopRecord" + "Image": "assets/Record/Key Icon/startRecord" } ], "SupportedInMultiActions": true, + "DisableAutomaticStates": true, "Tooltip": "Start or stop recording.", "UUID": "co.meldstudio.streamdeck.toggle-record", - "PropertyInspectorPath": "actions/toggle-record/inspector.html" + "PropertyInspectorPath": "ui/toggle-record.html" }, { "Icon": "assets/Go live/Action Icons/goLive", "Name": "Go Live", "States": [ { - "Image": "assets/Go live/Key Icons/goLive" + "Image": "assets/Go live/Key Icons/goOffiline" }, { - "Image": "assets/Go live/Key Icons/goOffiline" + "Image": "assets/Go live/Key Icons/goLive" } ], "SupportedInMultiActions": true, + "DisableAutomaticStates": true, "Tooltip": "Go Live or go offline.", "UUID": "co.meldstudio.streamdeck.toggle-stream", - "PropertyInspectorPath": "actions/toggle-stream/inspector.html" + "PropertyInspectorPath": "ui/toggle-stream.html" }, { "Icon": "assets/Replay/Action Icon/replay-clip-controls", @@ -221,7 +231,7 @@ "SupportedInMultiActions": true, "Tooltip": "Control replay clip audio and display.", "UUID": "co.meldstudio.streamdeck.replay", - "PropertyInspectorPath": "actions/replay/inspector.html" + "PropertyInspectorPath": "ui/replay.html" }, { "Icon": "assets/Media/Action Icon/media-controls-action", @@ -234,7 +244,7 @@ "SupportedInMultiActions": true, "Tooltip": "Control media layer playback.", "UUID": "co.meldstudio.streamdeck.media", - "PropertyInspectorPath": "actions/media/inspector.html" + "PropertyInspectorPath": "ui/media.html" }, { "Icon": "assets/Widget/Action Icon/widget-controls-action", @@ -247,7 +257,7 @@ "SupportedInMultiActions": true, "Tooltip": "Trigger events for Meld Studio widgets.", "UUID": "co.meldstudio.streamdeck.widget-control", - "PropertyInspectorPath": "actions/widget-control/inspector.html" + "PropertyInspectorPath": "ui/widget-control.html" } ] -} \ No newline at end of file +} diff --git a/co.meldstudio.streamdeck.sdPlugin/ui/clip.html b/co.meldstudio.streamdeck.sdPlugin/ui/clip.html new file mode 100644 index 0000000..a303061 --- /dev/null +++ b/co.meldstudio.streamdeck.sdPlugin/ui/clip.html @@ -0,0 +1,48 @@ + + + + + + + co.meldstudio.streamdeck.clip Property Inspector + + + + +
+
+
Seconds
+ +
+ +
+ + + + + + diff --git a/co.meldstudio.streamdeck.sdPlugin/ui/lib/meld-pi.js b/co.meldstudio.streamdeck.sdPlugin/ui/lib/meld-pi.js new file mode 100644 index 0000000..68b88c2 --- /dev/null +++ b/co.meldstudio.streamdeck.sdPlugin/ui/lib/meld-pi.js @@ -0,0 +1,298 @@ +// Copyright (c) 2024 Meld Studio, Inc. +// Licensed under the MIT license. See LICENSE file in the project root for details. +// +// Thin property-inspector runtime. Unlike the old inspectors, this NEVER opens a +// connection to Meld Studio — it speaks only the Stream Deck property-inspector +// protocol and asks the plugin for session data. The plugin (the single source +// of truth) answers and pushes live updates, so dropdowns stay in sync with the +// keys without a second, drifting connection. + +(function () { + let ws; + let uuid; + let actionInfo = {}; + let currentSettings = {}; + let session = { items: {}, connected: false }; + + const settingsListeners = []; + const sessionListeners = []; + const readyListeners = []; + + /** Entry point invoked by the Stream Deck application. */ + function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, _inInfo, inActionInfo) { + uuid = inUUID; + try { + actionInfo = inActionInfo ? JSON.parse(inActionInfo) : {}; + } catch { + actionInfo = {}; + } + currentSettings = (actionInfo && actionInfo.payload && actionInfo.payload.settings) || {}; + + ws = new WebSocket("ws://127.0.0.1:" + inPort); + + ws.onopen = function () { + ws.send(JSON.stringify({ event: inRegisterEvent, uuid: inUUID })); + readyListeners.forEach((fn) => fn()); + MeldPI.requestSession(); + }; + + ws.onmessage = function (event) { + let msg; + try { + msg = JSON.parse(event.data); + } catch { + return; + } + + if (msg.event === "didReceiveSettings") { + currentSettings = (msg.payload && msg.payload.settings) || {}; + settingsListeners.forEach((fn) => fn(currentSettings)); + } else if (msg.event === "sendToPropertyInspector") { + const payload = msg.payload || {}; + if (payload.event === "meld.session") { + session = { items: payload.items || {}, connected: !!payload.connected }; + sessionListeners.forEach((fn) => fn(session)); + } + } + }; + } + + /** Builds an `