From efe4837fe4817ccd559717710dd6f48443bf5ae0 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sat, 13 Jul 2013 19:18:15 -0700 Subject: [PATCH 01/33] new v2 draft: event-driven restructure --- background.js | 364 ------------------------------------------- blocking/content.css | 28 ++++ blocking/content.js | 35 +++++ blocking/router.js | 4 + browserAction.js | 22 +++ eventPage.js | 16 ++ manifest.json | 14 +- phases.js | 55 +++++++ 8 files changed, 172 insertions(+), 366 deletions(-) delete mode 100644 background.js create mode 100644 blocking/content.css create mode 100644 blocking/content.js create mode 100644 blocking/router.js create mode 100644 browserAction.js create mode 100644 eventPage.js create mode 100644 phases.js diff --git a/background.js b/background.js deleted file mode 100644 index d09ff79..0000000 --- a/background.js +++ /dev/null @@ -1,364 +0,0 @@ -/* - - Constants - -*/ - -var PREFS = loadPrefs(), -BADGE_BACKGROUND_COLORS = { - work: [192, 0, 0, 255], - break: [0, 192, 0, 255] -}, RING = new Audio("ring.ogg"), -ringLoaded = false; - -loadRingIfNecessary(); - -function defaultPrefs() { - return { - siteList: [ - 'facebook.com', - 'youtube.com', - 'twitter.com', - 'tumblr.com', - 'pinterest.com', - 'myspace.com', - 'livejournal.com', - 'digg.com', - 'stumbleupon.com', - 'reddit.com', - 'kongregate.com', - 'newgrounds.com', - 'addictinggames.com', - 'hulu.com' - ], - durations: { // in seconds - work: 25 * 60, - break: 5 * 60 - }, - shouldRing: true, - clickRestarts: false, - whitelist: false - } -} - -function loadPrefs() { - if(typeof localStorage['prefs'] !== 'undefined') { - return updatePrefsFormat(JSON.parse(localStorage['prefs'])); - } else { - return savePrefs(defaultPrefs()); - } -} - -function updatePrefsFormat(prefs) { - // Sometimes we need to change the format of the PREFS module. When just, - // say, adding boolean flags with false as the default, there's no - // compatibility issue. However, in more complicated situations, we need - // to modify an old PREFS module's structure for compatibility. - - if(prefs.hasOwnProperty('domainBlacklist')) { - // Upon adding the whitelist feature, the domainBlacklist property was - // renamed to siteList for clarity. - - prefs.siteList = prefs.domainBlacklist; - delete prefs.domainBlacklist; - savePrefs(prefs); - console.log("Renamed PREFS.domainBlacklist to PREFS.siteList"); - } - - if(!prefs.hasOwnProperty('showNotifications')) { - // Upon adding the option to disable notifications, added the - // showNotifications property, which defaults to true. - prefs.showNotifications = true; - savePrefs(prefs); - console.log("Added PREFS.showNotifications"); - } - - return prefs; -} - -function savePrefs(prefs) { - localStorage['prefs'] = JSON.stringify(prefs); - return prefs; -} - -function setPrefs(prefs) { - PREFS = savePrefs(prefs); - loadRingIfNecessary(); - return prefs; -} - -function loadRingIfNecessary() { - console.log('is ring necessary?'); - if(PREFS.shouldRing && !ringLoaded) { - console.log('ring is necessary'); - RING.onload = function () { - console.log('ring loaded'); - ringLoaded = true; - } - RING.load(); - } -} - -var ICONS = { - ACTION: { - CURRENT: {}, - PENDING: {} - }, - FULL: {}, -}, iconTypeS = ['default', 'work', 'break'], - iconType; -for(var i in iconTypeS) { - iconType = iconTypeS[i]; - ICONS.ACTION.CURRENT[iconType] = "icons/" + iconType + ".png"; - ICONS.ACTION.PENDING[iconType] = "icons/" + iconType + "_pending.png"; - ICONS.FULL[iconType] = "icons/" + iconType + "_full.png"; -} - -/* - - Models - -*/ - -function Pomodoro(options) { - this.mostRecentMode = 'break'; - this.nextMode = 'work'; - this.running = false; - - this.onTimerEnd = function (timer) { - this.running = false; - } - - this.start = function () { - var mostRecentMode = this.mostRecentMode, timerOptions = {}; - this.mostRecentMode = this.nextMode; - this.nextMode = mostRecentMode; - - for(var key in options.timer) { - timerOptions[key] = options.timer[key]; - } - timerOptions.type = this.mostRecentMode; - timerOptions.duration = options.getDurations()[this.mostRecentMode]; - this.running = true; - this.currentTimer = new Pomodoro.Timer(this, timerOptions); - this.currentTimer.start(); - } - - this.restart = function () { - if(this.currentTimer) { - this.currentTimer.restart(); - } - } -} - -Pomodoro.Timer = function Timer(pomodoro, options) { - var tickInterval, timer = this; - this.pomodoro = pomodoro; - this.timeRemaining = options.duration; - this.type = options.type; - - this.start = function () { - tickInterval = setInterval(tick, 1000); - options.onStart(timer); - options.onTick(timer); - } - - this.restart = function() { - this.timeRemaining = options.duration; - options.onTick(timer); - } - - this.timeRemainingString = function () { - if(this.timeRemaining >= 60) { - return Math.round(this.timeRemaining / 60) + "m"; - } else { - return (this.timeRemaining % 60) + "s"; - } - } - - function tick() { - timer.timeRemaining--; - options.onTick(timer); - if(timer.timeRemaining <= 0) { - clearInterval(tickInterval); - pomodoro.onTimerEnd(timer); - options.onEnd(timer); - } - } -} - -/* - - Views - -*/ - -// The code gets really cluttered down here. Refactor would be in order, -// but I'm busier with other projects >_< - -function locationsMatch(location, listedPattern) { - return domainsMatch(location.domain, listedPattern.domain) && - pathsMatch(location.path, listedPattern.path); -} - -function parseLocation(location) { - var components = location.split('/'); - return {domain: components.shift(), path: components.join('/')}; -} - -function pathsMatch(test, against) { - /* - index.php ~> [null]: pass - index.php ~> index: pass - index.php ~> index.php: pass - index.php ~> index.phpa: fail - /path/to/location ~> /path/to: pass - /path/to ~> /path/to: pass - /path/to/ ~> /path/to/location: fail - */ - - return !against || test.substr(0, against.length) == against; -} - -function domainsMatch(test, against) { - /* - google.com ~> google.com: case 1, pass - www.google.com ~> google.com: case 3, pass - google.com ~> www.google.com: case 2, fail - google.com ~> yahoo.com: case 3, fail - yahoo.com ~> google.com: case 2, fail - bit.ly ~> goo.gl: case 2, fail - mail.com ~> gmail.com: case 2, fail - gmail.com ~> mail.com: case 3, fail - */ - - // Case 1: if the two strings match, pass - if(test === against) { - return true; - } else { - var testFrom = test.length - against.length - 1; - - // Case 2: if the second string is longer than first, or they are the same - // length and do not match (as indicated by case 1 failing), fail - if(testFrom < 0) { - return false; - } else { - // Case 3: if and only if the first string is longer than the second and - // the first string ends with a period followed by the second string, - // pass - return test.substr(testFrom) === '.' + against; - } - } -} - -function isLocationBlocked(location) { - for(var k in PREFS.siteList) { - listedPattern = parseLocation(PREFS.siteList[k]); - if(locationsMatch(location, listedPattern)) { - // If we're in a whitelist, a matched location is not blocked => false - // If we're in a blacklist, a matched location is blocked => true - return !PREFS.whitelist; - } - } - - // If we're in a whitelist, an unmatched location is blocked => true - // If we're in a blacklist, an unmatched location is not blocked => false - return PREFS.whitelist; -} - -function executeInTabIfBlocked(action, tab) { - var file = "content_scripts/" + action + ".js", location; - location = tab.url.split('://'); - location = parseLocation(location[1]); - - if(isLocationBlocked(location)) { - chrome.tabs.executeScript(tab.id, {file: file}); - } -} - -function executeInAllBlockedTabs(action) { - var windows = chrome.windows.getAll({populate: true}, function (windows) { - var tabs, tab, domain, listedDomain; - for(var i in windows) { - tabs = windows[i].tabs; - for(var j in tabs) { - executeInTabIfBlocked(action, tabs[j]); - } - } - }); -} - -var notification, mainPomodoro = new Pomodoro({ - getDurations: function () { return PREFS.durations }, - timer: { - onEnd: function (timer) { - chrome.browserAction.setIcon({ - path: ICONS.ACTION.PENDING[timer.pomodoro.nextMode] - }); - chrome.browserAction.setBadgeText({text: ''}); - - if(PREFS.showNotifications) { - var nextModeName = chrome.i18n.getMessage(timer.pomodoro.nextMode); - notification = webkitNotifications.createNotification( - ICONS.FULL[timer.type], - chrome.i18n.getMessage("timer_end_notification_header"), - chrome.i18n.getMessage("timer_end_notification_body", nextModeName) - ); - notification.onclick = function () { - console.log("Will get last focused"); - chrome.windows.getLastFocused(function (window) { - chrome.windows.update(window.id, {focused: true}); - }); - this.cancel(); - }; - notification.show(); - } - - if(PREFS.shouldRing) { - console.log("playing ring", RING); - RING.play(); - } - }, - onStart: function (timer) { - chrome.browserAction.setIcon({ - path: ICONS.ACTION.CURRENT[timer.type] - }); - chrome.browserAction.setBadgeBackgroundColor({ - color: BADGE_BACKGROUND_COLORS[timer.type] - }); - if(timer.type == 'work') { - executeInAllBlockedTabs('block'); - } else { - executeInAllBlockedTabs('unblock'); - } - if(notification) notification.cancel(); - var tabViews = chrome.extension.getViews({type: 'tab'}), tab; - for(var i in tabViews) { - tab = tabViews[i]; - if(typeof tab.startCallbacks !== 'undefined') { - tab.startCallbacks[timer.type](); - } - } - }, - onTick: function (timer) { - chrome.browserAction.setBadgeText({text: timer.timeRemainingString()}); - } - } -}); - -chrome.browserAction.onClicked.addListener(function (tab) { - if(mainPomodoro.running) { - if(PREFS.clickRestarts) { - mainPomodoro.restart(); - } - } else { - mainPomodoro.start(); - } -}); - -chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { - if(mainPomodoro.mostRecentMode == 'work') { - executeInTabIfBlocked('block', tab); - } -}); - diff --git a/blocking/content.css b/blocking/content.css new file mode 100644 index 0000000..33aeae4 --- /dev/null +++ b/blocking/content.css @@ -0,0 +1,28 @@ +/* When blocked, hide all non-overlay elements, so we don't have to get into a + z-index fight. Not perfect, but it helps.(Text nodes that are direct + children of the body will not be hidden by this rule, but that's okay, since + they also can't be given a z-index and will therefore be covered up.) */ +html.__MSG_@@extension_id__-blocked body > * { + display: none; +} + +#__MSG_@@extension_id__-overlay { + background-image: url('chrome-extension://__MSG_@@extension_id__/icons/work_full.png'), + -webkit-linear-gradient(bottom, #ccc 0%, #fff 75%); + background-position: center 64px, top; + background-repeat: no-repeat; + color: #222; + display: block; + font: normal normal normal 16px/1 sans-serif; + left: 0; + height: 100%; + padding: 200px 1em 1em; + position: fixed; + text-align: center; + top: 0; + width: 100%; +} + +#__MSG_@@extension_id__-overlay p { + margin: 0 0 1em 0; +} \ No newline at end of file diff --git a/blocking/content.js b/blocking/content.js new file mode 100644 index 0000000..18f69ac --- /dev/null +++ b/blocking/content.js @@ -0,0 +1,35 @@ +var extensionId = chrome.i18n.getMessage("@@extension_id"); +var blockedClassName = extensionId + "-blocked"; +var overlayId = extensionId + "-overlay"; + +function buildOverlay() { + var overlay = document.createElement("div"); + overlay.id = overlayId; + var messageKeys = ["site_blocked_info", "site_blocked_motivator"]; + messageKeys.forEach(function(key) { + var p = document.createElement("p"); + p.innerText = chrome.i18n.getMessage(key); + overlay.appendChild(p); + }); + return overlay; +} + +function block() { + document.documentElement.classList.add(blockedClassName); + document.body.appendChild(buildOverlay()); +} + +function unblock() { + document.documentElement.classList.remove(blockedClassName); + document.body.removeChild(document.getElementById(overlayId)); +} + +chrome.runtime.onMessage.addListener(function(message) { + if ("blocked" in message) { + if (message.blocked) { + block(); + } else { + unblock(); + } + } +}); diff --git a/blocking/router.js b/blocking/router.js new file mode 100644 index 0000000..b50b74f --- /dev/null +++ b/blocking/router.js @@ -0,0 +1,4 @@ +Phases.onChanged.addListener(function(phaseName) { + var phase = Phases.get(phaseName); + // TODO: route blocking messages to tabs +}); \ No newline at end of file diff --git a/browserAction.js b/browserAction.js new file mode 100644 index 0000000..78e00fc --- /dev/null +++ b/browserAction.js @@ -0,0 +1,22 @@ +Phases.onChanged.addListener(function(phaseName) { + var phase = Phases.get(phaseName); + if (phase.browserAction) { + var iconName = phaseName; + chrome.browserAction.setBadgeBackgroundColor({ + color: phase.browserAction.badgeBackgroundColor + }); + } else { + var iconName = phase.on.start + "_pending"; + } + chrome.browserAction.setIcon({ + path: "icons/" + iconName + ".png" + }); +}); + +chrome.browserAction.onClicked.addListener(function() { + Phases.getCurrent(function(phase) { + if (phase.on.start) { + Phases.setCurrentName(phase.on.start); + } + }); +}); diff --git a/eventPage.js b/eventPage.js new file mode 100644 index 0000000..cdf10b0 --- /dev/null +++ b/eventPage.js @@ -0,0 +1,16 @@ +chrome.alarms.onAlarm.addListener(function(alarm) { + if (alarm.name === "phaseComplete") { + Phases.getCurrent(function(phase) { + Phases.setCurrentName(phase.on.alarm); + }); + } +}); + +function reset() { + console.log("Resetting."); + chrome.alarms.clear("phaseComplete"); + Phases.setCurrentName("free"); +} + +chrome.runtime.onStartup.addListener(reset); +chrome.runtime.onInstalled.addListener(reset); diff --git a/manifest.json b/manifest.json index ed779f5..88b20c8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,20 @@ { "background": { - "scripts": ["background.js"] + "scripts": ["phases.js", "browserAction.js", "blocking/router.js", + "eventPage.js"], + "persistent": false }, "browser_action": { "default_icon": "icons/work_pending.png" }, + "content_scripts": [ + { + "matches": [""], + "css": ["blocking/content.css"], + "js": ["blocking/content.js"], + "run_at": "document_end" + } + ], "default_locale": "en", "description": "__MSG_ext_description__", "icons": { @@ -15,7 +25,7 @@ "manifest_version": 2, "name": "__MSG_ext_name__", "options_page": "options.html", - "permissions": [ "notifications", "tabs", "" ], + "permissions": [ "alarms", "notifications", "tabs", "storage", "" ], "version": "1.6.1", "web_accessible_resources": [ "icons/work_full.png", diff --git a/phases.js b/phases.js new file mode 100644 index 0000000..683e43f --- /dev/null +++ b/phases.js @@ -0,0 +1,55 @@ +var Phases = { + _ALL: { + "free": { + on: {start: "work"} + }, + "work": { + blocked: true, + on: {alarm: "afterWork"}, + browserAction: {badgeBackgroundColor: [192, 0, 0, 255]} + }, + "afterWork": { + blocked: true, + on: {start: "break", exit: "free"} + }, + "break": { + on: {alarm: "afterBreak"}, + browserAction: {badgeBackgroundColor: [0, 192, 0, 255]} + }, + "afterBreak": { + on: {start: "work", exit: "free"} + } + }, + get: function(phaseName) { return this._ALL[phaseName] }, + getCurrentName: function(callback) { + chrome.storage.local.get({currentPhaseName: "free"}, function(items) { + callback(items.currentPhaseName); + }); + }, + getCurrent: function(callback) { + this.getCurrentName(function(phaseName) { + callback(Phases.get(phaseName)); + }); + }, + setCurrentName: function(phaseName) { + var phase = Phases.get(phaseName); + var message = {phaseName: phaseName}; + if (phase.on.alarm) { + var completeAt = Date.now() + 5000; // TODO: actual durations + message.completeAt = completeAt; + chrome.alarms.create("phaseComplete", {when: completeAt}); + } + chrome.storage.local.set({currentPhaseName: phaseName}, function() { + chrome.runtime.sendMessage({phaseChanged: message}); + }); + }, + onChanged: { + addListener: function(callback) { + chrome.runtime.onMessage.addListener(function(request) { + if ("phaseChanged" in request) { + callback(request.phaseChanged.phaseName); + } + }); + } + } +}; From 29ceec63418503fa0e81df686b8660895ed172f8 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sun, 14 Jul 2013 15:39:44 -0700 Subject: [PATCH 02/33] event-based page blocking is pretty --- blocking/content.js | 29 +++++++++++++----- blocking/matcher.js | 75 +++++++++++++++++++++++++++++++++++++++++++++ blocking/router.js | 19 ++++++++++-- manifest.json | 2 +- phases.js | 21 +++++++------ 5 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 blocking/matcher.js diff --git a/blocking/content.js b/blocking/content.js index 18f69ac..a80eef2 100644 --- a/blocking/content.js +++ b/blocking/content.js @@ -2,6 +2,8 @@ var extensionId = chrome.i18n.getMessage("@@extension_id"); var blockedClassName = extensionId + "-blocked"; var overlayId = extensionId + "-overlay"; +var blocked = false; + function buildOverlay() { var overlay = document.createElement("div"); overlay.id = overlayId; @@ -15,21 +17,34 @@ function buildOverlay() { } function block() { + console.log("Blocked."); + blocked = true; document.documentElement.classList.add(blockedClassName); document.body.appendChild(buildOverlay()); } function unblock() { + console.log("Unblocked."); + blocked = false; document.documentElement.classList.remove(blockedClassName); document.body.removeChild(document.getElementById(overlayId)); } -chrome.runtime.onMessage.addListener(function(message) { - if ("blocked" in message) { - if (message.blocked) { - block(); - } else { - unblock(); - } +function toggleBlocked(phase) { + console.log("Current phase:", phase); + if (phase.blocked !== blocked) { + SiteMatcher.getCurrent(function(matcher) { + if (phase.blocked) { + block(); + } else { + unblock(); + } + }); } +} + +Phases.onChanged.addListener(function(phaseName) { + toggleBlocked(Phases.get(phaseName)); }); + +Phases.getCurrent(toggleBlocked); diff --git a/blocking/matcher.js b/blocking/matcher.js new file mode 100644 index 0000000..b350e5d --- /dev/null +++ b/blocking/matcher.js @@ -0,0 +1,75 @@ +function SiteMatcher(sites, isWhitelist) { + function parseLocation(location) { + var components = location.split('/'); + return {domain: components.shift(), path: components.join('/')}; + } + + function locationsMatch(location, listedPattern) { + return domainsMatch(location.domain, listedPattern.domain) && + pathsMatch(location.path, listedPattern.path); + } + + function pathsMatch(test, against) { + /* + index.php ~> [null]: pass + index.php ~> index: pass + index.php ~> index.php: pass + index.php ~> index.phpa: fail + /path/to/location ~> /path/to: pass + /path/to ~> /path/to: pass + /path/to/ ~> /path/to/location: fail + */ + return !against || test.substr(0, against.length) == against; + } + + function domainsMatch(test, against) { + /* + google.com ~> google.com: case 1, pass + www.google.com ~> google.com: case 3, pass + google.com ~> www.google.com: case 2, fail + google.com ~> yahoo.com: case 3, fail + yahoo.com ~> google.com: case 2, fail + bit.ly ~> goo.gl: case 2, fail + mail.com ~> gmail.com: case 2, fail + gmail.com ~> mail.com: case 3, fail + */ + // Case 1: if the two strings match, pass + if(test === against) { + return true; + } else { + var testFrom = test.length - against.length - 1; + + // Case 2: if the second string is longer than first, or they are the same + // length and do not match (as indicated by case 1 failing), fail + if(testFrom < 0) { + return false; + } else { + // Case 3: if and only if the first string is longer than the second and + // the first string ends with a period followed by the second string, + // pass + return test.substr(testFrom) === '.' + against; + } + } + } + + this.allows = function allows(url) { + var location = parseLocation(url.split('://')[1]); + for(var k in sites) { + listedPattern = parseLocation(sites[k]); + if(locationsMatch(location, listedPattern)) { + // If we're in a whitelist, a matched location is allowed => true + // If we're in a blacklist, a matched location is not allowed => false + return isWhitelist; + } + } + + // If we're in a whitelist, an unmatched location is not allowed => false + // If we're in a blacklist, an unmatched location is allowed => true + return !isWhitelist; + } +} + +SiteMatcher.getCurrent = function(callback) { + // TODO: get from prefs + callback(new SiteMatcher(["matchusian.com", "youtube.com"], false)); +} diff --git a/blocking/router.js b/blocking/router.js index b50b74f..b91227a 100644 --- a/blocking/router.js +++ b/blocking/router.js @@ -1,4 +1,17 @@ +// TODO: Right now we're putting the content script on every page and sending +// messages to every page, too. It simplifies the code a *ton*, but +// consider keeping track of the tabs to decide when we even need to +// inject the script at all. Warning: more difficult than it seems. + +function forwardPhaseChanged(tabId, phaseName) { + chrome.tabs.sendMessage(tabId, {phaseChanged: {phaseName: phaseName}}); +} + +// Forward phase changes to listening tabs Phases.onChanged.addListener(function(phaseName) { - var phase = Phases.get(phaseName); - // TODO: route blocking messages to tabs -}); \ No newline at end of file + chrome.tabs.query({}, function(tabs) { + tabs.forEach(function(tab) { + forwardPhaseChanged(tab.id, phaseName); + }); + }); +}); diff --git a/manifest.json b/manifest.json index 88b20c8..5cf4ecf 100644 --- a/manifest.json +++ b/manifest.json @@ -11,7 +11,7 @@ { "matches": [""], "css": ["blocking/content.css"], - "js": ["blocking/content.js"], + "js": ["phases.js", "blocking/matcher.js", "blocking/content.js"], "run_at": "document_end" } ], diff --git a/phases.js b/phases.js index 683e43f..14e1c99 100644 --- a/phases.js +++ b/phases.js @@ -1,23 +1,26 @@ var Phases = { _ALL: { "free": { - on: {start: "work"} + blocked: false, + on: {start: "work"} }, "work": { - blocked: true, - on: {alarm: "afterWork"}, - browserAction: {badgeBackgroundColor: [192, 0, 0, 255]} + blocked: true, + on: {alarm: "afterWork"}, + browserAction: {badgeBackgroundColor: [192, 0, 0, 255]} }, "afterWork": { - blocked: true, - on: {start: "break", exit: "free"} + blocked: true, + on: {start: "break", exit: "free"} }, "break": { - on: {alarm: "afterBreak"}, - browserAction: {badgeBackgroundColor: [0, 192, 0, 255]} + blocked: false, + on: {alarm: "afterBreak"}, + browserAction: {badgeBackgroundColor: [0, 192, 0, 255]} }, "afterBreak": { - on: {start: "work", exit: "free"} + blocked: true, + on: {start: "work", exit: "free"} } }, get: function(phaseName) { return this._ALL[phaseName] }, From 5c698d303590d5352ecc2dd97b72324c1e0541f9 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sun, 14 Jul 2013 15:41:02 -0700 Subject: [PATCH 03/33] use constant naming conventions for content script constants --- blocking/content.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/blocking/content.js b/blocking/content.js index a80eef2..07b0731 100644 --- a/blocking/content.js +++ b/blocking/content.js @@ -1,12 +1,12 @@ -var extensionId = chrome.i18n.getMessage("@@extension_id"); -var blockedClassName = extensionId + "-blocked"; -var overlayId = extensionId + "-overlay"; +var EXTENSION_ID = chrome.i18n.getMessage("@@extension_id"); +var BLOCKED_CLASS_NAME = EXTENSION_ID + "-blocked"; +var OVERLAY_ID = EXTENSION_ID + "-overlay"; var blocked = false; function buildOverlay() { var overlay = document.createElement("div"); - overlay.id = overlayId; + overlay.id = OVERLAY_ID; var messageKeys = ["site_blocked_info", "site_blocked_motivator"]; messageKeys.forEach(function(key) { var p = document.createElement("p"); @@ -19,15 +19,15 @@ function buildOverlay() { function block() { console.log("Blocked."); blocked = true; - document.documentElement.classList.add(blockedClassName); + document.documentElement.classList.add(BLOCKED_CLASS_NAME); document.body.appendChild(buildOverlay()); } function unblock() { console.log("Unblocked."); blocked = false; - document.documentElement.classList.remove(blockedClassName); - document.body.removeChild(document.getElementById(overlayId)); + document.documentElement.classList.remove(BLOCKED_CLASS_NAME); + document.body.removeChild(document.getElementById(OVERLAY_ID)); } function toggleBlocked(phase) { From a3174387f3864db79c41d1bfbdd9b28fd725888f Mon Sep 17 00:00:00 2001 From: Matchu Date: Sun, 14 Jul 2013 15:43:26 -0700 Subject: [PATCH 04/33] oops, actually use the matcher for block/unblock --- blocking/content.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/blocking/content.js b/blocking/content.js index 07b0731..f393d63 100644 --- a/blocking/content.js +++ b/blocking/content.js @@ -34,10 +34,12 @@ function toggleBlocked(phase) { console.log("Current phase:", phase); if (phase.blocked !== blocked) { SiteMatcher.getCurrent(function(matcher) { - if (phase.blocked) { - block(); - } else { - unblock(); + if (!matcher.allows(document.location.href)) { + if (phase.blocked) { + block(); + } else { + unblock(); + } } }); } From 4a6e4b499e3360b620902e7be324a245b1c044e8 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sun, 14 Jul 2013 16:13:09 -0700 Subject: [PATCH 05/33] update badge text --- _locales/en/messages.json | 11 +++++++++++ browserAction.js | 34 +++++++++++++++++++++++++++++++++- eventPage.js | 2 +- phases.js | 29 ++++++++++++++++++----------- 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d244cdd..159b1a5 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -100,5 +100,16 @@ "example": "work" } } + }, + + "browser_action_badge_time_remaining": { + "message": "$MINUTES$m", + "description": "Appears on the icon to indicate how many minutes remain until the next phase", + "placeholders": { + "minutes": { + "content": "$1", + "example": "25" + } + } } } diff --git a/browserAction.js b/browserAction.js index 78e00fc..d7a90b6 100644 --- a/browserAction.js +++ b/browserAction.js @@ -1,5 +1,19 @@ -Phases.onChanged.addListener(function(phaseName) { +function updateBadgeText(completeAt) { + if (completeAt) { + var timeRemainingInMilliseconds = completeAt - Date.now(); + var timeRemainingInMinutes = Math.round(timeRemainingInMilliseconds / 1000 / 60); + var text = chrome.i18n.getMessage("browser_action_badge_time_remaining", + [timeRemainingInMinutes]); + } else { + var text = ""; + } + chrome.browserAction.setBadgeText({text: text}); +} + +Phases.onChanged.addListener(function(phaseName, completeAt) { var phase = Phases.get(phaseName); + + // Update browser action appearance if (phase.browserAction) { var iconName = phaseName; chrome.browserAction.setBadgeBackgroundColor({ @@ -11,6 +25,16 @@ Phases.onChanged.addListener(function(phaseName) { chrome.browserAction.setIcon({ path: "icons/" + iconName + ".png" }); + + // Start alarms for badge text + if (completeAt) { + chrome.alarms.create("browserActionTick", {periodInMinutes: 1}); + } else { + // Clearing the timer may throw an warning if it doesn't exist yet, but + // it'll happen asynchronously and not interrupt the current function. + chrome.alarms.clear("browserActionTick"); + } + updateBadgeText(completeAt); }); chrome.browserAction.onClicked.addListener(function() { @@ -20,3 +44,11 @@ chrome.browserAction.onClicked.addListener(function() { } }); }); + +chrome.alarms.onAlarm.addListener(function(alarm) { + if (alarm.name === "browserActionTick") { + Phases.getCurrentState(function(phaseName, completeAt) { + updateBadgeText(completeAt); + }); + } +}); diff --git a/eventPage.js b/eventPage.js index cdf10b0..0006066 100644 --- a/eventPage.js +++ b/eventPage.js @@ -8,7 +8,7 @@ chrome.alarms.onAlarm.addListener(function(alarm) { function reset() { console.log("Resetting."); - chrome.alarms.clear("phaseComplete"); + chrome.alarms.clearAll(); Phases.setCurrentName("free"); } diff --git a/phases.js b/phases.js index 14e1c99..1f35058 100644 --- a/phases.js +++ b/phases.js @@ -1,3 +1,5 @@ +var DEFAULT_STATE = {name: "free", completeAt: null}; + var Phases = { _ALL: { "free": { @@ -24,33 +26,38 @@ var Phases = { } }, get: function(phaseName) { return this._ALL[phaseName] }, - getCurrentName: function(callback) { - chrome.storage.local.get({currentPhaseName: "free"}, function(items) { - callback(items.currentPhaseName); + getCurrentState: function(callback) { + chrome.storage.local.get({phaseState: DEFAULT_STATE}, function(items) { + callback(items.phaseState.name, items.phaseState.completeAt); }); }, getCurrent: function(callback) { - this.getCurrentName(function(phaseName) { - callback(Phases.get(phaseName)); + this.getCurrentState(function(phaseName, completeAt) { + callback(Phases.get(phaseName), completeAt); }); }, setCurrentName: function(phaseName) { var phase = Phases.get(phaseName); - var message = {phaseName: phaseName}; if (phase.on.alarm) { - var completeAt = Date.now() + 5000; // TODO: actual durations - message.completeAt = completeAt; + var completeAt = Date.now() + (1000 * 60 * 2); // TODO: actual durations chrome.alarms.create("phaseComplete", {when: completeAt}); } - chrome.storage.local.set({currentPhaseName: phaseName}, function() { - chrome.runtime.sendMessage({phaseChanged: message}); + var phaseState = { + name: phaseName, + completeAt: completeAt + }; + chrome.storage.local.set({phaseState: phaseState}, function() { + chrome.runtime.sendMessage({ + phaseChanged: phaseState + }); }); }, onChanged: { addListener: function(callback) { chrome.runtime.onMessage.addListener(function(request) { if ("phaseChanged" in request) { - callback(request.phaseChanged.phaseName); + var e = request.phaseChanged; + callback(e.name, e.completeAt); } }); } From 1ccf6850f69e0cccd865f21b03e80c13cea6b16e Mon Sep 17 00:00:00 2001 From: Matchu Date: Sun, 14 Jul 2013 17:27:44 -0700 Subject: [PATCH 06/33] new options module and page --- manifest.json | 6 +- options.js | 124 ---------------------------- options.html => options/edit.html | 33 +++----- options/edit.js | 131 ++++++++++++++++++++++++++++++ options/storage.js | 54 ++++++++++++ 5 files changed, 201 insertions(+), 147 deletions(-) delete mode 100644 options.js rename options.html => options/edit.html (74%) create mode 100644 options/edit.js create mode 100644 options/storage.js diff --git a/manifest.json b/manifest.json index 5cf4ecf..8e8e68d 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "background": { - "scripts": ["phases.js", "browserAction.js", "blocking/router.js", - "eventPage.js"], + "scripts": ["options/storage.js", "phases.js", "browserAction.js", + "blocking/router.js", "eventPage.js"], "persistent": false }, "browser_action": { @@ -24,7 +24,7 @@ }, "manifest_version": 2, "name": "__MSG_ext_name__", - "options_page": "options.html", + "options_page": "options/edit.html", "permissions": [ "alarms", "notifications", "tabs", "storage", "" ], "version": "1.6.1", "web_accessible_resources": [ diff --git a/options.js b/options.js deleted file mode 100644 index 32dada8..0000000 --- a/options.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - Localization -*/ - -// Localize all elements with a data-i18n="message_name" attribute -var localizedElements = document.querySelectorAll('[data-i18n]'), el, message; -for(var i = 0; i < localizedElements.length; i++) { - el = localizedElements[i]; - message = chrome.i18n.getMessage(el.getAttribute('data-i18n')); - - // Capitalize first letter if element has attribute data-i18n-caps - if(el.hasAttribute('data-i18n-caps')) { - message = message.charAt(0).toUpperCase() + message.substr(1); - } - - el.innerHTML = message; -} - -/* - Form interaction -*/ - -var form = document.getElementById('options-form'), - siteListEl = document.getElementById('site-list'), - whitelistEl = document.getElementById('blacklist-or-whitelist'), - showNotificationsEl = document.getElementById('show-notifications'), - shouldRingEl = document.getElementById('should-ring'), - clickRestartsEl = document.getElementById('click-restarts'), - saveSuccessfulEl = document.getElementById('save-successful'), - timeFormatErrorEl = document.getElementById('time-format-error'), - background = chrome.extension.getBackgroundPage(), - startCallbacks = {}, durationEls = {}; - -durationEls['work'] = document.getElementById('work-duration'); -durationEls['break'] = document.getElementById('break-duration'); - -var TIME_REGEX = /^([0-9]+)(:([0-9]{2}))?$/; - -form.onsubmit = function () { - console.log("form submitted"); - var durations = {}, duration, durationStr, durationMatch; - - for(var key in durationEls) { - durationStr = durationEls[key].value; - durationMatch = durationStr.match(TIME_REGEX); - if(durationMatch) { - console.log(durationMatch); - durations[key] = (60 * parseInt(durationMatch[1], 10)); - if(durationMatch[3]) { - durations[key] += parseInt(durationMatch[3], 10); - } - } else { - timeFormatErrorEl.className = 'show'; - return false; - } - } - - console.log(durations); - - background.setPrefs({ - siteList: siteListEl.value.split(/\r?\n/), - durations: durations, - showNotifications: showNotificationsEl.checked, - shouldRing: shouldRingEl.checked, - clickRestarts: clickRestartsEl.checked, - whitelist: whitelistEl.selectedIndex == 1 - }) - saveSuccessfulEl.className = 'show'; - return false; -} - -siteListEl.onfocus = formAltered; -showNotificationsEl.onchange = formAltered; -shouldRingEl.onchange = formAltered; -clickRestartsEl.onchange = formAltered; -whitelistEl.onchange = formAltered; - -function formAltered() { - saveSuccessfulEl.removeAttribute('class'); - timeFormatErrorEl.removeAttribute('class'); -} - -siteListEl.value = background.PREFS.siteList.join("\n"); -showNotificationsEl.checked = background.PREFS.showNotifications; -shouldRingEl.checked = background.PREFS.shouldRing; -clickRestartsEl.checked = background.PREFS.clickRestarts; -whitelistEl.selectedIndex = background.PREFS.whitelist ? 1 : 0; - -var duration, minutes, seconds; -for(var key in durationEls) { - duration = background.PREFS.durations[key]; - seconds = duration % 60; - minutes = (duration - seconds) / 60; - if(seconds >= 10) { - durationEls[key].value = minutes + ":" + seconds; - } else if(seconds > 0) { - durationEls[key].value = minutes + ":0" + seconds; - } else { - durationEls[key].value = minutes; - } - durationEls[key].onfocus = formAltered; -} - -function setInputDisabled(state) { - siteListEl.disabled = state; - whitelistEl.disabled = state; - for(var key in durationEls) { - durationEls[key].disabled = state; - } -} - -startCallbacks.work = function () { - document.body.className = 'work'; - setInputDisabled(true); -} - -startCallbacks.break = function () { - document.body.removeAttribute('class'); - setInputDisabled(false); -} - -if(background.mainPomodoro.mostRecentMode == 'work') { - startCallbacks.work(); -} diff --git a/options.html b/options/edit.html similarity index 74% rename from options.html rename to options/edit.html index d97ca87..bb40f73 100644 --- a/options.html +++ b/options/edit.html @@ -3,13 +3,11 @@ -