diff --git a/pi/main_pi.html b/pi/main_pi.html
index 27d3612..68ddb9a 100644
--- a/pi/main_pi.html
+++ b/pi/main_pi.html
@@ -26,6 +26,10 @@
Entry Name
diff --git a/pi/main_pi.js b/pi/main_pi.js
index 78d5b9f..d5c98ff 100644
--- a/pi/main_pi.js
+++ b/pi/main_pi.js
@@ -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
@@ -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
@@ -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
@@ -102,6 +105,7 @@ async function updateWorkspaces (apiToken) {
await getWorkspaces(apiToken).then(workspaceData => {
document.getElementById('wid').innerHTML = '
'
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')
@@ -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')
diff --git a/plugin/main.js b/plugin/main.js
index f10a5fd..fb3b81a 100644
--- a/plugin/main.js
+++ b/plugin/main.js
@@ -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
@@ -24,162 +22,193 @@ 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
@@ -187,14 +216,3 @@ 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
-}