Skip to content

Commit

Permalink
v0.2.0: view reset allowed + timer in background (#5)
Browse files Browse the repository at this point in the history
- removed unneeded rxjs code
- moved timer logic to background
- implemented timer sync with background
  • Loading branch information
th0rgall committed Aug 25, 2017
1 parent 7dc7b78 commit 0f3b313
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 58 deletions.
101 changes: 92 additions & 9 deletions background.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,96 @@

chrome.extension.onRequest.addListener(
function(request, sender, sendResponse) {
chrome.notifications.create("id", {
type: "basic",
title: "Time's up!",
message: "The timer you set went off... try to stop now.",
iconUrl: "images/logo.png"
});

setTimeout(function(){ chrome.notifications.clear("id"); }, 7000);
sendResponse({returnMsg: "All good!"}); // optional response
if (request && request.type) {
switch (request.type) {
case "notify":
//timerNotification(); TODO: shouldn't be necessary anymore
sendResponse({message: "Background notified"});
break;
case "startTimer": // params: {currentTime: ..., timerGoal: ... }
startTimer(request.params.currentTime, request.params.timerGoal);
sendResponse({message: "Background started timer: " + JSON.stringify(request.params)});
console.log("Started timer: " + JSON.stringify(getTimer()));
break;
case "getTimer":
console.log("getTimer: " + JSON.stringify(getTimer()));
sendResponse(getTimer());
break;
default:
sendResponse({message: "Can't handle request."}); // optional response
}
}
sendResponse({message: "Can't handle request."}); // optional response
});

function timerNotification() {
chrome.notifications.create("id", {
type: "basic",
title: "Time's up!",
message: "The timer you set went off... try to stop now.",
iconUrl: "images/logo.png"
});

setTimeout(function(){ chrome.notifications.clear("id"); }, 7000);
}

// Timer: {alarm: , timeStarted: }

var currentTimer = null;


// params: {currentTime: int (in sec), timerGoal: int (in sec)}
function startTimer(currentTime, timerGoal) {
stopTimer();
currentTimer = {timerGoal: timerGoal, durationStarted: currentTime, timeStarted: Date.now()};
if (timerGoal > currentTime) {
startAlarm(timerGoal - currentTime);
}
}

function stopTimer() {
clearTimer();
clearAlarm();
}

function clearTimer() {
currentTimer = null;
}

/**
@return null if there is no active timer
@return {timerGoal: int, runningTime: int, durationStarted: int} if there is an active timer,
*/
function getTimer() {
if (currentTimer != null) {
var runningTime = (Date.now() - currentTimer.timeStarted) / 1000;
return {timerGoal: currentTimer.timerGoal,
runningTime: runningTime,
durationStarted: currentTimer.durationStarted};
}
else {
return null;
}
}

var currentAlarm = null;

function startAlarm(sec) {
currentAlarm = setTimeout(() => {
timerNotification();
stopAlarm();
}, sec * 1000);
}

function stopAlarm() {
clearAlarm();
clearTimer();
}

function clearAlarm() {
if ( currentAlarm != null ) {
clearTimeout(currentAlarm);
}
currentAlarm = null;

}
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Toggl Notify",
"version": "0.0.1",
"version": "0.2.0",
"author": "th0rgall",
"permissions": [
"notifications"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "toggl-notify",
"version": "0.1.0",
"version": "0.2.0",
"description": "Chrome extension that notifies you when your current Toggl timer exceeds a configurable time bound.",
"main": "toggl-notify.js",
"dependencies": {
Expand Down
176 changes: 129 additions & 47 deletions toggl-notify.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
*/


var Rx = require('rxjs/Rx');
var $ = require("jquery");

Expand All @@ -15,6 +14,9 @@ var $ = require("jquery");
@return the amount of seconds that the input duration specifies
*/
function durToSec(hhmmss) {

if (!hhmmss) return(0);

var tokens = hhmmss.split(":");
// parse functions
var pHour = (hour) => +(hour) * 3600;
Expand All @@ -31,6 +33,27 @@ function durToSec(hhmmss) {
}
}

function pad(s) {
if (("" + s).length == 1) {
return "0" + s;
}
else {
return s;
}
}

function secToDur(s) {
if (!s) return("00:00:00");

h = Math.floor(s/3600);
s -= h * 3600;
m = Math.floor(s/60);
s -= m * 60;

return `${pad(h)}:${pad(m)}:${pad(s)}`;

}

/**creates the HTML controls element
@param f a function that will be passed the <input> tag so an event handler can be bound
@param tval is the default input (time) value when creating the control
Expand All @@ -50,6 +73,7 @@ function createControls(f, tval) {
var inputCtrl = $("<input>", {
type: "text",
class: "DateTimeDurationPopdown__duration",
id: "timer-input",
value: tval,
style: "padding: 0.3em 0 0.5em 0;"
});
Expand All @@ -63,7 +87,7 @@ function createControls(f, tval) {
/**
@param storeF a function that gets passed the produced even stream
@param inputElem the HTML element which changes when a new timer is set
@return an observable event stream input with timer input changes
@return an observable event stream with timer input changes (in seconds)
*/
function createInputStream(storeF, inputElem) {
var eventStream = Rx.Observable.fromEvent(inputElem, "change")
Expand All @@ -74,7 +98,7 @@ function createInputStream(storeF, inputElem) {
// realistic time bounds
.filter((sec) => sec > 0 && sec < 259200);

eventStream.subscribe((input) => console.log("input " + input))
eventStream.subscribe((input) => console.log("input " + input));

// store the stream somewhere
storeF(eventStream);
Expand All @@ -85,73 +109,131 @@ function createInputStream(storeF, inputElem) {
Triggers a notification to the background page.
*/
function sendNotification() {
chrome.extension.sendRequest({msg: "Sup?"}, function(response) {
console.log(response.returnMsg);
chrome.extension.sendRequest({type: "notify"}, function(response) {
if (response && response.message) console.log(response.message);
});
};

function sendNewTimer(currentTime, timerGoal) {
chrome.extension.sendRequest({type: "startTimer", params: {currentTime: currentTime, timerGoal: timerGoal}}, function(response) {
if (response && response.message) console.log(response.message);
});
}

/**
@return the current Toggl timer duration text in the form "hh:mm:ss"
Gets the current timer goal from the background
returns null if there is none
calls back with timerGoal in seconds
*/
function getTimerGoal(callback) {
chrome.extension.sendRequest({type: "getTimer"}, function(response) {
if (response && response.timerGoal) {
callback(response.timerGoal);
}
else {
callback(response);
}
});
}

function getInitialTimerGoal(callback) {
getTimerGoal((goal) => {
if (goal) {
callback(secToDur(goal));
}
else {
callback(getDuration());
}
});
}

/**
Abstracts a pattern to handle individual elements retrieved with jQuery
@param query jquery sring
@param action function to execute jquery result, if existing
@return the result of the action
*/
function queryAction(query, action) {
var queryObject = $(query);
if (queryObject && queryObject.length > 0) {
return action(queryObject);
}
else {
console.log("Query: '" + query + "' did not give a parseable result.");
}
}

/**
@return the current Toggl timer DOM duration text in the form "hh:mm:ss"
@throws an error when it couldn't be parsed
*/
function getDOMDuration() {
var timerWrapper = $(".Timer__duration .time-format-utils__duration");
if (timerWrapper) {
function getDuration() {
return queryAction(".Timer__duration .time-format-utils__duration", function(timerWrapper) {
return timerWrapper[0].innerText;
});
}

/**
(re)initialze the controls (remove & create them)
calls back the generated inputStream
*/
function initializeControls(callback) {
// init controls if needed
if ($("#notify-controls").length == 0) {
console.log("will check reinit");
// insert controls & initialize inputStream
// query checks for right page
queryAction(".Timer__timer .DateTimeDurationPopdown__popdown > div", function(timerDuration) {
console.log("doing reinit");
getInitialTimerGoal((goal) =>
timerDuration.append(
createControls(createInputStream.bind(null, (stream) => callback(stream)), goal) ) );
});
}
else { throw "Can't parse Toggl timer from the DOM" }
}

/**
main injection function
*/
function getData() {
function initialize() {

var inputStream = null;

// insert controls & initialize inputStream
$(".Timer__timer .DateTimeDurationPopdown__popdown > div")
.append(createControls(createInputStream.bind(null, (stream) => inputStream = stream), getDOMDuration()));

// log time
var timer_duration = $(".Timer__duration");
if (timer_duration.length > 0) {

// creates a stream of Toggl timer ticks from the DOM
var timeObs = Rx.Observable.fromEvent(timer_duration[0], "DOMNodeRemoved")
// two text nodes get removed and added again every second
// so bundle these
.bufferCount(2,2)
// every second counted by Toggl, get the new value
.map(() => durToSec(getDOMDuration()));

// creates a stream of events where notifications should be sent
var notifyObs = timeObs
// combine latest time tick with latest input
.combineLatest(inputStream)
// check whether the timer has exceeded the input
.filter((inputs) => {
// 0 has the timer, 1 the input strea
return (inputs[0] > inputs[1]);
})
// bundle every two successive events, starting with 0
.startWith(0)
.bufferCount(2,1)
// now compare to the previous event
// only notify when a different input was given
.filter( (arr) => arr[0][1] != arr[1][1]);


notifyObs.subscribe((time) => {
sendNotification();
initializeControls((stream) => {
inputStream = stream;

// log time
// query to guard for getDuration()
queryAction(".Timer__duration", function(timer_duration) {
inputStream
.map((sec) => [sec, durToSec(getDuration())])
.filter((arr) => arr[0] > arr[1]) //
.subscribe((arr) => sendNewTimer(arr[1], arr[0]));
});
});

/* Set an interval to check when to (re)initialize the controls
Disable periodical checking when the tab is hidden */
var refreshId = null;

function resetInterval() {
if (refreshId) { clearInterval(refreshId);} // to be sure
console.log("Visibility change:" + document.visibilityState);
if (document.visibilityState == "visible") {
refreshId = setInterval(initializeControls.bind(null, (stream) => inputStream = stream), 2500);
}
}

resetInterval(); // first run
document.addEventListener('visibilitychange', resetInterval);


}

$(document).ready(
function() {
// TODO: timeout needed to let the page load
// find a safer way to do this
setTimeout(getData, 3500);
setTimeout(initialize, 3500);
}
);

0 comments on commit 0f3b313

Please sign in to comment.