Files
cinny-desktop/streamdeck-plugin/com.paarrot.streamdeck.sdPlugin/plugin/index.js
Max Litruv Boonzaayer 07feddd44b
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
Add Paarrot Stream Deck Plugin with core functionality
- 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.
2026-03-23 20:34:26 +11:00

372 lines
7.9 KiB
JavaScript

/**
* 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();
};
}