├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
├── release.yaml
└── workflows
│ └── release.yaml
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc.js
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── LICENSE
├── README.md
├── index.html
├── main.js
├── networkInterfaces.html
├── networkInterfaces.js
├── package-lock.json
├── package.json
├── plugins
├── artnet
│ ├── icon.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── atem
│ ├── icon.png
│ ├── img
│ │ ├── button_green.png
│ │ ├── button_off.png
│ │ ├── button_red.png
│ │ ├── button_white.png
│ │ ├── button_yellow.png
│ │ ├── tbar_bg.png
│ │ └── tbar_handle.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── digico
│ ├── fader.ejs
│ ├── icon.afphoto
│ ├── icon.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── eos
│ ├── cue.ejs
│ ├── cue.js
│ ├── icon.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── lightfactory
│ ├── icon.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── livestream-studio
│ ├── icon.png
│ ├── img
│ │ ├── tbar_bg.png
│ │ └── tbar_handle.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── pjlink
│ ├── icon.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── posistagenet
│ ├── icon.afphoto
│ ├── icon.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── qlab
│ ├── cart.ejs
│ ├── cue.ejs
│ ├── cuelist.ejs
│ ├── icon.png
│ ├── img
│ │ ├── arm.png
│ │ ├── arrow-down.png
│ │ ├── arrow-right.png
│ │ ├── audio.png
│ │ ├── auto_continue.png
│ │ ├── auto_continue_stubby.png
│ │ ├── auto_follow.png
│ │ ├── camera.png
│ │ ├── devamp.png
│ │ ├── disarm.png
│ │ ├── disarmed-pattern-light.png
│ │ ├── disarmed-pattern-light.tiff
│ │ ├── empty.png
│ │ ├── fade.png
│ │ ├── goto.png
│ │ ├── group-arrow.png
│ │ ├── group.png
│ │ ├── light.png
│ │ ├── load.png
│ │ ├── memo.png
│ │ ├── mic.png
│ │ ├── midi-file.png
│ │ ├── midi.png
│ │ ├── network.png
│ │ ├── new_group_arrow.png
│ │ ├── pause.png
│ │ ├── pause_circled.png
│ │ ├── play_circled.png
│ │ ├── playhead.afdesign
│ │ ├── playhead.png
│ │ ├── reset.png
│ │ ├── script.png
│ │ ├── start.png
│ │ ├── status_broken.png
│ │ ├── status_broken_white.png
│ │ ├── status_flagged.png
│ │ ├── status_loaded.png
│ │ ├── status_paused.png
│ │ ├── status_running.png
│ │ ├── status_spinner.gif
│ │ ├── status_spinner.psd
│ │ ├── stop.png
│ │ ├── target.png
│ │ ├── text.png
│ │ ├── timecode.png
│ │ ├── v5
│ │ │ ├── group-1.png
│ │ │ ├── group-2.png
│ │ │ ├── group-3.png
│ │ │ ├── group-4.png
│ │ │ └── group-6.png
│ │ ├── video.png
│ │ └── wait.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ ├── template.ejs
│ └── tile.ejs
├── sacn
│ ├── icon.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── shure
│ ├── channel.js
│ ├── icon.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── watchout
│ ├── icon.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── x32
│ ├── icon.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
└── xair
│ ├── icon.png
│ ├── info.html
│ ├── main.js
│ ├── styles.css
│ └── template.ejs
├── preload.js
└── src
├── assets
├── css
│ ├── index.css
│ └── plugin_default.css
├── font
│ └── MaterialIcons-Regular.ttf
└── img
│ ├── icon.icns
│ ├── icon.ico
│ ├── icon.png
│ ├── outline_add_box_white_18dp.png
│ ├── outline_add_white_18dp.png
│ ├── outline_broken_image_white_18dp.png
│ ├── outline_clear_white_18dp.png
│ ├── outline_done_white_18dp.png
│ ├── outline_info_white_18dp.png
│ ├── outline_link_white_18dp.png
│ ├── outline_push_pin_white_18dp.png
│ ├── outline_refresh_white_18dp.png
│ └── outline_search_white_18dp.png
├── device.js
├── index.js
├── plugins.js
├── saveSlots.js
├── search.js
└── view.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | commonjs: true,
5 | es2021: true,
6 | node: true,
7 | },
8 | extends: ['airbnb-base', 'prettier'],
9 | parserOptions: {
10 | ecmaVersion: 13,
11 | },
12 | rules: {
13 | 'import/no-extraneous-dependencies': 0,
14 | 'no-unused-vars': ['error', { args: 'none' }],
15 | 'no-console': 'off',
16 | 'no-alert': 'off',
17 | 'no-plusplus': 'off',
18 | 'import/extensions': 'off',
19 | 'prefer-destructuring': 'off',
20 | 'no-use-before-define': 'off',
21 | 'space-infix-ops': 'warn',
22 | 'no-bitwise': 'off',
23 | 'no-restricted-globals': 'off',
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Let us know what isn't working
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. Windows 10]
28 | - Version [e.g. 22]
29 |
30 | **Additional context**
31 | Add any other context about the problem here.
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'npm'
4 | directory: '/'
5 | schedule:
6 | interval: 'weekly'
7 | - package-ecosystem: 'github-actions'
8 | directory: '/'
9 | schedule:
10 | interval: 'weekly'
11 |
--------------------------------------------------------------------------------
/.github/release.yaml:
--------------------------------------------------------------------------------
1 | changelog:
2 | exclude:
3 | authors:
4 | - dependabot
5 | categories:
6 | - title: Plugins 🖥️
7 | labels:
8 | - plugin
9 | - title: Interface
10 | labels:
11 | - interface
12 | - title: Other Changes
13 | labels:
14 | - '*'
15 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | workflow_dispatch:
4 |
5 | env:
6 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7 |
8 | jobs:
9 | build-linux:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: 20
16 | cache: 'npm'
17 | - run: npm ci
18 | - run: npm run release
19 | build-windows:
20 | runs-on: windows-latest
21 | steps:
22 | - uses: actions/checkout@v4
23 | - uses: actions/setup-node@v4
24 | with:
25 | node-version: 20
26 | cache: 'npm'
27 | - run: npm ci
28 | - run: npm run release
29 | build-macos:
30 | runs-on: macos-13
31 | env:
32 | CSC_LINK: ${{ secrets.MACOS_CSC_LINK }}
33 | CSC_KEY_PASSWORD: ${{ secrets.MACOS_CSC_KEY_PASSWORD }}
34 | APPLE_ID: ${{ secrets.APPLE_ID }}
35 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
36 | APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
37 | steps:
38 | - uses: actions/checkout@v4
39 | - uses: actions/setup-node@v4
40 | with:
41 | node-version: 20
42 | cache: 'npm'
43 | - run: npm ci
44 | - run: npm run release
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.12.1
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | out
3 | dist
4 | *.ejs
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'es5',
3 | tabWidth: 2,
4 | semi: true,
5 | singleQuote: true,
6 | bracketSameLine: true,
7 | printWidth: 120,
8 | };
9 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "node",
6 | "request": "launch",
7 | "name": "Electron: Main",
8 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
9 | "runtimeArgs": ["--remote-debugging-port=9223", "."],
10 | "windows": {
11 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
12 | }
13 | },
14 | {
15 | "name": "Electron: Renderer",
16 | "type": "chrome",
17 | "request": "attach",
18 | "port": 9223,
19 | "webRoot": "${workspaceFolder}",
20 | "timeout": 30000
21 | }
22 | ],
23 | "compounds": [
24 | {
25 | "name": "Electron: All",
26 | "configurations": ["Electron: Main", "Electron: Renderer"]
27 | }
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "editor.formatOnSave": true,
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll.eslint": "explicit"
6 | },
7 | "eslint.validate": ["javascript"],
8 | "html.validate.styles": false // accept templates inside HTML style attributes
9 | }
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
Cue View
2 |
3 | A dashboard for everything in your show.
4 |
5 |
6 |
7 | ## Features
8 |
9 | - Tons of supported equipment
10 | - Auto discover devices on the network
11 | - Live updating
12 | - Configurable layout
13 |
14 | ## Supported Devices
15 |
16 | - QLab 4 & 5
17 | - ETC Eos Consoles
18 | - Watchout
19 | - PJLink Projectors
20 | - X32 Audio Consoles
21 | - XAir Audio Consoles
22 | - Art-Net Universes
23 | - sACN Universes
24 | - ATEM Video Mixers
25 | - Shure ULXD Wireless
26 | - DiGico SD Consoles
27 | - PosiStageNet
28 | - Livestream Studio
29 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Cue View
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Click the Search button to find devices on the network.
19 |
20 |
21 |
22 |
34 |
35 |
36 |
81 |
82 |
No Device Selected
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/networkInterfaces.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Network Information
4 |
76 |
77 |
78 | Attached Network Interfaces:
79 |
80 |
81 |
82 | ID |
83 | IP Address |
84 | Subnet Mask |
85 | Searchable Address Ranges |
86 |
87 |
88 |
89 |
90 |
93 |
--------------------------------------------------------------------------------
/networkInterfaces.js:
--------------------------------------------------------------------------------
1 | const SEARCH = require('./src/search.js');
2 |
3 | window.init = function init() {
4 | const networkInterfaces = SEARCH.getNetworkInterfaces();
5 | let html = '';
6 |
7 | for (let i = 0; i < Object.keys(networkInterfaces).length; i++) {
8 | const interfaceID = Object.keys(networkInterfaces)[i];
9 | const interfaceObj = networkInterfaces[interfaceID];
10 |
11 | html += `
12 |
13 | ${interfaceID} |
14 | ${interfaceObj[0].address} |
15 | ${interfaceObj[0].netmask} |
16 | `;
17 |
18 | if (interfaceObj[0].searchTruncated) {
19 | html += ``;
20 | } else {
21 | html += ` | `;
22 | }
23 |
24 | html += `
25 | ${interfaceObj[0].firstSearchAddress} - ${interfaceObj[0].lastSearchAddress} |
26 |
27 | `;
28 |
29 | document.getElementById('network-interfaces').innerHTML = html;
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cue-view",
3 | "productName": "Cue View",
4 | "version": "1.2.2",
5 | "description": "A dashboard for everything in your show",
6 | "main": "main.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "pretty": "prettier --write **/*",
10 | "start": "electron .",
11 | "build": "electron-builder -m -w -l",
12 | "build:mac": "electron-builder -m",
13 | "build:windows": "electron-builder -w",
14 | "build:linux": "electron-builder -l",
15 | "release": "electron-builder"
16 | },
17 | "author": "alec@stagehacks.com",
18 | "license": "CC BY-SA 4.0",
19 | "homepage": "https://github.com/stagehacks/Cue-View",
20 | "repository": "https://github.com/stagehacks/Cue-View",
21 | "devDependencies": {
22 | "@electron/notarize": "^2.5.0",
23 | "electron": "^34.2.0",
24 | "electron-builder": "^25.1.8",
25 | "eslint": "^8.57.0",
26 | "eslint-config-airbnb-base": "^15.0.0",
27 | "eslint-config-prettier": "^9.1.0",
28 | "eslint-plugin-import": "^2.31.0",
29 | "prettier": "^3.4.2"
30 | },
31 | "dependencies": {
32 | "@jwetzell/posistagenet": "^1.1.0",
33 | "atem-connection": "^3.5.0",
34 | "bonjour": "^3.5.0",
35 | "electron-updater": "^6.3.9",
36 | "lodash": "^4.17.21",
37 | "md5": "^2.3.0",
38 | "netmask": "^2.0.2",
39 | "osc": "^2.4.5",
40 | "uuid": "^11.0.5"
41 | },
42 | "build": {
43 | "appId": "com.stagehacks.cueview",
44 | "icon": "src/assets/img/",
45 | "artifactName": "${name}.${os}-${arch}.v${version}.${ext}",
46 | "npmRebuild": false,
47 | "mac": {
48 | "category": "Utilities",
49 | "icon": "src/assets/img/icon.icns",
50 | "hardenedRuntime": true,
51 | "electronLanguages": [
52 | "en"
53 | ],
54 | "target": [
55 | {
56 | "target": "zip",
57 | "arch": [
58 | "x64",
59 | "arm64"
60 | ]
61 | }
62 | ],
63 | "publish": [
64 | "github"
65 | ]
66 | },
67 | "win": {
68 | "target": "NSIS",
69 | "icon": "src/assets/img/icon.ico",
70 | "publish": [
71 | "github"
72 | ]
73 | },
74 | "nsis": {
75 | "oneClick": false
76 | },
77 | "linux": {
78 | "target": [
79 | {
80 | "target": "AppImage",
81 | "arch": [
82 | "x64",
83 | "arm64",
84 | "armv7l"
85 | ]
86 | }
87 | ],
88 | "maintainer": "alec@stagehacks.com",
89 | "category": "Utility",
90 | "publish": [
91 | "github"
92 | ]
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/plugins/artnet/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/artnet/icon.png
--------------------------------------------------------------------------------
/plugins/artnet/info.html:
--------------------------------------------------------------------------------
1 | Quit any other applications that use Art-Net before using this plugin.
2 | Art-Net™ Designed by and Copyright Artistic Licence Holdings Ltd
3 |
--------------------------------------------------------------------------------
/plugins/artnet/main.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | defaultName: 'Art-Net',
3 | connectionType: 'UDPsocket',
4 | remotePort: 6454,
5 | mayChangePorts: false,
6 | heartbeatInterval: 5000,
7 | heartbeatTimeout: 15000,
8 | searchOptions: {
9 | type: 'UDPsocket',
10 | searchBuffer: Buffer.from([0x00]),
11 | devicePort: 6454,
12 | listenPort: 6454,
13 | validateResponse(msg, info, devices) {
14 | return msg.toString('utf8', 0, 7) === 'Art-Net';
15 | },
16 | },
17 | };
18 |
19 | exports.ready = function ready(_device) {
20 | const device = _device;
21 | device.data.universes = {};
22 | device.data.orderedUniverses = [];
23 | };
24 |
25 | exports.data = function data(_device, buf) {
26 | const device = _device;
27 |
28 | if (buf.length < 18 || buf.slice(0, 7).toString() !== 'Art-Net') {
29 | return;
30 | }
31 |
32 | const opCode = buf.readUInt16BE(8);
33 |
34 | if (opCode === 33) {
35 | sendArtPollReply(device);
36 | return;
37 | }
38 |
39 | const universeIndex = buf.readUInt8(14);
40 |
41 | let universe = device.data.universes[universeIndex];
42 |
43 | if (!universe) {
44 | device.data.universes[universeIndex] = {};
45 | universe = device.data.universes[universeIndex];
46 | }
47 |
48 | universe.sequence = buf.readUInt8(12);
49 | universe.subnet = buf.readUInt8(15);
50 | universe.opCode = buf.readUInt8(9);
51 | universe.version = buf.readUInt16BE(10);
52 | universe.slots = buf.slice(18);
53 | device.data.ip = device.addresses[0];
54 |
55 | if (!device.data.orderedUniverses.includes(universeIndex)) {
56 | device.data.orderedUniverses.push(universeIndex);
57 | device.data.orderedUniverses.sort();
58 | universe.slotElems = [];
59 | universe.slotElemsSet = false;
60 |
61 | device.draw();
62 | device.update('elementCache');
63 | }
64 |
65 | device.update('universeData', {
66 | universeIndex,
67 | universe,
68 | });
69 | };
70 |
71 | exports.heartbeat = function heartbeat(device) {};
72 |
73 | let lastUpdate = Date.now();
74 | exports.update = function update(_device, _document, updateType, updateData) {
75 | const device = _device;
76 | const data = updateData;
77 | const document = _document;
78 |
79 | if (updateType === 'universeData' && data.universe) {
80 | if (Date.now() - lastUpdate > 1000) {
81 | lastUpdate = Date.now();
82 | device.update('elementCache');
83 | }
84 |
85 | const $elem = document.getElementById(`universe-${data.universeIndex}`);
86 |
87 | if ($elem && data.universe.slotElemsSet) {
88 | for (let i = 0; i < 512; i++) {
89 | data.universe.slotElems[i].textContent = data.universe.slots[i];
90 | }
91 |
92 | document.getElementById(`universe-${data.universeIndex}-sequence`).textContent = data.universe.sequence;
93 | } else {
94 | device.draw();
95 | device.update('elementCache');
96 | }
97 | } else if (updateType === 'elementCache') {
98 | device.data.orderedUniverses.forEach((universeIndex) => {
99 | for (let i = 0; i < 512; i++) {
100 | device.data.universes[universeIndex].slotElems[i] = document.getElementById(`${universeIndex}-${i}`);
101 | }
102 | device.data.universes[universeIndex].slotElemsSet = true;
103 | });
104 | }
105 | };
106 |
107 | function sendArtPollReply(device) {
108 | // minimum viable Art-Net packet
109 | // not to full Art-Net spec
110 | // https://art-net.org.uk/how-it-works/discovery-packets/artpollreply/
111 |
112 | const interfaces = device.getNetworkInterfaces();
113 |
114 | for (let i = 0; i < Object.keys(interfaces).length; i++) {
115 | const buf = Buffer.alloc(213);
116 | buf.write('Art-Net', 0);
117 |
118 | buf.writeInt16LE(0x2100, 8);
119 |
120 | const addr = interfaces[Object.keys(interfaces)[i]][0].address.split('.');
121 |
122 | buf.writeUInt8(addr[0], 10);
123 | buf.writeUInt8(addr[1], 11);
124 | buf.writeUInt8(addr[2], 12);
125 | buf.writeUInt8(addr[3], 13);
126 |
127 | buf.writeInt16LE(6454, 14);
128 |
129 | buf.write('Cue View', 26);
130 |
131 | buf.writeUInt8(4, 173);
132 |
133 | buf.writeUInt8(0xc0, 174);
134 | buf.writeUInt8(0xc0, 175);
135 | buf.writeUInt8(0xc0, 176);
136 | buf.writeUInt8(0xc0, 177);
137 |
138 | buf.writeUInt8(0x80, 178);
139 | buf.writeUInt8(0x80, 179);
140 | buf.writeUInt8(0x80, 180);
141 | buf.writeUInt8(0x80, 181);
142 |
143 | buf.writeUInt8(0x80, 182);
144 | buf.writeUInt8(0x80, 183);
145 | buf.writeUInt8(0x80, 184);
146 | buf.writeUInt8(0x80, 185);
147 |
148 | buf.writeUInt8(0x00, 186);
149 | buf.writeUInt8(0x01, 187);
150 | buf.writeUInt8(0x02, 188);
151 | buf.writeUInt8(0x03, 189);
152 |
153 | device.send(buf);
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/plugins/artnet/styles.css:
--------------------------------------------------------------------------------
1 | table {
2 | margin-bottom: 50px;
3 | background-color: #333;
4 | }
5 | td,
6 | th {
7 | width: 35px;
8 | }
9 | td {
10 | background-color: black;
11 | text-align: center;
12 | border-radius: 3px;
13 | color: white;
14 | font-size: 13px;
15 | }
16 | th {
17 | background-color: #222;
18 | color: #ccc;
19 | font-size: 11px;
20 | }
21 | td.data {
22 | padding: 7px;
23 | font-size: 12px;
24 | text-align: left;
25 | }
26 | td.data em {
27 | font-size: 14px;
28 | color: #008bd2;
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/artnet/template.ejs:
--------------------------------------------------------------------------------
1 |
2 | <%= data.source %> Art-Net
3 |
4 |
5 |
6 |
7 | <% data.orderedUniverses.forEach(universeIndex => {
8 | let universe = data.universes[universeIndex];
9 | %>
10 |
11 |
12 |
13 |
14 | Universe
15 | <%= universeIndex %>
16 | |
17 |
18 | Sub-Net
19 | <%= universe.subnet %>
20 | |
21 |
22 | Sequence
23 | <%= universe.sequence %>
24 | |
25 |
26 | OpCode
27 | <%= universe.opCode %>
28 | |
29 |
30 | Version
31 | <%= universe.version %>
32 | |
33 |
34 | IP
35 | <%= data.ip %>
36 | |
37 | |
38 |
39 |
40 |
41 | |
42 | <% for(col=1; col<=16; col++){ %>
43 | <%= col %> |
44 | <% } %>
45 |
46 |
47 | <% let slot = 0; %>
48 | <% for(row=0; row<32; row++){ %>
49 |
50 | <%= row*16+1 %> |
51 | <% for(col=0; col<16; col++){ %>
52 | <% slot++%>
53 | <%= universe.slots[slot-1] %> |
54 | <% } %>
55 |
56 | <% } %>
57 |
58 | <% }); %>
59 |
--------------------------------------------------------------------------------
/plugins/atem/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/icon.png
--------------------------------------------------------------------------------
/plugins/atem/img/button_green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/button_green.png
--------------------------------------------------------------------------------
/plugins/atem/img/button_off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/button_off.png
--------------------------------------------------------------------------------
/plugins/atem/img/button_red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/button_red.png
--------------------------------------------------------------------------------
/plugins/atem/img/button_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/button_white.png
--------------------------------------------------------------------------------
/plugins/atem/img/button_yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/button_yellow.png
--------------------------------------------------------------------------------
/plugins/atem/img/tbar_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/tbar_bg.png
--------------------------------------------------------------------------------
/plugins/atem/img/tbar_handle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/tbar_handle.png
--------------------------------------------------------------------------------
/plugins/atem/info.html:
--------------------------------------------------------------------------------
1 | ATEM Configuration
2 |
3 | - Set the IP Address, Subnet Mask, and Gateway of the ATEM using the ATEM Setup utility
4 | - The ATEM must be connected to a computer using a USB cable to change its Network settings
5 |
6 |
7 | ATEM Software Download
8 |
11 | (Look for ATEM Switchers Update)
12 |
--------------------------------------------------------------------------------
/plugins/atem/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #282828;
3 | }
4 | h1 {
5 | font-family: 'Open Sans', sans-serif;
6 | }
7 | h3 {
8 | color: #6d6d6d !important;
9 | margin: 30px 0px 10px 9px;
10 | font-size: 0.9em;
11 | font-weight: 400;
12 | font-family: 'Open Sans', sans-serif;
13 | }
14 |
15 | .me-label {
16 | margin-top: 0;
17 | margin-bottom: 2px;
18 | }
19 |
20 | .atem-input {
21 | width: 52px;
22 | height: 52px;
23 | background-image: url('img/button_white.png');
24 | }
25 |
26 | .source-wrapper {
27 | display: flex;
28 | flex-wrap: wrap;
29 | background-color: #1f1f1f;
30 | border: #1a1a1a 2px solid;
31 | border-radius: 8px;
32 | max-width: 416px;
33 | padding: 12px;
34 | }
35 |
36 | .atem-red {
37 | background-image: url('img/button_red.png');
38 | box-shadow: 0px 0px 10px 1px #ff0000;
39 | z-index: 10000;
40 | }
41 |
42 | .atem-green {
43 | background-image: url('img/button_green.png');
44 | box-shadow: 0px 0px 10px 1px #06c300;
45 | z-index: 10000;
46 | }
47 |
48 | .atem-yellow {
49 | background-image: url('img/button_yellow.png');
50 | box-shadow: 0px 0px 10px 1px #c7ca00;
51 | z-index: 10000;
52 | }
53 |
54 | .atem-disabled {
55 | background-image: url('img/button_off.png');
56 | color: #3a3a3a;
57 | }
58 |
59 | .atem-gray {
60 | color: #575757;
61 | }
62 |
63 | .source-label {
64 | display: flex;
65 | align-items: center;
66 | justify-content: center;
67 | height: 100%;
68 | font-size: 11px;
69 | font-weight: bold;
70 | }
71 |
72 | .transition-container {
73 | display: flex;
74 | flex-wrap: wrap;
75 | justify-content: space-between;
76 | }
77 |
78 | .transition-settings {
79 | display: flex;
80 | flex-direction: column;
81 | justify-content: space-between;
82 | margin-right: 30px;
83 | }
84 |
85 | .tbar-container {
86 | display: flex;
87 | flex-direction: column-reverse;
88 | background-color: #1f1f1f;
89 | border: 2px solid;
90 | border-color: #1a1a1a;
91 | border-radius: 6px;
92 | width: 88px;
93 | height: 429px;
94 | position: relative;
95 | margin-top: 58px;
96 | margin-right: 60px;
97 | }
98 |
99 | .tbar-bg {
100 | position: absolute;
101 | background-image: url('img/tbar_bg.png');
102 | width: 88px;
103 | height: 429px;
104 | }
105 | .tbar-handle {
106 | position: absolute;
107 | background-image: url('img/tbar_handle.png');
108 | width: 126px;
109 | height: 50px;
110 | left: -3px;
111 | }
112 | .tbar-div {
113 | width: 100%;
114 | background-color: #7aff58;
115 | overflow: hidden;
116 | }
117 |
118 | .dsk {
119 | width: 100%;
120 | }
121 |
122 | .dsk .source-wrapper {
123 | display: flex;
124 | flex-direction: column;
125 | align-content: center;
126 | width: 100%;
127 | padding: 5px;
128 | }
129 |
130 | .dsk h3 {
131 | text-align: center;
132 | }
133 |
134 | .fade-to-black-container {
135 | display: flex;
136 | flex-direction: column;
137 | align-items: flex-end;
138 | margin-right: 30px;
139 | }
140 |
141 | .fade-to-black .source-wrapper {
142 | display: flex;
143 | width: 100%;
144 | padding: 5px;
145 | }
146 |
147 | .fade-to-black h3 {
148 | text-align: center;
149 | }
150 | .rate-heading {
151 | text-align: center;
152 | font-size: small;
153 | margin-bottom: 4px;
154 | }
155 |
156 | .dsk-rate,
157 | .ftb-rate,
158 | .transition-rate {
159 | display: flex;
160 | justify-content: center;
161 | flex-direction: column;
162 | }
163 |
164 | .dsk-rate-label,
165 | .ftb-rate-label,
166 | .transition-rate-label {
167 | color: rgb(235, 110, 0);
168 | background-color: black;
169 | border-radius: 3px;
170 | padding: 6px 2px;
171 | text-align: center;
172 | }
173 |
174 | .clear {
175 | background: transparent;
176 | border-color: transparent;
177 | background-color: transparent;
178 | }
179 |
180 | .hide {
181 | display: none;
182 | }
183 |
184 | .no-wrap {
185 | flex-wrap: nowrap;
186 | }
187 |
188 | .show-small {
189 | display: none;
190 | }
191 |
192 | @media screen and (min-width: 0px) and (max-width: 480px) {
193 | .hide-small {
194 | display: none;
195 | }
196 | .show-small {
197 | display: block;
198 | }
199 | }
200 |
201 | .float-left {
202 | float: left;
203 | }
204 |
--------------------------------------------------------------------------------
/plugins/digico/fader.ejs:
--------------------------------------------------------------------------------
1 |
2 | <% if(fader && fader.meter>0){ %>
3 |
4 | <%=index%>
5 | |
6 | <% }else{ %>
7 | <%=index%> |
8 | <% } %>
9 |
10 | <% if(type=="Control_Groups"){ %>
11 |
12 | <%= fader ? fader.name : "-" %>
13 | |
14 | <% }else if(type=="Input_Channels"){ %>
15 |
16 | <%= fader ? (fader.Channel_Input ? fader.Channel_Input.name: "") : "" %>
17 | |
18 | <% }else if(type=="Group_Outputs"){ %>
19 |
20 | <%= fader ? (fader.Buss_Trim ? fader.Buss_Trim.name: "") : "" %>
21 | |
22 | <% }else if(type=="Aux_Outputs"){ %>
23 |
24 | <%= fader ? (fader.Buss_Trim ? fader.Buss_Trim.name: "") : "" %>
25 | |
26 | <% } %>
27 |
28 | |
29 |
30 | <% if(fader){ %>
31 | <% let percent = (fader.fader-10)*1.5+100 %>
32 |
33 | <% if(fader.fader>-100){ %>
34 | <%= fader ? Math.round(fader.fader*10.0)/10.0 : '-' %>db
35 | <% } %>
36 | |
37 | <% }else{ %>
38 | - |
39 | <% } %>
40 |
41 |
42 |
--------------------------------------------------------------------------------
/plugins/digico/icon.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/digico/icon.afphoto
--------------------------------------------------------------------------------
/plugins/digico/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/digico/icon.png
--------------------------------------------------------------------------------
/plugins/digico/info.html:
--------------------------------------------------------------------------------
1 | Cue View emulates to be the DiGiCo SD iPad app.
2 |
3 | - On the SD console open Master Screen → Setup → External Control
4 | - Enable External Control by pressing the button at the top of the panel
5 | - Press the add device button and select DiGiCo Pad
6 | - Enter a Device Name for Cue View and then enter the IP Address of this computer
7 | - Enter Send and Receive Port numbers
8 |
9 | - Cue View Port = DiGiCo Receive
10 | - Cue View Local = DiGiCo Send
11 |
12 | - Tick the Enable column for this device
13 | -
14 | Press the Load button in the bottom right corner of the panel and select the commands button for the
15 | relevant console
16 |
17 |
18 |
--------------------------------------------------------------------------------
/plugins/digico/main.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const fs = require('fs');
3 | const path = require('path');
4 |
5 | exports.config = {
6 | defaultName: 'DiGiCo SD',
7 | connectionType: 'osc-udp',
8 | remotePort: 8000,
9 | localPort: 8001,
10 | mayChangePorts: true,
11 | heartbeatInterval: 2000,
12 | heartbeatTimeout: 11000,
13 | searchOptions: {
14 | type: 'UDPsocket',
15 | // send "/Console/Name/?" this is what the iPad sends to check connection
16 | searchBuffer: Buffer.from([
17 | 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, 0x4e, 0x61, 0x6d, 0x65, 0x2f, 0x3f, 0x00, 0x2c, 0x00, 0x00,
18 | 0x00,
19 | ]),
20 | devicePort: 8000,
21 | listenPort: 9000,
22 | validateResponse(msg, info) {
23 | console.log(msg);
24 | console.log(msg.toString());
25 | return msg.toString().includes('/Console');
26 | },
27 | },
28 | };
29 |
30 | exports.ready = function ready(_device) {
31 | const device = _device;
32 | device.data = new DiGiCo();
33 |
34 | device.send('/Console/Name/?');
35 | device.send('/Console/Session/Filename/?');
36 | device.send('/Console/Channels/?');
37 | device.send('/Snapshots/Current_Snapshot/?');
38 | device.send('/Meters/clear');
39 | };
40 | exports.data = function data(_device, oscData) {
41 | const device = _device;
42 | this.deviceInfoUpdate(device, 'status', 'ok');
43 |
44 | const properties = oscData.address.split('/');
45 | properties.shift();
46 |
47 | if (properties[0] !== 'Meters') {
48 | setObjectProperty(device.data, properties, oscData.args);
49 | // console.log(properties);
50 | }
51 |
52 | if (properties[0] === 'Console' && properties[1] === 'Control_Groups') {
53 | for (let i = 1; i <= device.data.Console.Control_Groups; i++) {
54 | device.send(`/Control_Groups/${i}/?`);
55 | }
56 | device.draw();
57 | } else if (properties[0] === 'Console' && properties[1] === 'Input_Channels') {
58 | for (let i = 1; i <= device.data.Console.Input_Channels; i++) {
59 | device.send(`/Input_Channels/${i}/Channel_Input/name/?`);
60 | device.send(`/Input_Channels/${i}/mute/?`);
61 | device.send(`/Input_Channels/${i}/solo/?`);
62 | device.send(`/Input_Channels/${i}/fader/?`);
63 | device.send(`/Meters/request/1${i}`, [
64 | { type: 's', value: `/Input_Channels/${i}/Channel_Input/post_meter/left` },
65 | ]);
66 | }
67 | device.draw();
68 | } else if (properties[0] === 'Console' && properties[1] === 'Group_Outputs') {
69 | for (let i = 1; i <= device.data.Console.Group_Outputs; i++) {
70 | device.send(`/Group_Outputs/${i}/mute/?`);
71 | device.send(`/Group_Outputs/${i}/solo/?`);
72 | device.send(`/Group_Outputs/${i}/fader/?`);
73 | device.send(`/Group_Outputs/${i}/Buss_Trim/name/?`);
74 | device.send(`/Meters/request/3${i}`, [{ type: 's', value: `/Group_Outputs/${i}/fader_meter/left` }]);
75 | }
76 | device.draw();
77 | } else if (properties[0] === 'Console' && properties[1] === 'Aux_Outputs') {
78 | for (let i = 1; i <= device.data.Console.Aux_Outputs; i++) {
79 | device.send(`/Aux_Outputs/${i}/mute/?`);
80 | device.send(`/Aux_Outputs/${i}/solo/?`);
81 | device.send(`/Aux_Outputs/${i}/fader/?`);
82 | device.send(`/Aux_Outputs/${i}/Buss_Trim/name/?`);
83 | device.send(`/Meters/request/4${i}`, [{ type: 's', value: `/Aux_Outputs/${i}/fader_meter/left` }]);
84 | }
85 | device.draw();
86 | } else if (properties[0] === 'Console' && properties[1] === 'Name') {
87 | this.deviceInfoUpdate(device, 'defaultName', device.data.Console.Name);
88 | } else if (['Control_Groups', 'Input_Channels', 'Group_Outputs', 'Aux_Outputs'].includes(properties[0])) {
89 | device.update('fader', {
90 | type: properties[0],
91 | channel: properties[1],
92 | });
93 | } else if (properties[0] === 'Snapshots') {
94 | device.update('updateSnapshot', {
95 | snapshots: device.data.Snapshots,
96 | });
97 | } else if (properties[0] === 'Meters' && properties[1] === 'values') {
98 | for (let i = 0; i < oscData.args.length; i += 2) {
99 | const channelType = oscData.args[i].toString()[0];
100 | const channelNumber = oscData.args[i].toString().substring(1);
101 | const meterValue = BigInt(oscData.args[i + 1]) / -40000n + 100n;
102 |
103 | if (channelType === '1' && device.data.Input_Channels[channelNumber]) {
104 | device.data.Input_Channels[channelNumber].meter = meterValue;
105 | device.update('fader', {
106 | type: 'Input_Channels',
107 | channel: channelNumber,
108 | });
109 | } else if (channelType === '3' && device.data.Group_Outputs[channelNumber]) {
110 | device.data.Group_Outputs[channelNumber].meter = meterValue;
111 | device.update('fader', {
112 | type: 'Group_Outputs',
113 | channel: channelNumber,
114 | });
115 | } else if (channelType === '4' && device.data.Aux_Outputs[channelNumber]) {
116 | device.data.Aux_Outputs[channelNumber].meter = meterValue;
117 | device.update('fader', {
118 | type: 'Aux_Outputs',
119 | channel: channelNumber,
120 | });
121 | }
122 | }
123 | }
124 | };
125 |
126 | exports.heartbeat = function heartbeat(device) {
127 | device.send('/Console/Name/?');
128 | console.log(device.data);
129 |
130 | // Need to do a /Meters/clear when quitting Cue View or closing the View
131 | };
132 |
133 | class DiGiCo {
134 | constructor() {
135 | this.Console = {};
136 | this.Input_Channels = {};
137 | this.Aux_Outputs = {};
138 | this.Control_Groups = {};
139 | this.Group_Outputs = {};
140 | this.Matrix_Outputs = {};
141 | this.Layout = {};
142 | }
143 | }
144 |
145 | /*
146 | This function takes somethings like this /Console/Session/Filename with a osc args like ['session.ses']
147 | and turns it into a nested object like. An easy way to set properties converting OSC paths to properties
148 | {
149 | Console: {
150 | Session: {
151 | Filename: 'session.ses'
152 | }
153 | }
154 | }
155 |
156 | */
157 | function setObjectProperty(_object, _properties, value) {
158 | let object = _object;
159 | const properties = _properties;
160 |
161 | for (let i = 0; i < properties.length - 1; i++) {
162 | const property = properties[i];
163 | if (object[property] === undefined) {
164 | object[property] = {};
165 | }
166 | object = object[property];
167 | }
168 | const property = properties[properties.length - 1];
169 | if (Array.isArray(value)) {
170 | // if this is an array of length one just set the property to the contents so ['string'] becomes 'string'
171 | if (value.length === 1) {
172 | object[property] = value[0];
173 | return;
174 | }
175 | }
176 | object[property] = value;
177 | }
178 |
179 | const faderTemplate = _.template(fs.readFileSync(path.join(__dirname, `/fader.ejs`)));
180 | exports.update = function update(device, doc, updateType, data) {
181 | if (updateType === 'fader') {
182 | const $elem = doc.getElementById(`${data.type}-${data.channel}`);
183 | $elem.outerHTML = faderTemplate({
184 | type: data.type,
185 | fader: device.data[data.type][data.channel],
186 | index: data.channel,
187 | });
188 | } else if (updateType === 'snapshot') {
189 | const $elem = doc.getElementById(`snapshot`);
190 | $elem.textContent = `${data.snapshots.Current_Snapshot}`;
191 | }
192 | };
193 |
--------------------------------------------------------------------------------
/plugins/digico/styles.css:
--------------------------------------------------------------------------------
1 | .column {
2 | float: left;
3 | margin: 5px;
4 | }
5 | .section {
6 | border: #333 3px solid;
7 | border-bottom: none;
8 | border-top-left-radius: 10px;
9 | border-top-right-radius: 10px;
10 | padding: 10px;
11 | margin-bottom: 30px;
12 | }
13 | .section h3 {
14 | font-weight: 300;
15 | margin-top: 0px;
16 | color: #aaa !important;
17 | }
18 | table {
19 | border-collapse: collapse;
20 | display: block;
21 | }
22 | .fader:nth-child(even) {
23 | background: #1a1a1a;
24 | }
25 | .fader:nth-child(odd) {
26 | background: #1f1f1f;
27 | }
28 | .fader td {
29 | text-align: center;
30 | width: 30px;
31 | height: 20px;
32 | border: black 1px solid;
33 | }
34 | .fader .fader-channel {
35 | color: #777;
36 | font-size: 10px;
37 | }
38 | .fader .fader-name {
39 | width: 80px;
40 | color: black;
41 | font-family: monospace;
42 | }
43 | .fader .lcd-control-group {
44 | background-color: rgb(66, 132, 255);
45 | }
46 | .fader .lcd-channel-input {
47 | background-color: rgb(135, 175, 235);
48 | }
49 | .fader .lcd-group-output {
50 | background-color: rgb(241, 71, 48);
51 | }
52 | .fader .lcd-aux-output {
53 | background-color: rgb(197, 0, 187);
54 | }
55 | .fader .fader-name.state-1 {
56 | background-color: rgb(0, 185, 0);
57 | }
58 | .fader .fader-mute {
59 | width: 10px;
60 | background-color: #444;
61 | }
62 | .fader .fader-mute.state-1 {
63 | background-color: red;
64 | }
65 | .fader .fader-fader {
66 | text-align: left;
67 | width: 70px;
68 | padding: 0px 10px;
69 | color: rgb(68, 68, 68);
70 | }
71 |
72 | #snapshot {
73 | font-size: 36px;
74 | font-weight: bold;
75 | font-family: 'Arial Black', 'Impact';
76 | }
77 |
--------------------------------------------------------------------------------
/plugins/digico/template.ejs:
--------------------------------------------------------------------------------
1 |
2 | <%= listName %>
3 | <%= data.Console.Session.Filename %>
4 |
5 |
6 | <%
7 | const fs = require('fs');
8 | let _ = require('lodash');
9 | const path = require('path');
10 |
11 | let faderTemplate = _.template(fs.readFileSync(path.join(__dirname, `/plugins/digico/fader.ejs`)));
12 | %>
13 |
14 |
15 |
16 |
snapshot
17 |
18 |
19 |
20 |
control groups
21 |
22 | <% for(let i=1; i<=data.Console.Control_Groups; i++){ %>
23 | <%= faderTemplate({type: "Control_Groups", fader: data.Control_Groups[i], index: i}) %>
24 | <% } %>
25 |
26 |
27 |
28 |
29 |
group outputs
30 |
31 | <% for(let i=1; i<=data.Console.Group_Outputs; i++){ %>
32 | <%= faderTemplate({type: "Group_Outputs", fader: data.Group_Outputs[i], index: i}) %>
33 | <% } %>
34 |
35 |
36 |
37 |
38 |
aux outputs
39 |
40 | <% for(let i=1; i<=data.Console.Aux_Outputs; i++){ %>
41 | <%= faderTemplate({type: "Aux_Outputs", fader: data.Aux_Outputs[i], index: i}) %>
42 | <% } %>
43 |
44 |
45 |
46 |
47 |
48 |
input channels
49 |
50 | <% for(let i=1; i<=data.Console.Input_Channels; i++){ %>
51 | <%= faderTemplate({type: "Input_Channels", fader: data.Input_Channels[i], index: i}) %>
52 | <% } %>
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/plugins/eos/cue.ejs:
--------------------------------------------------------------------------------
1 |
2 | <% for(var partNumber in q){ %>
3 | <% var part = q[partNumber] %>
4 | <%if(part.scene.length>0 && partNumber==0){ %>
5 |
6 | |
7 |
8 |
9 | <%= part.scene %>
10 | |
11 |
12 | <% } %>
13 |
14 | <% if(isActive){ %>
15 |
16 | <% }else{ %>
17 |
18 |
19 | <% } %>
20 |
21 | <% if(partNumber!=0){ %>
22 |
23 | P<%= partNumber %>
24 | |
25 | <% }else{ %>
26 | <%= cueNumber %> |
27 | <% } %>
28 |
29 | <% if(partNumber !== 0 || part.partCount === 0){ %>
30 |
31 |
32 | <% if(part.uptimeDuration === part.downtimeDuration || part.downtimeDuration === -1){ %>
33 |
34 | <%= part.prettyDuration(part.uptimeDelay) || "" %>
35 | <%= part.prettyDuration(part.uptimeDuration, true) %>
36 | |
37 | <% }else{ %>
38 |
39 | <%= part.prettyDuration(part.uptimeDelay) || "" %>
40 | <%= part.prettyDuration(part.uptimeDuration, true) %>
41 | |
42 |
43 | <%= part.prettyDuration(part.downtimeDuration, true) %>
44 | |
45 | <% } %>
46 |
47 |
48 | <%= part.prettyDuration(part.focusTimeDuration, true) %>
49 | |
50 |
51 | <%= part.prettyDuration(part.colorTimeDuration, true) %>
52 | |
53 |
54 | <%= part.prettyDuration(part.beamTimeDuration, true) %>
55 | |
56 | <%= part.prettyDuration(part.duration) %> |
57 |
58 | <% }else{ %>
59 | |
60 | |
61 | |
62 | |
63 | |
64 | <% } %>
65 |
66 | <%= part.mark %> |
67 | <%= part.block %> |
68 | <%= part.assert %> |
69 |
70 | <%= (part.follow>=0)? "F"+part.prettyDuration(part.follow) : "" %>
71 | <%= (part.hang>=0)? "H"+part.prettyDuration(part.hang) : "" %>
72 | |
73 |
74 | <%= (partNumber==0 ? "": " ") %>
75 | <%= part.label %>
76 | |
77 | <%= part.extLinks %> |
78 |
79 | <% } %>
80 |
81 |
--------------------------------------------------------------------------------
/plugins/eos/cue.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable class-methods-use-this */
2 | class Cue {
3 | constructor(args) {
4 | this.uid = args[1];
5 | this.label = args[2];
6 | this.uptimeDuration = args[3];
7 | this.uptimeDelay = args[4];
8 | this.downtimeDuration = args[5];
9 | this.downtimeDelay = args[6];
10 | this.focusTimeDuration = args[7];
11 | this.focusTimeDelay = args[8];
12 | this.colorTimeDuration = args[9];
13 | this.colorTimeDelay = args[10];
14 | this.beamTimeDuration = args[11];
15 | this.beamTimeDelay = args[12];
16 | this.mark = args[16];
17 | this.block = args[17];
18 | this.assert = args[18];
19 | this.follow = args[20];
20 | this.hang = args[21];
21 | this.partCount = args[26];
22 | this.scene = args[28];
23 | this.duration = Math.max(args[3], args[5], args[7], args[9], args[11]);
24 | }
25 |
26 | prettyDuration(milliseconds, box) {
27 | if (milliseconds === -1) {
28 | return '';
29 | }
30 |
31 | const num = Math.round(milliseconds / 100) / 10;
32 | if (box) {
33 | return `${num}
`;
34 | }
35 |
36 | return num;
37 | }
38 | }
39 |
40 | module.exports = Cue;
41 |
--------------------------------------------------------------------------------
/plugins/eos/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/eos/icon.png
--------------------------------------------------------------------------------
/plugins/eos/info.html:
--------------------------------------------------------------------------------
1 | Connection Requirements: Eos 3.2 and newer
2 | Browser: Setup → Device Settings → Network
3 |
4 | - Enable Third Party OSC on the network interface(s) you are using
5 | - Ensure this Cue View device's port is set to 3037
6 |
7 |
8 |
9 |
10 | Connection Requirements: Eos 3.1 and earlier
11 | Shell/ECU → Network
12 |
13 | - Enable ✔ TCP OSC. The TCP format should be "TCP Format for OSC 1.1 (SLIP)".
14 | - Enable Third Party OSC
15 |
16 | If the "Third Party OSC" option is not available, Eos must be updated to 3.1 or newer.
17 |
18 | Browser: System Settings → Show Control → OSC
19 |
20 | - Enable ✔ OSC RX
21 | - Enable ✔ OSC TX
22 | - All other fields may be left to defaults or blank.
23 | - If a custom port is desired specify it in under OSC TCP Server Ports.
24 |
25 |
--------------------------------------------------------------------------------
/plugins/eos/main.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const fs = require('fs');
3 | const path = require('path');
4 | const Cue = require('./cue');
5 |
6 | exports.config = {
7 | defaultName: 'ETC Eos',
8 | connectionType: 'osc',
9 | defaultPort: 3037,
10 | mayChangePorts: true,
11 | heartbeatInterval: 5000,
12 | heartbeatTimeout: 6000,
13 | searchOptions: {
14 | type: 'TCPport',
15 | searchBuffer: Buffer.from('\xc0/eos/ping\x00\x00\x2c\x00\x00\x00\xc0', 'ascii'),
16 | testPort: 3037,
17 | validateResponse(msg, info) {
18 | return msg.toString().includes('/eos/out');
19 | },
20 | },
21 | fields: [
22 | {
23 | key: 'cueListFilter',
24 | label: 'Q List',
25 | type: 'numberinput',
26 | value: '',
27 | action(_device) {
28 | const device = _device;
29 | device.data.cueListFilter = device.fields.cueListFilter;
30 | device.draw();
31 | },
32 | },
33 | ],
34 | };
35 |
36 | exports.ready = function ready(_device) {
37 | const device = _device;
38 | device.data.EOS = new EOS();
39 | device.templates = {
40 | cue: _.template(fs.readFileSync(path.join(__dirname, `cue.ejs`))),
41 | };
42 | device.data.cueListFilter = device.fields.cueListFilter;
43 |
44 | device.send('/eos/get/cuelist/count');
45 | device.send('/eos/get/version');
46 | device.send('/eos/subscribe', [{ type: 'i', value: 1 }]);
47 | };
48 |
49 | exports.data = function data(_device, osc) {
50 | const device = _device;
51 | this.deviceInfoUpdate(device, 'status', 'ok');
52 |
53 | const addressParts = osc.address.split('/');
54 | addressParts.shift();
55 |
56 | if (match(addressParts, ['eos', 'out', 'show', 'name'])) {
57 | device.data.EOS.showName = osc.args[0];
58 | device.data.EOS.cueLists = {};
59 | this.deviceInfoUpdate(device, 'defaultName', osc.args[0]);
60 | } else if (match(addressParts, ['eos', 'out', 'get', 'cuelist', 'count'])) {
61 | for (let i = 0; i < osc.args[0]; i++) {
62 | device.send(`/eos/get/cuelist/index/${i}`);
63 | }
64 | } else if (match(addressParts, ['eos', 'out', 'get', 'cuelist', '*', 'list', '*', '*'])) {
65 | device.data.EOS.cueLists[addressParts[4]] = {};
66 | device.send(`/eos/get/cue/${addressParts[4]}/count`);
67 | } else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', 'count'])) {
68 | for (let i = 0; i < osc.args[0]; i++) {
69 | device.send(`/eos/get/cue/${addressParts[4]}/index/${i}`);
70 | }
71 | } else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', '*', '*', 'list', '*', '*'])) {
72 | if (device.data.EOS.cueLists[addressParts[4]] === undefined) {
73 | device.data.EOS.cueLists[addressParts[4]] = {};
74 | device.send(`/eos/get/cue/${addressParts[4]}/count`);
75 | }
76 | if (device.data.EOS.cueLists[addressParts[4]][addressParts[5]] === undefined) {
77 | device.data.EOS.cueLists[addressParts[4]][addressParts[5]] = {};
78 | }
79 | device.data.EOS.cueLists[addressParts[4]][addressParts[5]][addressParts[6]] = new Cue(osc.args);
80 |
81 | device.update('cueData', {
82 | cue: device.data.EOS.cueLists[addressParts[4]][addressParts[5]],
83 | cueNumber: addressParts[5],
84 | uid: osc.args[1],
85 | });
86 | } else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', '*'])) {
87 | // There's no OSC notification of the deletion of a part. It just tells you to update the parent cue and remaining children.
88 | // So: we should fetch all the parts of a cue somehow every time there's an update to a cue.
89 | delete device.data.EOS.cueLists[addressParts[4]][addressParts[5]];
90 | device.draw();
91 | } else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', '*', '*', 'actions', 'list', '*', '*'])) {
92 | if (osc.args.length === 3) {
93 | device.data.EOS.cueLists[addressParts[4]][addressParts[5]][0].extLinks = osc.args[2];
94 | }
95 | } else if (match(addressParts, ['eos', 'out', 'event', 'cue', '*', '*', 'fire'])) {
96 | device.data.EOS.activeCue = addressParts[5];
97 | device.draw();
98 | const cues = device.data.EOS.cueLists[addressParts[4]][addressParts[5]];
99 | if (cues) {
100 | device.update('activeCue', {
101 | uid: cues[0].uid,
102 | });
103 | }
104 | } else if (match(addressParts, ['eos', 'out', 'notify', 'cue', '*', '*', '*', '*'])) {
105 | const cueList = addressParts[4];
106 | const cueNumber = osc.args[1];
107 | device.send(`/eos/get/cue/${cueList}/${cueNumber}`);
108 | } else if (match(addressParts, ['eos', 'out', 'get', 'version'])) {
109 | device.data.EOS.version = osc.args[0];
110 | } else {
111 | // console.log(osc);
112 | }
113 | };
114 |
115 | exports.update = function update(device, doc, updateType, data) {
116 | if (updateType === 'cueData') {
117 | const $elem = doc.getElementById(data.uid);
118 | if ($elem) {
119 | $elem.outerHTML = device.templates.cue({
120 | q: data.cue,
121 | cueNumber: data.cueNumber,
122 | isActive: false,
123 | });
124 | } else {
125 | device.draw();
126 | }
127 | } else if (updateType === 'activeCue') {
128 | const $elem = doc.getElementById(data.uid);
129 | $elem.scrollIntoView({ behavior: 'smooth', block: 'center' });
130 | }
131 | };
132 |
133 | exports.heartbeat = function heartbeat(device) {
134 | device.send('/eos/ping');
135 | };
136 |
137 | function match(testArray, patternArray) {
138 | let out = true;
139 | if (testArray.length !== patternArray.length) {
140 | return false;
141 | }
142 | patternArray.forEach((patternPart, i) => {
143 | if (testArray[i] !== patternPart && patternPart !== '*') {
144 | out = false;
145 | }
146 | });
147 | return out;
148 | }
149 | class EOS {
150 | constructor() {
151 | this.version = '';
152 | this.showName = '';
153 | this.cueLists = {};
154 | this.activeCue = undefined;
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/plugins/eos/styles.css:
--------------------------------------------------------------------------------
1 | table {
2 | background-color: #141414;
3 | border: #404040 1px solid;
4 | border-radius: 4px;
5 | color: #a59baa;
6 | width: 100%;
7 | }
8 | th {
9 | font-size: 14px;
10 | font-weight: normal;
11 | padding: 5px;
12 | }
13 | td {
14 | text-align: center;
15 | padding: 4px 6px;
16 | }
17 | td:first-child {
18 | text-align: left;
19 | }
20 | td.black {
21 | background-color: black;
22 | border-right: #212021 1px solid;
23 | }
24 | td.num {
25 | color: white;
26 | font-weight: 600;
27 | font-size: 16px;
28 | }
29 |
30 | .scene {
31 | background-color: black;
32 | border-top: #141414 2px solid;
33 | border-bottom: #141414 2px solid;
34 | }
35 | .scene hr {
36 | border: #001f10 4px solid;
37 | }
38 | .scene span {
39 | display: table;
40 | margin: 0px auto;
41 | margin-top: -25px;
42 | padding: 4px;
43 | background-color: black;
44 | font-weight: bold;
45 | font-size: 14px;
46 | }
47 | .time {
48 | border: #2f2b35 2px solid;
49 | border-radius: 5px;
50 | }
51 |
52 | tr.active-cue {
53 | color: #c78b07;
54 | }
55 | tr.active-cue td.num div {
56 | margin: 0px;
57 | padding: 1px 6px;
58 | background-color: #c78b07;
59 | border-radius: 4px;
60 | color: black;
61 | }
62 | tr.active-cue .time {
63 | border-color: #c78b07;
64 | }
65 |
66 | .list_name {
67 | color: lightgray;
68 | margin-left: 5px;
69 | margin-bottom: 2px;
70 | }
71 |
72 | .list_container {
73 | margin-bottom: 10px;
74 | }
75 |
76 | @media screen and (min-width: 0px) and (max-width: 750px) {
77 | .hide-medium {
78 | display: none;
79 | }
80 | }
81 | @media screen and (min-width: 0px) and (max-width: 550px) {
82 | .hide-small {
83 | display: none;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/plugins/eos/template.ejs:
--------------------------------------------------------------------------------
1 |
2 | <%= listName %>
3 | Eos <%= data.EOS.version %>
4 |
5 |
6 | <% for(var i in data.EOS.cueLists){ %>
7 | <% if(data.cueListFilter == i || data.cueListFilter.length==0){ %>
8 |
9 |
List <%= i %>
10 |
11 |
12 | Cue |
13 | Int Up |
14 | Int Down |
15 | Focus |
16 | Color |
17 | Beam |
18 | Dur |
19 | M |
20 | B |
21 | A |
22 | Fw/Hg |
23 | Label |
24 | Ext Links |
25 |
26 |
27 | <% var cues = Object.keys(data.EOS.cueLists[i]).sort(function(a, b){return Number(a)-Number(b)}) %>
28 |
29 | <% for(var j=0; j
30 | <% var q = data.EOS.cueLists[i][cues[j]] %>
31 |
32 | <%= templates.cue({
33 | q: q,
34 | cues: cues,
35 | cueNumber: cues[j],
36 | isActive: (cues[j]==data.EOS.activeCue+"")
37 | }) %>
38 |
39 | <% } %>
40 |
41 |
42 | <% } %>
43 |
44 | <% } %>
45 |
--------------------------------------------------------------------------------
/plugins/lightfactory/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/lightfactory/icon.png
--------------------------------------------------------------------------------
/plugins/lightfactory/info.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/lightfactory/info.html
--------------------------------------------------------------------------------
/plugins/lightfactory/main.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | defaultName: 'LightFactory',
3 | connectionType: 'TCPsocket',
4 | remotePort: 3100,
5 | mayChangePorts: true,
6 | heartbeatInterval: 5000,
7 | heartbeatTimeout: 10000,
8 | searchOptions: {
9 | type: 'TCPport',
10 | searchBuffer: Buffer.from('\n', 'ascii'),
11 | testPort: 3100,
12 | validateResponse(msg, info) {
13 | return msg.toString().includes('LightFactory');
14 | },
15 | },
16 | };
17 |
18 | exports.ready = function ready(_device) {
19 | const device = _device;
20 | device.data.cueLists = {};
21 | device.send('get cue list\n');
22 | };
23 |
24 | function cleanMessage(message) {
25 | return message.split('\n').filter((line) => line.trim() !== '');
26 | }
27 |
28 | // TODO(jwetzell): try to grab notes?
29 | exports.data = function data(_device, _message) {
30 | const message = _message.toString();
31 | const device = _device;
32 |
33 | if (message.includes('Current Cue List')) {
34 | // NOTE(jwetzell): loading cue list info
35 | const cueListMessage = cleanMessage(message);
36 | if (cueListMessage.includes('') && cueListMessage.includes('')) {
37 | const cueListstartIndex = cueListMessage.indexOf('');
38 | const cueListFinishedIndex = cueListMessage.indexOf('');
39 |
40 | for (let i = cueListstartIndex + 1; i < cueListFinishedIndex; i++) {
41 | const cueListName = cueListMessage[i];
42 | if (device.data.cueLists[cueListName] === undefined) {
43 | device.data.cueLists[cueListName] = {
44 | name: cueListName,
45 | cues: {},
46 | active: false,
47 | };
48 | } else {
49 | // NOTE(jwetzell): reset active as it will be computed in a few lines
50 | device.data.cueLists[cueListName].active = false;
51 | }
52 | }
53 | const activeCueListLine = cueListMessage[cueListFinishedIndex + 1];
54 | if (activeCueListLine) {
55 | const activeCueListLineParts = activeCueListLine.split('Current Cue List');
56 | const activeCueList = activeCueListLineParts[activeCueListLineParts.length - 1].trim();
57 | if (activeCueList && device.data.cueLists[activeCueList] !== undefined) {
58 | device.data.cueLists[activeCueList].active = true;
59 | }
60 | }
61 | device.data.cueListToLoad = Object.keys(device.data.cueLists)[0];
62 | device.send(`get cues ${device.data.cueListToLoad}\n`);
63 | }
64 | } else if (message.includes('Current Live Cue')) {
65 | // NOTE(jwetzell): loading cues for a cue list
66 | const cueList = device.data.cueLists[device.data.cueListToLoad];
67 | const cueListCuesMessage = cleanMessage(message);
68 |
69 | // NOTE(jwetzell): this is so gross
70 | if (cueListCuesMessage.includes('') && cueListCuesMessage.includes('')) {
71 | const cuesStartIndex = cueListCuesMessage.indexOf('');
72 | const cuesFinishedIndex = cueListCuesMessage.indexOf('');
73 |
74 | const validCueNumbers = [];
75 | for (let i = cuesStartIndex + 1; i < cuesFinishedIndex; i++) {
76 | const cueData = cueListCuesMessage[i];
77 | const cueSplit = cueData.indexOf(' ');
78 | const cueNumber = cueSplit > 0 ? cueData.substring(0, cueSplit).trim() : cueData.trim();
79 | const cueName = cueSplit > 0 ? cueData.substring(cueSplit + 1).trim() : '';
80 | cueList.cues[cueNumber] = {
81 | name: cueName,
82 | number: cueNumber,
83 | active: false,
84 | };
85 | validCueNumbers.push(cueNumber);
86 | }
87 |
88 | const invalidCueNumbers = Object.keys(cueList.cues).filter((cueNumber) => !validCueNumbers.includes(cueNumber));
89 |
90 | invalidCueNumbers.forEach((cueNumber) => {
91 | delete cueList.cues[cueNumber];
92 | });
93 |
94 | const activeCueLine = cueListCuesMessage[cuesFinishedIndex + 1];
95 | if (activeCueLine) {
96 | const activeCueLineParts = activeCueLine.split('Current Live Cue');
97 | const activeCue = activeCueLineParts[activeCueLineParts.length - 1].trim();
98 | if (activeCue && cueList.cues[activeCue] !== undefined) {
99 | cueList.cues[activeCue].active = true;
100 | }
101 | }
102 | }
103 | // NOTE(jwetzell): load next cue list's cues if there is one
104 | const cueListNames = Object.keys(device.data.cueLists);
105 | const cueListIndex = cueListNames.indexOf(device.data.cueListToLoad);
106 | device.data.cueListToLoad = cueListNames.at(cueListIndex + 1);
107 | if (device.data.cueListToLoad !== undefined) {
108 | device.send(`get cues ${device.data.cueListToLoad}\n`);
109 | }
110 | } else if (message.includes('Running cue') || message.includes('Goto cue')) {
111 | const cueUpdateMessage = message
112 | .trim()
113 | .slice(0, -1)
114 | .split(':')
115 | .map((item) => item.trim());
116 | if (cueUpdateMessage.length === 3) {
117 | const cueListName = cueUpdateMessage[1];
118 | const updatedCue = cueUpdateMessage[2];
119 |
120 | const split = updatedCue.indexOf(' ');
121 |
122 | const updatedCueNumber = split > 0 ? updatedCue.substring(0, split).trim() : updatedCue.trim();
123 | if (device.data.cueLists[cueListName] !== undefined) {
124 | Object.entries(device.data.cueLists[cueListName].cues).forEach(([cueName, cueData]) => {
125 | cueData.active = cueData.number === updatedCueNumber;
126 | });
127 | }
128 | }
129 | }
130 | device.draw();
131 | };
132 |
133 | // TODO(jwetzell): keep alive? refresh cue list info?
134 | exports.heartbeat = function heartbeat(device) {
135 | device.send('get cue list\n');
136 | };
137 |
--------------------------------------------------------------------------------
/plugins/lightfactory/styles.css:
--------------------------------------------------------------------------------
1 | section {
2 | background-color: #363636;
3 | border: gray 1px solid;
4 | padding: 2px;
5 | padding-top: 0px;
6 | overflow: hidden;
7 | font-size: 8px;
8 | margin-bottom: 10px;
9 | width: 300px;
10 | }
11 | table {
12 | border: #414142 1px solid;
13 | background-color: #272727;
14 | width: 100%;
15 | }
16 | table,
17 | th,
18 | td {
19 | border-collapse: collapse;
20 | color: #ccc;
21 | font-size: 12px;
22 | }
23 | th {
24 | padding: 2px 10px;
25 | border: #282828 1px solid;
26 | background: linear-gradient(#404040, #363636);
27 | text-align: left;
28 | }
29 | td {
30 | text-align: left;
31 | padding: 7px 7px;
32 | font-family: sans-serif;
33 | position: relative;
34 | }
35 | tr {
36 | border-bottom: #363636 1px solid;
37 | }
38 | tr:nth-child(odd) {
39 | background-color: #2a2a2a;
40 | }
41 | tr:nth-child(even) {
42 | background-color: #202020;
43 | }
44 |
45 | .active_indicator {
46 | color: #00ff00;
47 | font-size: 13px;
48 | position: absolute;
49 | top: 4px;
50 | left: -4px;
51 | transform: rotate(-45deg);
52 | }
53 |
--------------------------------------------------------------------------------
/plugins/lightfactory/template.ejs:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | <% Object.entries(data.cueLists).forEach(([cueListName,cueListData]) => { %>
8 |
9 |
10 | Cue List: <%= cueListData.name %> <%= cueListData.active ? '(Active)': '' %>
11 |
12 |
13 |
14 | |
15 | Cue No |
16 | Description |
17 |
18 |
19 |
20 | <% Object.entries(cueListData.cues).forEach(([cueName,cueData]) => { %>
21 |
22 |
23 | <%= cueData.active ? '◢': '' %> |
24 | <%= cueData.number %> |
25 | <%= cueData.name %> |
26 |
27 |
28 | <% }); %>
29 |
30 |
31 |
32 | <%
33 | });
34 | %>
35 |
--------------------------------------------------------------------------------
/plugins/livestream-studio/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/livestream-studio/icon.png
--------------------------------------------------------------------------------
/plugins/livestream-studio/img/tbar_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/livestream-studio/img/tbar_bg.png
--------------------------------------------------------------------------------
/plugins/livestream-studio/img/tbar_handle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/livestream-studio/img/tbar_handle.png
--------------------------------------------------------------------------------
/plugins/livestream-studio/info.html:
--------------------------------------------------------------------------------
1 | Livestream Studio Configuration
2 |
3 | - enable Third-party Controllers under Settings->Hardware Control->Allow Incoming Connections
4 | - allow the connection from Cue View the first time it connects under the same settings menu
5 |
6 |
--------------------------------------------------------------------------------
/plugins/livestream-studio/main.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | defaultName: 'Livestream Studio',
3 | connectionType: 'TCPsocket',
4 | remotePort: 9923,
5 | mayChangePort: false,
6 | searchOptions: {
7 | type: 'TCPport',
8 | searchBuffer: Buffer.from(''),
9 | testPort: 9923,
10 | validateResponse(msg, info) {
11 | return msg.toString().startsWith('ILCC:');
12 | },
13 | },
14 | };
15 |
16 | exports.ready = function ready(_device) {
17 | console.log('livestream studio ready');
18 | const device = _device;
19 | device.draw();
20 |
21 | setInterval(() => {
22 | device.update('inputs', device.data);
23 | device.update('fadeToBlack', device.data);
24 | }, 1000);
25 | };
26 |
27 | exports.update = function update(device, _document, updateType, data) {
28 | const document = _document;
29 | if (updateType === 'programInput' || updateType === 'previewInput' || updateType === 'inputs') {
30 | device.data.inputs.forEach((input) => {
31 | if (device.data.program === input.number) {
32 | document.getElementById(`input-${input.number}`).classList.add('lss-red');
33 | } else {
34 | document.getElementById(`input-${input.number}`).classList.remove('lss-red');
35 | }
36 |
37 | if (device.data.preview === input.number) {
38 | document.getElementById(`input-${input.number}`).classList.add('lss-green');
39 | } else {
40 | document.getElementById(`input-${input.number}`).classList.remove('lss-green');
41 | }
42 | });
43 | } else if (updateType === 'fadeToBlack') {
44 | const ftbId = 'ftb';
45 |
46 | if (device.data.fadeToBlack) {
47 | document.getElementById(ftbId).classList.add('lss-ftb-glow');
48 | } else {
49 | document.getElementById(ftbId).classList.remove('lss-ftb-glow');
50 | }
51 | } else if (updateType === 'tbar') {
52 | const tbarId = `tbar-div`;
53 | const tbarHandleId = `tbar-handle-div`;
54 | if (document.getElementById(tbarId)) {
55 | document.getElementById(tbarId).style.height = `${device.data.tBar.percent * 1.4 + 10}px`;
56 | }
57 | if (document.getElementById(tbarHandleId)) {
58 | document.getElementById(tbarHandleId).style.bottom = `${device.data.tBar.percent * 1.4 + 10}px`;
59 | }
60 |
61 | if (device.data.tBar.status === 'Automatic') {
62 | document.getElementById(`auto`).classList.add('lss-red');
63 | } else {
64 | document.getElementById(`auto`).classList.remove('lss-red');
65 | }
66 | } else if (updateType === 'cut') {
67 | document.getElementById('cut').classList.add('lss-red');
68 | setTimeout(() => {
69 | document.getElementById('cut').classList.remove('lss-red');
70 | }, 250);
71 | }
72 | };
73 |
74 | exports.data = function data(_device, msg) {
75 | const device = _device;
76 |
77 | this.deviceInfoUpdate(device, 'status', 'ok');
78 |
79 | const packets = msg.toString().split('\n');
80 |
81 | packets.forEach((packet) => {
82 | const packetParts = packet.split(':');
83 | const packetType = packetParts[0];
84 | if (packetType === 'ILC') {
85 | if (device.data.inputs === undefined) {
86 | device.data.inputs = [];
87 | }
88 | device.data.inputs[packetParts[1]] = {
89 | number: Number.parseInt(packetParts[1], 10) + 1,
90 | name: packetParts[2].replaceAll('"', ''),
91 | audio: {
92 | level: parseFloat(packetParts[3]) / 1000,
93 | gain: parseFloat(packetParts[4]) / 1000,
94 | mute: packetParts[5] === '1',
95 | solo: packetParts[6] === '1',
96 | programLock: packetParts[7] === '1',
97 | },
98 | type: packetParts[8],
99 | };
100 | device.draw();
101 | device.update('inputs', device.data);
102 | } else if (packetType === 'ILCC') {
103 | if (device.data.inputCount !== undefined) {
104 | device.data.inputs = [];
105 | }
106 | device.data.inputCount = Number.parseInt(packetParts[1], 10);
107 | } else if (packetType === 'PmIS') {
108 | device.data.program = Number.parseInt(packetParts[1], 10) + 1;
109 | device.update('programInput', device.data);
110 | } else if (packetType === 'PwIS') {
111 | device.data.preview = Number.parseInt(packetParts[1], 10) + 1;
112 | device.update('previewInput', device.data);
113 | } else if (packetType === 'Cut') {
114 | [device.data.preview, device.data.program] = [device.data.program, device.data.preview];
115 | device.data.tBar.percent = 0;
116 | device.data.tBar.status = 'Stop';
117 | device.update('cut');
118 | device.update('inputs', device.data);
119 | device.update('tbar', device.data);
120 | } else if (packetType === 'FOut') {
121 | device.data.fadeToBlack = true;
122 | device.update('fadeToBlack', device.data);
123 | } else if (packetType === 'FIn') {
124 | device.data.fadeToBlack = false;
125 | device.update('fadeToBlack', device.data);
126 | } else if (packetType === 'TrASp' || packetType === 'TrMSp') {
127 | if (device.data.tBar === undefined) {
128 | device.data.tBar = {};
129 | }
130 | device.data.tBar.status = packetType === 'TrASp' ? 'Automatic' : 'Manual';
131 | device.data.tBar.percent = Number.parseInt(packetParts[1], 10) / 10;
132 | if (device.data.tBar.percent === 0) {
133 | device.data.tBar.status = 'Stop';
134 | }
135 | device.update('tbar', device.data);
136 | } else if (packetType === 'TrAStart' || packetType === 'TrAStop') {
137 | if (device.data.tBar === undefined) {
138 | device.data.tBar = {};
139 | }
140 | device.data.tBar.status = packetType.slice(3);
141 | if (packetType === 'TrAStop') {
142 | device.data.tBar.percent = 0;
143 | }
144 | device.update('tbar', device.data);
145 | }
146 | });
147 | };
148 |
--------------------------------------------------------------------------------
/plugins/livestream-studio/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #191919;
3 | }
4 | h1 {
5 | font-family: 'Open Sans', sans-serif;
6 | }
7 | h5 {
8 | color: white;
9 | margin: 0px 0px 10px 3px;
10 | font-weight: bold;
11 | width: 100%;
12 | /* font-family: 'Open Sans', sans-serif; */
13 | }
14 |
15 | .lss-input {
16 | width: 120px;
17 | height: 40px;
18 | background-color: #242424;
19 | color: white;
20 | border: #242424 6px solid;
21 | outline: black 1px solid;
22 | outline-offset: -1px;
23 | }
24 |
25 | .lss-button {
26 | width: 80px;
27 | height: 30px;
28 | background: linear-gradient(#363636, #282828);
29 | border-radius: 5px;
30 | border: black 2px solid;
31 | color: gray;
32 | margin-bottom: 5px;
33 | }
34 |
35 | .source-wrapper {
36 | display: flex;
37 | flex-wrap: wrap;
38 | background-color: #000000;
39 | width: 122px;
40 | border: black 1px solid;
41 | }
42 |
43 | .lss-red {
44 | border-color: #6f1607;
45 | outline-color: #ff0000;
46 | }
47 | .lss-button.lss-red {
48 | color: #6f1607;
49 | }
50 |
51 | .lss-green {
52 | border-color: #395a13;
53 | outline-color: #558522;
54 | }
55 |
56 | .lss-disabled {
57 | color: #3a3a3a;
58 | }
59 |
60 | .lss-ftb-glow {
61 | border: gray 2px solid;
62 | animation: ftb-glow 0.75s infinite alternate;
63 | }
64 |
65 | @keyframes ftb-glow {
66 | 0% {
67 | border-color: #666666;
68 | color: #666666;
69 | }
70 | 100% {
71 | border-color: #fc584b;
72 | color: #fc584b;
73 | }
74 | }
75 |
76 | .source-label {
77 | display: flex;
78 | align-items: center;
79 | justify-content: center;
80 | height: 100%;
81 | font-size: 11px;
82 | font-weight: bold;
83 | }
84 |
85 | .inputs-container {
86 | width: 200px;
87 | }
88 |
89 | .transition-container {
90 | display: flex;
91 | flex-wrap: wrap;
92 | justify-content: space-between;
93 | }
94 |
95 | .transition-settings {
96 | display: flex;
97 | flex-direction: column;
98 | justify-content: space-between;
99 | margin-right: 30px;
100 | }
101 |
102 | .tbar-container {
103 | display: flex;
104 | flex-direction: column-reverse;
105 | background-color: red;
106 | border: 2px black solid;
107 | width: 80px;
108 | height: 176px;
109 | position: relative;
110 | margin-bottom: 10px;
111 | }
112 |
113 | .tbar-bg {
114 | position: absolute;
115 | background-image: url('img/tbar_bg.png');
116 | width: 80px;
117 | height: 176px;
118 | }
119 | .tbar-handle {
120 | position: absolute;
121 | background-image: url('img/tbar_handle.png');
122 | width: 42px;
123 | height: 20px;
124 | left: 19px;
125 | bottom: 10px;
126 | }
127 | .tbar-div {
128 | width: 100%;
129 | background-color: #7aff58;
130 | overflow: hidden;
131 | }
132 |
133 | .fade-to-black-container {
134 | display: flex;
135 | flex-direction: column;
136 | align-items: flex-end;
137 | }
138 |
139 | .fade-to-black .source-wrapper {
140 | display: flex;
141 | width: 100%;
142 | padding: 5px;
143 | justify-content: center;
144 | }
145 |
146 | .fade-to-black h3 {
147 | text-align: center;
148 | }
149 |
150 | .clear {
151 | background: transparent;
152 | border-color: transparent;
153 | background-color: transparent;
154 | }
155 |
156 | .hide {
157 | display: none;
158 | }
159 |
160 | .no-wrap {
161 | flex-wrap: nowrap;
162 | }
163 |
164 | .show-small {
165 | display: none;
166 | }
167 |
168 | @media screen and (min-width: 0px) and (max-width: 480px) {
169 | .hide-small {
170 | display: none;
171 | }
172 | .show-small {
173 | display: block;
174 | }
175 | }
176 |
177 | .float-left {
178 | float: left;
179 | }
180 |
--------------------------------------------------------------------------------
/plugins/livestream-studio/template.ejs:
--------------------------------------------------------------------------------
1 |
4 |
5 | <% if (data.inputCount) { %>
6 |
16 |
17 |
18 |
19 |
45 |
46 |
47 |
TRANSITION
48 |
51 |
54 |
59 |
62 |
63 |
64 |
65 |
66 | <% } %>
67 |
--------------------------------------------------------------------------------
/plugins/pjlink/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/pjlink/icon.png
--------------------------------------------------------------------------------
/plugins/pjlink/info.html:
--------------------------------------------------------------------------------
1 | Connection Requirements
2 |
3 | - Requires no password on the projector
4 |
5 |
--------------------------------------------------------------------------------
/plugins/pjlink/main.js:
--------------------------------------------------------------------------------
1 | const md5 = require('md5');
2 |
3 | exports.config = {
4 | defaultName: 'PJLink Projector',
5 | connectionType: 'TCPsocket',
6 | remotePort: 4352,
7 | mayChangePorts: false,
8 | heartbeatInterval: 5000,
9 | heartbeatTimeout: 15000,
10 | searchOptions: {
11 | type: 'UDPsocket',
12 | searchBuffer: Buffer.from([0x25, 0x32, 0x53, 0x52, 0x43, 0x48, 0x0d]),
13 | devicePort: 4352,
14 | listenPort: 4352,
15 | validateResponse(msg, info) {
16 | return msg.toString().includes('%2ACKN=');
17 | },
18 | },
19 | fields: [
20 | {
21 | key: 'password',
22 | label: 'Pass',
23 | type: 'textinput',
24 | value: '',
25 | action(device) {
26 | device.plugin.heartbeat(device);
27 | },
28 | },
29 | ],
30 | };
31 |
32 | exports.ready = function ready(device) {
33 | // Power status query
34 | // device.send("%1POWR ?\r");
35 | };
36 |
37 | const PJLinkCmds = [
38 | '%1POWR=',
39 | '%1INPT=',
40 | '%1AVMT=',
41 | '%1ERST=',
42 | '%1LAMP=',
43 | '%1NAME=',
44 | '%1INF1=',
45 | '%1INF2=',
46 | '%2SNUM=',
47 | '%2SVER=',
48 | ];
49 |
50 | let passwordMD5 = false;
51 | let passwordSeed = false;
52 |
53 | function processPJLink(_device, str, that) {
54 | const arr = str.split('%');
55 | arr.shift();
56 | const device = _device;
57 |
58 | arr.forEach((response) => {
59 | const parts = response.split('=');
60 | const cmd = parts[0];
61 | const value = parts[1];
62 |
63 | switch (cmd) {
64 | case '1POWR':
65 | device.data.power = value;
66 | break;
67 |
68 | case '1INPT':
69 | device.data.input = value;
70 | break;
71 |
72 | case '1AVMT':
73 | device.data.avmute = value;
74 | break;
75 |
76 | case '1ERST':
77 | device.data.fanError = value[0];
78 | device.data.lampError = value[1];
79 | device.data.tempError = value[2];
80 | device.data.coverError = value[3];
81 | device.data.filterError = value[4];
82 | device.data.otherError = value[5];
83 | break;
84 |
85 | case '1LAMP':
86 | device.data.lamp = value.split(' ');
87 | break;
88 |
89 | case '1NAME':
90 | device.data.name = value;
91 | that.deviceInfoUpdate(device, 'defaultName', device.data.name);
92 | break;
93 |
94 | case '1INF1':
95 | device.data.info1 = value;
96 | break;
97 |
98 | case '1INF2':
99 | device.data.info2 = value;
100 | break;
101 |
102 | case '2SNUM':
103 | device.data.serial = value;
104 | break;
105 |
106 | case '2SVER':
107 | device.data.version = value;
108 | break;
109 |
110 | default:
111 | break;
112 | }
113 | });
114 | device.draw();
115 | }
116 |
117 | exports.data = function data(_device, message) {
118 | const device = _device;
119 | this.deviceInfoUpdate(device, 'status', 'ok');
120 | const msg = message.toString();
121 |
122 | if (msg.substring(0, 8) === 'PJLINK 1') {
123 | passwordSeed = msg.substring(9, 17);
124 | passwordMD5 = md5(`${passwordSeed}${device.fields.password}`);
125 | device.data.authentication = 'ON';
126 |
127 | device.send(
128 | `${passwordMD5}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r`
129 | );
130 | device.draw();
131 | } else if (msg.substring(0, 8) === 'PJLINK 0') {
132 | device.data.authentication = 'OFF';
133 | device.draw();
134 | } else if (msg.startsWith('PJLINK ERRA')) {
135 | device.data.passwordOK = false;
136 | device.draw();
137 | }
138 |
139 | if (PJLinkCmds.includes(msg.substring(0, 7))) {
140 | processPJLink(device, msg, this);
141 | device.data.passwordOK = true;
142 | }
143 | };
144 |
145 | exports.heartbeat = function heartbeat(device) {
146 | passwordMD5 = md5(`${passwordSeed}${device.fields.password}`);
147 | if (device.fields.password.length > 0 && device.data.passwordOK) {
148 | device.send(
149 | `${passwordMD5}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r`
150 | );
151 | } else if (device.fields.password.length > 0) {
152 | device.send(`${passwordMD5}%1POWR ?\r`);
153 | } else if (device.data.passwordOK) {
154 | device.send(`%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r`);
155 | } else {
156 | device.send(`%1POWR ?\r`);
157 | }
158 |
159 | device.draw();
160 | };
161 |
--------------------------------------------------------------------------------
/plugins/pjlink/styles.css:
--------------------------------------------------------------------------------
1 | .warning {
2 | color: #e9873a;
3 | }
4 | .error {
5 | color: #ed5f5d;
6 | }
7 | .ok {
8 | color: #79b757;
9 | }
10 | table {
11 | margin-bottom: 30px;
12 | width: 350px;
13 | }
14 |
--------------------------------------------------------------------------------
/plugins/pjlink/template.ejs:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | Power |
8 |
9 | <% if(data.power==0){ %>OFF<% } %>
10 | <% if(data.power==1){ %>ON<% } %>
11 | <% if(data.power==2){ %>COOLING DOWN<% } %>
12 | <% if(data.power==3){ %>WARMING UP<% } %>
13 | |
14 |
15 |
16 | AV Mute |
17 |
18 | <% if(data.avmute==11){ %>Video Mute<% } %>
19 | <% if(data.avmute==21){ %>Audio Mute<% } %>
20 | <% if(data.avmute==31){ %>Video & Audio Mute<% } %>
21 | <% if(data.avmute==30){ %>...<% } %>
22 | |
23 |
24 |
25 | Errors |
26 |
27 | <% if(data.fanError==1){ %>Fan Warning <% } %>
28 | <% if(data.fanError==2){ %>Fan Error <% } %>
29 |
30 | <% if(data.lampError==1){ %>Lamp Warning <% } %>
31 | <% if(data.lampError==2){ %>Lamp Error <% } %>
32 |
33 | <% if(data.tempError==1){ %>Temp Warning <% } %>
34 | <% if(data.tempError==2){ %>Temp Error <% } %>
35 |
36 | <% if(data.coverError==1){ %>Cover Warning <% } %>
37 | <% if(data.coverError==2){ %>Cover Error <% } %>
38 |
39 | <% if(data.filterError==1){ %>Filter Warning <% } %>
40 | <% if(data.filterError==2){ %>Filter Error <% } %>
41 |
42 | <% if(data.otherError==1){ %>Other Warning <% } %>
43 | <% if(data.otherError==2){ %>Other Error <% } %>
44 | |
45 |
46 |
47 | Input |
48 |
49 | <% if(data.input){ %>
50 | <% if(data.input[0]==1){ %>RGB<% } %>
51 | <% if(data.input[0]==2){ %>Video<% } %>
52 | <% if(data.input[0]==3){ %>Digital<% } %>
53 | <% if(data.input[0]==4){ %>Storage<% } %>
54 | <% if(data.input[0]==5){ %>Network<% } %>
55 | <%= data.input[1] %>
56 | <% } %>
57 | |
58 |
59 |
60 | <% if(data.lamp){ %>
61 | <% for(var i=0; i
62 |
63 | Lamp <%= i/2 %> |
64 |
65 | <% if(Number(data.lamp[i+1])){ %>
66 | ON
67 | <% }else{ %>
68 | OFF
69 | <% } %>
70 | |
71 |
72 | <% } %>
73 | <% } %>
74 |
75 |
76 | Name |
77 | <%= data.name %> |
78 |
79 |
80 | Info 1 |
81 | <%= data.info1 %> |
82 |
83 |
84 | Info 2 |
85 | <%= data.info2 %> |
86 |
87 |
88 | Serial Number |
89 | <%= data.serial %> |
90 |
91 |
92 | Version |
93 | <%= data.version %> |
94 |
95 |
96 |
97 |
98 | Authentication |
99 |
100 | <% if(!data.passwordOK){ %>
101 | INCORRECT PASSWORD
102 | <% }else if(data.authentication=="ON"){ %>
103 | ON
104 | <% }else{ %>
105 | <%= data.authentication %>
106 | <% } %>
107 |
108 | |
109 |
110 |
--------------------------------------------------------------------------------
/plugins/posistagenet/icon.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/posistagenet/icon.afphoto
--------------------------------------------------------------------------------
/plugins/posistagenet/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/posistagenet/icon.png
--------------------------------------------------------------------------------
/plugins/posistagenet/info.html:
--------------------------------------------------------------------------------
1 | PosiStageNet requires no configuration.
2 |
--------------------------------------------------------------------------------
/plugins/posistagenet/main.js:
--------------------------------------------------------------------------------
1 | const { Decoder } = require('@jwetzell/posistagenet');
2 |
3 | exports.config = {
4 | defaultName: 'PosiStageNet',
5 | connectionType: 'multicast',
6 | remotePort: 56565,
7 | mayChangePorts: false,
8 | heartbeatInterval: 5000,
9 | heartbeatTimeout: 15000,
10 | searchOptions: {
11 | type: 'multicast',
12 | address: `236.10.10.10`,
13 | port: 56565,
14 | validateResponse(msg, info) {
15 | // TODO(jwetzell): find a way to check that this is a posistagenet message
16 | return true;
17 | },
18 | },
19 | };
20 |
21 | exports.ready = function ready(_device) {
22 | const device = _device;
23 | device.data = {};
24 | const networkInterfaces = device.getNetworkInterfaces();
25 |
26 | Object.keys(networkInterfaces).forEach((networkInterfaceID) => {
27 | const networkInterface = networkInterfaces[networkInterfaceID];
28 | device.connection.addMembership(`236.10.10.10`, networkInterface[0].address);
29 | });
30 | };
31 |
32 | exports.data = function data(_device, buf, info) {
33 | const device = _device;
34 |
35 | if (!device.decoders) {
36 | device.decoders = {};
37 | }
38 |
39 | if (!device.decoders[info.address]) {
40 | device.decoders[info.address] = new Decoder();
41 | }
42 |
43 | device.decoders[info.address].decode(buf);
44 |
45 | Object.keys(device.decoders).forEach((address) => {
46 | device.data[address] = {
47 | trackers: device.decoders[address].trackers,
48 | system_name: device.decoders[address].system_name,
49 | fields: device.decoders[address].getTrackerFields(),
50 | };
51 | });
52 | device.draw();
53 | };
54 |
55 | exports.heartbeat = function heartbeat(device) {};
56 |
57 | exports.update = function update(_device, doc, updateType, updateData) {};
58 |
--------------------------------------------------------------------------------
/plugins/posistagenet/styles.css:
--------------------------------------------------------------------------------
1 | section {
2 | background-color: #2d2d2d;
3 | border: gray 1px solid;
4 | padding: 10px;
5 | padding-top: 0px;
6 | border-radius: 10px;
7 | overflow: hidden;
8 | }
9 | table {
10 | border: #414142 1px solid;
11 | background-color: #272727;
12 | }
13 | table,
14 | th,
15 | td {
16 | border-collapse: collapse;
17 | color: #ccc;
18 |
19 | font-size: 13px;
20 | }
21 | th {
22 | padding: 2px 10px;
23 | }
24 | td {
25 | text-align: center;
26 | padding: 2px 7px;
27 | font-family: 'Courier New', Courier, monospace;
28 | }
29 | .bg-pos {
30 | background-color: #443727;
31 | }
32 | .bg-speed {
33 | background-color: #32422e;
34 | }
35 | .bg-ori {
36 | background-color: #2e4242;
37 | }
38 | .bg-accel {
39 | background-color: #392b3f;
40 | }
41 | .bg-trgtpos {
42 | background-color: #3a2732;
43 | }
44 | .border-right {
45 | border-right: 1px solid #272727;
46 | }
47 |
--------------------------------------------------------------------------------
/plugins/posistagenet/template.ejs:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | <% Object.keys(data).forEach((address) => {
8 | const instanceData = data[address]
9 | %>
10 |
11 | Server: <%= instanceData.system_name %> (<%= address %>)
12 | Tracker Count: <%= Object.keys(instanceData.trackers).length %>
13 |
14 |
15 |
16 | |
17 | <% if(instanceData.fields.has('tracker_name')) { %>
18 | |
19 | <% } %>
20 |
21 | <% if(instanceData.fields.has('pos')) { %>
22 | POS |
23 | <% } %>
24 |
25 | <% if(instanceData.fields.has('speed')) { %>
26 | SPEED |
27 | <% } %>
28 |
29 | <% if(instanceData.fields.has('ori')) { %>
30 | ORI |
31 | <% } %>
32 |
33 | <% if(instanceData.fields.has('status')) { %>
34 | |
35 | <% } %>
36 |
37 | <% if(instanceData.fields.has('accel')) { %>
38 | ACCEL |
39 | <% } %>
40 |
41 | <% if(instanceData.fields.has('trgtpos')) { %>
42 | TRGTPOS |
43 | <% } %>
44 |
45 | <% if(instanceData.fields.has('timestamp')) { %>
46 | |
47 | <% } %>
48 |
49 |
50 | ID |
51 | <% if(instanceData.fields.has('tracker_name')) { %>
52 | Name |
53 | <% } %>
54 |
55 | <% if(instanceData.fields.has('pos')) { %>
56 | x |
57 | y |
58 | z |
59 | <% } %>
60 |
61 | <% if(instanceData.fields.has('speed')) { %>
62 | x |
63 | y |
64 | z |
65 | <% } %>
66 |
67 | <% if(instanceData.fields.has('ori')) { %>
68 | x |
69 | y |
70 | z |
71 | <% } %>
72 |
73 | <% if(instanceData.fields.has('status')) { %>
74 | validity |
75 | <% } %>
76 |
77 | <% if(instanceData.fields.has('accel')) { %>
78 | x |
79 | y |
80 | z |
81 | <% } %>
82 |
83 | <% if(instanceData.fields.has('trgtpos')) { %>
84 | x |
85 | y |
86 | z |
87 | <% } %>
88 |
89 | <% if(instanceData.fields.has('timestamp')) { %>
90 | tracker_timestamp |
91 | <% } %>
92 |
93 |
94 |
95 | <% Object.entries(instanceData.trackers).forEach(([trackerId,trackerData]) => { %>
96 |
97 |
98 | <%= trackerId %> |
99 | <% if(instanceData.fields.has('tracker_name')) { %>
100 | <%= trackerData.tracker_name?.tracker_name%> |
101 | <% } %>
102 |
103 | <% if(instanceData.fields.has('pos')) { %>
104 |
105 | <%= trackerData.pos?.pos_x?.toFixed(2) %> |
106 | <%= trackerData.pos?.pos_y?.toFixed(2) %> |
107 | <%= trackerData.pos?.pos_z?.toFixed(2) %> |
108 | <% } %>
109 |
110 | <% if(instanceData.fields.has('speed')) { %>
111 |
112 | <%= trackerData.speed?.speed_x?.toFixed(2) %> |
113 | <%= trackerData.speed?.speed_y?.toFixed(2) %> |
114 | <%= trackerData.speed?.speed_z?.toFixed(2) %> |
115 | <% } %>
116 |
117 | <% if(instanceData.fields.has('ori')) { %>
118 |
119 | <%= trackerData.ori?.ori_x?.toFixed(2) %> |
120 | <%= trackerData.ori?.ori_y?.toFixed(2) %> |
121 | <%= trackerData.ori?.ori_z?.toFixed(2) %> |
122 | <% } %>
123 |
124 | <% if(instanceData.fields.has('status')) { %>
125 | <%= trackerData.status?.validity?.toFixed(2) %> |
126 | <% } %>
127 |
128 | <% if(instanceData.fields.has('accel')) { %>
129 |
130 | <%= trackerData.accel?.accel_x?.toFixed(2) %> |
131 | <%= trackerData.accel?.accel_y?.toFixed(2) %> |
132 | <%= trackerData.accel?.accel_z?.toFixed(2) %> |
133 | <% } %>
134 |
135 | <% if(instanceData.fields.has('trgtpos')) { %>
136 |
137 | <%= trackerData.trgtpos?.trgtpos_x?.toFixed(2) %> |
138 | <%= trackerData.trgtpos?.trgtpos_y?.toFixed(2) %> |
139 | <%= trackerData.trgtpos?.trgtpos_z?.toFixed(2) %> |
140 | <% } %>
141 |
142 | <% if(instanceData.fields.has('timestamp')) { %>
143 | <%= trackerData.timestamp?.tracker_timestamp %> |
144 | <% } %>
145 |
146 |
147 |
148 | <% }); %>
149 |
150 |
151 | <%
152 | });
153 | %>
154 |
--------------------------------------------------------------------------------
/plugins/qlab/cart.ejs:
--------------------------------------------------------------------------------
1 | <% let cueKeys = allCues[cueList.uniqueID]; %>
2 |
3 |
4 |
5 |
<%= workspace.displayName %> — <%= cueList.listName %>
6 | <%
7 | let width = 100/cueKeys.cartColumns;
8 | let height = 600/(cueKeys.cartRows);
9 | %>
10 |
11 |
12 |
13 | <% for(let r=0; r
14 | <% for(let c=0; c
15 | <% let style = `style="left:${width*c}%; top:${height*r}px; width:${width }%; height:${height}px;"`; %>
16 |
17 |
18 |
19 | <% } %>
20 | <% } %>
21 |
22 |
23 | <% for(let i=0; i=cueKeys.cartColumns || row>=cueKeys.cartRows){
38 | continue;
39 | }
40 |
41 | %>
42 |
43 | <%= tileTemplate({cue: cueList.cues[i], allCues: allCues, workspace: workspace}) %>
44 |
45 | <% } %>
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/plugins/qlab/cue.ejs:
--------------------------------------------------------------------------------
1 | <% let cueKeys = allCues[cue.uniqueID]; %>
2 | <% let playbackPosition = workspace.playbackPosition==cue.uniqueID ? "playback-position" : "" %>
3 | <% let selected = workspace.selected.includes(cue.uniqueID) ? "selected": "" %>
4 |
5 | <% if(cueKeys && cueKeys.parent){ %>
6 |
7 |
8 |
9 |
10 | |
11 |
12 |
13 |
14 |
15 | <% if(cueKeys.isBroken){ %>
16 |
17 |
18 | <% }else if(cueKeys.isRunning){ %>
19 |
20 |
21 | <% }else if(cueKeys.isPaused){ %>
22 |
23 |
24 | <% }else if(cueKeys.isLoaded){ %>
25 |
26 |
27 | <% }else if(cueKeys.isFlagged){ %>
28 |
29 |
30 | <% }else{ %>
31 |
32 | <% } %>
33 | |
34 |
35 |
36 |
37 | <% if(cueKeys.type == "Group" && cueKeys.mode){ %>
38 |  |
39 |
40 | <% }else{ %>
41 | .replace(' ','-') %>.png) |
42 |
43 | <% } %>
44 |
45 |
46 |
47 |
48 | <%= cueKeys.number || " " %> |
49 |
50 |
51 | <% let nextCue = cueKeys.cueInWorkspace.parent.cues[cueKeys.cueInWorkspace.sortIndex+1]; %>
52 |
53 |
54 | <% for(let i=0; i
55 | <% let parent = cueKeys.parentKeys[i+1]; %>
56 | <% %>
57 |
58 | <% if(cueKeys.type=="Group" && cueKeys.cues.length==0 && i==cueKeys.parentKeys.length-1){ %>
59 | |
60 |
61 | <% }else if(cueKeys.type=="Group" && i==cueKeys.parentKeys.length-1){ %>
62 | |
63 |
64 | <%}else if(parent){%>
65 |
66 | <% let parentCues = parent.cueInWorkspace.cues %>
67 | <% let lastCueInMe = parentCues[parentCues.length-1]%>
68 |
69 | <% while(lastCueInMe.type=="Group" && lastCueInMe.cues.length){
70 | parentCues = lastCueInMe.cues;
71 | lastCueInMe = parentCues[parentCues.length-1];
72 | } %>
73 |
74 | <% if(lastCueInMe.uniqueID == cueKeys.uniqueID){ %>
75 | |
76 |
77 | <% }else{ %>
78 | |
79 |
80 |
81 | <% } %>
82 |
83 |
84 | <% }else if(cueKeys.parentKeys[i]){ %>
85 | <% let nextCue2 = cueKeys.parentKeys[i].cues[cueKeys.cueInWorkspace.sortIndex+1]; %>
86 |
87 | <% if(!nextCue2){ %>
88 | |
89 |
90 | <% }else{ %>
91 | |
92 |
93 | <% } %>
94 |
95 |
96 | <% } %>
97 |
98 | <% } %>
99 |
100 |
101 |
102 |
103 |
104 | <% if(cue.type=="Group"){ %>
105 |
106 | <% if(cue.cues && cue.cues.length==0){ %>
107 | <%= cueKeys.listName %> |
108 |
109 | <% }else{ %>
110 | <%= cueKeys.listName %> |
111 |
112 | <% } %>
113 |
114 | <% }else if(!nextCue){ %>
115 | <%= cueKeys.listName %> |
116 |
117 | <% }else{ %>
118 | <%= cueKeys.listName %> |
119 |
120 | <% } %>
121 |
122 |
123 |
124 |
125 |
126 | <% if(cueKeys.currentCueTarget){ %>
127 | <%= allCues[cueKeys.currentCueTarget].number || allCues[cueKeys.currentCueTarget].listName %> |
128 |
129 | <% }else{ %>
130 | |
131 |
132 | <% } %>
133 |
134 |
135 |
136 |
137 | <% if(cueKeys.preWait){ %>
138 | <%= elapsedTime(cueKeys.preWait, cueKeys.preWaitElapsed, "preWait", cueKeys) %> |
139 |
140 | <% }else{ %>
141 | 00:00.00 |
142 |
143 | <% } %>
144 |
145 |
146 |
147 |
148 | <% let cueTypesWithAction = ["Audio", "Mic", "Video", "Camera", "Text", "Light", "Fade", "Network", "MIDI File", "Timecode", "Wait"]; %>
149 | <% if((cueKeys.type=="Group" && cueKeys.mode==3) || (cueKeys.type=="Group" && cueKeys.mode==6) || cueTypesWithAction.includes(cueKeys.type)){ %>
150 | <%= elapsedTime(cueKeys.duration, cueKeys.actionElapsed, "action", cueKeys) %> |
151 |
152 | <% }else{ %>
153 | |
154 |
155 | <% } %>
156 |
157 |
158 |
159 | <% if(cueKeys.postWait){ %>
160 | <%= elapsedTime(cueKeys.postWait, cueKeys.postWaitElapsed, "postWait", cueKeys) %> |
161 |
162 | <% }else{ %>
163 | 00:00.00 |
164 |
165 | <% } %>
166 |
167 |
168 |
169 |
170 | <% if(cueKeys.continueMode==2){ %>
171 |  |
172 |
173 | <% }else if(cueKeys.continueMode==1){ %>
174 |  |
175 |
176 | <% }else{ %>
177 | |
178 |
179 | <% } %>
180 |
181 |
182 |
183 |
184 |
185 | <% }else{ %>
186 |
187 |
188 | |
189 | |
190 | |
191 | <%= cue.number || " " %> |
192 | <%= cue.listName %> |
193 | |
194 | 00:00.00 |
195 | |
196 | 00:00.00 |
197 | |
198 |
199 |
200 | <% } %>
201 |
202 | <%
203 | function prettyFormatTime(seconds){
204 | if(!seconds){
205 | return "00:00.00";
206 | }
207 | var startIndex = 14;
208 | if(seconds>=3600){
209 | startIndex = 11;
210 | }
211 | var string = new Date(seconds * 1000).toISOString()
212 | return string.substring(startIndex, string.length-2)
213 | }
214 |
215 | function elapsedTime(def, value, type, cue){
216 |
217 | if(type=="action" && cue.isPaused){
218 | value-=cue.preWait;
219 | }
220 |
221 | let border, fill;
222 | if(cue.isPaused){
223 | border = "#f6e737";
224 | fill = "rgba(255, 240, 60, 0.7)";
225 | }else{
226 | border = "#48ba41";
227 | fill = "rgba(0, 200, 50, 0.5)";
228 | }
229 |
230 |
231 | if(value>0){
232 | let percent = value/def*100;
233 | let bg = `style='background: black; background: linear-gradient(90deg, ${fill} ${percent}%, transparent ${percent}%);`;
234 |
235 | if(cue.isPaused){
236 | bg+= "outline-color: "+border+";";
237 | bg+= "color: white;";
238 | }
239 | bg+="'";
240 |
241 | return `${prettyFormatTime(Math.min(def, value))}`;
242 |
243 | }else if(type=="postWait" && cue.continueMode==0){
244 | return `
${prettyFormatTime(def)}`;
245 |
246 | }else{
247 | return prettyFormatTime(def);
248 |
249 | }
250 | }
251 | %>
--------------------------------------------------------------------------------
/plugins/qlab/cuelist.ejs:
--------------------------------------------------------------------------------
1 |
<%= workspace.displayName %> — <%= cueList.listName %>
2 |
3 |
4 |
5 | |
6 | |
7 | |
8 | Number |
9 | Name |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
20 | Target |
21 | Pre-Wait |
22 | Duration |
23 | Post-Wait |
24 |
25 |
26 | |
27 |
28 |
29 |
30 | <% for(let i=0; i
31 | <%= displayCueRow(cueList.cues[i]) %>
32 | <% } %>
33 |
34 |
35 |
36 |
37 |
38 | <%
39 | function displayCueRow(q){
40 | let html = rowTemplate({cue: q, allCues: allCues, workspace: workspace});
41 |
42 | if(q.cues){
43 | for(let i=0; i
--------------------------------------------------------------------------------
/plugins/qlab/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/icon.png
--------------------------------------------------------------------------------
/plugins/qlab/img/arm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/arm.png
--------------------------------------------------------------------------------
/plugins/qlab/img/arrow-down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/arrow-down.png
--------------------------------------------------------------------------------
/plugins/qlab/img/arrow-right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/arrow-right.png
--------------------------------------------------------------------------------
/plugins/qlab/img/audio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/audio.png
--------------------------------------------------------------------------------
/plugins/qlab/img/auto_continue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/auto_continue.png
--------------------------------------------------------------------------------
/plugins/qlab/img/auto_continue_stubby.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/auto_continue_stubby.png
--------------------------------------------------------------------------------
/plugins/qlab/img/auto_follow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/auto_follow.png
--------------------------------------------------------------------------------
/plugins/qlab/img/camera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/camera.png
--------------------------------------------------------------------------------
/plugins/qlab/img/devamp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/devamp.png
--------------------------------------------------------------------------------
/plugins/qlab/img/disarm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/disarm.png
--------------------------------------------------------------------------------
/plugins/qlab/img/disarmed-pattern-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/disarmed-pattern-light.png
--------------------------------------------------------------------------------
/plugins/qlab/img/disarmed-pattern-light.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/disarmed-pattern-light.tiff
--------------------------------------------------------------------------------
/plugins/qlab/img/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/empty.png
--------------------------------------------------------------------------------
/plugins/qlab/img/fade.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/fade.png
--------------------------------------------------------------------------------
/plugins/qlab/img/goto.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/goto.png
--------------------------------------------------------------------------------
/plugins/qlab/img/group-arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/group-arrow.png
--------------------------------------------------------------------------------
/plugins/qlab/img/group.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/group.png
--------------------------------------------------------------------------------
/plugins/qlab/img/light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/light.png
--------------------------------------------------------------------------------
/plugins/qlab/img/load.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/load.png
--------------------------------------------------------------------------------
/plugins/qlab/img/memo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/memo.png
--------------------------------------------------------------------------------
/plugins/qlab/img/mic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/mic.png
--------------------------------------------------------------------------------
/plugins/qlab/img/midi-file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/midi-file.png
--------------------------------------------------------------------------------
/plugins/qlab/img/midi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/midi.png
--------------------------------------------------------------------------------
/plugins/qlab/img/network.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/network.png
--------------------------------------------------------------------------------
/plugins/qlab/img/new_group_arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/new_group_arrow.png
--------------------------------------------------------------------------------
/plugins/qlab/img/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/pause.png
--------------------------------------------------------------------------------
/plugins/qlab/img/pause_circled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/pause_circled.png
--------------------------------------------------------------------------------
/plugins/qlab/img/play_circled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/play_circled.png
--------------------------------------------------------------------------------
/plugins/qlab/img/playhead.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/playhead.afdesign
--------------------------------------------------------------------------------
/plugins/qlab/img/playhead.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/playhead.png
--------------------------------------------------------------------------------
/plugins/qlab/img/reset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/reset.png
--------------------------------------------------------------------------------
/plugins/qlab/img/script.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/script.png
--------------------------------------------------------------------------------
/plugins/qlab/img/start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/start.png
--------------------------------------------------------------------------------
/plugins/qlab/img/status_broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_broken.png
--------------------------------------------------------------------------------
/plugins/qlab/img/status_broken_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_broken_white.png
--------------------------------------------------------------------------------
/plugins/qlab/img/status_flagged.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_flagged.png
--------------------------------------------------------------------------------
/plugins/qlab/img/status_loaded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_loaded.png
--------------------------------------------------------------------------------
/plugins/qlab/img/status_paused.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_paused.png
--------------------------------------------------------------------------------
/plugins/qlab/img/status_running.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_running.png
--------------------------------------------------------------------------------
/plugins/qlab/img/status_spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_spinner.gif
--------------------------------------------------------------------------------
/plugins/qlab/img/status_spinner.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_spinner.psd
--------------------------------------------------------------------------------
/plugins/qlab/img/stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/stop.png
--------------------------------------------------------------------------------
/plugins/qlab/img/target.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/target.png
--------------------------------------------------------------------------------
/plugins/qlab/img/text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/text.png
--------------------------------------------------------------------------------
/plugins/qlab/img/timecode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/timecode.png
--------------------------------------------------------------------------------
/plugins/qlab/img/v5/group-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/v5/group-1.png
--------------------------------------------------------------------------------
/plugins/qlab/img/v5/group-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/v5/group-2.png
--------------------------------------------------------------------------------
/plugins/qlab/img/v5/group-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/v5/group-3.png
--------------------------------------------------------------------------------
/plugins/qlab/img/v5/group-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/v5/group-4.png
--------------------------------------------------------------------------------
/plugins/qlab/img/v5/group-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/v5/group-6.png
--------------------------------------------------------------------------------
/plugins/qlab/img/video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/video.png
--------------------------------------------------------------------------------
/plugins/qlab/img/wait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/wait.png
--------------------------------------------------------------------------------
/plugins/qlab/info.html:
--------------------------------------------------------------------------------
1 | Connection Requirements
2 |
3 | - View permission enabled in OSC Access
4 | - Provide passcode if necessary
5 |
6 |
--------------------------------------------------------------------------------
/plugins/qlab/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin-bottom: 200px !important;
3 | }
4 | table {
5 | background-color: #323232;
6 | border: 0px;
7 | color: #b8b8b8;
8 | font-family: sans-serif;
9 | width: 100%;
10 | table-layout: fixed;
11 | margin-bottom: 30px;
12 | }
13 | th {
14 | font-size: 12px;
15 | font-weight: normal;
16 | padding: 0px 4px;
17 | background-color: #2d2c2d;
18 | border: #545454 1px solid;
19 | border-right: none;
20 | }
21 | th.no-border {
22 | border-left: none;
23 | border-right: none;
24 | padding: 0px;
25 | }
26 |
27 | td {
28 | padding: -1px;
29 | height: 25px;
30 | overflow: hidden;
31 | white-space: nowrap;
32 | text-overflow: ellipsis;
33 | }
34 | td img {
35 | padding-top: 2px;
36 | }
37 |
38 | tr:nth-child(even) {
39 | background: #2d2d2d;
40 | }
41 | tr:nth-child(odd) {
42 | background: #262626;
43 | }
44 |
45 | tr:nth-child(odd).q-red {
46 | background: linear-gradient(to top, #262626, #49302f 1px);
47 | }
48 | tr:nth-child(even).q-red {
49 | background: linear-gradient(to top, #2d2c2d, #513635 1px);
50 | }
51 | .q-red td.playhead {
52 | background: linear-gradient(to right, #ff4242 55%, transparent 55%);
53 | }
54 |
55 | tr:nth-child(odd).q-orange {
56 | background: linear-gradient(to top, #262626, #443526 1px);
57 | }
58 | tr:nth-child(even).q-orange {
59 | background: linear-gradient(to top, #2d2c2d, #4a3b2c 1px);
60 | }
61 | .q-orange td.playhead {
62 | background: linear-gradient(to right, #ffa500 55%, transparent 55%);
63 | }
64 |
65 | tr:nth-child(odd).q-green {
66 | background: linear-gradient(to top, #262626, #2d3d27 1px);
67 | }
68 | tr:nth-child(even).q-green {
69 | background: linear-gradient(to top, #2d2c2d, #32422e 1px);
70 | }
71 | .q-green td.playhead {
72 | background: linear-gradient(to right, #01d52f 55%, transparent 55%);
73 | }
74 |
75 | tr:nth-child(odd).q-blue {
76 | background: linear-gradient(to top, #262626, #292d40 1px);
77 | }
78 | tr:nth-child(even).q-blue {
79 | background: linear-gradient(to top, #2d2c2d, #2f3346 1px);
80 | }
81 | .q-blue td.playhead {
82 | background: linear-gradient(to right, #536de0 55%, transparent 55%);
83 | }
84 |
85 | tr:nth-child(odd).q-purple {
86 | background: linear-gradient(to top, #262626, #342639 1px);
87 | }
88 | tr:nth-child(even).q-purple {
89 | background: linear-gradient(to top, #2d2c2d, #382c3f 1px);
90 | }
91 | .q-purple td.playhead {
92 | background: linear-gradient(to right, #a601c0 55%, transparent 55%);
93 | }
94 |
95 | tr.q-armed-false td {
96 | background: url('img/disarmed-pattern-light.png');
97 | background-attachment: fixed;
98 | }
99 |
100 | tr.playback-position {
101 | background: #444 !important;
102 | color: white;
103 | }
104 | tr.playback-position td.playhead {
105 | padding: 0px;
106 | }
107 | tr.playback-position td.playhead::before {
108 | content: url(img/playhead.png);
109 | }
110 | tr.playback-position .q-gray-text {
111 | color: white !important;
112 | }
113 | tr.selected {
114 | background: #1557da !important;
115 | color: white !important;
116 | }
117 |
118 | .gLeft {
119 | height: 24px;
120 | width: 12px;
121 | border-left: 2px solid;
122 | }
123 | .gTop {
124 | border-top: 2px solid;
125 | }
126 | .gBot {
127 | border-bottom: 2px solid;
128 | }
129 | .gRight {
130 | border-right: 2px solid;
131 | }
132 | .gMode-1.gLeft.gTop {
133 | border-radius: 6px 0px 0px 0px;
134 | }
135 | .gMode-1.gLeft.gBot {
136 | border-radius: 0px 0px 0px 6px;
137 | }
138 | .gMode-1.gRight.gBot {
139 | border-radius: 0px 0px 6px 0px;
140 | }
141 | .gMode-1.gLeft.gTop.gBot {
142 | border-radius: 6px 0px 0px 6px;
143 | }
144 | .gMode-1.gLeft.gTop.gRight {
145 | border-radius: 6px 6px 0px 0px;
146 | }
147 | .gMode-1.gTop.gRight.gBot {
148 | border-radius: 0px 6px 6px 0px;
149 | }
150 | .gMode-1.gLeft.gTop.gRight.gBot {
151 | border-radius: 6px;
152 | }
153 | .gMode-1 {
154 | border-color: #48477f;
155 | }
156 | .gMode-2 {
157 | border-color: #48477f;
158 | }
159 | .gMode-3 {
160 | border-color: #43a424;
161 | }
162 | .gMode-4 {
163 | border-color: #7f26a5;
164 | }
165 | .gMode-6 {
166 | border-color: #ee6a21;
167 | }
168 | .gMode-,
169 | .gMode-0 {
170 | border-color: rgba(0, 0, 0, 0);
171 | }
172 | .group-arrow {
173 | width: 13px;
174 | background-image: url('img/new_group_arrow.png');
175 | background-position: center;
176 | background-repeat: no-repeat;
177 | }
178 |
179 | .q-time {
180 | font-size: 14px;
181 | text-align: center;
182 | }
183 | .q-target {
184 | text-align: center;
185 | font-size: 12px;
186 | padding-left: 5px;
187 | padding-right: 5px;
188 | }
189 | .q-gray-text {
190 | color: #424242;
191 | }
192 | .q-time-elapsed {
193 | margin: 0px 2px;
194 | outline: #49c042 1px solid;
195 | line-height: 18px;
196 | }
197 |
198 | .cart {
199 | position: relative;
200 | height: 600px;
201 | width: 100%;
202 | background-color: #2c2b2a;
203 | }
204 | .cartCueWrapper {
205 | display: block;
206 | position: absolute;
207 | box-sizing: border-box;
208 | padding: 3px;
209 | }
210 | .cartCueWrapper.selected .cartCue {
211 | border-color: #89b4db;
212 | box-shadow: #89b4db 0px 0px 2px 3px;
213 | }
214 | .cartCue {
215 | height: 100%;
216 | box-sizing: border-box;
217 |
218 | border: 3px solid;
219 | border-radius: 6px;
220 | color: white;
221 | }
222 | .cartCue p {
223 | margin: 10px;
224 | }
225 | .cartCueIcon {
226 | position: absolute;
227 | right: 13px;
228 | top: 13px;
229 | }
230 | div.cartColor-red {
231 | border-color: #e7443a;
232 | background-color: #942f27;
233 | }
234 | div.cartColor-orange {
235 | border-color: #f18f28;
236 | background-color: #a95023;
237 | }
238 | div.cartColor-green {
239 | border-color: #4ebf32;
240 | background-color: #397824;
241 | }
242 | div.cartColor-blue {
243 | border-color: #3a54cf;
244 | background-color: #25378a;
245 | }
246 | div.cartColor-purple {
247 | border-color: #7e25a5;
248 | background-color: #4c2269;
249 | }
250 | div.cartColor-none {
251 | border-color: #95929f;
252 | background-color: #3b3b3b;
253 | }
254 | .cartBlank {
255 | border-color: #1f1f1f;
256 | background-color: #1f1f1f;
257 | }
258 | .cartCueWrapper.playback-position .cartCue {
259 | border-color: #88b3db;
260 | outline: #88b3db 1px solid;
261 | box-shadow: 0px 0px 3px 3px #88b3db, inset 0px 0px 5px #88b3db;
262 | }
263 |
264 | #playhead-information {
265 | width: 80%;
266 | height: 120px;
267 | position: fixed;
268 | bottom: 30px;
269 | left: 10%;
270 | padding: 16px;
271 | box-sizing: border-box;
272 |
273 | background-color: #2e2d2d;
274 | border: #424242 3px solid;
275 | border-radius: 4px;
276 | box-shadow: black 0px 0px 20px 5px;
277 | }
278 | #playhead-information.playhead-active {
279 | border-color: #4cbe34;
280 | }
281 | .playhead-name {
282 | width: 100%;
283 | padding: 6px 12px;
284 | box-sizing: border-box;
285 | margin-bottom: 12px;
286 |
287 | background-color: #434343;
288 | border: #434343 1px solid;
289 | font-size: 18px;
290 | color: white;
291 | border-radius: 3px;
292 | }
293 | .playhead-disarmed {
294 | background: url('img/disarmed-pattern-light.png');
295 | background-attachment: fixed;
296 | }
297 | #playhead-notes {
298 | width: 100%;
299 | padding: 6px 8px;
300 | box-sizing: border-box;
301 | height: 36px;
302 |
303 | border: #5c5b5b 1px solid;
304 | font-size: 18px;
305 | color: #9a9a99;
306 | border-radius: 3px;
307 | overflow: hidden;
308 | text-overflow: ellipsis;
309 | }
310 |
311 | @media screen and (min-width: 0px) and (max-width: 750px) {
312 | .hide-medium {
313 | display: none;
314 | }
315 | }
316 | @media screen and (min-width: 0px) and (max-width: 550px) {
317 | .hide-small {
318 | display: none;
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/plugins/qlab/template.ejs:
--------------------------------------------------------------------------------
1 |
2 | <%= listName %>
3 | QLab <%= data.version || "" %>
4 |
5 |
6 |
7 |
8 | <% const workspaceKeys = Object.keys(data.workspaces); %>
9 |
10 | <% if(workspaceKeys.length==0){ %>
11 | QLab is open but there isn't an open Workspace
12 | <% } %>
13 |
14 |
15 | <% for(let i=0; i
16 | <% const workspace = data.workspaces[workspaceKeys[i]]; %>
17 |
18 | <% if(workspace.permission=="ok"){ %>
19 |
20 | <% for(let j=0; j
21 | <% const cueList = workspace.cueLists[j]; %>
22 |
23 | <% if(cueList.type=="Cue List"){ %>
24 | <%= templates.cuelist({cueList: cueList, allCues: data.cueKeys, rowTemplate: templates.cue, workspace: workspace}) %>
25 | <% }else if(cueList.type=="Cart"){ %>
26 | <%= templates.cart({cueList: cueList, allCues: data.cueKeys, tileTemplate: templates.tile, workspace: workspace}) %>
27 | <% } %>
28 |
29 | <% } %>
30 |
31 | <% }else{ %>
32 |
33 | <%= workspace.displayName %> — Incorrect Passcode or OSC Access Permissions
34 |
35 |
36 | <% } %>
37 |
38 | <% } %>
39 |
40 |
44 |
45 |
--------------------------------------------------------------------------------
/plugins/qlab/tile.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | let height = 0;
3 | let width = 0;
4 | let left = 0;
5 | let top = 0;
6 | let cueKeys = allCues[cue.uniqueID];
7 | let parent = allCues[cue.parent];
8 |
9 | if(cueKeys.parent){
10 | width = 100/cueKeys.parent.cartColumns;
11 | height = 600/cueKeys.parent.cartRows;
12 |
13 | top = (cueKeys.cartPosition[0]-1)*height;
14 | left = (cueKeys.cartPosition[1]-1)*width;
15 | }
16 |
17 | let style = `style="left:${left}%; top:${top}px; width:${width }%; height:${height}px;"`;
18 |
19 | %>
20 |
21 | >
22 |
23 |
24 | <% if(cueKeys.isBroken){ %>
25 |

26 | <% }else if(cueKeys.isRunning){ %>
27 |

28 | <% }else{ %>
29 |

30 | <% } %>
31 |
32 |
33 |
34 |
35 | <%= cueKeys.number %>
36 | <% if(cueKeys.number){ %> • <% } %>
37 | <%= cueKeys.displayName %>
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/plugins/sacn/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/sacn/icon.png
--------------------------------------------------------------------------------
/plugins/sacn/info.html:
--------------------------------------------------------------------------------
1 | sACN requires no configuration.
2 |
--------------------------------------------------------------------------------
/plugins/sacn/main.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 |
3 | exports.config = {
4 | defaultName: 'sACN',
5 | connectionType: 'multicast',
6 | remotePort: 5568,
7 | mayChangePorts: false,
8 | heartbeatInterval: 5000,
9 | heartbeatTimeout: 15000,
10 | searchOptions: {
11 | type: 'multicast',
12 | address: getMulticastGroup(1),
13 | port: 5568,
14 | validateResponse(msg, info) {
15 | return msg.toString('utf8', 4, 13) === 'ASC-E1.17';
16 | },
17 | },
18 | };
19 |
20 | exports.ready = function ready(device) {
21 | const d = device;
22 | d.data.universes = {};
23 | d.data.priorities = {};
24 | d.data.source = 'Unknown Source';
25 | d.data.orderedUniverses = [];
26 |
27 | const networkInterfaces = d.getNetworkInterfaces();
28 |
29 | for (let i = 1; i <= 16; i++) {
30 | for (let j = 0; j < Object.keys(networkInterfaces).length; j++) {
31 | const networkInterfaceID = Object.keys(networkInterfaces)[j];
32 | const networkInterface = networkInterfaces[networkInterfaceID];
33 | d.connection.addMembership(getMulticastGroup(i), networkInterface[0].address);
34 | }
35 | }
36 | };
37 |
38 | exports.data = function data(_device, buf) {
39 | const universeIndex = buf.readUInt16BE(113);
40 | const device = _device;
41 |
42 | let universe = device.data.universes[universeIndex];
43 | let priorities = device.data.priorities[universeIndex];
44 |
45 | if (!universe) {
46 | device.data.universes[universeIndex] = {};
47 | universe = device.data.universes[universeIndex];
48 | }
49 | if (!priorities) {
50 | device.data.priorities[universeIndex] = new Array(512).fill(0);
51 | priorities = device.data.priorities[universeIndex];
52 | }
53 |
54 | universe.sequence = buf.readUInt8(111);
55 | universe.priority = buf.readUInt8(108);
56 | universe.cid = buf.toString('hex', 22, 38);
57 | universe.slots = buf.slice(126);
58 | universe.startCode = buf.readUInt8(125);
59 |
60 | device.data.source = buf.toString('utf8', 44, 108);
61 | device.displayName = `${device.data.source} sACN`;
62 | device.data.ip = device.addresses[0];
63 |
64 | if (!_.includes(device.data.orderedUniverses, universeIndex)) {
65 | device.data.orderedUniverses.push(universeIndex);
66 | device.data.orderedUniverses.sort();
67 | universe.slotElems = [];
68 | universe.slotElemsSet = false;
69 |
70 | if (universe.priority > 0 && universe.startCode === 0) {
71 | device.draw();
72 | device.update('elementCache');
73 | }
74 | }
75 | if (universe.priority > 0 && universe.startCode === 0) {
76 | device.update('universeData', {
77 | universeIndex,
78 | universe,
79 | startCode: universe.startCode,
80 | });
81 | } else {
82 | device.data.priorities[universeIndex] = buf.slice(126);
83 | }
84 | };
85 |
86 | exports.heartbeat = function heartbeat(device) {};
87 |
88 | let lastUpdate = Date.now();
89 | exports.update = function update(_device, doc, updateType, updateData) {
90 | const device = _device;
91 | const data = updateData;
92 |
93 | if (updateType === 'universeData' && data.universe) {
94 | if (Date.now() - lastUpdate > 1000) {
95 | lastUpdate = Date.now();
96 | device.update('elementCache');
97 | }
98 |
99 | const $elem = doc.getElementById(`universe-${data.universeIndex}`);
100 |
101 | if ($elem && data.universe.slotElemsSet) {
102 | for (let i = 0; i < 512; i++) {
103 | data.universe.slotElems[i].textContent = data.universe.slots[i];
104 | }
105 | const $code = doc.getElementById(`universe-${data.universeIndex}-code`);
106 | if (data.startCode === 0xdd) {
107 | $code.textContent = 'Net3';
108 | } else if (data.startCode === 0x17) {
109 | $code.textContent = 'Text';
110 | } else if (data.startCode === 0xcf) {
111 | $code.textContent = 'SIP';
112 | } else if (data.startCode === 0xcc) {
113 | $code.textContent = 'RDM';
114 | }
115 | } else if (data.universe.startCode === 0) {
116 | device.draw();
117 | device.update('elementCache');
118 | }
119 | } else if (updateType === 'elementCache') {
120 | device.data.orderedUniverses.forEach((universeIndex) => {
121 | const universe = device.data.universes[universeIndex];
122 |
123 | if (doc.getElementById(`${universeIndex}-0`)) {
124 | for (let i = 0; i < 512; i++) {
125 | universe.slotElems[i] = doc.getElementById(`${universeIndex}-${i}`);
126 | universe.slotElems[i].title = `${universeIndex}/${i} ${device.data.priorities[universeIndex][i]}`;
127 | }
128 | universe.slotElemsSet = true;
129 | }
130 | });
131 | }
132 | };
133 |
134 | // From https://github.com/hhromic/e131-node/blob/master/lib/e131.js
135 | function getMulticastGroup(universe) {
136 | if (universe < 1 || universe > 63999) {
137 | throw new RangeError('universe should be in the range [1-63999]');
138 | }
139 | return `239.255.${universe >> 8}.${universe & 0xff}`;
140 | }
141 |
--------------------------------------------------------------------------------
/plugins/sacn/styles.css:
--------------------------------------------------------------------------------
1 | table {
2 | margin-bottom: 50px;
3 | background-color: #333;
4 | }
5 | td,
6 | th {
7 | width: 35px;
8 | }
9 | td {
10 | background-color: black;
11 | text-align: center;
12 | border-radius: 3px;
13 | color: white;
14 | font-size: 13px;
15 | }
16 | th {
17 | background-color: #222;
18 | color: #ccc;
19 | font-size: 11px;
20 | }
21 | td.data {
22 | padding: 7px;
23 | font-size: 12px;
24 | text-align: left;
25 | }
26 | td.data em {
27 | font-size: 14px;
28 | color: #ff0033;
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/sacn/template.ejs:
--------------------------------------------------------------------------------
1 |
2 | <%= data.source %> sACN (E1.31)
3 |
4 |
5 |
6 |
7 | <% data.orderedUniverses.forEach(universeIndex => {
8 | let universe = data.universes[universeIndex];
9 | %>
10 |
11 |
12 |
13 |
14 | Universe
15 | <%= universeIndex %>
16 | |
17 |
18 | Priority
19 | <%= universe.priority %>
20 | |
21 |
22 | CID
23 | <%= universe.cid %>
24 | |
25 |
26 | IP
27 | <%= data.ip %>
28 | |
29 |
30 | Flavor
31 | 0
32 | |
33 |
34 |
35 |
36 | |
37 | <% for(col=1; col<=16; col++){ %>
38 | <%= col %> |
39 | <% } %>
40 |
41 |
42 | <% let slot = 0; %>
43 | <% for(row=0; row<32; row++){ %>
44 |
45 | <%= row*16+1 %> |
46 | <% for(col=0; col<16; col++){ %>
47 | <% slot++%>
48 | <%= universe.slots[slot-1] %> |
49 | <% } %>
50 |
51 | <% } %>
52 |
53 |
54 |
55 |
56 | <% }); %>
57 |
58 |
--------------------------------------------------------------------------------
/plugins/shure/channel.js:
--------------------------------------------------------------------------------
1 | class Channel {
2 | constructor() {
3 | this.chan_name = '?';
4 | this.batt_bars = 255;
5 | this.batt_run_time = 65535;
6 | this.audio_gain = 0;
7 | this.audio_lvl = 0;
8 | this.rx_rf_lvl = 0;
9 | this.rf_antenna = 0;
10 | this.tx_type = 0;
11 | this.rx_graph_bars = 0;
12 | }
13 | }
14 |
15 | module.exports = Channel;
16 |
--------------------------------------------------------------------------------
/plugins/shure/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/shure/icon.png
--------------------------------------------------------------------------------
/plugins/shure/info.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/shure/info.html
--------------------------------------------------------------------------------
/plugins/shure/main.js:
--------------------------------------------------------------------------------
1 | const Channel = require('./channel');
2 |
3 | exports.config = {
4 | defaultName: 'Shure Wireless',
5 | connectionType: 'TCPsocket',
6 | remotePort: 2202,
7 | mayChangePorts: false,
8 | heartbeatInterval: 5000,
9 | heartbeatTimeout: 10000,
10 | searchOptions: {
11 | type: 'TCPport',
12 | searchBuffer: Buffer.from('< GET DEVICE_ID >', 'ascii'),
13 | testPort: 2202,
14 | validateResponse(msg, info) {
15 | return msg.toString().includes('DEVICE_ID');
16 | },
17 | },
18 | };
19 |
20 | exports.ready = function ready(_device) {
21 | const device = _device;
22 | device.data.channelCount = 0;
23 | device.data.channels = [{}, new Channel(), new Channel(), new Channel(), new Channel()];
24 |
25 | device.send('< GET 0 ALL >');
26 | device.send('< SET 0 METER_RATE 00100 >');
27 | device.send('< SAMPLE 0 AUDIO_LVL>');
28 | };
29 |
30 | exports.data = function data(_device, message) {
31 | const device = _device;
32 | let msgStr = message.toString();
33 |
34 | if (!msgStr.startsWith('< ')) {
35 | return;
36 | }
37 |
38 | // console.log(msgStr);
39 |
40 | msgStr = msgStr.slice(2).slice(0, -1);
41 | const msgs = msgStr.split('><');
42 |
43 | msgs.forEach((ms, i) => {
44 | const msg = ms.trim();
45 | const msgParts = msg.split(' ');
46 | const channelNumber = Number(msgParts[1]);
47 | const channel = device.data.channels[channelNumber];
48 |
49 | if (msgParts[0] === 'REP') {
50 | if (msgParts[2] === 'CHAN_NAME') {
51 | channel.chan_name = msg.substring(17).slice(0, -2).trim();
52 | if (device.data.channelCount < channelNumber) {
53 | device.data.channelCount = channelNumber;
54 | }
55 | device.draw();
56 | } else if (msgParts[2] === 'BATT_BARS') {
57 | channel.batt_bars = Number(msgParts[3]);
58 | } else if (msgParts[2] === 'BATT_RUN_TIME') {
59 | channel.batt_run_time = Number(msgParts[3]);
60 | } else if (msgParts[2] === 'AUDIO_GAIN') {
61 | channel.audio_gain = Number(msgParts[3]) - 18;
62 | } else if (msgParts[2] === 'AUDIO_LVL') {
63 | channel.audio_lvl = Number(msgParts[3]);
64 | } else if (msgParts[2] === 'RX_RF_LVL') {
65 | channel.rx_rf_lvl = Number(msgParts[3]) - 128;
66 | } else if (msgParts[2] === 'RF_ANTENNA') {
67 | channel.rf_antenna = msgParts[3];
68 | } else if (msgParts[2] === 'TX_TYPE') {
69 | channel.tx_type = msgParts[3];
70 | device.draw();
71 | } else if (msgParts[1] === 'DEVICE_ID') {
72 | const id = msg.substring(15).slice(0, -1).trim();
73 | this.deviceInfoUpdate(device, 'defaultName', id);
74 | } else if (msgParts[1] === 'FW_VER') {
75 | device.data.version = msgParts[2].substring(1);
76 | }
77 | } else if (msgParts[0] === 'SAMPLE') {
78 | channel.rf_antenna = msgParts[3];
79 | channel.rx_rf_lvl = Number(msgParts[4]) - 128;
80 | channel.audio_lvl = Number(msgParts[5]);
81 | if (channelNumber === 4) {
82 | device.update('updateSample', {
83 | channels: device.data.channels,
84 | });
85 | }
86 | }
87 | });
88 | };
89 |
90 | exports.update = function update(device, doc, updateType, data) {
91 | for (let i = 1; i < data.channels.length; i++) {
92 | const channel = data.channels[i];
93 | const $audio = doc.getElementById(`ch-${i}-audio`);
94 | const $audioText = doc.getElementById(`ch-${i}-audio-text`);
95 | if ($audio) {
96 | $audio.style.height = 90 - channel.audio_lvl * 2;
97 | }
98 | if ($audioText) {
99 | $audioText.textContent = channel.audio_lvl;
100 | }
101 |
102 | const $rfA = doc.getElementById(`ch-${i}-a`);
103 | const $rfB = doc.getElementById(`ch-${i}-b`);
104 | let rfClass = '';
105 |
106 | if ($rfA) {
107 | if (channel.rf_antenna.charAt(0) === 'A') {
108 | $rfA.style.color = '#53c3c3';
109 | rfClass = 'color-1';
110 | } else {
111 | $rfA.style.color = '#333';
112 | }
113 | }
114 | if ($rfB) {
115 | if (channel.rf_antenna.charAt(1) === 'B') {
116 | $rfB.style.color = '#53c3c3';
117 | rfClass = 'color-2';
118 | } else {
119 | $rfB.style.color = '#333';
120 | }
121 | }
122 |
123 | const $rf = doc.getElementById(`ch-${i}-rf`);
124 | const $rfGraph = doc.getElementById(`ch-${i}-graph`);
125 | const $rfText = doc.getElementById(`ch-${i}-rf-text`);
126 | const rfHeight = 90 - (channel.rx_rf_lvl + 90) * 2;
127 | if ($rf) {
128 | $rf.style.height = rfHeight;
129 | }
130 | if ($rfGraph) {
131 | if ($rfGraph.childElementCount > 115) {
132 | $rfGraph.removeChild($rfGraph.firstElementChild);
133 | }
134 | $rfGraph.insertAdjacentHTML(
135 | 'beforeend',
136 | ``
137 | );
138 | channel.rx_graph_bars++;
139 | }
140 | if ($rfText) {
141 | $rfText.textContent = channel.rx_rf_lvl;
142 | }
143 | }
144 | };
145 |
146 | exports.heartbeat = function heartbeat(device) {
147 | device.send('< GET 0 ALL >');
148 | device.send('< SET 0 METER_RATE 00100 >');
149 | device.send('< SAMPLE 0 AUDIO_LVL>');
150 | };
151 |
--------------------------------------------------------------------------------
/plugins/shure/styles.css:
--------------------------------------------------------------------------------
1 | table.channel {
2 | width: 120px;
3 | height: 320px;
4 | background-color: #1a1a1d;
5 | border: #3b3c42 1px solid;
6 | border-collapse: collapse;
7 | color: white;
8 | text-align: center;
9 | float: left;
10 | margin: 2px;
11 | margin-bottom: 20px;
12 | }
13 | table td {
14 | border: #3b3c42 1px solid;
15 | vertical-align: top;
16 | }
17 |
18 | .chan_name {
19 | width: 100px;
20 | color: #b2ff33 !important;
21 | font-size: 18px;
22 | font-weight: bold;
23 | }
24 |
25 | .bar-wrapper {
26 | width: 20px;
27 | height: 90px;
28 | margin: 0px auto;
29 | margin-top: 5px;
30 | margin-bottom: 5px;
31 |
32 | border-radius: 6px;
33 | outline: #1a1a1d 2px solid;
34 | outline-offset: -1px;
35 | overflow: hidden;
36 | }
37 | .bar-wrapper.green {
38 | background: linear-gradient(0deg, #4742af, #554ed4 30%, #554ed4 80%, #df664d);
39 | }
40 | .bar-wrapper.pink {
41 | background: linear-gradient(0deg, #383943, #4e505d 40%);
42 | }
43 | .bar-wrapper.orange {
44 | background: linear-gradient(0deg, #50c3c3, #5bdfe0 30%, #5bdfe0 80%);
45 | }
46 | .bar {
47 | width: 20px;
48 | background-color: #202124;
49 | outline: rgba(255, 255, 255, 0.1) 1px solid;
50 | }
51 |
52 | .batt-wrapper {
53 | width: 84px;
54 | height: 35px;
55 | margin: 0px auto;
56 | border: #333 2px solid;
57 | border-radius: 5px;
58 | padding: 2px;
59 | margin: 10px auto;
60 | position: relative;
61 | }
62 | .batt-wrapper.green {
63 | border-color: greenyellow;
64 | }
65 | .batt-knob {
66 | background-color: #333;
67 | position: absolute;
68 | right: -5px;
69 | top: 10px;
70 | width: 5px;
71 | height: 16px;
72 | border-radius: 2px;
73 | }
74 | .batt-knob.green {
75 | background-color: #adff2f;
76 | }
77 | .batt-bar {
78 | height: 26px;
79 | background: linear-gradient(0deg, rgb(145, 220, 33), greenyellow 100%);
80 | border-radius: 2px;
81 | color: #1a1a1d;
82 | padding-top: 9px;
83 | }
84 |
85 | .rf-indicator-wrapper {
86 | line-height: 8px;
87 | margin-bottom: 4px;
88 | font-size: 6px;
89 | padding-top: 4px;
90 | letter-spacing: 1px;
91 | }
92 | .rf-indicator {
93 | color: #333;
94 | }
95 |
96 | small {
97 | color: dimgray;
98 | }
99 | small small {
100 | color: #444;
101 | }
102 |
103 | .rf-graph {
104 | height: 80px;
105 | vertical-align: bottom;
106 | text-align: left;
107 | }
108 | .rf-graph-bar {
109 | padding: 0px;
110 | margin: 0px;
111 | width: 1px;
112 | height: 1px;
113 | background: #383943;
114 | display: inline-block;
115 | }
116 | .color-1 {
117 | background: #292662;
118 | border-top: #554ed4 1px solid;
119 | }
120 | .color-2 {
121 | background: #283636;
122 | border-top: #50c3c3 1px solid;
123 | }
124 |
--------------------------------------------------------------------------------
/plugins/shure/template.ejs:
--------------------------------------------------------------------------------
1 |
2 | <%= listName %>
3 | <%= data.version || "" %>
4 |
5 |
6 | <% for(let i=1; i<=data.channelCount; i++){ %> <% let ch = data.channels[i]; %>
7 |
8 |
9 | <%= i %> |
10 |
11 |
12 | <%= ch.chan_name %> |
13 |
14 |
15 |
16 |
17 | ⬤
18 | ⬤
19 |
20 | rf
21 |
27 | <%= ch.rx_rf_lvl %> dBm
28 | |
29 |
30 |
31 | audio
32 |
38 | <%= ch.audio_lvl %> dBFS
39 | |
40 |
41 |
42 | gain
43 |
46 | <%= ch.audio_gain %> dB
47 | |
48 |
49 |
50 |
51 |
52 | |
53 |
54 |
55 |
56 | <% if(ch.batt_bars==255){ %>
57 |
58 | <% }else{ %>
59 |
60 |
61 | <% if(ch.batt_run_time<=65532){ %> <%= Math.floor(ch.batt_run_time/60)
62 | %>: <%= ch.batt_run_time%60 %> <% } %>
63 |
64 |
65 |
66 | <% } %>
67 | |
68 |
69 |
70 |
71 | <% if(ch.tx_type=="UNKN"){ %> No Transmitter <%
72 | }else{ %> <%= ch.tx_type %> <% } %>
73 | |
74 |
75 |
76 |
77 | <% } %>
78 |
--------------------------------------------------------------------------------
/plugins/watchout/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/watchout/icon.png
--------------------------------------------------------------------------------
/plugins/watchout/info.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/watchout/info.html
--------------------------------------------------------------------------------
/plugins/watchout/main.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | defaultName: 'Dataton Watchout',
3 | connectionType: 'TCPsocket',
4 | remotePort: 3040,
5 | mayChangePorts: false,
6 | heartbeatInterval: 250,
7 | heartbeatTimeout: 5000,
8 | searchOptions: {
9 | type: 'TCPport',
10 | searchBuffer: Buffer.from('authenticate 1\n', 'ascii'),
11 | testPort: 3040,
12 | validateResponse(msg, info) {
13 | return msg.toString().substring(0, 5) === 'Ready';
14 | },
15 | },
16 | };
17 |
18 | exports.ready = function ready(device) {
19 | device.send('authenticate 1\n');
20 | };
21 |
22 | exports.data = function data(_device, _message) {
23 | const message = _message.toString();
24 | const device = _device;
25 |
26 | if (message.substring(0, 5) === 'Ready') {
27 | // device.send('getStatus\n');
28 | } else if (message.substring(0, 5) === 'Reply') {
29 | const arr = message.split(' ');
30 |
31 | device.data.showName = '';
32 |
33 | let i = 0;
34 | while (arr[i][arr[i].length - 1] !== '"') {
35 | i++;
36 | device.data.showName += `${arr[i]} `;
37 | }
38 |
39 | device.data.showName = device.data.showName.substring(1, device.data.showName.length - 2);
40 |
41 | i--;
42 | device.data.busy = arr[i + 2];
43 | device.data.health = arr[i + 3];
44 | device.data.displayOpen = arr[i + 4];
45 | device.data.showActive = arr[i + 5];
46 | device.data.programmerOnline = arr[i + 6];
47 | device.data.position = Number(arr[i + 7]).toFixed(2);
48 | device.data.rate = arr[i + 8];
49 | device.data.standby = arr[i + 9];
50 |
51 | this.deviceInfoUpdate(device, 'defaultName', device.data.showName);
52 | device.draw();
53 | }
54 | };
55 |
56 | exports.heartbeat = function heartbeat(device) {
57 | device.send('getStatus\n');
58 | };
59 |
--------------------------------------------------------------------------------
/plugins/watchout/styles.css:
--------------------------------------------------------------------------------
1 | .warning {
2 | color: #e9873a;
3 | }
4 | .error {
5 | color: #ed5f5d;
6 | }
7 | .ok {
8 | color: #79b757;
9 | }
10 |
--------------------------------------------------------------------------------
/plugins/watchout/template.ejs:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | <% if(data.rate=="true"){ %>
9 | Playing
10 | <% }else{ %>
11 | Stopped
12 | <% } %>
13 | |
14 |
15 |
16 | <% if(data.position){ %>
17 | <% var seconds = Math.floor(data.position/1000) %>
18 | <% var minutes = Math.floor(seconds/60) %>
19 | <% var millis = data.position %>
20 | <%= minutes %>:<%= ((seconds%60)+"").padStart(2, "0") %>.<%= millis%1000 %> |
21 | <% }else{ %>
22 | 00:00.00 |
23 | <% } %>
24 |
25 |
26 |
27 | Busy |
28 | <%= data.busy %> |
29 |
30 |
31 | Display Open |
32 | <%= data.displayOpen %> |
33 |
34 |
35 | Active |
36 | <%= data.showActive %> |
37 |
38 |
39 | Programmer |
40 | <%= data.programmerOnline %> |
41 |
42 |
43 | Health |
44 | <%= data.health %> |
45 |
46 |
47 | Standby |
48 | <%= data.standby %> |
49 |
50 |
51 | Error |
52 |
53 | <% if(data.error==1){ %>Operating System Error<% } %>
54 | <% if(data.error==2){ %>QuickTime Error<% } %>
55 | <% if(data.error==3){ %>Rendering API Error<% } %>
56 | <% if(data.error==4){ %>Network Error<% } %>
57 | <% if(data.error==5){ %>File Server Error<% } %>
58 | <% if(data.error==6){ %>Syntax/Parser Error<% } %>
59 | <% if(data.error==7){ %>General Runtime Error<% } %>
60 | <% if(data.error==8){ %>Authentication Error<% } %>
61 | |
62 |
63 |
--------------------------------------------------------------------------------
/plugins/x32/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/x32/icon.png
--------------------------------------------------------------------------------
/plugins/x32/info.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/x32/info.html
--------------------------------------------------------------------------------
/plugins/x32/main.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | defaultName: 'X32 Mixer',
3 | connectionType: 'osc-udp',
4 | remotePort: 10023,
5 | mayChangePorts: false,
6 | heartbeatInterval: 9000,
7 | heartbeatTimeout: 11000,
8 | searchOptions: {
9 | type: 'UDPsocket',
10 | searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]),
11 | devicePort: 10023,
12 | listenPort: 0,
13 | validateResponse(msg, info) {
14 | return msg.toString().includes('/xinfo');
15 | },
16 | },
17 | };
18 |
19 | exports.ready = function ready(_device) {
20 | const device = _device;
21 | device.data = new Console();
22 | device.send('/xinfo');
23 |
24 | device.send('/batchsubscribe', [
25 | { type: 's', value: '/ch/meters' },
26 | { type: 's', value: '/meters/0' },
27 | { type: 'i', value: 0 },
28 | { type: 'i', value: 0 },
29 | { type: 'i', value: 1 },
30 | ]);
31 |
32 | device.send('/batchsubscribe', [
33 | { type: 's', value: '/main/meters' },
34 | { type: 's', value: '/meters/2' },
35 | { type: 'i', value: 0 },
36 | { type: 'i', value: 0 },
37 | { type: 'i', value: 1 },
38 | ]);
39 | };
40 |
41 | function parseAddress(msg) {
42 | const addr = msg.split('/');
43 | addr.shift();
44 | return addr;
45 | }
46 |
47 | exports.data = function data(_device, oscData) {
48 | this.deviceInfoUpdate(_device, 'status', 'ok');
49 |
50 | const device = _device;
51 |
52 | if (oscData.address === '/xinfo') {
53 | device.data.info.name = oscData.args[1];
54 | device.data.info.ip = oscData.args[0];
55 | device.data.info.firmware = oscData.args[3];
56 | device.data.info.model = oscData.args[2];
57 |
58 | this.deviceInfoUpdate(_device, 'defaultName', device.data.info.name);
59 |
60 | device.send('/main/st/config/name');
61 |
62 | for (let i = 0; i <= 32; i++) {
63 | device.send(`/ch/${i.toString().padStart(2, '0')}/config/name`);
64 | }
65 | device.draw();
66 | } else if (oscData.address.includes('/ch/meters')) {
67 | const buf = Buffer.from(oscData.args[0]);
68 |
69 | let offset = 4; // skip first 4 bytes they are the length bytes
70 | for (let i = 0; i < 70; i++) {
71 | if (i >= 0 && i < 32) {
72 | // These are channel meters
73 | device.data.inputs.channels[i].meter = Console.getBehringerDB(buf.readFloatLE(offset));
74 | }
75 |
76 | offset += 4;
77 | }
78 | device.draw();
79 | } else if (oscData.address.includes('/main/meters')) {
80 | const buf = Buffer.from(oscData.args[0]);
81 | let offset = 4; // skip first 4 bytes they are the length bytes
82 |
83 | for (let i = 0; i < 49; i++) {
84 | if (i === 22) {
85 | // STEREO LEFT METER
86 | device.data.main.stereo.meter[0] = Console.getBehringerDB(buf.readFloatLE(offset));
87 | } else if (i === 23) {
88 | // STEREO RIGHT METER
89 | device.data.main.stereo.meter[1] = Console.getBehringerDB(buf.readFloatLE(offset));
90 | }
91 | offset += 4;
92 | }
93 | } else if (oscData.address.includes('/mix/fader')) {
94 | const addr = parseAddress(oscData.address);
95 |
96 | if (addr[0] === 'ch') {
97 | const channel = Number(addr[1]);
98 | device.data.inputs.channels[channel - 1].fader = oscData.args[0];
99 | device.data.inputs.channels[channel - 1].faderDB = Console.getBehringerDB(oscData.args[0]);
100 | } else if (addr[0] === 'main') {
101 | device.data.main.stereo.fader = oscData.args[0];
102 | device.data.main.stereo.faderDB = Console.getBehringerDB(oscData.args[0]);
103 | }
104 |
105 | device.draw();
106 | } else if (oscData.address.includes('/mix/on')) {
107 | const addr = parseAddress(oscData.address);
108 | if (addr[0] === 'ch') {
109 | const channel = Number(addr[1]);
110 | device.data.inputs.channels[channel - 1].mute = oscData.args[0];
111 | device.send(`/ch/${addr[1]}/mix/fader`);
112 | } else if (addr[0] === 'main') {
113 | device.data.main.stereo.mute = oscData.args[0];
114 | device.send(`/main/${addr[1]}/mix/fader`);
115 | }
116 | device.draw();
117 | } else if (oscData.address.includes('/config/name')) {
118 | const addr = parseAddress(oscData.address);
119 | if (addr[0] === 'main') {
120 | if (addr[1] === 'st') {
121 | device.data.main.stereo.name = oscData.args[0];
122 | if (device.data.main.stereo.name === '') {
123 | device.data.main.stereo.name = 'LR';
124 | }
125 | device.send(`/main/${addr[1]}/config/color`);
126 | }
127 | } else if (addr[0] === 'ch') {
128 | const channel = Number(addr[1]);
129 | device.data.inputs.channels[channel - 1].name = oscData.args[0];
130 | device.send(`/ch/${addr[1]}/config/color`);
131 | }
132 | device.draw();
133 | } else if (oscData.address.includes('/config/color')) {
134 | const addr = parseAddress(oscData.address);
135 | if (addr[0] === 'main') {
136 | device.data.main.stereo.color = oscData.args[0];
137 | device.send(`/main/${addr[1]}/mix/on`);
138 | } else if (addr[0] === 'ch') {
139 | const channel = Number(addr[1]);
140 | device.data.inputs.channels[channel - 1].color = oscData.args[0];
141 | device.send(`/ch/${addr[1]}/mix/on`);
142 | }
143 | device.draw();
144 | }
145 | };
146 |
147 | exports.heartbeat = function heartbeat(device) {
148 | device.send('/xremote');
149 |
150 | device.send('/renew', [{ type: 's', value: '/ch/meters' }]);
151 | device.send('/renew', [{ type: 's', value: '/main/meters' }]);
152 | };
153 |
154 | class Console {
155 | constructor() {
156 | this.inputs = {
157 | channels: new Array(32).fill(0).map(() => ({
158 | fader: 0,
159 | faderDB: 0,
160 | mute: 0,
161 | name: 'end',
162 | color: undefined,
163 | meter: -90,
164 | })),
165 | };
166 |
167 | this.main = {
168 | stereo: {
169 | fader: 0,
170 | faderDB: 0,
171 | mute: 0,
172 | name: 'LR',
173 | color: 7,
174 | meter: new Array(2).fill(-90),
175 | },
176 | };
177 |
178 | this.info = {
179 | name: '',
180 | ip: '',
181 | firmware: '',
182 | model: '',
183 | };
184 | }
185 |
186 | static getBehringerDB(level) {
187 | const f = level;
188 | if (f >= 0.5) {
189 | return f * 40 - 30;
190 | }
191 | if (f >= 0.25) {
192 | return f * 80 - 50;
193 | }
194 | if (f >= 0.0625) {
195 | return f * 160 - 70;
196 | }
197 | return f * 480 - 90;
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/plugins/x32/styles.css:
--------------------------------------------------------------------------------
1 | td {
2 | padding: 3px 16px !important;
3 | }
4 | tr td:first-child {
5 | text-align: center;
6 | }
7 |
8 | .mute-0,
9 | .mute-1 {
10 | width: 30px;
11 | padding: 4px;
12 | font-size: 14px;
13 | border: black 2px solid;
14 | text-align: center;
15 | border-radius: 10px;
16 | }
17 | .mute-1 {
18 | border-color: gray;
19 | color: gray;
20 | }
21 | .mute-0 {
22 | border-color: #fc3344;
23 | color: #fc3344;
24 | }
25 | .white {
26 | color: white;
27 | }
28 |
29 | .color {
30 | padding: 1px 6px;
31 | border-radius: 10px;
32 | color: black;
33 | text-align: center;
34 | text-overflow: ellipsis;
35 | white-space: nowrap;
36 | overflow: hidden;
37 | }
38 | .color-0 {
39 | /*Black*/
40 | background-color: #212121;
41 | color: white;
42 | }
43 | .color-1 {
44 | /*Red*/
45 | background-color: #fc545b;
46 | }
47 | .color-2 {
48 | /*Green*/
49 | background-color: #65b84d;
50 | }
51 | .color-3 {
52 | /*Yellow*/
53 | background-color: #fec52e;
54 | }
55 | .color-4 {
56 | /*Blue*/
57 | background-color: #157efb;
58 | }
59 | .color-5 {
60 | /*Purple*/
61 | background-color: #a453a5;
62 | }
63 | .color-6 {
64 | /*Teal*/
65 | background-color: #00dae3;
66 | }
67 | .color-7 {
68 | /*White*/
69 | background-color: #e0e0e0;
70 | }
71 | .color-8 {
72 | /*Gray*/
73 | background-color: #8c8c8c;
74 | }
75 | .color-9 {
76 | /*Red*/
77 | color: #fc545b;
78 | border: #fc545b 1px solid;
79 | }
80 | .color-10 {
81 | /*Green*/
82 | color: #65b84d;
83 | border: #65b84d 1px solid;
84 | }
85 | .color-11 {
86 | /*Yellow*/
87 | color: #fec52e;
88 | border: #fec52e 1px solid;
89 | }
90 | .color-12 {
91 | /*Blue*/
92 | color: #157efb;
93 | border: #157efb 1px solid;
94 | }
95 | .color-13 {
96 | /*Purple*/
97 | color: #a453a5;
98 | border: #a453a5 1px solid;
99 | }
100 | .color-14 {
101 | /*Teal*/
102 | color: #00dae3;
103 | border: #00dae3 1px solid;
104 | }
105 | .color-15 {
106 | /*White*/
107 | color: #e0e0e0;
108 | border: #e0e0e0 1px solid;
109 | }
110 |
111 | input[type='range'] {
112 | -webkit-appearance: none;
113 | margin: 0px;
114 | width: 100%;
115 | }
116 | input[type='range']::-webkit-slider-thumb {
117 | -webkit-appearance: none;
118 | margin-top: -9px;
119 | height: 26px;
120 | width: 8px;
121 | border-radius: 4px;
122 | background: #929292;
123 | }
124 | input[type='range']::-webkit-slider-runnable-track {
125 | width: 100%;
126 | height: 8px;
127 | background: #3b3b3b;
128 | border-radius: 4px;
129 | }
130 |
131 | .infin {
132 | font-size: 20px;
133 | vertical-align: middle;
134 | }
135 |
136 | .meter {
137 | height: 3px;
138 | background: linear-gradient(90deg, rgba(19, 126, 30, 1) 0%, rgba(255, 238, 30, 1) 85%, rgba(255, 0, 0, 1) 100%);
139 | }
140 |
141 | /* need to find a way to match the color of this with the table row */
142 | .meter-cover {
143 | height: 100%;
144 | background-color: rgb(22, 23, 25);
145 | float: right;
146 | }
147 |
148 | table.cv-table tr:nth-child(odd) td div.meter div.meter-cover {
149 | background-color: #292929;
150 | }
151 |
--------------------------------------------------------------------------------
/plugins/x32/template.ejs:
--------------------------------------------------------------------------------
1 |
2 | <%= listName %>
3 | <%= data.info.model %>
4 |
5 |
6 |
61 |
62 | <% function formatAsDB(val){ if(val==-90){ return '-∞'; } if(val>0){
63 | return "+"+val.toFixed(1); } return val.toFixed(1); } %>
64 |
--------------------------------------------------------------------------------
/plugins/xair/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/xair/icon.png
--------------------------------------------------------------------------------
/plugins/xair/info.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/xair/info.html
--------------------------------------------------------------------------------
/plugins/xair/main.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | defaultName: 'X Air Mixer',
3 | connectionType: 'UDPsocket',
4 | remotePort: 10024,
5 | mayChangePorts: false,
6 | heartbeatInterval: 9000,
7 | heartbeatTimeout: 15000,
8 | searchOptions: {
9 | type: 'UDPsocket',
10 | searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]),
11 | devicePort: 10024,
12 | listenPort: 0,
13 | validateResponse(msg, info) {
14 | return msg.toString().startWith('/xinfo');
15 | },
16 | },
17 | };
18 |
19 | exports.ready = function ready(device) {
20 | const d = device;
21 | d.data.channelFaders = new Array(32).fill(0);
22 | d.data.channelFadersDB = new Array(32).fill(0);
23 | d.data.channelMutes = new Array(32).fill(0);
24 | d.data.channelNames = new Array(32).fill('end');
25 | d.data.channelColors = new Array(32);
26 |
27 | d.data.stereoFader = 0;
28 | d.data.stereoFaderDB = 0;
29 | d.data.stereoMute = 0;
30 |
31 | d.send('/xinfo');
32 | };
33 |
34 | function parseAddress(msg) {
35 | const addr = msg.split('/');
36 | addr.shift();
37 | return addr;
38 | }
39 |
40 | function convertToDBTheBehringerWay(f) {
41 | if (f >= 0.5) {
42 | return f * 40 - 30;
43 | }
44 | if (f >= 0.25) {
45 | return f * 80 - 50;
46 | }
47 | if (f >= 0.0625) {
48 | return f * 160 - 70;
49 | }
50 | return f * 480 - 90;
51 | }
52 |
53 | exports.data = function data(device, buf) {
54 | this.deviceInfoUpdate(device, 'status', 'ok');
55 | const msg = buf.toString().split('\x00');
56 | const d = device;
57 |
58 | if (msg[0] === '/xinfo') {
59 | if (msg[7].length > 0) {
60 | d.data.name = msg[7];
61 | } else {
62 | d.data.name = msg[6];
63 | }
64 |
65 | d.data.ip = msg[5];
66 | d.data.firmware = msg[13];
67 | d.data.model = msg[9];
68 | this.deviceInfoUpdate(d, 'defaultName', d.data.name);
69 |
70 | d.send('/lr/mix/fader\x00\x00\x00\x00');
71 | d.send('/lr/mix/on\x00\x00\x00\x00');
72 |
73 | for (let i = 0; i <= 32; i++) {
74 | d.send(Buffer.from(`/ch/${i.toString().padStart(2, '0')}/config/name\x00\x00\x00\x00`));
75 | }
76 | d.draw();
77 | } else if (msg[0] === '/meters/0') {
78 | // console.log(msg)
79 | } else if (msg[0].includes('/mix/fader')) {
80 | const addr = parseAddress(msg[0]);
81 | const channel = Number(addr[1]);
82 |
83 | if (addr[0] === 'ch') {
84 | d.data.channelFaders[channel - 1] = buf.readFloatBE(24);
85 | d.data.channelFadersDB[channel - 1] = convertToDBTheBehringerWay(buf.readFloatBE(24));
86 | } else if (addr[0] === 'lr') {
87 | d.data.stereoFader = buf.readFloatBE(20);
88 | d.data.stereoFaderDB = convertToDBTheBehringerWay(buf.readFloatBE(20));
89 | }
90 |
91 | d.draw();
92 | } else if (msg[0].includes('/mix/on')) {
93 | const addr = parseAddress(msg[0]);
94 | const channel = Number(addr[1]);
95 |
96 | if (addr[0] === 'ch') {
97 | d.data.channelMutes[channel - 1] = buf[23];
98 | } else if (addr[0] === 'lr') {
99 | d.data.stereoMute = buf[19];
100 | }
101 | device.draw();
102 | device.send(`/ch/${addr[1]}/mix/fader\x00\x00\x00\x00`);
103 | } else if (msg[0].includes('/config/name')) {
104 | const addr = parseAddress(msg[0]);
105 | const channel = Number(addr[1]);
106 | d.data.channelNames[channel - 1] = msg[4];
107 | d.draw();
108 | d.send(`/ch/${addr[1]}/config/color\x00\x00\x00\x00`);
109 | } else if (msg[0].includes('/config/color')) {
110 | const addr = parseAddress(msg[0]);
111 | const channel = Number(addr[1]);
112 | d.data.channelColors[channel - 1] = buf.readInt8(27);
113 | d.draw();
114 | d.send(`/ch/${addr[1]}/mix/on\x00\x00\x00\x00`);
115 | } else {
116 | // console.log(msg)
117 | }
118 | // console.log(msg)
119 | };
120 |
121 | exports.heartbeat = function heartbeat(device) {
122 | device.send('/xremote');
123 | };
124 |
--------------------------------------------------------------------------------
/plugins/xair/styles.css:
--------------------------------------------------------------------------------
1 | td {
2 | padding: 3px 16px !important;
3 | }
4 | tr td:first-child {
5 | text-align: center;
6 | }
7 |
8 | .mute-0,
9 | .mute-1 {
10 | width: 30px;
11 | padding: 4px;
12 | font-size: 14px;
13 | border: black 2px solid;
14 | text-align: center;
15 | border-radius: 10px;
16 | }
17 | .mute-1 {
18 | border-color: gray;
19 | color: gray;
20 | }
21 | .mute-0 {
22 | border-color: #fc3344;
23 | color: #fc3344;
24 | }
25 | .white {
26 | color: white;
27 | }
28 |
29 | .color {
30 | padding: 1px 6px;
31 | border-radius: 10px;
32 | color: black;
33 | text-align: center;
34 | text-overflow: ellipsis;
35 | white-space: nowrap;
36 | overflow: hidden;
37 | }
38 | .color-0 {
39 | /*Black*/
40 | background-color: #212121;
41 | color: white;
42 | }
43 | .color-1 {
44 | /*Red*/
45 | background-color: #fc545b;
46 | }
47 | .color-2 {
48 | /*Green*/
49 | background-color: #65b84d;
50 | }
51 | .color-3 {
52 | /*Yellow*/
53 | background-color: #fec52e;
54 | }
55 | .color-4 {
56 | /*Blue*/
57 | background-color: #157efb;
58 | }
59 | .color-5 {
60 | /*Purple*/
61 | background-color: #a453a5;
62 | }
63 | .color-6 {
64 | /*Teal*/
65 | background-color: #00dae3;
66 | }
67 | .color-7 {
68 | /*White*/
69 | background-color: #e0e0e0;
70 | }
71 | .color-8 {
72 | /*Gray*/
73 | background-color: #8c8c8c;
74 | }
75 | .color-9 {
76 | /*Red*/
77 | color: #fc545b;
78 | border: #fc545b 1px solid;
79 | }
80 | .color-10 {
81 | /*Green*/
82 | color: #65b84d;
83 | border: #65b84d 1px solid;
84 | }
85 | .color-11 {
86 | /*Yellow*/
87 | color: #fec52e;
88 | border: #fec52e 1px solid;
89 | }
90 | .color-12 {
91 | /*Blue*/
92 | color: #157efb;
93 | border: #157efb 1px solid;
94 | }
95 | .color-13 {
96 | /*Purple*/
97 | color: #a453a5;
98 | border: #a453a5 1px solid;
99 | }
100 | .color-14 {
101 | /*Teal*/
102 | color: #00dae3;
103 | border: #00dae3 1px solid;
104 | }
105 | .color-15 {
106 | /*White*/
107 | color: #e0e0e0;
108 | border: #e0e0e0 1px solid;
109 | }
110 |
111 | input[type='range'] {
112 | -webkit-appearance: none;
113 | margin: 0px;
114 | width: 100%;
115 | }
116 | input[type='range']::-webkit-slider-thumb {
117 | -webkit-appearance: none;
118 | margin-top: -9px;
119 | height: 26px;
120 | width: 8px;
121 | border-radius: 4px;
122 | background: #929292;
123 | }
124 | input[type='range']::-webkit-slider-runnable-track {
125 | width: 100%;
126 | height: 8px;
127 | background: #3b3b3b;
128 | border-radius: 4px;
129 | }
130 |
131 | .infin {
132 | font-size: 20px;
133 | vertical-align: middle;
134 | }
135 |
--------------------------------------------------------------------------------
/plugins/xair/template.ejs:
--------------------------------------------------------------------------------
1 |
2 | <%= listName %>
3 | <%= data.model %>
4 |
5 |
6 |
49 |
50 | <% function formatAsDB(val){ if(val==-90){ return '-∞'; } if(val>0){
51 | return "+"+val.toFixed(1); } return val.toFixed(1); } %>
52 |
--------------------------------------------------------------------------------
/preload.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer } = require('electron');
2 |
3 | const DEVICE = require('./src/device.js');
4 | const PLUGINS = require('./src/plugins.js');
5 | const SEARCH = require('./src/search.js');
6 | const VIEW = require('./src/view.js');
7 | const SAVESLOTS = require('./src/saveSlots.js');
8 |
9 | window.addDevice = DEVICE.addDevice;
10 | window.searchAll = SEARCH.searchAll;
11 |
12 | window.init = function init() {
13 | console.log('init!');
14 |
15 | ipcRenderer.send('enableDeviceDropdown');
16 | ipcRenderer.send('enableSearchAll');
17 |
18 | // load autoUpdate setting from storage and send to main process
19 | const autoUpdate = JSON.parse(localStorage.getItem('autoUpdate'));
20 | if (autoUpdate !== undefined && autoUpdate !== null) {
21 | if (autoUpdate) {
22 | ipcRenderer.send('checkForUpdates');
23 | }
24 | // send message so main process knows the state of autoUpdate
25 | ipcRenderer.send('setAutoUpdate', autoUpdate);
26 | }
27 |
28 | PLUGINS.init(() => {
29 | VIEW.init();
30 | SAVESLOTS.loadDevices();
31 | SAVESLOTS.loadSlot(1);
32 | });
33 |
34 | document.getElementById('search-button').onclick = (e) => {
35 | SEARCH.searchAll();
36 | };
37 |
38 | document.getElementById('device-settings-table').onclick = function settingsClick(e) {
39 | e.stopPropagation();
40 | };
41 |
42 | document.getElementById('device-settings-name').onchange = function nameChange(e) {
43 | e.stopPropagation();
44 | DEVICE.changeActiveName(e.target.value);
45 | };
46 |
47 | document.getElementById('device-settings-plugin-dropdown').onchange = function dropdownChange(e) {
48 | e.stopPropagation();
49 | DEVICE.changeActiveType(e.target.value);
50 | };
51 |
52 | document.getElementById('device-settings-ip').onchange = function ipChange(e) {
53 | e.stopPropagation();
54 | DEVICE.changeActiveIP(e.target.value);
55 | };
56 |
57 | document.getElementById('device-settings-port').onchange = function portChange(e) {
58 | e.stopPropagation();
59 | DEVICE.changeActivePort(e.target.value);
60 | };
61 |
62 | document.getElementById('device-settings-rx-port').onchange = function portChange(e) {
63 | e.stopPropagation();
64 | DEVICE.changeActiveRxPort(e.target.value);
65 | };
66 |
67 | document.getElementById('device-settings-pin').onchange = function pinChange(e) {
68 | e.stopPropagation();
69 | if (e.target.checked) {
70 | VIEW.pinActiveDevice();
71 | } else {
72 | VIEW.unpinActiveDevice();
73 | }
74 | };
75 |
76 | const saveSlots = document.getElementsByClassName('save-slot');
77 |
78 | for (let i = 0; i < saveSlots.length; i++) {
79 | const saveSlot = saveSlots[i];
80 | saveSlot.addEventListener('click', (event) => {
81 | // get save slot from button id save-slot-1 = 1
82 | const saveSlotIndex = parseInt(event.target.id.replace('save-slot-', ''), 10);
83 | if (saveSlotIndex) {
84 | SAVESLOTS.loadSlot(saveSlotIndex);
85 | }
86 | });
87 | }
88 |
89 | document.getElementById('refresh-device-button').onclick = function refreshClick(e) {
90 | e.stopPropagation();
91 | DEVICE.refreshActive();
92 | };
93 |
94 | document.getElementById('device-list').onclick = function listClick(e) {
95 | e.stopPropagation();
96 | const deviceID = e.srcElement.id;
97 | if (e.srcElement.id !== 'device-list') {
98 | VIEW.switchDevice(deviceID);
99 | } else {
100 | VIEW.switchDevice(undefined);
101 | }
102 | };
103 |
104 | document.getElementById('add-device-button').onchange = function addDeviceClick(e) {
105 | const newDevice = DEVICE.registerDevice(
106 | {
107 | type: e.target.value,
108 | defaultName: 'New Device',
109 | remotePort: PLUGINS.all[e.target.value].config.remotePort || '',
110 | addresses: [],
111 | },
112 | 'fromAddButton'
113 | );
114 | e.target.selectedIndex = 0;
115 |
116 | VIEW.switchDevice(newDevice.id);
117 | SAVESLOTS.saveAll();
118 | };
119 |
120 | document.getElementById('network-info-button').onclick = function fooBar(e) {
121 | ipcRenderer.send('openNetworkInfoWindow');
122 | };
123 |
124 | document.onkeyup = function keyUp(e) {
125 | if (e.key === 'ArrowUp') {
126 | VIEW.selectPreviousDevice();
127 | } else if (e.key === 'ArrowDown') {
128 | VIEW.selectNextDevice();
129 | } else if (e.key === 'Tab') {
130 | if (document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'SELECT') {
131 | document.getElementById('device-settings-name').select();
132 | }
133 | }
134 | };
135 | };
136 |
137 | ipcRenderer.on('setActiveDevicePinned', (event, message) => {
138 | if (!message) {
139 | VIEW.unpinActiveDevice();
140 | } else {
141 | VIEW.pinActiveDevice();
142 | }
143 | });
144 |
145 | ipcRenderer.on('searchAll', (event, message) => {
146 | window.searchAll();
147 | });
148 |
149 | ipcRenderer.on('deleteActive', (event, message) => {
150 | DEVICE.deleteActive();
151 | VIEW.selectPreviousDevice();
152 | });
153 |
154 | ipcRenderer.on('clearSavedData', (event, message) => {
155 | SAVESLOTS.clearSavedData();
156 | });
157 |
158 | ipcRenderer.on('toggleSidebar', (event, message) => {
159 | document.getElementById('main').classList.toggle('sidebar-hidden');
160 | });
161 |
162 | ipcRenderer.on('loadSlot', (event, slot) => {
163 | if (slot) {
164 | SAVESLOTS.loadSlot(slot);
165 | }
166 | });
167 |
168 | // message from main process to set autoUpdate state
169 | ipcRenderer.on('setAutoUpdate', (event, autoUpdate) => {
170 | localStorage.setItem('autoUpdate', autoUpdate);
171 | if (autoUpdate) {
172 | ipcRenderer.send('checkForUpdates');
173 | }
174 | // message to main process that we have updated the state
175 | ipcRenderer.send('setAutoUpdate', autoUpdate);
176 | });
177 |
178 | function switchClass(element, className) {
179 | try {
180 | document.getElementsByClassName(className)[0].classList.remove(className);
181 | } catch (err) {
182 | // console.log(err)
183 | }
184 | element.classList.add(className);
185 | }
186 | window.switchClass = switchClass;
187 |
--------------------------------------------------------------------------------
/src/assets/css/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-size: 1rem;
3 | font-family:
4 | ui-sans-serif,
5 | system-ui,
6 | -apple-system,
7 | BlinkMacSystemFont;
8 | }
9 | *,
10 | a,
11 | button {
12 | cursor: default;
13 | user-select: none;
14 | }
15 |
16 | body {
17 | color: #6e6c6f;
18 | margin: 0px;
19 | user-select: none;
20 | overflow-x: hidden;
21 | }
22 | a {
23 | text-decoration: none;
24 | }
25 | .red {
26 | color: #eb6b5e;
27 | }
28 | .green {
29 | color: #61c650;
30 | }
31 | .left {
32 | float: left;
33 | }
34 | .right {
35 | float: right;
36 | }
37 |
38 | #main.sidebar-hidden {
39 | grid-template-columns: auto;
40 | }
41 |
42 | #main.sidebar-hidden > #device-list-col {
43 | display: none;
44 | }
45 |
46 | #main {
47 | box-sizing: border-box;
48 | display: grid;
49 | width: 100%;
50 | height: 100vh;
51 | grid-template-columns: 270px 1fr;
52 | }
53 | .col {
54 | border-right: rgba(0, 0, 0, 0.5) 1px solid;
55 | height: 100vh !important;
56 | overflow: hidden;
57 | box-sizing: border-box;
58 | position: relative;
59 | }
60 |
61 | /* FIRST COL */
62 | #device-list-col {
63 | background-color: #2b2b2b;
64 | color: #707070;
65 | }
66 | #view-buttons-bar {
67 | box-sizing: border-box;
68 | text-align: right;
69 | padding-left: 100px;
70 | padding-right: 7px;
71 | -webkit-app-region: drag;
72 | z-index: 1000;
73 | }
74 | #view-buttons-bar .active {
75 | background-color: rgba(255, 255, 255, 0.3);
76 | color: white;
77 | border: #61c650;
78 | border-style: solid;
79 | }
80 | #view-buttons-bar button {
81 | background-color: rgba(255, 255, 255, 0.07);
82 | width: 26px;
83 | border-radius: 10px;
84 | font-size: 0.8rem;
85 | }
86 | #view-buttons-bar button:hover {
87 | background-color: rgba(255, 255, 255, 0.25);
88 | }
89 |
90 | #device-list {
91 | box-sizing: border-box;
92 | width: 100%;
93 | height: calc(100% - 387px);
94 | padding: 0px 10px;
95 | overflow-x: hidden;
96 | overflow-y: scroll;
97 | }
98 | #device-list div {
99 | pointer-events: none;
100 | }
101 | #device-list .device {
102 | box-sizing: border-box;
103 | clear: both;
104 | display: block;
105 | width: 100%;
106 | height: 40px;
107 | padding: 10px 5px;
108 | overflow: hidden;
109 | border-radius: 5px;
110 | }
111 | #device-list .device.active-device {
112 | /*background-color: #0e5ccd;*/
113 | /*background-color: #c9961f;*/
114 | background-color: rgba(255, 255, 255, 0.2);
115 | }
116 | #device-list .status {
117 | float: left;
118 | width: 25px;
119 | font-size: 18px;
120 | }
121 | #device-list .type {
122 | float: left;
123 | width: 30px;
124 | color: #f6bd26;
125 | }
126 | #device-list .name {
127 | position: relative;
128 | left: 0px;
129 | color: white;
130 | font-weight: 600;
131 |
132 | text-overflow: ellipsis;
133 | white-space: nowrap;
134 | overflow: hidden;
135 | }
136 | #device-list h3.init {
137 | margin: 0px auto;
138 | padding-top: 200px;
139 | text-align: center;
140 | font-weight: 300;
141 | font-size: 16px;
142 | width: 210px;
143 | pointer-events: none;
144 | }
145 | #device-inspector {
146 | position: absolute;
147 | bottom: 0px;
148 | width: 100%;
149 | }
150 | #device-tools {
151 | /* position: absolute;
152 | bottom: 210px;
153 | left: 0px;*/
154 | width: 262px;
155 | height: 30px;
156 | padding-right: 10px;
157 | /*background: linear-gradient(#2f2f2f 0%, #2c2c2c 100%);*/
158 | background-color: rgba(0, 0, 0, 0.2);
159 | text-align: center;
160 | border-top: rgba(0, 0, 0, 0.3) 1px solid;
161 | /*border-bottom: #3a3a3a 1px solid;*/
162 | }
163 | #device-tools select {
164 | text-align: center;
165 | }
166 |
167 | #device-settings {
168 | height: 300px;
169 | padding: 5px;
170 | color: white;
171 | font-weight: 300;
172 | width: 100%;
173 | background-color: rgba(0, 0, 0, 0.2);
174 | }
175 | #device-settings th {
176 | text-align: right;
177 | padding-right: 3px;
178 | color: white;
179 | font-weight: normal;
180 | }
181 | #device-settings td {
182 | padding: 6px;
183 | }
184 | #device-settings-table {
185 | display: none;
186 | }
187 | #device-settings h3 {
188 | padding-top: 90px;
189 | text-align: center;
190 | font-weight: 300;
191 | }
192 | #device-settings #device-settings-name,
193 | #device-settings #device-settings-ip,
194 | #device-settings #device-settings-plugin-dropdown {
195 | width: 170px;
196 | }
197 | #device-settings #device-settings-port,
198 | #device-settings #device-settings-rx-port {
199 | width: 70px;
200 | }
201 | #network-indicator-dot {
202 | position: absolute;
203 | left: 250px;
204 | bottom: 8px;
205 | background: #00ff00;
206 | width: 8px;
207 | height: 8px;
208 | border-radius: 4px;
209 | }
210 |
211 | /* SECOND COL */
212 | #all-devices {
213 | display: grid;
214 | grid-template-columns: repeat(4, 1fr);
215 | background-color: rgba(0, 0, 0, 0.3);
216 | }
217 | .device-pin {
218 | position: absolute;
219 | right: 45px;
220 | top: 4px;
221 | height: 20px;
222 | padding: inherit;
223 | }
224 | .device-traffic-signal {
225 | position: absolute;
226 | right: 15px;
227 | top: 4px;
228 | height: 20px;
229 | padding: inherit;
230 | opacity: 0.3;
231 | transition: opacity 0.1s ease-in-out;
232 | }
233 | .device-wrapper {
234 | box-sizing: border-box;
235 | border: black 1px solid;
236 | padding: 2px;
237 | }
238 | .draw-area {
239 | height: 100%;
240 | overflow-y: scroll;
241 | width: 100%;
242 | height: 100%;
243 | border: 0;
244 | }
245 | .active-device-outline {
246 | padding: 0px;
247 | border: #fdea08 3px solid;
248 | }
249 |
250 | input {
251 | display: block;
252 | box-sizing: content-box;
253 | background-color: rgba(0, 0, 0, 0.2);
254 | height: 28px;
255 | padding: 2px 6px;
256 | margin: 0px;
257 | border: rgba(255, 255, 255, 0.1) 1px solid;
258 | border-radius: 4px;
259 | color: #fff;
260 | font-size: 16px;
261 | font-weight: 500;
262 | cursor: text;
263 | }
264 | select {
265 | display: block;
266 | box-sizing: content-box;
267 | background-color: rgba(255, 255, 255, 0.15);
268 | height: 28px;
269 | padding: 2px 6px;
270 | margin: 0px;
271 | border: rgba(255, 255, 255, 0.1) 1px solid;
272 | border-radius: 4px;
273 | color: #fff;
274 | font-size: 16px;
275 | font-weight: 500;
276 | }
277 | select option {
278 | /* these styles don't affect macOS but are important for Windows! */
279 | text-align: left;
280 | background-color: #333333;
281 | }
282 | input:focus,
283 | select:focus {
284 | outline: #fdea08 2px solid;
285 | color: white;
286 | }
287 | input[type='checkbox'] {
288 | position: relative;
289 | height: 14px;
290 | width: 14px;
291 | padding: 2px;
292 | -webkit-appearance: none;
293 | cursor: default;
294 | }
295 | input[type='checkbox']:checked {
296 | background-color: #616064;
297 | }
298 | input[type='checkbox']:checked:after {
299 | content: '\2713';
300 | font-size: 18px;
301 | position: absolute;
302 | top: -1px;
303 | left: 1px;
304 | color: white;
305 | }
306 | input::-webkit-outer-spin-button,
307 | input::-webkit-inner-spin-button {
308 | -webkit-appearance: none;
309 | margin: 0;
310 | }
311 | input:disabled {
312 | color: gray;
313 | cursor: not-allowed;
314 | }
315 | button,
316 | select.button {
317 | box-sizing: content-box;
318 | width: 40px;
319 | height: 25px;
320 | margin-top: 7px;
321 | margin-bottom: 7px;
322 | margin-left: 4px;
323 | border: none;
324 | outline: none;
325 | padding: 0px;
326 |
327 | color: white;
328 | background: rgba(255, 255, 255, 0.15);
329 | border-radius: 4px;
330 | font-size: 1rem;
331 | user-select: none;
332 | }
333 | button:hover {
334 | background-color: rgba(255, 255, 255, 0.2);
335 | }
336 | button:focus {
337 | outline: none;
338 | }
339 | button:disabled {
340 | background: rgba(255, 255, 255, 0.03);
341 | }
342 | button img {
343 | height: 18px;
344 | }
345 |
346 | select.button:focus {
347 | outline: none;
348 | }
349 |
350 | @font-face {
351 | font-family: 'Material Icons';
352 | font-style: normal;
353 | font-weight: 400;
354 | src:
355 | local('Material Icons'),
356 | local('MaterialIcons-Regular'),
357 | url(../font/MaterialIcons-Regular.ttf) format('truetype');
358 | }
359 | .material-icons {
360 | font-family: 'Material Icons';
361 | font-weight: normal;
362 | font-style: normal;
363 | font-size: 24px; /* Preferred icon size */
364 | display: inline-block;
365 | line-height: 1;
366 | text-transform: none;
367 | letter-spacing: normal;
368 | word-wrap: normal;
369 | white-space: nowrap;
370 | direction: ltr;
371 |
372 | /* Support for all WebKit browsers. */
373 | -webkit-font-smoothing: antialiased;
374 | /* Support for Safari and Chrome. */
375 | text-rendering: optimizeLegibility;
376 |
377 | /* Support for Firefox. */
378 | -moz-osx-font-smoothing: grayscale;
379 |
380 | /* Support for IE. */
381 | font-feature-settings: 'liga';
382 | }
383 |
384 | /* only allow select on input fields */
385 | body :not(input):not(select):not(textarea) {
386 | user-select: none;
387 | }
388 |
389 | ::-webkit-scrollbar {
390 | /* background-color: black; */
391 | width: 12px;
392 | }
393 | ::-webkit-scrollbar-track,
394 | ::-webkit-scrollbar-corner {
395 | background-color: rgba(0, 0, 0, 0.1);
396 | }
397 | ::-webkit-scrollbar-thumb {
398 | background-color: #6b6b6b;
399 | border-radius: 16px;
400 | border: 3px solid #2b2b2b;
401 | }
402 | ::-webkit-scrollbar-button {
403 | display: none;
404 | }
405 |
--------------------------------------------------------------------------------
/src/assets/css/plugin_default.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: system-ui;
3 | margin: 0px;
4 | margin-top: 40px;
5 | user-select: none;
6 | visibility: visible !important;
7 | max-width: 100%;
8 | padding: 10px;
9 | }
10 | header {
11 | position: fixed;
12 | top: 0px;
13 | left: 0px;
14 | height: 33px;
15 | width: 100%;
16 | box-sizing: border-box;
17 | padding-right: 34px;
18 | overflow: hidden;
19 | background-color: #111111;
20 | border-bottom: black 1px solid;
21 | -webkit-user-select: none;
22 | -webkit-app-region: drag;
23 | }
24 | h1,
25 | h2,
26 | h3,
27 | h4,
28 | h5,
29 | h6 {
30 | color: white;
31 | }
32 |
33 | header h1,
34 | header h2 {
35 | margin: 4px;
36 | margin-left: 20px;
37 | font-weight: normal;
38 | font-size: 18px;
39 | float: left;
40 | height: 24px;
41 | overflow: hidden;
42 | }
43 | header h2 {
44 | color: #b6b6b6;
45 | }
46 | h3 {
47 | font-weight: 300;
48 | }
49 | table.cv-table {
50 | border: none;
51 | table-layout: fixed;
52 | border-collapse: collapse;
53 | color: #dddddd;
54 | font-size: 15px;
55 | }
56 | table.cv-table th {
57 | padding: 6px 16px;
58 | color: #969696;
59 | text-align: left;
60 | border-bottom: #555 1px solid;
61 | border-bottom: #555 1px solid;
62 | font-weight: normal;
63 | background-color: #1e1e1e;
64 | }
65 | table.cv-table tr:nth-child(odd) {
66 | background-color: #292929;
67 | }
68 | table.cv-table tr td:first-child {
69 | padding-left: 10px;
70 | border-top-left-radius: 10px;
71 | border-bottom-left-radius: 10px;
72 | }
73 | table.cv-table tr td:last-child {
74 | padding-right: 10px;
75 | border-top-right-radius: 10px;
76 | border-bottom-right-radius: 10px;
77 | }
78 | table.cv-table td {
79 | padding: 7px 16px;
80 | }
81 |
82 | .not-responding {
83 | color: white;
84 | }
85 | .not-responding em {
86 | background-color: #3f3f3f;
87 | padding: 2px 10px;
88 | display: inline-block;
89 | border-radius: 5px;
90 | }
91 | .not-responding hr {
92 | margin-top: 100px;
93 | border-color: #333;
94 | }
95 |
96 | .device-info {
97 | color: #aaa;
98 | }
99 | .device-info h4,
100 | .device-info h2 {
101 | color: #bbb;
102 | }
103 | .device-info em {
104 | background-color: #3f3f3f;
105 | padding: 2px 5px;
106 | display: inline-block;
107 | border-radius: 5px;
108 | color: #fdea08;
109 | margin: 1px;
110 | }
111 | button {
112 | display: block;
113 | box-sizing: content-box;
114 | background-color: rgba(255, 255, 255, 0.15);
115 | height: 28px;
116 | padding: 2px 20px;
117 | margin: 0px;
118 | border: rgba(255, 255, 255, 0.1) 1px solid;
119 | border-radius: 4px;
120 | color: #fff;
121 | font-size: 16px;
122 | font-weight: 500;
123 | }
124 |
--------------------------------------------------------------------------------
/src/assets/font/MaterialIcons-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/font/MaterialIcons-Regular.ttf
--------------------------------------------------------------------------------
/src/assets/img/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/icon.icns
--------------------------------------------------------------------------------
/src/assets/img/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/icon.ico
--------------------------------------------------------------------------------
/src/assets/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/icon.png
--------------------------------------------------------------------------------
/src/assets/img/outline_add_box_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_add_box_white_18dp.png
--------------------------------------------------------------------------------
/src/assets/img/outline_add_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_add_white_18dp.png
--------------------------------------------------------------------------------
/src/assets/img/outline_broken_image_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_broken_image_white_18dp.png
--------------------------------------------------------------------------------
/src/assets/img/outline_clear_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_clear_white_18dp.png
--------------------------------------------------------------------------------
/src/assets/img/outline_done_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_done_white_18dp.png
--------------------------------------------------------------------------------
/src/assets/img/outline_info_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_info_white_18dp.png
--------------------------------------------------------------------------------
/src/assets/img/outline_link_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_link_white_18dp.png
--------------------------------------------------------------------------------
/src/assets/img/outline_push_pin_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_push_pin_white_18dp.png
--------------------------------------------------------------------------------
/src/assets/img/outline_refresh_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_refresh_white_18dp.png
--------------------------------------------------------------------------------
/src/assets/img/outline_search_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_search_white_18dp.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | window.init();
2 |
--------------------------------------------------------------------------------
/src/plugins.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | /* eslint-disable import/no-dynamic-require */
3 | const fs = require('fs');
4 | const _ = require('lodash');
5 | const path = require('path');
6 |
7 | const DEVICE = require('./device.js');
8 | const VIEW = require('./view.js');
9 |
10 | const allPlugins = {};
11 | module.exports.all = allPlugins;
12 |
13 | module.exports.init = function init(callback) {
14 | const pluginDirectoryPath = path.normalize(path.join(__dirname, `../plugins`));
15 |
16 | console.log(`Loading plugin files... ${pluginDirectoryPath}`);
17 |
18 | fs.readdir(pluginDirectoryPath, (err, files) => {
19 | files.forEach((pluginDir) => {
20 | if (pluginDir[0] !== '.') {
21 | allPlugins[pluginDir] = require(path.join(pluginDirectoryPath, `/${pluginDir}/main.js`));
22 |
23 | const plugin = allPlugins[pluginDir];
24 |
25 | plugin.deviceInfoUpdate = function deviceInfoUpdate(device, param, value) {
26 | DEVICE.infoUpdate(device, param, value);
27 | };
28 | plugin.draw = (device) => {
29 | VIEW.draw(device);
30 | };
31 |
32 | plugin.template = _.template(
33 | fs.readFileSync(path.join(pluginDirectoryPath, `/${pluginDir}/template.ejs`), 'utf8')
34 | );
35 |
36 | plugin.info = _.template(fs.readFileSync(path.join(pluginDirectoryPath, `/${pluginDir}/info.html`), 'utf8'));
37 |
38 | plugin.heartbeatTimeout = plugin.config.heartbeatTimeout;
39 | plugin.heartbeatInterval = plugin.config.heartbeatInterval;
40 |
41 | console.log(`${pluginDir} loaded`);
42 | }
43 | });
44 |
45 | callback();
46 | });
47 | };
48 |
--------------------------------------------------------------------------------
/src/saveSlots.js:
--------------------------------------------------------------------------------
1 | const VIEW = require('./view.js');
2 | const DEVICE = require('./device.js');
3 |
4 | let activeSlot = false;
5 | let savedSlots = [[], [], [], []];
6 | let savedDevices = [];
7 |
8 | const storedSlots = localStorage.getItem('savedSlots');
9 | if (storedSlots) {
10 | savedSlots = JSON.parse(storedSlots);
11 | // can probably be removed but I've noticed some older versions left some [null] slots in the savedSlots
12 | savedSlots = savedSlots.map((savedSlot) => {
13 | if (savedSlot.length === 1 && savedSlot[0] === null) {
14 | return [];
15 | }
16 | return savedSlot;
17 | });
18 | }
19 |
20 | const storedDevices = localStorage.getItem('savedDevices');
21 | if (storedDevices) {
22 | savedDevices = JSON.parse(storedDevices);
23 | }
24 |
25 | function loadSlot(slotIndex) {
26 | VIEW.toggleSlotButtons(slotIndex);
27 | activeSlot = slotIndex;
28 |
29 | Object.keys(DEVICE.all).forEach((d) => {
30 | DEVICE.changePinIndex(DEVICE.all[d], false);
31 | });
32 | VIEW.resetPinned();
33 |
34 | Object.keys(savedSlots[slotIndex]).forEach((slot) => {
35 | const savedDevice = savedSlots[slotIndex][slot];
36 |
37 | Object.keys(DEVICE.all).forEach((d) => {
38 | const device = DEVICE.all[d];
39 |
40 | if (device.id === savedDevice.id) {
41 | VIEW.pinDevice(device);
42 | VIEW.switchDevice(device.id);
43 | } else if (
44 | device.addresses[0] === savedDevice.addresses[0] &&
45 | device.type === savedDevice.type &&
46 | savedDevice.addresses[0] !== undefined
47 | ) {
48 | VIEW.pinDevice(device);
49 | VIEW.switchDevice(device.id);
50 | }
51 | });
52 | });
53 | }
54 | module.exports.loadSlot = loadSlot;
55 |
56 | module.exports.loadDevices = function loadDevices() {
57 | console.log(`Loading ${savedDevices.length} saved devices...`);
58 |
59 | for (let i = 0; i < savedDevices.length; i++) {
60 | DEVICE.registerDevice(
61 | {
62 | type: savedDevices[i].type,
63 | displayName: savedDevices[i].displayName,
64 | defaultName: savedDevices[i].defaultName,
65 | remotePort: savedDevices[i].remotePort,
66 | localPort: savedDevices[i].localPort,
67 | addresses: savedDevices[i].addresses,
68 | id: savedDevices[i].id,
69 | fields: savedDevices[i].fields,
70 | },
71 | 'fromSave'
72 | );
73 | }
74 | };
75 |
76 | module.exports.saveAll = function saveAll() {
77 | console.log('Saving...');
78 | const currentPins = VIEW.getPinnedDevices();
79 |
80 | savedSlots[activeSlot] = [];
81 | for (let i = 0; i < currentPins.length; i++) {
82 | if (currentPins[i]) {
83 | // only include saved devices
84 | if (currentPins[i].id in DEVICE.all) {
85 | savedSlots[activeSlot][i] = {
86 | addresses: currentPins[i].addresses,
87 | type: currentPins[i].type,
88 | id: currentPins[i].id,
89 | };
90 | }
91 | }
92 | }
93 |
94 | // can probably be removed but I've noticed some older versions left some [null] slots in the savedSlots
95 | savedSlots = savedSlots.map((savedSlot) => {
96 | if (savedSlot.length === 1 && savedSlot[0] === null) {
97 | return [];
98 | }
99 | return savedSlot;
100 | });
101 |
102 | localStorage.setItem('savedSlots', JSON.stringify(savedSlots));
103 | console.log(`Saved ${currentPins.length} pinned devices to slot ${activeSlot}!`);
104 |
105 | savedDevices = [];
106 | let i = 0;
107 | Object.keys(DEVICE.all).forEach((d) => {
108 | const device = DEVICE.all[d];
109 | savedDevices[i] = {
110 | addresses: device.addresses,
111 | type: device.type,
112 | displayName: device.displayName,
113 | defaultName: device.defaultName,
114 | remotePort: device.remotePort,
115 | localPort: device.localPort,
116 | id: device.id,
117 | fields: device.fields,
118 | };
119 | i++;
120 | });
121 |
122 | localStorage.setItem('savedDevices', JSON.stringify(savedDevices));
123 | console.log(`Saved ${savedDevices.length} devices to storage!`);
124 | };
125 |
126 | module.exports.removeDevice = function removeDevice(_device) {
127 | // remove devices from local savedDevices
128 | savedDevices = savedDevices.filter((device) => device.id !== _device.id);
129 |
130 | // remove devices from all saved slots
131 | for (let i = 1; i < savedSlots.length; i++) {
132 | savedSlots[i] = savedSlots[i].filter((device) => device.id !== _device.id);
133 | }
134 |
135 | // things might have changed so run a save
136 | this.saveAll();
137 | };
138 |
139 | module.exports.reloadActiveSlot = function reloadActiveSlot() {
140 | loadSlot(activeSlot);
141 | };
142 |
143 | module.exports.clearSavedData = function clearSavedData() {
144 | localStorage.removeItem('savedSlots');
145 | localStorage.removeItem('savedDevices');
146 | };
147 |
--------------------------------------------------------------------------------
/src/search.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer } = require('electron');
2 | const dgram = require('dgram');
3 | const bonjour = require('bonjour')();
4 | const net = require('net');
5 | const os = require('os');
6 | const ip = require('ip');
7 |
8 | const { Netmask } = require('netmask');
9 | const DEVICE = require('./device.js');
10 | const PLUGINS = require('./plugins.js');
11 |
12 | let searching = false;
13 | let allServers = false;
14 | let validInterfaces = {};
15 |
16 | function getServers() {
17 | const interfaces = os.networkInterfaces();
18 | const result = [];
19 | validInterfaces = {};
20 |
21 | Object.keys(interfaces).forEach((key) => {
22 | const addresses = interfaces[key];
23 |
24 | for (let i = addresses.length; i--; ) {
25 | const address = addresses[i];
26 | if (address.family === 'IPv4' && !address.internal && address.address.substring(0, 3) !== '169') {
27 | let subnet = ip.subnet(address.address, address.netmask);
28 | let current = ip.toLong(subnet.firstAddress);
29 | let last = ip.toLong(subnet.lastAddress) - 1;
30 | address.searchTruncated = false;
31 |
32 | if (last - current > 2296) {
33 | subnet = ip.subnet(address.address, '255.255.248.0');
34 | last = ip.toLong(subnet.lastAddress) - 1;
35 | address.searchTruncated = true;
36 | }
37 |
38 | // console.log(`range ${subnet.firstAddress} - ${subnet.lastAddress}`);
39 |
40 | address.broadcastAddress = subnet.broadcastAddress;
41 | address.firstSearchAddress = subnet.firstAddress;
42 | address.lastSearchAddress = subnet.lastAddress;
43 |
44 | if (!validInterfaces[key]) {
45 | validInterfaces[key] = [];
46 | }
47 | validInterfaces[key].push(address);
48 |
49 | while (current++ < last) result.push(ip.fromLong(current));
50 | }
51 | }
52 | });
53 | return result;
54 | }
55 | function getNetworkInterfaces() {
56 | getServers();
57 | return validInterfaces;
58 | }
59 | module.exports.getNetworkInterfaces = getNetworkInterfaces;
60 |
61 | const searchSockets = [];
62 | function searchAll() {
63 | if (searching) {
64 | return;
65 | }
66 | searching = true;
67 | ipcRenderer.send('disableSearchAll', '');
68 | document.getElementById('search-button').style.opacity = 0.2;
69 | console.log('Searching...');
70 | allServers = getServers();
71 | let TCPFlag = true;
72 | if (allServers.length > 2296) {
73 | alert(
74 | 'Unable to search for TCP devices - subnet too large!\n\nCue View requires subnet 255.255.248.0 (/21) or smaller.'
75 | );
76 | TCPFlag = false;
77 | }
78 |
79 | Object.keys(PLUGINS.all).forEach((pluginType) => {
80 | const plugin = PLUGINS.all[pluginType];
81 |
82 | try {
83 | const searchType = plugin.config.searchOptions.type;
84 |
85 | if (searchType === 'TCPport') {
86 | if (TCPFlag) {
87 | searchTCP(pluginType, plugin.config);
88 | }
89 | } else if (searchType === 'Bonjour') {
90 | searchBonjour(pluginType, plugin.config);
91 | } else if (searchType === 'UDPsocket') {
92 | searchUDP(pluginType, plugin.config);
93 | } else if (searchType === 'multicast') {
94 | searchMulticast(pluginType, plugin.config);
95 | } else if (searchType === 'UDPScan') {
96 | searchUDPScan(pluginType, plugin.config);
97 | }
98 | } catch (err) {
99 | console.error(`Unable to search for plugin ${pluginType}`);
100 | }
101 | });
102 |
103 | setTimeout(() => {
104 | searching = false;
105 | document.getElementById('search-button').style.opacity = '';
106 |
107 | for (let i = 0; i < searchSockets.length; i++) {
108 | try {
109 | searchSockets[i].close();
110 | } catch (err) {
111 | //
112 | }
113 | }
114 |
115 | ipcRenderer.send('enableSearchAll', '');
116 | }, 10000);
117 | }
118 | module.exports.searchAll = searchAll;
119 |
120 | function searchBonjour(pluginType, pluginConfig) {
121 | bonjour.find({ type: pluginConfig.searchOptions.bonjourName }, (e) => {
122 | const validAddresses = [];
123 | e.addresses.forEach((address) => {
124 | if (!address.includes(':')) {
125 | validAddresses.push(address);
126 | }
127 | });
128 |
129 | DEVICE.registerDevice(
130 | {
131 | type: pluginType,
132 | defaultName: e.name,
133 | remotePort: e.port,
134 | addresses: validAddresses,
135 | },
136 | 'fromSearch'
137 | );
138 | });
139 | }
140 |
141 | function searchTCP(pluginType, pluginConfig) {
142 | for (let i = 0; i < allServers.length; i++) {
143 | TCPtest(allServers[i], pluginType, pluginConfig);
144 | }
145 | }
146 |
147 | function TCPtest(ipAddr, pluginType, pluginConfig) {
148 | const client = net.createConnection(pluginConfig.searchOptions.testPort, ipAddr, () => {
149 | client.write(pluginConfig.searchOptions.searchBuffer);
150 | });
151 | client.on('data', (data) => {
152 | if (pluginConfig.searchOptions.validateResponse(data)) {
153 | client.end(
154 | '',
155 | 'utf8',
156 | DEVICE.registerDevice(
157 | {
158 | type: pluginType,
159 | defaultName: pluginConfig.defaultName,
160 | remotePort: pluginConfig.remotePort,
161 | addresses: [ipAddr],
162 | },
163 | 'fromSearch'
164 | )
165 | );
166 | }
167 | });
168 | client.on('error', (err) => {
169 | // no device here
170 | });
171 | }
172 |
173 | function searchUDPScan(pluginType, pluginConfig) {
174 | for (let i = 0; i < Object.keys(validInterfaces).length; i++) {
175 | const interfaceID = Object.keys(validInterfaces)[i];
176 | const interfaceObj = validInterfaces[interfaceID];
177 | interfaceObj.forEach((netInterface) => {
178 | const udpSocket = dgram.createSocket('udp4');
179 | udpSocket.bind(pluginConfig.searchOptions.listenPort, netInterface.address);
180 |
181 | udpSocket.on('message', (msg, info) => {
182 | if (pluginConfig.searchOptions.validateResponse(msg, info, DEVICE.all)) {
183 | udpSocket.close();
184 | DEVICE.registerDevice(
185 | {
186 | type: pluginType,
187 | defaultName: pluginConfig.defaultName,
188 | port: pluginConfig.defaultPort,
189 | addresses: [info.address],
190 | },
191 | 'fromSearch'
192 | );
193 | }
194 | });
195 | const interfaceBlock = new Netmask(netInterface.cidr);
196 | interfaceBlock.forEach((address, long, index) => {
197 | udpSocket.send(pluginConfig.searchOptions.searchBuffer, pluginConfig.searchOptions.devicePort, address);
198 | });
199 | });
200 | }
201 | }
202 |
203 | function searchUDP(pluginType, pluginConfig) {
204 | for (let i = 0; i < Object.keys(validInterfaces).length; i++) {
205 | const interfaceID = Object.keys(validInterfaces)[i];
206 | const interfaceObj = validInterfaces[interfaceID];
207 |
208 | const j = searchSockets.push(dgram.createSocket('udp4')) - 1;
209 |
210 | searchSockets[j].bind(pluginConfig.searchOptions.listenPort, () => {
211 | searchSockets[j].on('message', (msg, info) => {
212 | if (pluginConfig.searchOptions.validateResponse(msg, info, DEVICE.all)) {
213 | searchSockets[j].close();
214 | DEVICE.registerDevice(
215 | {
216 | type: pluginType,
217 | defaultName: pluginConfig.defaultName,
218 | remotePort: pluginConfig.remotePort,
219 | addresses: [info.address],
220 | },
221 | 'fromSearch'
222 | );
223 | }
224 | });
225 | });
226 |
227 | searchSockets[j].on('listening', () => {
228 | searchSockets[j].setBroadcast(true);
229 | searchSockets[j].send(
230 | pluginConfig.searchOptions.searchBuffer,
231 | pluginConfig.searchOptions.devicePort,
232 | interfaceObj[0].broadcastAddress,
233 | (err) => {
234 | // console.log(err);
235 | }
236 | );
237 | });
238 | }
239 | }
240 |
241 | function searchMulticast(pluginType, pluginConfig) {
242 | const socket = dgram.createSocket('udp4');
243 | socket.on('message', (msg, info) => {
244 | if (pluginConfig.searchOptions.validateResponse(msg, info)) {
245 | socket.close(() => {
246 | DEVICE.registerDevice(
247 | {
248 | type: pluginType,
249 | defaultName: pluginConfig.defaultName,
250 | remotePort: pluginConfig.remotePort,
251 | addresses: [info.address],
252 | },
253 | 'fromSearch'
254 | );
255 | });
256 | }
257 | });
258 |
259 | socket.bind(pluginConfig.searchOptions.port, () => {
260 | for (let i = 0; i < Object.keys(validInterfaces).length; i++) {
261 | const interfaceID = Object.keys(validInterfaces)[i];
262 | const interfaceObj = validInterfaces[interfaceID];
263 |
264 | socket.addMembership(pluginConfig.searchOptions.address, interfaceObj[0].address);
265 | }
266 | });
267 | }
268 |
--------------------------------------------------------------------------------