// this is our global websocket, used to communicate from/to Stream Deck software // and some info about our plugin, as sent by Stream Deck software var websocket = null, uuid = null, actionInfo = {}, settings = {}, globalSettings = {}, isQT = navigator.appVersion.includes('QtWebEngine'); // 'oninput'; // change this, if you want interactive elements act on any change, or while they're modified const websiteAction = 'tf.meow.remote.website'; function connectSocket ( inPort, inUUID, inMessageType, inApplicationInfo, inActionInfo ) { connectElgatoStreamDeckSocket( inPort, inUUID, inMessageType, inApplicationInfo, inActionInfo ); } function connectElgatoStreamDeckSocket (inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) { uuid = inUUID; // please note: the incoming arguments are of type STRING, so // in case of the inActionInfo, we must parse it into JSON first actionInfo = JSON.parse(inActionInfo); // cache the info inInfo = JSON.parse(inInfo); websocket = new WebSocket('ws://localhost:' + inPort); /** Since the PI doesn't have access to your OS native settings * Stream Deck sends some color settings to PI * We use these to adjust some styles (e.g. highlight-colors for checkboxes) */ addDynamicStyles(inInfo.colors, 'connectElgatoStreamDeckSocket'); /** let's see, if we have some settings */ settings = getPropFromString(actionInfo, 'payload.settings'); console.log(settings, actionInfo); initPropertyInspector(5); // if connection was established, the websocket sends // an 'onopen' event, where we need to register our PI websocket.onopen = function () { var json = { event: inRegisterEvent, uuid: inUUID }; websocket.send(JSON.stringify(json)); if (isAction(websiteAction)) { getGlobalSettings(); } }; websocket.onmessage = function (evt) { // Received message from Stream Deck let jsonObj = JSON.parse(evt.data); let event = jsonObj['event']; console.log('Got event', event); switch (event) { case 'didReceiveGlobalSettings': didReceiveGlobalSettings(jsonObj); break; } }; } function initPropertyInspector(initDelay) { const action = actionInfo['action']; $('[data-action=' + action + ']').removeClass('hidden'); Object.keys(settings).forEach(function (item) { let $item = $('#' + item), value = settings[item]; switch ($item.attr('type')) { case 'checkbox': let itemVal = $item.attr('value'); if (itemVal == 'false' || itemVal == 'true') { itemVal = (/^true$/i).test(itemVal); } if (itemVal === value) { $item.prop('checked', true); } break; default: $item.val(value); } }); $('input').each(function() { let $this = $(this), id = $this.attr('id'); let $item = $this.closest('.sdpi-item'); $this.on('change', function() { const type = $this.attr('type'); let val = $this.val(); switch (type) { case 'checkbox': // If unchecked, unset the setting if (!this.checked) { removeSetting(id); return; } if (val == 'false' || val == 'true') { val = (/^true$/i).test(val); } break; case 'file': const info = $item.find('.sdpi-file-info'); if (info) { const s = decodeURIComponent($this.val().replace(/^C:\\fakepath\\/, '')).split('/').pop(); info.text(s.length > 28 ? s.substr(0, 10) + '...' + s.substr(s.length - 10, s.length) : s); } break; } updateSetting(id, val); if (isAction(websiteAction) && (id == 'remote_host' || id == 'remote_token')) { updateGlobalSetting(id, val); } }); }); $('#ssh_key').change(function(e) { if (e.target.files.length < 1) { // Hide passphrase field $('#ssh_key_passphrase_container').hide(); $('#ssh_password_container').show(); return; } else { $('#ssh_password_container').hide(); } let f = e.target.files[0]; let reader = new FileReader(); // Closure to capture the file information. reader.onload = function(e) { let result = e.target.result; let pki = forge.pki; try { pki.privateKeyFromPem(result); $('#ssh_key_passphrase_container').hide(); } catch (ex) { if (ex.message.indexOf('PEM is encrypted.') !== -1) { $('#ssh_key_passphrase_container').show(); } else { // Show error message? } } }; // Read in the image file as a data URL. reader.readAsText(f); }); } if (!isQT) { document.addEventListener('DOMContentLoaded', function () { initPropertyInspector(100); }); } /** Stream Deck software passes system-highlight color information * to Property Inspector. Here we 'inject' the CSS styles into the DOM * when we receive this information. */ function addDynamicStyles (clrs, fromWhere) { const node = document.getElementById('#sdpi-dynamic-styles') || document.createElement('style'); if (!clrs.mouseDownColor) clrs.mouseDownColor = fadeColor(clrs.highlightColor, -100); const clr = clrs.highlightColor.slice(0, 7); const clr1 = fadeColor(clr, 100); const clr2 = fadeColor(clr, 60); const metersActiveColor = fadeColor(clr, -60); node.setAttribute('id', 'sdpi-dynamic-styles'); node.innerHTML = ` input[type="radio"]:checked + label span, input[type="checkbox"]:checked + label span { background-color: ${clrs.highlightColor}; } input[type="radio"]:active:checked + label span, input[type="radio"]:active + label span, input[type="checkbox"]:active:checked + label span, input[type="checkbox"]:active + label span { background-color: ${clrs.mouseDownColor}; } input[type="radio"]:active + label span, input[type="checkbox"]:active + label span { background-color: ${clrs.buttonPressedBorderColor}; } td.selected, td.selected:hover, li.selected:hover, li.selected { color: white; background-color: ${clrs.highlightColor}; } .sdpi-file-label > label:active, .sdpi-file-label.file:active, label.sdpi-file-label:active, label.sdpi-file-info:active, input[type="file"]::-webkit-file-upload-button:active, button:active { background-color: ${clrs.buttonPressedBackgroundColor}; color: ${clrs.buttonPressedTextColor}; border-color: ${clrs.buttonPressedBorderColor}; } ::-webkit-progress-value, meter::-webkit-meter-optimum-value { background: linear-gradient(${clr2}, ${clr1} 20%, ${clr} 45%, ${clr} 55%, ${clr2}) } ::-webkit-progress-value:active, meter::-webkit-meter-optimum-value:active { background: linear-gradient(${clr}, ${clr2} 20%, ${metersActiveColor} 45%, ${metersActiveColor} 55%, ${clr}) } `; document.body.appendChild(node); }; /** UTILITIES */ /** get a JSON property from a (dot-separated) string * Works on nested JSON, e.g.: * jsn = { * propA: 1, * propB: 2, * propC: { * subA: 3, * subB: { * testA: 5, * testB: 'Hello' * } * } * } * getPropFromString(jsn,'propC.subB.testB') will return 'Hello'; */ const getPropFromString = (jsn, str, sep = '.') => { const arr = str.split(sep); return arr.reduce((obj, key) => (obj && obj.hasOwnProperty(key)) ? obj[key] : undefined, jsn); }; /* Quick utility to lighten or darken a color (doesn't take color-drifting, etc. into account) Usage: fadeColor('#061261', 100); // will lighten the color fadeColor('#200867'), -100); // will darken the color */ function fadeColor (col, amt) { const min = Math.min, max = Math.max; const num = parseInt(col.replace(/#/g, ''), 16); const r = min(255, max((num >> 16) + amt, 0)); const g = min(255, max((num & 0x0000FF) + amt, 0)); const b = min(255, max(((num >> 8) & 0x00FF) + amt, 0)); return '#' + (g | (b << 8) | (r << 16)).toString(16).padStart(6, 0); } function updateSetting(setting, value) { if (!settings) { settings = {}; } settings[setting] = value; setSettings(settings); } function removeSetting(setting) { if (!settings) { settings = {}; } delete settings[setting]; setSettings(settings); } function setSettings(settings) { let json = { "event": "setSettings", "context": uuid, "payload": settings }; if (websocket) { websocket.send(JSON.stringify(json)); } } function updateGlobalSetting(id, val) { globalSettings[id] = val; setGlobalSettings(globalSettings); } function getGlobalSettings() { let json = { "event": "getGlobalSettings", "context": uuid }; if (websocket) { websocket.send(JSON.stringify(json)); } } function setGlobalSettings(settings) { let json = { "event": "setGlobalSettings", "context": uuid, "payload": settings }; if (websocket) { websocket.send(JSON.stringify(json)); } } function didReceiveGlobalSettings(obj) { globalSettings = getPropFromString(obj, 'payload.settings'); // Load defaults for fields not set Object.keys(globalSettings).forEach(function(item) { if (!(item in settings)) { $('#' + item).val(globalSettings[item]); } }); } function isAction(action) { return actionInfo['action'] === action; }