Add Paarrot Stream Deck Plugin with core functionality
Some checks failed
Build / increment-version (push) Successful in 8s
Build / build-linux (push) Successful in 2m26s
Build Paarrot Windows / build (push) Has been cancelled
Build Paarrot Windows / start-vm (push) Has been cancelled
Build / build-windows (push) Successful in 4m28s
Build / create-release (push) Successful in 30s
- Implemented main plugin structure including manifest, HTML, and JavaScript files. - Added actions for toggling mute, toggling deafen, changing channels, sending messages, and getting status. - Created property inspector for changing channels and sending messages. - Developed SVG icons for actions and status display. - Established WebSocket connection for communication with the Stream Deck. - Implemented API calls to interact with the Paarrot voice chat application. - Added validation script to ensure proper plugin structure and manifest compliance. - Included test Electron application for window creation and loading content.
@@ -0,0 +1,9 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<rect x="30" y="30" width="40" height="40" rx="8" fill="#666" stroke="none"/>
|
||||
<rect x="74" y="30" width="40" height="40" rx="8" fill="#888" stroke="none"/>
|
||||
<rect x="30" y="74" width="40" height="40" rx="8" fill="#666" stroke="none"/>
|
||||
<rect x="74" y="74" width="40" height="40" rx="8" fill="#666" stroke="none"/>
|
||||
<path d="M50 50 L50 42 L58 50 L50 58 Z" fill="#444" stroke="none"/>
|
||||
<path d="M94 50 L94 42 L102 50 L94 58 Z" fill="#fff" stroke="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 606 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M40 50 L40 94 L56 94 L76 110 L76 34 L56 50 Z" fill="#888" stroke="none"/>
|
||||
<path d="M88 58 C94 64 94 80 88 86" stroke="#888" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
<path d="M98 50 C108 60 108 84 98 94" stroke="#888" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 444 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M40 50 L40 94 L56 94 L76 110 L76 34 L56 50 Z" fill="#444" stroke="none"/>
|
||||
<path d="M88 58 C94 64 94 80 88 86" stroke="#444" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
<path d="M98 50 C108 60 108 84 98 94" stroke="#444" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
<line x1="112" y1="32" x2="32" y2="112" stroke="#ff4444" stroke-width="8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 545 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M72 36 L90 54 L90 90 C90 100 82 108 72 108 C62 108 54 100 54 90 L54 54 Z" fill="#888" stroke="none"/>
|
||||
<ellipse cx="72" cy="54" rx="18" ry="24" fill="#888"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M72 36 L90 54 L90 90 C90 100 82 108 72 108 C62 108 54 100 54 90 L54 54 Z" fill="#444" stroke="none"/>
|
||||
<ellipse cx="72" cy="54" rx="18" ry="24" fill="#444"/>
|
||||
<line x1="108" y1="36" x2="36" y2="108" stroke="#ff4444" stroke-width="8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 416 B |
5634
streamdeck-plugin/com.paarrot.streamdeck.sdPlugin/images/paarrot.svg
Normal file
|
After Width: | Height: | Size: 889 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M30 72 L114 36 L90 108 L64 88 L74 64 L50 78 Z" fill="#888" stroke="none"/>
|
||||
<line x1="64" y1="88" x2="74" y2="64" stroke="#1a1a1a" stroke-width="4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<circle cx="72" cy="72" r="36" fill="none" stroke="#888" stroke-width="6"/>
|
||||
<circle cx="72" cy="72" r="8" fill="#888"/>
|
||||
<line x1="72" y1="72" x2="72" y2="48" stroke="#888" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="72" y1="72" x2="90" y2="72" stroke="#888" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 461 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M40 50 L40 94 L56 94 L76 110 L76 34 L56 50 Z" fill="#888" stroke="none"/>
|
||||
<path d="M88 58 C94 64 94 80 88 86" stroke="#888" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
<path d="M98 50 C108 60 108 84 98 94" stroke="#888" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
<path d="M108 54 L118 64 M108 90 L118 80" stroke="#888" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 544 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M72 36 L90 54 L90 90 C90 100 82 108 72 108 C62 108 54 100 54 90 L54 54 Z" fill="#888" stroke="none"/>
|
||||
<ellipse cx="72" cy="54" rx="18" ry="24" fill="#888"/>
|
||||
<path d="M98 58 L108 68 M98 86 L108 76" stroke="#888" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 413 B |
113
streamdeck-plugin/com.paarrot.streamdeck.sdPlugin/manifest.json
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"$schema": "https://schemas.elgato.com/streamdeck/plugins/manifest.json",
|
||||
"Actions": [
|
||||
{
|
||||
"DisableAutomaticStates": true,
|
||||
"Icon": "images/toggle-mute",
|
||||
"Name": "Toggle Mute",
|
||||
"States": [
|
||||
{
|
||||
"Image": "images/mute-off",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
},
|
||||
{
|
||||
"Image": "images/mute-on",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
}
|
||||
],
|
||||
"SupportedInMultiActions": true,
|
||||
"Tooltip": "Toggle microphone mute",
|
||||
"UUID": "com.paarrot.streamdeck.togglemute"
|
||||
},
|
||||
{
|
||||
"DisableAutomaticStates": true,
|
||||
"Icon": "images/toggle-deafen",
|
||||
"Name": "Toggle Deafen",
|
||||
"States": [
|
||||
{
|
||||
"Image": "images/deafen-off",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
},
|
||||
{
|
||||
"Image": "images/deafen-on",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
}
|
||||
],
|
||||
"SupportedInMultiActions": true,
|
||||
"Tooltip": "Toggle deafen (mute speakers)",
|
||||
"UUID": "com.paarrot.streamdeck.toggledeafen"
|
||||
},
|
||||
{
|
||||
"Icon": "images/change-channel",
|
||||
"Name": "Change Channel",
|
||||
"PropertyInspectorPath": "propertyinspector/change-channel.html",
|
||||
"States": [
|
||||
{
|
||||
"Image": "images/change-channel",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
}
|
||||
],
|
||||
"SupportedInMultiActions": true,
|
||||
"Tooltip": "Switch to a specific channel/room",
|
||||
"UUID": "com.paarrot.streamdeck.changechannel"
|
||||
},
|
||||
{
|
||||
"Icon": "images/send-message",
|
||||
"Name": "Send Message",
|
||||
"PropertyInspectorPath": "propertyinspector/send-message.html",
|
||||
"States": [
|
||||
{
|
||||
"Image": "images/send-message",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
}
|
||||
],
|
||||
"SupportedInMultiActions": true,
|
||||
"Tooltip": "Send a message to current room",
|
||||
"UUID": "com.paarrot.streamdeck.sendmessage"
|
||||
},
|
||||
{
|
||||
"Icon": "images/status",
|
||||
"Name": "Get Status",
|
||||
"States": [
|
||||
{
|
||||
"Image": "images/status",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
}
|
||||
],
|
||||
"SupportedInMultiActions": false,
|
||||
"Tooltip": "Display current status (mute/deafen state)",
|
||||
"UUID": "com.paarrot.streamdeck.getstatus"
|
||||
}
|
||||
],
|
||||
"Author": "Paarrot",
|
||||
"Category": "Paarrot",
|
||||
"CategoryIcon": "images/paarrot",
|
||||
"CodePath": "plugin/index.html",
|
||||
"Description": "Control Paarrot voice chat (mute, deafen, channels, messages)",
|
||||
"Icon": "images/paarrot",
|
||||
"Name": "Paarrot",
|
||||
"OS": [
|
||||
{
|
||||
"Platform": "windows",
|
||||
"MinimumVersion": "10"
|
||||
},
|
||||
{
|
||||
"Platform": "mac",
|
||||
"MinimumVersion": "10.14"
|
||||
}
|
||||
],
|
||||
"SDKVersion": 2,
|
||||
"Software": {
|
||||
"MinimumVersion": "6.6"
|
||||
},
|
||||
"UUID": "com.paarrot.streamdeck",
|
||||
"URL": "https://github.com/yourusername/paarrot-streamdeck",
|
||||
"Version": "1.0.0.0"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Paarrot Plugin</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* Paarrot Stream Deck Plugin
|
||||
* Controls the Paarrot desktop app via its local API
|
||||
*/
|
||||
|
||||
const API_BASE_URL = 'http://127.0.0.1:33384';
|
||||
const POLL_INTERVAL = 1000; // Poll status every second
|
||||
|
||||
/**
|
||||
* Global plugin instance
|
||||
*/
|
||||
let websocket = null;
|
||||
let pluginUUID = null;
|
||||
let statusPollInterval = null;
|
||||
|
||||
/**
|
||||
* Track state for each action instance
|
||||
*/
|
||||
const actionStates = new Map();
|
||||
|
||||
/**
|
||||
* Action UUIDs
|
||||
*/
|
||||
const ACTIONS = {
|
||||
TOGGLE_MUTE: 'com.paarrot.streamdeck.togglemute',
|
||||
TOGGLE_DEAFEN: 'com.paarrot.streamdeck.toggledeafen',
|
||||
CHANGE_CHANNEL: 'com.paarrot.streamdeck.changechannel',
|
||||
SEND_MESSAGE: 'com.paarrot.streamdeck.sendmessage',
|
||||
GET_STATUS: 'com.paarrot.streamdeck.getstatus'
|
||||
};
|
||||
|
||||
/**
|
||||
* Make API call to Paarrot
|
||||
*/
|
||||
async function callAPI(endpoint, method = 'GET', body = null) {
|
||||
try {
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'API call failed');
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.error(`API Error (${endpoint}):`, error);
|
||||
showAlert();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current app status
|
||||
*/
|
||||
async function getStatus() {
|
||||
return await callAPI('/status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set mute state explicitly
|
||||
* @param {boolean} muted - Whether to mute
|
||||
*/
|
||||
async function setMute(muted) {
|
||||
return await callAPI('/mute', 'POST', { muted });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set deafen state explicitly
|
||||
* @param {boolean} deafened - Whether to deafen
|
||||
*/
|
||||
async function setDeafen(deafened) {
|
||||
return await callAPI('/deafen', 'POST', { deafened });
|
||||
}
|
||||
|
||||
/**
|
||||
* Change channel
|
||||
*/
|
||||
async function changeChannel(roomId) {
|
||||
return await callAPI('/channel', 'POST', { roomId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to current room
|
||||
*/
|
||||
async function sendMessage(message) {
|
||||
return await callAPI('/message/current', 'POST', { message });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of channels
|
||||
*/
|
||||
async function getChannels() {
|
||||
return await callAPI('/channels');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update button state based on status
|
||||
*/
|
||||
function updateButtonStates(status) {
|
||||
actionStates.forEach((state, context) => {
|
||||
if (state.action === ACTIONS.TOGGLE_MUTE) {
|
||||
const newState = status.muted ? 1 : 0;
|
||||
if (state.currentState !== newState) {
|
||||
setState(context, newState);
|
||||
state.currentState = newState;
|
||||
}
|
||||
} else if (state.action === ACTIONS.TOGGLE_DEAFEN) {
|
||||
const newState = status.deafened ? 1 : 0;
|
||||
if (state.currentState !== newState) {
|
||||
setState(context, newState);
|
||||
state.currentState = newState;
|
||||
}
|
||||
} else if (state.action === ACTIONS.GET_STATUS) {
|
||||
const title = `${status.muted ? 'M' : ''}${status.deafened ? 'D' : ''}${!status.muted && !status.deafened ? 'OK' : ''}`;
|
||||
setTitle(context, title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start polling for status updates
|
||||
*/
|
||||
function startStatusPolling() {
|
||||
if (statusPollInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
statusPollInterval = setInterval(async () => {
|
||||
try {
|
||||
const status = await getStatus();
|
||||
updateButtonStates(status);
|
||||
} catch (error) {
|
||||
// Silently fail - server might not be running
|
||||
}
|
||||
}, POLL_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop polling for status updates
|
||||
*/
|
||||
function stopStatusPolling() {
|
||||
if (statusPollInterval) {
|
||||
clearInterval(statusPollInterval);
|
||||
statusPollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send command to Stream Deck
|
||||
*/
|
||||
function sendToStreamDeck(event, context, payload = {}) {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
event,
|
||||
context,
|
||||
...payload
|
||||
};
|
||||
websocket.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set button state
|
||||
*/
|
||||
function setState(context, state) {
|
||||
sendToStreamDeck('setState', context, { payload: { state } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set button title
|
||||
*/
|
||||
function setTitle(context, title) {
|
||||
sendToStreamDeck('setTitle', context, { payload: { title } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Show alert on button
|
||||
*/
|
||||
function showAlert(context) {
|
||||
if (context) {
|
||||
sendToStreamDeck('showAlert', context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show OK on button
|
||||
*/
|
||||
function showOk(context) {
|
||||
sendToStreamDeck('showOk', context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle button press
|
||||
*/
|
||||
async function handleKeyDown(context, settings, action) {
|
||||
try {
|
||||
switch (action) {
|
||||
case ACTIONS.TOGGLE_MUTE: {
|
||||
// Get current state and set opposite
|
||||
const status = await getStatus();
|
||||
await setMute(!status.muted);
|
||||
showOk(context);
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTIONS.TOGGLE_DEAFEN: {
|
||||
// Get current state and set opposite
|
||||
const status = await getStatus();
|
||||
await setDeafen(!status.deafened);
|
||||
showOk(context);
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTIONS.CHANGE_CHANNEL:
|
||||
if (settings.roomId) {
|
||||
await changeChannel(settings.roomId);
|
||||
showOk(context);
|
||||
} else {
|
||||
showAlert(context);
|
||||
}
|
||||
break;
|
||||
|
||||
case ACTIONS.SEND_MESSAGE:
|
||||
if (settings.message) {
|
||||
await sendMessage(settings.message);
|
||||
showOk(context);
|
||||
} else {
|
||||
showAlert(context);
|
||||
}
|
||||
break;
|
||||
|
||||
case ACTIONS.GET_STATUS:
|
||||
const status = await getStatus();
|
||||
updateButtonStates(status);
|
||||
showOk(context);
|
||||
break;
|
||||
}
|
||||
|
||||
// Force immediate status update
|
||||
const status = await getStatus();
|
||||
updateButtonStates(status);
|
||||
} catch (error) {
|
||||
showAlert(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action appearing
|
||||
*/
|
||||
function handleWillAppear(context, settings, action) {
|
||||
actionStates.set(context, {
|
||||
action,
|
||||
settings,
|
||||
currentState: 0
|
||||
});
|
||||
|
||||
// Get initial state
|
||||
getStatus()
|
||||
.then(status => updateButtonStates(status))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action disappearing
|
||||
*/
|
||||
function handleWillDisappear(context) {
|
||||
actionStates.delete(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle settings change
|
||||
*/
|
||||
function handleDidReceiveSettings(context, settings, action) {
|
||||
const state = actionStates.get(context);
|
||||
if (state) {
|
||||
state.settings = settings;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle property inspector request
|
||||
*/
|
||||
function handleSendToPlugin(context, action, payload) {
|
||||
// Handle requests from property inspector
|
||||
if (payload.event === 'getChannels') {
|
||||
getChannels()
|
||||
.then(channels => {
|
||||
sendToStreamDeck('sendToPropertyInspector', context, {
|
||||
action,
|
||||
payload: {
|
||||
event: 'channelList',
|
||||
channels
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to get channels:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup WebSocket connection to Stream Deck
|
||||
*/
|
||||
function connectElgatoStreamDeckSocket(inPort, inPluginUUID, inRegisterEvent, inInfo) {
|
||||
pluginUUID = inPluginUUID;
|
||||
|
||||
websocket = new WebSocket(`ws://127.0.0.1:${inPort}`);
|
||||
|
||||
websocket.onopen = () => {
|
||||
const registerPayload = {
|
||||
event: inRegisterEvent,
|
||||
uuid: inPluginUUID
|
||||
};
|
||||
websocket.send(JSON.stringify(registerPayload));
|
||||
|
||||
// Start status polling
|
||||
startStatusPolling();
|
||||
};
|
||||
|
||||
websocket.onmessage = (evt) => {
|
||||
try {
|
||||
const message = JSON.parse(evt.data);
|
||||
const { event, action, context, payload } = message;
|
||||
const settings = payload?.settings || {};
|
||||
|
||||
switch (event) {
|
||||
case 'keyDown':
|
||||
handleKeyDown(context, settings, action);
|
||||
break;
|
||||
|
||||
case 'willAppear':
|
||||
handleWillAppear(context, settings, action);
|
||||
break;
|
||||
|
||||
case 'willDisappear':
|
||||
handleWillDisappear(context);
|
||||
break;
|
||||
|
||||
case 'didReceiveSettings':
|
||||
handleDidReceiveSettings(context, settings, action);
|
||||
break;
|
||||
|
||||
case 'sendToPlugin':
|
||||
handleSendToPlugin(context, action, payload);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebSocket message error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
websocket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
websocket.onclose = () => {
|
||||
stopStatusPolling();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Change Channel Settings</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 10px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #d0d0d0;
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
select, input {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #3d3d3d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
color: #d0d0d0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
select:focus, input:focus {
|
||||
outline: none;
|
||||
border-color: #0e99e0;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 8px;
|
||||
background-color: #3d3d3d;
|
||||
border-left: 3px solid #0e99e0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: #3d3d3d;
|
||||
color: #d0d0d0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="info">
|
||||
Select a room/channel to switch to when this button is pressed.
|
||||
</div>
|
||||
|
||||
<label for="roomId">Channel/Room:</label>
|
||||
<select id="roomId">
|
||||
<option value="">Loading channels...</option>
|
||||
</select>
|
||||
|
||||
<label for="customRoomId">Or enter Room ID manually:</label>
|
||||
<input type="text" id="customRoomId" placeholder="!abc123:matrix.org" />
|
||||
|
||||
<script>
|
||||
let websocket = null;
|
||||
let uuid = null;
|
||||
let actionInfo = null;
|
||||
let currentSettings = {};
|
||||
|
||||
function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) {
|
||||
uuid = inUUID;
|
||||
actionInfo = JSON.parse(inActionInfo);
|
||||
currentSettings = actionInfo.payload.settings || {};
|
||||
|
||||
websocket = new WebSocket('ws://127.0.0.1:' + inPort);
|
||||
|
||||
websocket.onopen = function() {
|
||||
const json = {
|
||||
event: inRegisterEvent,
|
||||
uuid: inUUID
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
|
||||
// Request channel list
|
||||
sendToPlugin({ event: 'getChannels' });
|
||||
};
|
||||
|
||||
websocket.onmessage = function(evt) {
|
||||
const message = JSON.parse(evt.data);
|
||||
|
||||
if (message.event === 'sendToPropertyInspector') {
|
||||
if (message.payload.event === 'channelList') {
|
||||
populateChannels(message.payload.channels);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set current values
|
||||
if (currentSettings.roomId) {
|
||||
document.getElementById('customRoomId').value = currentSettings.roomId;
|
||||
}
|
||||
}
|
||||
|
||||
function sendToPlugin(payload) {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
action: actionInfo.action,
|
||||
event: 'sendToPlugin',
|
||||
context: uuid,
|
||||
payload: payload
|
||||
};
|
||||
websocket.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
const settings = {
|
||||
roomId: document.getElementById('customRoomId').value || document.getElementById('roomId').value
|
||||
};
|
||||
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
event: 'setSettings',
|
||||
context: uuid,
|
||||
payload: settings
|
||||
};
|
||||
websocket.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
function populateChannels(channels) {
|
||||
const select = document.getElementById('roomId');
|
||||
select.innerHTML = '<option value="">-- Select a channel --</option>';
|
||||
|
||||
if (!channels || channels.length === 0) {
|
||||
select.innerHTML = '<option value="">No channels available</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by server
|
||||
const grouped = {};
|
||||
channels.forEach(channel => {
|
||||
if (!grouped[channel.server]) {
|
||||
grouped[channel.server] = [];
|
||||
}
|
||||
grouped[channel.server].push(channel);
|
||||
});
|
||||
|
||||
// Add options grouped by server
|
||||
Object.keys(grouped).sort().forEach(server => {
|
||||
const optgroup = document.createElement('optgroup');
|
||||
optgroup.label = server;
|
||||
|
||||
grouped[server].forEach(channel => {
|
||||
const option = document.createElement('option');
|
||||
option.value = channel.roomId;
|
||||
option.textContent = channel.name;
|
||||
if (currentSettings.roomId === channel.roomId) {
|
||||
option.selected = true;
|
||||
}
|
||||
optgroup.appendChild(option);
|
||||
});
|
||||
|
||||
select.appendChild(optgroup);
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('roomId').addEventListener('change', saveSettings);
|
||||
document.getElementById('customRoomId').addEventListener('input', saveSettings);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Send Message Settings</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 10px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #d0d0d0;
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #3d3d3d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
color: #d0d0d0;
|
||||
font-size: 13px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: #0e99e0;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 8px;
|
||||
background-color: #3d3d3d;
|
||||
border-left: 3px solid #0e99e0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.examples {
|
||||
padding: 8px;
|
||||
background-color: #3d3d3d;
|
||||
border-left: 3px solid #666;
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.examples h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.examples ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.examples li {
|
||||
margin-bottom: 4px;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="info">
|
||||
Enter the message to send to the currently active room when this button is pressed.
|
||||
</div>
|
||||
|
||||
<label for="message">Message:</label>
|
||||
<textarea id="message" placeholder="Type your message here..."></textarea>
|
||||
|
||||
<div class="examples">
|
||||
<h4>Example Messages:</h4>
|
||||
<ul>
|
||||
<li>BRB! (Be Right Back)</li>
|
||||
<li>AFK for a few minutes</li>
|
||||
<li>On my way!</li>
|
||||
<li>Thanks everyone!</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let websocket = null;
|
||||
let uuid = null;
|
||||
let actionInfo = null;
|
||||
let currentSettings = {};
|
||||
let saveTimeout = null;
|
||||
|
||||
function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) {
|
||||
uuid = inUUID;
|
||||
actionInfo = JSON.parse(inActionInfo);
|
||||
currentSettings = actionInfo.payload.settings || {};
|
||||
|
||||
websocket = new WebSocket('ws://127.0.0.1:' + inPort);
|
||||
|
||||
websocket.onopen = function() {
|
||||
const json = {
|
||||
event: inRegisterEvent,
|
||||
uuid: inUUID
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
};
|
||||
|
||||
// Set current value
|
||||
if (currentSettings.message) {
|
||||
document.getElementById('message').value = currentSettings.message;
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
// Debounce saving
|
||||
if (saveTimeout) {
|
||||
clearTimeout(saveTimeout);
|
||||
}
|
||||
|
||||
saveTimeout = setTimeout(() => {
|
||||
const settings = {
|
||||
message: document.getElementById('message').value
|
||||
};
|
||||
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
event: 'setSettings',
|
||||
context: uuid,
|
||||
payload: settings
|
||||
};
|
||||
websocket.send(JSON.stringify(message));
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Event listener
|
||||
document.getElementById('message').addEventListener('input', saveSettings);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||