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.
372 lines
7.9 KiB
JavaScript
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();
|
|
};
|
|
}
|