From 0f3b3139c2c5b12080956d62f3e2c7e34903a494 Mon Sep 17 00:00:00 2001 From: Thor Galle Date: Fri, 25 Aug 2017 23:09:46 +0200 Subject: [PATCH] v0.2.0: view reset allowed + timer in background (#5) - removed unneeded rxjs code - moved timer logic to background - implemented timer sync with background --- background.js | 101 ++++++++++++++++++++++++--- manifest.json | 2 +- package.json | 2 +- toggl-notify.js | 176 +++++++++++++++++++++++++++++++++++------------- 4 files changed, 223 insertions(+), 58 deletions(-) diff --git a/background.js b/background.js index b64d954..6ae82eb 100644 --- a/background.js +++ b/background.js @@ -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; + +} diff --git a/manifest.json b/manifest.json index 1ea8f65..57800ba 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Toggl Notify", - "version": "0.0.1", + "version": "0.2.0", "author": "th0rgall", "permissions": [ "notifications" diff --git a/package.json b/package.json index 6cd32bc..38ed247 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/toggl-notify.js b/toggl-notify.js index 89f7cc9..b682579 100644 --- a/toggl-notify.js +++ b/toggl-notify.js @@ -2,7 +2,6 @@ */ - var Rx = require('rxjs/Rx'); var $ = require("jquery"); @@ -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; @@ -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 tag so an event handler can be bound @param tval is the default input (time) value when creating the control @@ -50,6 +73,7 @@ function createControls(f, tval) { var inputCtrl = $("", { type: "text", class: "DateTimeDurationPopdown__duration", + id: "timer-input", value: tval, style: "padding: 0.3em 0 0.5em 0;" }); @@ -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") @@ -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); @@ -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); } );