-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* First steps to pulling out functionality into seperate files * Adding eslint and prettier * Adding Stop Timer Action * Tidy up * Updating readme to represent changes Co-authored-by: Ben Hughes <bhughes.me@gmail.com>
- Loading branch information
Showing
18 changed files
with
2,154 additions
and
221 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
module.exports = { | ||
extends: ['airbnb', 'prettier'], | ||
rules: { | ||
'no-param-reassign': 'off', | ||
'no-console': 'off', | ||
}, | ||
globals: { | ||
PlugIn: 'readonly', | ||
Version: 'readonly', | ||
Data: 'readonly', | ||
flattenedTags: 'readonly', | ||
Alert: 'readonly', | ||
Tag: 'readonly', | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
.vscode |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// prettier.config.js or .prettierrc.js | ||
module.exports = { | ||
trailingComma: "all", | ||
singleQuote: true, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
/* eslint-disable no-bitwise, no-plusplus */ | ||
|
||
(() => { | ||
// Replace the string below with your API Token found here: https://track.toggl.com/profile | ||
const TOGGL_AUTH_TOKEN = 'REPLACE_ME'; | ||
// Name of the tag we use to assign what you're working on | ||
// (this makes it easier to reset the changes made to the name) | ||
const TRACKING_TAG_NAME = 'working-on'; | ||
// this is the name prefix so it's easy to identify what you're working on. | ||
// Replace this if you would like something different | ||
const TRACKING_NAME_PREFIX = '🎯'; | ||
|
||
// the following is a pollyfill for base64 taken from https://github.com/MaxArt2501/base64-js/blob/master/base64.js | ||
function btoa(stringParam) { | ||
const b64 = | ||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; | ||
const string = String(stringParam); | ||
let result = ''; | ||
const rest = string.length % 3; // To determine the final padding | ||
|
||
for (let i = 0; i < string.length; ) { | ||
const a = string.charCodeAt(i++); | ||
const b = string.charCodeAt(i++); | ||
const c = string.charCodeAt(i++); | ||
if (a > 255 || b > 255 || c > 255) { | ||
throw new Error( | ||
"Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.", | ||
); | ||
} | ||
|
||
// eslint-disable-next-line no-bitwise | ||
const bitmap = (a << 16) | (b << 8) | c; | ||
result += | ||
b64.charAt((bitmap >> 18) & 63) + | ||
b64.charAt((bitmap >> 12) & 63) + | ||
b64.charAt((bitmap >> 6) & 63) + | ||
b64.charAt(bitmap & 63); | ||
} | ||
// If there's need of padding, replace the last 'A's with equal signs | ||
return rest ? result.slice(0, rest - 3) + '==='.substring(rest) : result; | ||
} | ||
|
||
const buildErrorObject = (r) => ({ | ||
statusCode: r.statusCode, | ||
data: r.bodyString, | ||
}); | ||
|
||
const AuthHeader = `Basic ${btoa(`${TOGGL_AUTH_TOKEN}:api_token`)}`; | ||
|
||
const dependencyLibrary = new PlugIn.Library(new Version('1.0')); | ||
|
||
dependencyLibrary.startTogglTimer = async function startTogglTimer( | ||
timeEntry, | ||
) { | ||
const fetchRequest = new URL.FetchRequest(); | ||
fetchRequest.bodyData = Data.fromString( | ||
JSON.stringify({ | ||
time_entry: timeEntry, | ||
}), | ||
); | ||
fetchRequest.method = 'POST'; | ||
fetchRequest.headers = { | ||
Authorization: AuthHeader, | ||
'Content-Type': 'application/json', | ||
}; | ||
fetchRequest.url = URL.fromString( | ||
'https://www.toggl.com/api/v8/time_entries/start', | ||
); | ||
const r = await fetchRequest.fetch(); | ||
|
||
if (r.statusCode !== 200) { | ||
throw buildErrorObject(r); | ||
} | ||
|
||
return JSON.parse(r.bodyString).data; | ||
}; | ||
|
||
dependencyLibrary.getCurrentTogglTimer = async function getCurrentTogglTimer() { | ||
const fetchRequest = new URL.FetchRequest(); | ||
|
||
fetchRequest.method = 'GET'; | ||
fetchRequest.headers = { | ||
Authorization: AuthHeader, | ||
'Content-Type': 'application/json', | ||
}; | ||
fetchRequest.url = URL.fromString( | ||
'https://www.toggl.com/api/v8/time_entries/current', | ||
); | ||
const r = await fetchRequest.fetch(); | ||
|
||
if (r.statusCode !== 200) { | ||
throw buildErrorObject(r); | ||
} | ||
|
||
return JSON.parse(r.bodyString).data; | ||
}; | ||
|
||
dependencyLibrary.stopTogglTimer = async function stopTogglTimer(id) { | ||
const fetchRequest = new URL.FetchRequest(); | ||
|
||
fetchRequest.method = 'PUT'; | ||
fetchRequest.headers = { | ||
Authorization: AuthHeader, | ||
'Content-Type': 'application/json', | ||
}; | ||
fetchRequest.url = URL.fromString( | ||
`https://www.toggl.com/api/v8/time_entries/${id}/stop`, | ||
); | ||
const r = await fetchRequest.fetch(); | ||
|
||
if (r.statusCode !== 200) { | ||
throw buildErrorObject(r); | ||
} | ||
|
||
return JSON.parse(r.bodyString).data; | ||
}; | ||
|
||
dependencyLibrary.createTogglProject = async function createTogglProject( | ||
name, | ||
) { | ||
const fetchRequest = new URL.FetchRequest(); | ||
fetchRequest.bodyData = Data.fromString( | ||
JSON.stringify({ project: { name } }), | ||
); | ||
fetchRequest.method = 'POST'; | ||
fetchRequest.headers = { | ||
Authorization: AuthHeader, | ||
'Content-Type': 'application/json', | ||
}; | ||
fetchRequest.url = URL.fromString( | ||
'https://api.track.toggl.com/api/v8/projects', | ||
); | ||
const r = await fetchRequest.fetch(); | ||
|
||
if (r.statusCode !== 200) { | ||
throw buildErrorObject(r); | ||
} | ||
|
||
return JSON.parse(r.bodyString).data; | ||
}; | ||
|
||
dependencyLibrary.getTogglProjects = async function getTogglProjects() { | ||
const fetchRequest = new URL.FetchRequest(); | ||
fetchRequest.method = 'GET'; | ||
fetchRequest.headers = { | ||
Authorization: AuthHeader, | ||
'Content-Type': 'application/json', | ||
}; | ||
fetchRequest.url = URL.fromString( | ||
`https://api.track.toggl.com/api/v8/me?with_related_data=true`, | ||
); | ||
const r = await fetchRequest.fetch(); | ||
|
||
if (r.statusCode !== 200) { | ||
throw buildErrorObject(r); | ||
} | ||
|
||
return JSON.parse(r.bodyString).data.projects; | ||
}; | ||
|
||
dependencyLibrary.log = async function log(message, title = 'Log') { | ||
const a = new Alert(title, message); | ||
a.addOption('OK'); | ||
await a.show(); | ||
}; | ||
|
||
const config = { | ||
TOGGL_AUTH_TOKEN, | ||
TRACKING_TAG_NAME, | ||
TRACKING_NAME_PREFIX, | ||
}; | ||
|
||
dependencyLibrary.resetTasks = () => { | ||
let trackingTag = flattenedTags.find((t) => t.name === TRACKING_TAG_NAME); | ||
|
||
if (!trackingTag) { | ||
trackingTag = new Tag(TRACKING_TAG_NAME); | ||
} | ||
|
||
trackingTag.tasks.forEach((task) => { | ||
if (task.name.startsWith(TRACKING_NAME_PREFIX)) { | ||
task.name = task.name.replace(TRACKING_NAME_PREFIX, ''); | ||
} | ||
task.removeTag(trackingTag); | ||
}); | ||
}; | ||
|
||
dependencyLibrary.config = config; | ||
|
||
return dependencyLibrary; | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
"com.github.benhughes.OmniToggl" = "OmniToggl"; | ||
"com.github.benhughes.OmniToggl-dev" = "OmniToggl Dev"; |
4 changes: 4 additions & 0 deletions
4
OmniToggl.omnifocusjs/Resources/en.lproj/startTogglTimer.strings
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
"label" = "Start Toggl Timer"; | ||
"shortLable" = "Start Timer"; | ||
"mediumLable" = "Start Toggl Timer"; | ||
"longLabel" = "Start Toggl Timer"; |
4 changes: 4 additions & 0 deletions
4
OmniToggl.omnifocusjs/Resources/en.lproj/stopTogglTimer.strings
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
"label" = "Stop Toggl Timer"; | ||
"shortLable" = "Stop Timer"; | ||
"mediumLable" = "Stop Toggl Timer"; | ||
"longLabel" = "Stop Toggl Timer"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
(() => { | ||
// Main action | ||
const action = new PlugIn.Action(async function startTogglTimerAction( | ||
selection, | ||
) { | ||
const { | ||
config: { TRACKING_TAG_NAME, TRACKING_NAME_PREFIX }, | ||
startTogglTimer, | ||
createTogglProject, | ||
getTogglProjects, | ||
resetTasks, | ||
log, | ||
} = this.common; | ||
|
||
const trackingTag = flattenedTags.find((t) => t.name === TRACKING_TAG_NAME); | ||
|
||
try { | ||
resetTasks(); | ||
|
||
let projects = []; | ||
|
||
try { | ||
projects = await getTogglProjects(); | ||
} catch (e) { | ||
await log( | ||
'An error occurred getting projects', | ||
'See console for more info', | ||
); | ||
console.log(e); | ||
} | ||
|
||
const task = selection.tasks[0]; | ||
const projectName = task.containingProject && task.containingProject.name; | ||
|
||
const toggleProject = projects.find( | ||
(p) => p.name.trim().toLowerCase() === projectName.trim().toLowerCase(), | ||
); | ||
|
||
const taskName = task.name; | ||
let pid; | ||
if (!projectName) { | ||
pid = null; | ||
} else if (!toggleProject) { | ||
console.log(`project not found creating new ${projectName} project`); | ||
try { | ||
const r = await createTogglProject(projectName); | ||
console.log(`project created id: ${r.id}`); | ||
pid = r.id; | ||
} catch (e) { | ||
console.log(`Error creating project ${projectName}`); | ||
console.log(JSON.stringify(e, null, 2)); | ||
} | ||
} else { | ||
pid = toggleProject.id; | ||
} | ||
console.log('pid is: ', pid); | ||
|
||
const taskTags = task.tags.map((t) => t.name); | ||
|
||
try { | ||
const r = await startTogglTimer({ | ||
description: taskName, | ||
created_with: 'omnifocus', | ||
tags: taskTags, | ||
pid, | ||
}); | ||
task.name = TRACKING_NAME_PREFIX + task.name; | ||
task.addTag(trackingTag); | ||
console.log('Timer started successfully', JSON.stringify(r)); | ||
} catch (e) { | ||
await log('An error occurred', 'See console for more info'); | ||
console.log(JSON.stringify(e, null, 2)); | ||
} | ||
} catch (e) { | ||
await log('An error occurred', 'See console for more info'); | ||
console.log(e); | ||
console.log(JSON.stringify(e, null, 2)); | ||
} | ||
}); | ||
|
||
action.validate = function startTogglTimerValidate(selection) { | ||
// selection options: tasks, projects, folders, tags | ||
return selection.tasks.length === 1; | ||
}; | ||
|
||
return action; | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
(() => { | ||
// Main action | ||
const action = new PlugIn.Action(async function stopTogglTimerAction() { | ||
const { | ||
getCurrentTogglTimer, | ||
stopTogglTimer, | ||
resetTasks, | ||
log, | ||
} = this.common; | ||
|
||
try { | ||
const currentTimer = await getCurrentTogglTimer(); | ||
if (currentTimer) { | ||
await stopTogglTimer(currentTimer.id); | ||
} | ||
resetTasks(); | ||
} catch (e) { | ||
log('Please try again later', 'Error stopping current task'); | ||
console.log(JSON.stringify(e, null, 2)); | ||
} | ||
}); | ||
|
||
action.validate = function startTogglTimerValidate() { | ||
// selection options: tasks, projects, folders, tags | ||
return true; | ||
}; | ||
|
||
return action; | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"defaultLocale": "en", | ||
"author": "Ben Hughes", | ||
"identifier": "com.github.benhughes.OmniToggl", | ||
"version": "1.1", | ||
"description": "A collection of actions for starting Toggl from OmniFocus. See: https://github.com/benhughes/OmniToggl", | ||
"actions": [ | ||
{ | ||
"identifier": "startTogglTimer" | ||
}, | ||
{ | ||
"identifier": "stopTogglTimer" | ||
} | ||
], | ||
"libraries": [ | ||
{ | ||
"identifier": "common" | ||
} | ||
] | ||
} |
Oops, something went wrong.