Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

Commit

Permalink
Show buttons enabled/disabled based on associated timer
Browse files Browse the repository at this point in the history
Add custom label because we can't override 'title'
  • Loading branch information
WaldenL committed Jul 3, 2020
1 parent e6b2f9e commit 6f1f3d7
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 115 deletions.
4 changes: 4 additions & 0 deletions pi/main_pi.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
</details>
</div>
<hr>
<div class="sdpi-item invalidHidden hidden" id="labelWrapper">
<div class="sdpi-item-label">Button Label</div>
<input class="sdpi-item-value" id="label" value="" placeholder="Label" onchange="sendSettings()">
</div>
<div class="sdpi-item invalidHidden hidden" id="activityWrapper">
<div class="sdpi-item-label">Entry Name</div>
<input class="sdpi-item-value" id="activity" value="" placeholder="What are you doing?" onchange="sendSettings()">
Expand Down
7 changes: 6 additions & 1 deletion pi/main_pi.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ let uuid = null
function connectElgatoStreamDeckSocket (inPort, inPropertyInspectorUUID, inRegisterEvent, inInfo, inActionInfo) {
uuid = inPropertyInspectorUUID

websocket = new WebSocket('ws://localhost:' + inPort)
// Open the web socket (use 127.0.0.1 vs localhost because windows is "slow" resolving 'localhost')
websocket = new WebSocket('ws://127.0.0.1:' + inPort)

websocket.onopen = function () {
// WebSocket is connected, register the Property Inspector
Expand All @@ -31,6 +32,7 @@ function connectElgatoStreamDeckSocket (inPort, inPropertyInspectorUUID, inRegis
const payload = jsonObj.payload.settings

if (payload.apiToken) document.getElementById('apitoken').value = payload.apiToken
if (payload.label) document.getElementById('label').value = payload.label
if (payload.activity) document.getElementById('activity').value = payload.activity

const apiToken = document.getElementById('apitoken').value
Expand All @@ -56,6 +58,7 @@ function sendSettings () {
context: uuid,
payload: {
apiToken: document.getElementById('apitoken').value,
label: document.getElementById('label').value,
activity: document.getElementById('activity').value,
workspaceId: document.getElementById('wid').value,
projectId: document.getElementById('pid').value
Expand Down Expand Up @@ -102,6 +105,7 @@ async function updateWorkspaces (apiToken) {
await getWorkspaces(apiToken).then(workspaceData => {
document.getElementById('wid').innerHTML = '<option value="0"></option>'
document.getElementById('error').classList.add('hiddenError')
document.getElementById('labelWrapper').classList.remove('hidden')
document.getElementById('activityWrapper').classList.remove('hidden')
document.getElementById('workspaceWrapper').classList.remove('hidden')
const selectEl = document.getElementById('wid')
Expand All @@ -116,6 +120,7 @@ async function updateWorkspaces (apiToken) {
} catch (e) {
document.getElementById('error').classList.remove('hiddenError')
document.getElementById('workspaceWrapper').classList.add('hidden')
document.getElementById('labelWrapper').classList.add('hidden')
document.getElementById('activityWrapper').classList.add('hidden')
document.getElementById('projectWrapper').classList.add('hidden')
document.getElementById('workspaceError').classList.add('hiddenError')
Expand Down
246 changes: 132 additions & 114 deletions plugin/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
const togglBaseUrl = 'https://www.toggl.com/api/v8'

let websocket = null
let pluginUUID = null
const currentlyPolling = {}
let pollingInitialized = false
let currentButtons = new Map()
let polling = false

function connectElgatoStreamDeckSocket (inPort, inPluginUUID, inRegisterEvent, inInfo) {
pluginUUID = inPluginUUID
function connectElgatoStreamDeckSocket(inPort, inPluginUUID, inRegisterEvent, inInfo) {

// Open the web socket
websocket = new WebSocket('ws://localhost:' + inPort)
// Open the web socket (use 127.0.0.1 vs localhost because windows is "slow" resolving 'localhost')
websocket = new WebSocket('ws://127.0.0.1:' + inPort)

websocket.onopen = function () {
// WebSocket is connected, register the plugin
Expand All @@ -24,177 +22,197 @@ function connectElgatoStreamDeckSocket (inPort, inPluginUUID, inRegisterEvent, i
// Received message from Stream Deck
const jsonObj = JSON.parse(evt.data)
const { event, context, payload } = jsonObj
console.log(jsonObj)

switch (event) {
case 'keyDown':
stopPolling(context, payload.settings.apiToken)
!payload.settings.apiToken && showAlert(context)
!payload.settings.workspaceId && showAlert(context)
!payload.isInMultiAction && payload.settings.apiToken && startPolling(context, payload.settings.apiToken)
toggle(context, payload.settings)
break
case 'willAppear':
!pollingInitialized && initPolling()
!payload.settings.apiToken && showAlert(context)
!payload.isInMultiAction && payload.settings.apiToken && startPolling(context, payload.settings.apiToken)
!payload.isInMultiAction && payload.settings.apiToken && addButton(context, payload.settings)
break
case 'willDisappear':
!payload.isInMultiAction && stopPolling(context, payload.settings.apiToken)
!payload.isInMultiAction && removeButton(context)
break
case 'didReceiveSettings': // anything could have changed, pull it, add it, and refresh.
!payload.isInMultiAction && removeButton(context) && payload.settings.apiToken && addButton(context, payload.settings)
!payload.isInMultiAction && refreshButtons()
break
}
}
}


function removeButton(context) {
currentButtons.delete(context)
}

function addButton(context, settings) {
currentButtons.set(context, settings)
initPolling()
}

// Polling
async function stopPolling (context, apiToken) {
removeFromArray(currentlyPolling[apiToken], context)
setTitle(context)
}

function startPolling (context, apiToken) {
if (!currentlyPolling[apiToken]) currentlyPolling[apiToken] = []
currentlyPolling[apiToken].push(context)
}

async function initPolling () {
pollingInitialized = true
while (pollingInitialized) { // eslint-disable-line no-unmodified-loop-condition
for (apiToken in currentlyPolling) {
await getCurrentEntry(apiToken).then(entryData => {
for (cNum in currentlyPolling[apiToken]) {
const context = currentlyPolling[apiToken][cNum]
if (entryData) {
setState(context, 0)
setTitle(context, `${Math.floor((new Date() - new Date(entryData.start)) / 60000)} mins`)
} else {
setState(context, 1)
setTitle(context)
}
}
})
}
await wait(currentlyPolling.length * 3500)
async function initPolling() {
if (polling) return

polling = true

while (currentButtons.size > 0) { // eslint-disable-line no-unmodified-loop-condition
refreshButtons()

//nothing special about 5s, just a random choice
await new Promise(r => setTimeout(r, 5000));
}

polling = false
}

function wait (ms = 3000) {
return new Promise(resolve => {
setTimeout(resolve, ms)
function refreshButtons() {

//Get the list of unique apiTokens
var tokens = new Set([...currentButtons.values()].map(s=>s.apiToken))

tokens.forEach(apiToken => {

//Get the current entry for this token
getCurrentEntry(apiToken).then(entryData => {

//Loop over all the buttons and update as appropriate
currentButtons.forEach((settings, context) => {
if (apiToken != settings.apiToken) //not one of "our" buttons
return //We're in a forEach, this is effectively a 'continue'

if (entryData //Does button match the active timer?
&& entryData.wid == settings.workspaceId
&& entryData.pid == settings.projectId
&& entryData.description == settings.activity) {
setState(context, 0)
setTitle(context, `${formatElapsed(entryData.duration)}\n\n\n${settings.label}`)
} else { //if not, make sure it's 'off'
setState(context, 1)
setTitle(context, settings.label)
}
})
})
})
}

// Toggle
function formatElapsed(elapsedFromToggl)
{
const elapsed = Math.floor(Date.now()/1000) + elapsedFromToggl
return formatSeconds(elapsed)
}

function formatSeconds(seconds)
{
if (seconds < 3600)
return leadingZero(Math.floor(seconds/60)) + ':' + leadingZero(seconds % 60)

async function toggle (context, settings) {
return leadingZero(Math.floor(seconds/3600)) + ':' + formatSeconds(seconds % 3600)
}

function leadingZero(val)
{
if (val < 10)
return '0' + val
return val
}

async function toggle(context, settings) {
const { apiToken, activity, projectId, workspaceId } = settings

getCurrentEntry(apiToken).then(entryData => {
if (!entryData) {
startEntry(apiToken, activity, workspaceId, projectId).then(requestData => {
setTitle(context, '0 mins')
})
//Not running? Start a new one
startEntry(apiToken, activity, workspaceId, projectId).then(v=>refreshButtons())
} else if (entryData.wid == workspaceId && entryData.pid == projectId && entryData.description == activity) {
//The one running is "this one" -- toggle to stop
stopEntry(apiToken, entryData.id).then(v=>refreshButtons())
} else {
stopEntry(apiToken, entryData.id).then(requestData => {
setTitle(context)
})
//Just start the new one, old one will stop, it's toggl.
startEntry(apiToken, activity, workspaceId, projectId).then(v=>refreshButtons())
}
})
}

// Toggl API Helpers

async function startEntry (apiToken = isRequired(), activity = 'Time Entry created by Toggl for Stream Deck', workspaceId = 0, projectId = 0) {
const response = await fetch(
function startEntry(apiToken = isRequired(), activity = 'Time Entry created by Toggl for Stream Deck', workspaceId = 0, projectId = 0) {
return fetch(
`${togglBaseUrl}/time_entries/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${apiToken}:api_token`)}`
},
body: JSON.stringify({
time_entry: {
description: activity,
wid: workspaceId,
pid: projectId,
created_with: 'Stream Deck'
}
})
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${apiToken}:api_token`)}`
},
body: JSON.stringify({
time_entry: {
description: activity,
wid: workspaceId,
pid: projectId,
created_with: 'Stream Deck'
}
})
const data = await response.json()
return data.data
})
}

async function stopEntry (apiToken = isRequired(), entryId = isRequired()) {
const response = await fetch(
function stopEntry(apiToken = isRequired(), entryId = isRequired()) {
return fetch(
`${togglBaseUrl}/time_entries/${entryId}/stop`, {
method: 'PUT',
headers: {
Authorization: `Basic ${btoa(`${apiToken}:api_token`)}`
}
})
const data = await response.json()
return data.data
method: 'PUT',
headers: {
Authorization: `Basic ${btoa(`${apiToken}:api_token`)}`
}
})
}

async function getCurrentEntry (apiToken = isRequired()) {
async function getCurrentEntry(apiToken = isRequired()) {
const response = await fetch(
`${togglBaseUrl}/time_entries/current`, {
method: 'GET',
headers: {
Authorization: `Basic ${btoa(`${apiToken}:api_token`)}`
}
})
method: 'GET',
headers: {
Authorization: `Basic ${btoa(`${apiToken}:api_token`)}`
}
})
const data = await response.json()
return data.data
}

// Set Button State (for Polling)
function setState (context = isRequired(), state = isRequired()) {
function setState(context = isRequired(), state = isRequired()) {
websocket && (websocket.readyState === 1) &&
websocket.send(JSON.stringify({
event: 'setState',
context: context,
payload: {
state: state
}
}))
websocket.send(JSON.stringify({
event: 'setState',
context: context,
payload: {
state: state
}
}))
}

// Set Button Title (for Polling)
function setTitle (context = isRequired(), title = '') {
websocket && (websocket.readyState === 1) &&
websocket.send(JSON.stringify({
function setTitle(context = isRequired(), title = '') {
websocket && (websocket.readyState === 1) && websocket.send(JSON.stringify({
event: 'setTitle',
context: context,
payload: {
title: title,
target: 'both'
title: title
}
}))
}

function showAlert (context = isRequired()) {
function showAlert(context = isRequired()) {
websocket && (websocket.readyState === 1) &&
websocket.send(JSON.stringify({
event: 'showAlert',
context: context
}))
websocket.send(JSON.stringify({
event: 'showAlert',
context: context
}))
}

// throw error when required argument is not supplied
const isRequired = () => {
throw new Error('Missing required params')
}

// Remove from Array helper
function removeFromArray (arr) {
var what; var a = arguments; var L = a.length; var ax
while (L > 1 && arr.length) {
what = a[--L]
while ((ax = arr.indexOf(what)) !== -1) {
arr.splice(ax, 1)
}
}
return arr
}

0 comments on commit 6f1f3d7

Please sign in to comment.