├── README.md ├── src ├── format-time.js ├── manifest.json ├── summary │ ├── autoscroll.js │ ├── index.html │ ├── index.css │ └── index.js ├── popup │ ├── popup.js │ └── popup.html ├── options │ ├── options.js │ └── options.html ├── icons │ └── mind-the-time-icon-48.svg ├── ticker-timer-modes.js ├── new-day.js ├── main.js └── tracking-events.js └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # Mind the Time 2 | A browser add-on that keeps track of how much time you spend on the web, and where you spend it. 3 | 4 | More details: https://addons.mozilla.org/en-US/firefox/addon/mind-the-time/ 5 | 6 | -------------------------------------------------------------------------------- /src/format-time.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // We put these functions in a separate file so they can be loaded into the 6 | // summary page as well as the background page. 7 | 8 | function time_to_hours_and_minutes(time) { 9 | let absTime = Math.abs(time), 10 | h = Math.floor(absTime / 3600), 11 | m = Math.floor(absTime / 60) % 60; 12 | return [h, m]; 13 | }; 14 | 15 | function format_time(time) { 16 | let [h, m] = time_to_hours_and_minutes(time); 17 | return ((h < 1) ? "0:" : h + ":") + 18 | ((m < 10) ? ((m < 1) ? "00" : "0" + m) : m); 19 | }; 20 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Mind the Time", 4 | "version": "2.2.0", 5 | "description": "Keep track of how much time you spend on the web, and where you spend it. A ticker shows the time spent at the current site or total time spent on the web today. A summary page shows data for today and recent history.", 6 | "homepage_url": "https://addons.mozilla.org/en-US/firefox/addon/mind-the-time/", 7 | "author": "Paul Morris", 8 | "applications": { 9 | "gecko": { 10 | "id": "jid0-HYNmqxA9zQGfJADREri4n2AHKSI@jetpack" 11 | } 12 | }, 13 | "icons": { 14 | "48": "icons/mind-the-time-icon-48.svg", 15 | "96": "icons/mind-the-time-icon-48.svg" 16 | }, 17 | "options_ui": { 18 | "page": "options/options.html" 19 | }, 20 | "background": { 21 | "scripts": [ 22 | "format-time.js", 23 | "new-day.js", 24 | "ticker-timer-modes.js", 25 | "tracking-events.js", 26 | "main.js" 27 | ] 28 | }, 29 | "permissions": [ 30 | "tabs", 31 | "storage", 32 | "notifications", 33 | "idle" 34 | ], 35 | "browser_action": { 36 | "browser_style": true, 37 | "default_icon": { 38 | "48": "icons/mind-the-time-icon-48.svg" 39 | }, 40 | "default_title": "Mind the Time", 41 | "default_popup": "popup/popup.html" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/summary/autoscroll.js: -------------------------------------------------------------------------------- 1 | 2 | function current_y_position() { 3 | if (self.pageYOffset) 4 | return self.pageYOffset; 5 | if (document.documentElement && document.documentElement.scrollTop) 6 | return document.documentElement.scrollTop; 7 | if (document.body.scrollTop) 8 | return document.body.scrollTop; 9 | return 0; 10 | }; 11 | 12 | function elem_y_position(eID) { 13 | var elm = document.getElementById(eID), 14 | y = elm.offsetTop, 15 | node = elm; 16 | while (node.offsetParent && node.offsetParent != document.body) { 17 | node = node.offsetParent; 18 | y += node.offsetTop; 19 | } 20 | return y; 21 | }; 22 | 23 | function scroll_some(leapY) { 24 | window.scrollTo(0, leapY); 25 | }; 26 | 27 | function smooth_scroll(eID) { 28 | var startY = current_y_position(), 29 | stopY = elem_y_position(eID) - 35, 30 | distance = stopY > startY ? stopY - startY : startY - stopY; 31 | if (distance < 100) { 32 | scrollTo(0, stopY); 33 | return; 34 | } 35 | var speed = Math.round(distance / 100); 36 | if (speed >= 20) speed = 20; 37 | var step = Math.round(distance / 25), 38 | leapY = stopY > startY ? startY + step : startY - step; 39 | timer = 0; 40 | if (stopY > startY) { 41 | for (var i = startY; i < stopY; i += step) { 42 | setTimeout(scroll_some, timer * speed, leapY); 43 | leapY += step; 44 | if (leapY > stopY) leapY = stopY; 45 | timer += 1; 46 | } 47 | return; 48 | } 49 | for (var i = startY; i > stopY; i -= step) { 50 | setTimeout(scroll_some, timer * speed, leapY); 51 | leapY -= step; 52 | if (leapY < stopY) leapY = stopY; 53 | timer++; 54 | } 55 | }; 56 | 57 | // listen for "show only 10 rows" and scroll to boxId, passed as event 58 | window.addEventListener('message', (event) => smooth_scroll(event.data), false); 59 | -------------------------------------------------------------------------------- /src/popup/popup.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | async function handle_summary_button_click() { 6 | try { 7 | let url = browser.extension.getURL("summary/index.html"), 8 | // we have to query then filter because we can't query for 9 | // non-standard add-on url directly 10 | tabs = await browser.tabs.query({}), 11 | summaryTab = tabs.filter((t) => t.url === url); 12 | 13 | if (summaryTab[0]) { 14 | // We have to activate the tab first because if the active window 15 | // changes, the popup closes, taking this code down with it. 16 | await browser.tabs.update(summaryTab[0].id, {active: true}); 17 | await browser.windows.update(summaryTab[0].windowId, {focused: true}); 18 | } else { 19 | browser.tabs.create({url: url}); 20 | } 21 | // Close the popup dropdown, if it is still open and this code is still 22 | // running. 23 | window.close(); 24 | 25 | } catch (e) { console.error(e); } 26 | }; 27 | 28 | document.getElementById("summaryButton").addEventListener('click', handle_summary_button_click); 29 | 30 | function change_mode(mode) { 31 | browser.storage.local.set({timerMode: mode}); 32 | window.close(); 33 | }; 34 | 35 | document.getElementById("D").addEventListener('click', change_mode.bind(null, 'D')); 36 | document.getElementById("G").addEventListener('click', change_mode.bind(null, 'G')); 37 | document.getElementById("B").addEventListener('click', change_mode.bind(null, 'B')); 38 | document.getElementById("O").addEventListener('click', change_mode.bind(null, 'O')); 39 | 40 | // show the time in the popup panel 41 | async function update_ticker_div() { 42 | try { 43 | let gBackground = await browser.runtime.getBackgroundPage(), 44 | tickerDiv = document.getElementById("tickerDiv"); 45 | tickerDiv.textContent = await gBackground.get_popup_ticker(); 46 | 47 | } catch (e) { console.error(e); } 48 | }; 49 | 50 | update_ticker_div(); 51 | -------------------------------------------------------------------------------- /src/options/options.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | const STORAGE = browser.storage.local; 8 | var gBackground = browser.extension.getBackgroundPage(); 9 | 10 | function save_options(event) { 11 | let whitelistElement = document.querySelector("#whitelist"), 12 | whitelistArray = gBackground.sanitize_whitelist(whitelistElement.value); 13 | whitelistElement["value"] = whitelistArray.join(', ') || ""; 14 | 15 | STORAGE.set({ 16 | oButtonBadgeTotal: document.querySelector("#buttonBadgeTotal").checked || false, 17 | oNotificationsOn: document.querySelector("#notificationsOn").checked || false, 18 | oNotificationsRate: parseInt(document.querySelector("#notificationsRate").value) || 60, 19 | oDayStartOffset: parseInt(document.querySelector("#dayStartOffset").value) || 0, 20 | oWhitelistArray: whitelistArray 21 | }); 22 | event.preventDefault(); 23 | }; 24 | 25 | document.querySelector("form").addEventListener("submit", save_options); 26 | 27 | 28 | async function restore_options() { 29 | try { 30 | let fromStorage = await STORAGE.get(gBackground.OPTIONS); 31 | document.querySelector("#buttonBadgeSite").checked = !fromStorage.oButtonBadgeTotal || true; 32 | document.querySelector("#buttonBadgeTotal").checked = fromStorage.oButtonBadgeTotal || false; 33 | document.querySelector("#notificationsOff").checked = !fromStorage.oNotificationsOn || true; 34 | document.querySelector("#notificationsOn").checked = fromStorage.oNotificationsOn || false; 35 | document.querySelector("#notificationsRate")["value"] = fromStorage.oNotificationsRate.toString() || 60; 36 | document.querySelector("#dayStartOffset")["value"] = fromStorage.oDayStartOffset.toString() || 4; 37 | document.querySelector("#whitelist")["value"] = fromStorage.oWhitelistArray.join(', ') || ""; 38 | 39 | } catch (e) { console.error(e); } 40 | }; 41 | 42 | document.addEventListener('DOMContentLoaded', restore_options); 43 | document.querySelector("#deleteButton").addEventListener("click", gBackground.delete_all_data); 44 | -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 57 | 58 | 59 |
60 |
61 |
62 |
Show summary page.
63 |
64 |
65 |
66 |
67 |
Timer On
Pause timing after 15
seconds of inactivity.
68 |
69 |
70 |
71 |
Timer On
Keep timing despite
inactivity (video mode).
72 |
73 |
74 |
75 |
Timer On
Only log time and
not websites.
76 |
77 |
78 |
79 |
Timer Off
Stop logging time
and websites.
80 |
81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 25 | 26 | 27 | 28 |
29 |
30 | Options 31 |

NOTE: Remember to click the "Save Changes" button below.

32 |

Ticker (Button Badge)

33 |

34 |
35 | 36 |

37 | 38 |

Reminder Notifications

39 |

Transient notifications that indicate the amount of time spent on the web so far today.

40 |

41 |
42 | 43 |

44 | 45 |

Reminder Notification Rate (In Minutes)

46 |

The number of minutes logged between each reminder notification.

47 |

48 | 49 |

Exclude These Sites (White List)

50 |

Enter a list of domains separated by commas to prevent those sites from being logged, and any time spent on them from being counted. 51 | For example: "wordpress.org, addons.mozilla.org, www.ubuntu.com" There is no need to include the "http://" or "https://" from 52 | the URL, but you must include any subdomains (such as "www.") when they are part of the site's address.

53 |

54 | 55 |

When a New Day Begins

56 |

Midnight is where the day begins, but you can adjust that by one or more hours. Enter "0" for 57 | midnight, "2" for 2:00 AM, "-1" for 11:00 PM, etc.

58 |

59 | 60 |

Save Changes

61 |

62 | 63 |

Delete All Data

64 |

This cannot be undone.

65 |

66 |
67 |
68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/icons/mind-the-time-icon-48.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 37 | 40 | 47 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/summary/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Mind the Time 13 | 14 | 15 | 16 |
17 | 26 |
27 |
Hmmm... the data did not load, please reload the page.
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |

MONTHS

41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |

WEEKS

56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |

DAYS

79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |

PREFERENCES

88 |

To access preferences for Mind the Time:

89 |

Open the Add-ons Manager.

90 |

Select Extensions in the menu on the left.

91 |

Find Mind the Time in the list.

92 |

Click the Preferences button.

93 |

Download daily data as JSON.

94 |
95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/ticker-timer-modes.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // TICKER (AKA TOGGLE BUTTON AND BADGE) 6 | 7 | "use strict"; 8 | 9 | // Updates the internal and external timer mode indications. 10 | // Called when user changes the mode and on initialization/startup. 11 | function set_listeners_for_timer_mode(mode) { 12 | // O: timer off 13 | // D: default - normal mode 14 | // G: green - keep timing despite inactivity (video mode) 15 | // B: blue - only log time and not websites 16 | 17 | // idle handling 18 | let hasIdleListener = browser.idle.onStateChanged.hasListener(idle_handler); 19 | 20 | if (hasIdleListener && (mode === 'O' || mode === 'G')) { 21 | browser.idle.onStateChanged.removeListener(idle_handler); 22 | } else if (!hasIdleListener && (mode === 'B' || mode === 'D')) { 23 | browser.idle.onStateChanged.addListener(idle_handler); 24 | } 25 | 26 | // event listeners 27 | if (mode === 'O') { 28 | browser.tabs.onUpdated.removeListener(tabs_on_updated); 29 | browser.tabs.onActivated.removeListener(tabs_on_activated); 30 | browser.tabs.onRemoved.removeListener(tabs_on_removed); 31 | browser.windows.onFocusChanged.removeListener(windows_on_focus_changed); 32 | } else { 33 | browser.tabs.onUpdated.addListener(tabs_on_updated); 34 | browser.tabs.onActivated.addListener(tabs_on_activated); 35 | browser.tabs.onRemoved.addListener(tabs_on_removed); 36 | browser.windows.onFocusChanged.addListener(windows_on_focus_changed); 37 | } 38 | }; 39 | 40 | function set_pre_clock_on_2_function(mode) { 41 | if (mode === 'B') { 42 | let url = new URL("http://o3xr2485dmmdi78177v7c33wtu7315.net/"); 43 | pre_clock_on_2 = pre_clock_on_2_internal.bind(undefined, url); 44 | } else { 45 | pre_clock_on_2 = pre_clock_on_2_internal; 46 | } 47 | }; 48 | 49 | // updates the time shown in the button badge ticker 50 | function update_ticker_default(secsHere, totalSecs) { 51 | let value = secsHere ? format_time_minimal(secsHere) : "0"; 52 | browser.browserAction.setBadgeText({ text: value }); 53 | }; 54 | 55 | function update_ticker_total_secs(secsHere, totalSecs) { 56 | browser.browserAction.setBadgeText( {text: format_time_minimal(totalSecs) }); 57 | }; 58 | 59 | var update_ticker; 60 | 61 | async function set_ticker_update_function() { 62 | // Ticker update type depends on both timer mode and the user preference/option. 63 | try { 64 | let fromStorage = await STORAGE.get(['oButtonBadgeTotal', 'timerMode']); 65 | if (fromStorage.timerMode === 'O') { 66 | update_ticker = () => null; 67 | } else if (fromStorage.timerMode === 'B' || fromStorage.oButtonBadgeTotal) { 68 | update_ticker = update_ticker_total_secs; 69 | } else { 70 | update_ticker = update_ticker_default; 71 | } 72 | } catch (e) { console.error(e); } 73 | }; 74 | 75 | // For updating the time shown in the popup ticker. Returns a promise/string. 76 | async function get_popup_ticker_default() { 77 | try { 78 | let tab = await get_current_tab(), 79 | url = new URL(tab.url), 80 | domain = url.host, 81 | fromStorage = await STORAGE.get([domain, 'totalSecs']); 82 | return format_time(fromStorage[domain] || 0) + 83 | "\u00a0\u00a0/\u00a0\u00a0" + 84 | format_time(fromStorage.totalSecs); 85 | 86 | } catch (e) { console.error(e); } 87 | }; 88 | 89 | async function get_popup_ticker_total_only() { 90 | try { 91 | let fromStorage = await STORAGE.get('totalSecs'); 92 | return format_time(fromStorage.totalSecs); 93 | 94 | } catch (e) { console.error(e); } 95 | }; 96 | 97 | var get_popup_ticker; 98 | 99 | function set_popup_ticker_function(mode) { 100 | get_popup_ticker = mode === 'B' 101 | ? get_popup_ticker_total_only 102 | : get_popup_ticker_default; 103 | }; 104 | 105 | function set_badge_for_timer_mode(mode) { 106 | if (mode === 'O') { 107 | browser.browserAction.setBadgeText({ text: "" }); 108 | } else { 109 | let defaultColor = '#404040', 110 | colors = { 'G': "#00aa00", 'B': "#5555dd", 'O': defaultColor, 'D': defaultColor }, 111 | newColor = colors[mode]; 112 | browser.browserAction.setBadgeBackgroundColor({ color: newColor }); 113 | } 114 | }; 115 | -------------------------------------------------------------------------------- /src/summary/index.css: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /* Styles for the summary page (index.html) */ 6 | 7 | body { 8 | text-align: center; 9 | font-family: verdana,arial,helvetica,sans-serif; 10 | margin: 0px; 11 | background-image: linear-gradient(to bottom , rgb(173, 181, 194), rgb(191, 198, 209) 80px, rgb(201,208,219)); 12 | background-color: rgb(201, 208, 219); 13 | background-repeat: repeat-x; 14 | background-attachment: fixed; 15 | } 16 | 17 | #topnav { 18 | position: fixed; 19 | width: 100%; 20 | font-size: 11px; 21 | background: rgb(110, 118, 134); 22 | box-shadow: 0px 2px 2px rgba(100, 100, 100, 0.3); 23 | } 24 | 25 | #row0 { 26 | padding-top: 50px; 27 | } 28 | 29 | #headerUL { 30 | list-style: none; 31 | padding: 0 18px; 32 | margin: 0px; 33 | display: inline-block; 34 | } 35 | #headerUL li { 36 | display: inline-block; 37 | } 38 | #headerUL li a { 39 | display: inline-block; 40 | color: rgb(238, 238, 238); 41 | padding: 7px 18px; 42 | text-decoration: none; 43 | cursor: pointer; 44 | } 45 | #headerUL li a:hover { 46 | background: rgb(90, 98, 114); 47 | } 48 | #loadFailMessage { 49 | color: #737a88; 50 | padding-top: 80px; 51 | } 52 | 53 | .big-row { 54 | display: flex; 55 | justify-content: center; 56 | padding: 0 20px; 57 | } 58 | 59 | .big-row-header { 60 | 61 | margin-top: 30px; 62 | padding-top: 5px; 63 | color: rgb(105,112,126); 64 | font-size: 0.6em; 65 | display: none; 66 | } 67 | 68 | .sum-box { 69 | margin: 0px 20px; 70 | } 71 | 72 | .MTTtable { 73 | font-size: 12px; 74 | margin: 30px 0; 75 | border-collapse: separate; 76 | border-spacing: 0px 0px; 77 | } 78 | .MTTtable tr { 79 | background: rgb(238, 238, 238); 80 | } 81 | tr.headerrow { 82 | background: transparent; 83 | line-height: 2em; 84 | } 85 | .headertd { 86 | padding: 0; 87 | background: #8c94a4; 88 | border-top-right-radius: 8px; 89 | border-top-left-radius: 8px; 90 | color: #eee; 91 | } 92 | 93 | .dateheader { 94 | margin: 0; 95 | padding: 0; 96 | text-align: center; 97 | } 98 | 99 | .MTTtable tr.totalrow { 100 | font-weight: bold; 101 | background: rgb(221, 221, 221); 102 | } 103 | 104 | .MTTtable td { 105 | border: 0px; 106 | border-bottom: 1px solid #8c94a4; 107 | text-align: left; 108 | padding: 0.5em 1.2em; 109 | margin: 0px; 110 | white-space: nowrap; 111 | } 112 | 113 | .domain-td { 114 | max-width: 16em; 115 | overflow: hidden; 116 | } 117 | 118 | .domainlink { 119 | text-decoration: none; 120 | color: #000; 121 | } 122 | 123 | .domainlink:hover { 124 | text-decoration: underline; 125 | } 126 | 127 | .graphUL { 128 | list-style: none; 129 | margin: 0; 130 | padding: 0; 131 | display: block; 132 | /*max-width of 301 = 10 hours */ 133 | max-width: 301px; 134 | } 135 | 136 | .graphLI { 137 | background: #8c94a4; 138 | height: 10px; 139 | display: block; 140 | float: left; 141 | margin: 1px 0px 1px 0; 142 | border-left: 1px solid #aaafb7; 143 | /* width of 1px = 2 min, 30px = 1 hour */ 144 | width: 29px; 145 | } 146 | 147 | .showmore { 148 | font-size: 10px; 149 | cursor: pointer; 150 | } 151 | 152 | .showmore:hover { 153 | text-decoration: underline; 154 | } 155 | .MTTtable td.emptytableTD { 156 | max-width: 350px; 157 | white-space: normal; 158 | padding:12px 14px; 159 | text-align: center; 160 | color: rgb(119, 119, 119); 161 | } 162 | 163 | .smalltext { 164 | font-size: 12px; 165 | } 166 | 167 | #rowPrefs { 168 | margin-bottom: 550px; 169 | } 170 | 171 | #rowPrefs h1 { 172 | margin-bottom: 30px; 173 | } 174 | 175 | #rowPrefs p { 176 | font-size: 15px ; 177 | } 178 | 179 | #addonsManagerButton { 180 | color: rgb(105, 112, 126); 181 | text-decoration: underline; 182 | cursor: pointer; 183 | } 184 | 185 | #downloadAnchorElem { 186 | color: rgb(105, 112, 126); 187 | text-decoration: underline; 188 | cursor: pointer; 189 | } 190 | 191 | #showDaysButton { 192 | border: 1px solid grey; 193 | background: rgb(238, 238, 238); 194 | padding: 5px 15px; 195 | border-radius: 7px; 196 | margin: 30px auto; 197 | color: rgb(105,112,126); 198 | width: 260px 199 | } 200 | 201 | #showDaysButton:hover, 202 | #showDaysButton:active { 203 | background: rgb(222, 222, 222); 204 | cursor: pointer; 205 | } 206 | 207 | @media (max-width: 1110px) { 208 | .MTTtable { 209 | margin:30px auto; 210 | } 211 | .big-row { 212 | display:block; 213 | } 214 | .graphUL { 215 | max-width: none; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/new-day.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // Handle shuffling data for a new day 6 | 7 | "use strict"; 8 | 9 | function get_week_header_text(weekNum) { 10 | let from = new Date((weekNum + 1) * ONE_DAY_MS), 11 | to = new Date(from.getTime() + (6 * ONE_DAY_MS)), 12 | fromMonth = from.getMonth() + 1, 13 | fromDate = from.getDate(), 14 | toMonth = to.getMonth() + 1, 15 | toDate = to.getDate(); 16 | 17 | return WEEK_WORD + " " + fromMonth + "/" + fromDate + " - " + toMonth + "/" + toDate; 18 | }; 19 | 20 | function get_past7days_header_text(num) { 21 | let from = new Date((num - 6) * ONE_DAY_MS), // a week ago 22 | to = new Date(num * ONE_DAY_MS), // yesterday 23 | fromMonth = from.getMonth() + 1, 24 | fromDate = from.getDate(), 25 | toMonth = to.getMonth() + 1, 26 | toDate = to.getDate(); 27 | 28 | return PAST_7_DAYS_TEXT + " " + fromMonth + "/" + fromDate + " - " + toMonth + "/" + toDate; 29 | }; 30 | 31 | function combine_data_from_days(sourceArray) { 32 | let dmns = {}, 33 | summ = { totalSecs : 0 }; 34 | 35 | // loop through all the days and merge each day's data 36 | for (let day of sourceArray) { 37 | summ.totalSecs += day.totalSecs; 38 | 39 | for (let [key, val] of day.dmnsArray) { 40 | if (dmns.hasOwnProperty(key)) { 41 | dmns[key] += val; 42 | } else { 43 | dmns[key] = val; 44 | } 45 | } 46 | } 47 | 48 | // convert domains object to sorted array 49 | summ.dmnsArray = get_sorted_domains(dmns); 50 | return summ; 51 | }; 52 | 53 | function get_daily_totals(sourceArray) { 54 | let daysArray = []; 55 | for (let day of sourceArray) { 56 | daysArray.push([ 57 | day.headerText, 58 | day.totalSecs, 59 | day.dayNum 60 | ]); 61 | } 62 | return daysArray.sort((a, b) => a[2] - b[2]); 63 | }; 64 | 65 | function make_month_summ(monthNum, days) { 66 | 67 | let daysSubset = days.filter((day) => day && day.monthNum && day.monthNum === monthNum), 68 | summ = combine_data_from_days(daysSubset); 69 | 70 | summ.headerText = MONTH_NAMES[monthNum % 12]; 71 | summ.monthNum = monthNum; 72 | return summ; 73 | }; 74 | 75 | function make_week_summ(weekNum, days) { 76 | 77 | let daysSubset = days.filter((day) => day && day.weekNum && day.weekNum === weekNum), 78 | summ = combine_data_from_days(daysSubset); 79 | 80 | summ.daysArray = get_daily_totals(daysSubset); 81 | summ.headerText = get_week_header_text(weekNum); 82 | summ.weekNum = weekNum; 83 | return summ; 84 | }; 85 | 86 | function make_past7days_summ(num, days) { 87 | 88 | let daysSubset = days.filter((day) => day && day.dayNum > num - 8 && day.dayNum < num), 89 | summ = combine_data_from_days(daysSubset); 90 | 91 | summ.daysArray = get_daily_totals(daysSubset); 92 | summ.headerText = get_past7days_header_text(num); 93 | summ.firstDayNum = num - 7; 94 | return summ; 95 | }; 96 | 97 | function make_new_day_state(aStorage, aDateNow) { 98 | // aDateNow is Date.now(), the number of milliseconds elapsed since 99 | // 1 January 1970 00:00:00 UTC 100 | // We want the day to possibly change at other times than midnight, 101 | // so subtract offset in milliseconds from current UTC time. 102 | let date = get_date_with_offset(aStorage.oDayStartOffset, aDateNow), 103 | dayNumNow = get_day_number(date), 104 | monthNumNow = date.getMonth() + 1, 105 | weekNumNow = get_week_number(dayNumNow), 106 | 107 | // final dump of domain data to an array 108 | domainData = extract_domain_data(aStorage), 109 | domainsArray = get_sorted_domains(domainData); 110 | 111 | // create a new element in aStorage.days array (the new aStorage.days[0] ), copying the data over 112 | aStorage.days.unshift({ 113 | dayNum: aStorage.today.dayNum, 114 | dmnsArray: domainsArray, 115 | totalSecs: Math.round(aStorage.totalSecs), 116 | headerText: aStorage.today.headerText, 117 | monthNum: aStorage.today.monthNum, 118 | weekNum: aStorage.today.weekNum 119 | }); 120 | 121 | // delete old day data (keep 70 days) 122 | if (aStorage.days.length > 70) { 123 | aStorage.days.length = 70; 124 | } 125 | 126 | 127 | // Refresh summaries for past7days, week, month. 128 | aStorage.past7daySum = make_past7days_summ(dayNumNow, aStorage.days); 129 | aStorage.weekSums[0] = make_week_summ(aStorage.today.weekNum, aStorage.days); 130 | aStorage.monthSums[0] = make_month_summ(aStorage.today.monthNum, aStorage.days); 131 | 132 | // Check if we are in a new week or a new month, if so, add a new summary, 133 | // pushing previous one back, and remove any that are too old. 134 | if (aStorage.today.weekNum !== weekNumNow) { 135 | aStorage.weekSums.unshift(make_week_summ(weekNumNow, aStorage.days)); 136 | aStorage.weekSums.length = 10; 137 | } 138 | if (aStorage.today.monthNum !== monthNumNow) { 139 | aStorage.monthSums.unshift(make_month_summ(monthNumNow, aStorage.days)); 140 | aStorage.monthSums.length = 6; 141 | } 142 | 143 | // initialize aStorage.today object for new day's data 144 | aStorage.today = get_empty_today_object(aStorage.oDayStartOffset, aDateNow); 145 | aStorage.nextDayStartsAt = get_next_day_starts_at(aStorage.today.dayNum, aStorage.oDayStartOffset); 146 | 147 | // clear domain data (we have to do both STORAGE and aStorage) 148 | let domainKeys = get_domain_keys(aStorage); 149 | domainKeys.forEach(key => { delete aStorage[key]; }); 150 | STORAGE.remove(domainKeys); 151 | aStorage.totalSecs = 0; 152 | 153 | // reset alert messages 154 | aStorage.nextAlertAt = get_next_alert_at(aStorage.oNotificationsRate, 0); 155 | 156 | gState = get_null_gState(); 157 | return aStorage; 158 | }; 159 | 160 | async function start_new_day(aDateNow) { 161 | // aDateNow is Date.now(), the number of milliseconds elapsed since 162 | // 1 January 1970 00:00:00 UTC 163 | try { 164 | let storage = await STORAGE.get(); 165 | return STORAGE.set(make_new_day_state(storage, aDateNow)); 166 | 167 | } catch (e) { console.error(e); } 168 | }; 169 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // Contributor(s): Paul Morris. 6 | 7 | "use strict"; 8 | 9 | const ONE_DAY_MS = 86400000, 10 | ONE_MINUTE_MS = 60000, 11 | ONE_HOUR_MS = 3600000, 12 | IDLE_TIMEOUT_SECS = 30, 13 | DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], 14 | MONTH_NAMES = ["December", "January", "February", "March", 15 | "April", "May", "June", "July", "August", 16 | "September", "October", "November"], 17 | WEEK_WORD = "Week", 18 | PAST_7_DAYS_TEXT = "Past 7 Days", 19 | STORAGE = browser.storage.local, 20 | // all keys in storage that aren't domains, used to extract domain data for today 21 | STORAGE_KEYS = [ 22 | "days", 23 | "monthSums", 24 | "nextAlertAt", 25 | "nextDayStartsAt", 26 | 27 | "oButtonBadgeTotal", 28 | "oDayStartOffset", 29 | "oNotificationsOn", 30 | "oNotificationsRate", 31 | "oWhitelistArray", 32 | 33 | "past7daySum", 34 | "timerMode", 35 | "today", 36 | "totalSecs", 37 | "weekSums" 38 | ], 39 | OPTIONS = [ 40 | "oButtonBadgeTotal", 41 | "oDayStartOffset", 42 | "oNotificationsOn", 43 | "oNotificationsRate", 44 | "oWhitelistArray" 45 | ]; 46 | 47 | var gState = {}; 48 | 49 | function get_null_gState() { 50 | return { 51 | timing: { 52 | domain: null, 53 | stamp: null 54 | }, 55 | clockOnTimeout: null, 56 | preClockOnTimeout: null, 57 | notificationsMinutes: "" 58 | }; 59 | }; 60 | 61 | function is_null_or_undefined(x) { 62 | return x === null || x === undefined; 63 | }; 64 | 65 | function format_time_minimal(time) { 66 | // used for ticker button badge 67 | let [h, m] = time_to_hours_and_minutes(time); 68 | return ((h > 0) ? h + ":" : "") + 69 | ((h > 0) && (m < 10) ? "0" + m : m); 70 | }; 71 | 72 | function get_next_day_starts_at(dayNum, aDayStartOffset) { 73 | // determine when the next day starts in milliseconds since midnight on 1/1/1970 74 | // add one to get next day, convert to milliseconds, 75 | // adjust for local time zone, and add aDayStartOffset so new day starts at e.g. 4am 76 | let localTimeZoneOffsetMS = new Date().getTimezoneOffset() * ONE_MINUTE_MS, 77 | startsAt = ((dayNum + 1) * ONE_DAY_MS) + localTimeZoneOffsetMS + (aDayStartOffset * ONE_HOUR_MS); 78 | return startsAt; 79 | }; 80 | 81 | function get_domain_keys(aStorage) { 82 | let allKeys = Object.keys(aStorage), 83 | domainKeys = allKeys.filter(key => !STORAGE_KEYS.includes(key)); 84 | return domainKeys; 85 | }; 86 | 87 | function extract_domain_data(aStorage) { 88 | let domainKeys = get_domain_keys(aStorage), 89 | domainData = {}; 90 | domainKeys.forEach(key => { domainData[key] = aStorage[key] }); 91 | return domainData; 92 | }; 93 | 94 | function get_sorted_domains(aDomains) { 95 | // Takes an object {domain.com: 300.121, ...} and returns a sorted array of 96 | // arrays, rounding to the nearest second. [[domain.com, 300], ...] 97 | return Object.keys(aDomains) 98 | .filter((key) => aDomains[key] !== 0) 99 | .map((dmn) => [dmn, Math.round(aDomains[dmn])]) 100 | .sort((a, b) => b[1] - a[1]); 101 | }; 102 | 103 | function sanitize_whitelist(oldWhitelistString) { 104 | // takes a string (from the whitelist pref) and returns an array 105 | let items = oldWhitelistString.split(','), 106 | whitelistSet = new Set(); 107 | 108 | for (let item of items) { 109 | // trim whitespace 110 | item = item.trim(); 111 | 112 | // skip empty items 113 | if (item.length !== 0) { 114 | // remove any sub-directories, trailing slashes, and http:// or https:// 115 | try { item = new URL(item).host; } 116 | catch(e) { 117 | try { item = new URL("http://" + item).host; } 118 | catch(e) { } 119 | } 120 | whitelistSet.add(item); 121 | } 122 | } 123 | // convert set to an array 124 | return [...whitelistSet]; 125 | }; 126 | 127 | async function get_current_tab() { 128 | try { 129 | let tabs = await browser.tabs.query({currentWindow: true, active: true}); 130 | return tabs[0]; 131 | 132 | } catch (e) { console.error(e); } 133 | }; 134 | 135 | async function get_tab_by_url(aUrl) { 136 | try { 137 | let tabs = await browser.tabs.query({}), 138 | filteredTabs = tabs.filter(t => t.url === aUrl); 139 | return filteredTabs[0] || false; 140 | } catch (e) { 141 | console.error(e); 142 | return false; 143 | } 144 | }; 145 | 146 | async function delete_all_data() { 147 | gState = get_null_gState(); 148 | update_ticker(0); 149 | try { 150 | let savedData = await STORAGE.get(OPTIONS); 151 | await STORAGE.clear(); 152 | await STORAGE.set(get_initial_storage(savedData)); 153 | // reload the summary page if it is open 154 | let summaryUrl = browser.extension.getURL("summary/index.html"), 155 | summaryTab = await get_tab_by_url(summaryUrl); 156 | if (summaryTab) { 157 | browser.tabs.reload(summaryTab.id, {bypassCache: true}); 158 | } 159 | } catch (e) { console.error(e); } 160 | }; 161 | 162 | 163 | // INITIALIZE DATA STORAGE 164 | 165 | // Accepts a date object, returns the number of that day starting from 1/1/1970. 166 | // The date arg has already been adjusted for 4am day change. 167 | // The offset for the local time zone (getTimezoneOffset) is given in minutes 168 | // so convert it to milliseconds. 169 | // Subtract the time zone offset because it is positive if behind UTC and 170 | // negative if ahead. 171 | // Example: USA EST is +5 hours offset from UTC, so subtract 5 hours of MS 172 | // from UTC MS to get local MS. 173 | function get_day_number(date) { 174 | let localTimeMS = date.getTime() - (date.getTimezoneOffset() * ONE_MINUTE_MS); 175 | 176 | // console.log("timezoneOffset in hours: " + date.getTimezoneOffset() / 60); 177 | // console.log("dayNum: " + Math.floor( localTimeMS / ONE_DAY_MS )); 178 | 179 | return Math.floor( localTimeMS / ONE_DAY_MS ); 180 | }; 181 | 182 | function get_week_number(dayNumber) { 183 | // Returns the day number of the Sunday before the dayNumber argument. 184 | // We don't use Date.prototype.getDay to avoid time zone complications. 185 | return dayNumber - ((dayNumber - 3) % 7); 186 | }; 187 | 188 | function get_day_header_text(date) { 189 | return DAY_NAMES[date.getDay()] + " " + 190 | (date.getMonth() + 1) + "/" + date.getDate(); 191 | }; 192 | 193 | function get_date_with_offset(aOffset, aDateNow) { 194 | // aDateNow is Date.now(), the number of milliseconds elapsed since 195 | // 1 January 1970 00:00:00 UTC 196 | // Subtract offset in ms to get adjusted day change moment. 197 | return new Date(aDateNow - (aOffset * ONE_HOUR_MS)); 198 | }; 199 | 200 | function get_empty_today_object(aDayStartOffset, aDateNow) { 201 | // Used to initialize or reset today object, for add-on install, new day, 202 | // delete all data. aDateNow is Date.now(), the number of milliseconds 203 | // elapsed since 1 January 1970 00:00:00 UTC 204 | let date = get_date_with_offset(aDayStartOffset, aDateNow), 205 | dayNumber = get_day_number(date); 206 | return { 207 | headerText: get_day_header_text(date), 208 | monthNum: date.getMonth() + 1, 209 | dateNum: date.getDate(), 210 | dateObj: date, 211 | dayNum: dayNumber, 212 | weekNum: get_week_number(dayNumber) 213 | }; 214 | }; 215 | 216 | function get_empty_month_summary_object() { 217 | // month summary objects don't need a daysArray 218 | return { 219 | dmnsArray: [], 220 | totalSecs: 0, 221 | headerText: "" 222 | }; 223 | }; 224 | 225 | function get_empty_summary_object() { 226 | let result = get_empty_month_summary_object(); 227 | result.daysArray = []; 228 | return result; 229 | }; 230 | 231 | // Called on installation and when deleting all data. 232 | // Takes aStorage object and creates a newStorage object by adding any values 233 | // missing from aStorage. Returns newStorage. STORAGE can then be set with newStorage. 234 | // Called without an argument it returns a complete initial storage object. 235 | function get_initial_storage(aStorage = {}) { 236 | let simpleDefaults = { 237 | oButtonBadgeTotal: false, 238 | oNotificationsOn: false, 239 | oNotificationsRate: 60, 240 | oDayStartOffset: 0, 241 | oWhitelistArray: [], 242 | timerMode: "D", 243 | totalSecs: 0, 244 | days: [] 245 | }, 246 | newStorage = Object.assign(simpleDefaults, aStorage); 247 | 248 | if (is_null_or_undefined(aStorage.nextAlertAt)) { 249 | newStorage.nextAlertAt = get_next_alert_at(newStorage.oNotificationsRate, newStorage.totalSecs); 250 | } 251 | 252 | let dayNum; 253 | if (!aStorage.today) { 254 | newStorage.today = get_empty_today_object(newStorage.oDayStartOffset, Date.now()); 255 | dayNum = newStorage.today.dayNum; 256 | } else { 257 | dayNum = aStorage.today.dayNum; 258 | } 259 | newStorage.nextDayStartsAt = get_next_day_starts_at(dayNum, newStorage.oDayStartOffset); 260 | 261 | if (!aStorage.past7daySum) { 262 | newStorage.past7daySum = get_empty_summary_object(); 263 | } 264 | if (!aStorage.weekSums) { 265 | newStorage.weekSums = new Array(10).fill(get_empty_summary_object()); 266 | } 267 | if (!aStorage.monthSums) { 268 | newStorage.monthSums = new Array(6).fill(get_empty_month_summary_object()); 269 | } 270 | return newStorage; 271 | }; 272 | 273 | function initialize_state() { 274 | gState = get_null_gState(); 275 | browser.idle.setDetectionInterval(IDLE_TIMEOUT_SECS); 276 | }; 277 | 278 | 279 | // Initialize storage, globals, etc. 280 | 281 | function handle_incognito_tab(tab) { 282 | if (tab.incognito) { 283 | browser.browserAction.disable(tab.id); 284 | browser.browserAction.setBadgeText({ text: "", tabId: tab.id }); 285 | } 286 | } 287 | 288 | browser.tabs.onCreated.addListener(handle_incognito_tab); 289 | 290 | async function initialize_incognito_tabs() { 291 | let tabs = await browser.tabs.query({}); 292 | tabs.forEach(handle_incognito_tab); 293 | } 294 | 295 | async function handle_startup() { 296 | try { 297 | // Get the timer mode from storage and call the function to set up 298 | // listeners, etc. for the current timer mode. 299 | initialize_state(); 300 | let result = await STORAGE.get('timerMode'); 301 | handle_timer_mode_change(result.timerMode); 302 | initialize_incognito_tabs(); 303 | 304 | } catch (e) { console.error(e); } 305 | }; 306 | 307 | function handle_installed(details) { 308 | // console.log("handle_installed", details.reason); 309 | if (details.reason === 'install') { 310 | initialize_state(); 311 | STORAGE.set(get_initial_storage()); 312 | initialize_incognito_tabs(); 313 | } else { 314 | // When details.reason is update, chrome_update, etc. then 315 | // initialization is the same as for startup. 316 | handle_startup(); 317 | } 318 | }; 319 | 320 | browser.runtime.onInstalled.addListener(handle_installed); 321 | browser.runtime.onStartup.addListener(handle_startup); 322 | -------------------------------------------------------------------------------- /src/tracking-events.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // TIME-TRACKING 6 | 7 | "use strict"; 8 | 9 | /* 10 | There are two main operations. First, 'clock on' temporarily 11 | stores a domain and a starting time stamp for that domain. Second, 'clock off' 12 | calculates the time elapsed since the time stamp for the domain, and adds that 13 | to the tally in storage for that domain. It then clears the domain and the time 14 | stamp data. Various events trigger a clock off attempt first and then clock on. 15 | Clock off is always tried before clock on so that any time is logged before a 16 | new timing 'cycle' begins. 17 | */ 18 | 19 | function get_next_alert_at(aRateInMins, aTotalSecs) { 20 | let rateSecs = aRateInMins * 60; 21 | return aTotalSecs + (rateSecs - (aTotalSecs % rateSecs)); 22 | }; 23 | 24 | function get_notification_message(aStorage) { 25 | let domainData = extract_domain_data(aStorage), 26 | domainsArray = get_sorted_domains(domainData), 27 | topDomains = domainsArray.slice(0, 3), 28 | reducer = (msg, dmn) => msg + format_time(dmn[1]) + " " + dmn[0] + "\n", 29 | message = topDomains.reduce(reducer, ""); 30 | return message; 31 | }; 32 | 33 | async function show_notification(minutes) { 34 | try { 35 | let storage = await STORAGE.get(null), 36 | message = await get_notification_message(storage), 37 | id = await browser.notifications.create({ 38 | "type": "basic", 39 | "iconUrl": browser.extension.getURL("icons/mind-the-time-icon-48.svg"), 40 | "title": minutes + " Today", 41 | "message": message 42 | }); 43 | setTimeout(() => { 44 | browser.notifications.clear(id); 45 | gState.notificationIsShowing = false; 46 | }, 8000); 47 | 48 | } catch (e) { console.error(e); } 49 | }; 50 | 51 | async function maybe_show_notification() { 52 | try { 53 | let fromStorage = await STORAGE.get([ 54 | "totalSecs", 55 | "oNotificationsOn", 56 | "oNotificationsRate", 57 | "nextAlertAt" 58 | ]), 59 | totalSecs = fromStorage.totalSecs; 60 | 61 | if (fromStorage.oNotificationsOn && 62 | fromStorage.oNotificationsRate > 0 && 63 | totalSecs >= fromStorage.nextAlertAt) { 64 | 65 | // somehow we got duplicate notifications, so we prevent that 66 | let minutes = format_time(totalSecs); 67 | if (minutes !== gState.notificationsMinutes) { 68 | gState.notificationsMinutes = minutes; 69 | show_notification(minutes); 70 | } 71 | let next = get_next_alert_at(fromStorage.oNotificationsRate, totalSecs); 72 | STORAGE.set({nextAlertAt: next}); 73 | } 74 | } catch (e) { console.error(e); } 75 | }; 76 | 77 | async function log_seconds(aDomain, aRawSeconds) { 78 | try { 79 | let fromStorage = await STORAGE.get(["totalSecs", aDomain]), 80 | oldSeconds = fromStorage[aDomain] || 0, 81 | // Round to two decimal places. 82 | newSeconds = Math.round(aRawSeconds * 100) / 100, 83 | newData = {totalSecs: fromStorage.totalSecs + newSeconds}; 84 | 85 | // console.log('log_seconds', newSeconds, aDomain); 86 | 87 | newData[aDomain] = oldSeconds + newSeconds; 88 | STORAGE.set(newData); 89 | 90 | } catch (e) { console.error(e); } 91 | }; 92 | 93 | async function maybe_clock_off(aStartStamp, aTimingDomain) { 94 | // console.log('maybe_clock_off', aTimingDomain, aStartStamp); 95 | try { 96 | if (aStartStamp) { 97 | // console.log('clock off', aTimingDomain, aStartStamp); 98 | 99 | clearTimeout(gState.clockOnTimeout); 100 | 101 | // Clear timing data so we don't clock off again until after clock on. 102 | gState.timing.domain = null; 103 | gState.timing.stamp = null; 104 | 105 | let rawSeconds = (Date.now() - aStartStamp) / 1000; 106 | if (rawSeconds > 0.5) { 107 | await log_seconds(aTimingDomain, rawSeconds); 108 | maybe_show_notification(); 109 | } 110 | } 111 | } catch (e) { console.error(e); } 112 | }; 113 | 114 | function get_clock_on_timeout_MS(aTotalSecs) { 115 | // Wait at least some minimum amount. 116 | let secsUntilNextMinute = (62 - (aTotalSecs % 60)), 117 | min = 5, 118 | secs = secsUntilNextMinute > min ? secsUntilNextMinute : min; 119 | return secs * 1000; 120 | }; 121 | 122 | function restart_clock_on_timeout(aTotalSecs) { 123 | // Restarts the timeout for re-clocking-off/on. 124 | // We set this timeout to clock off and on again after the next minute 125 | // threshold has passed, to update the ticker, notifications, etc. when 126 | // the user has been active at same site/tab for more than a minute. 127 | let ms = get_clock_on_timeout_MS(aTotalSecs); 128 | gState.clockOnTimeout = setTimeout(clock_on_timeout_function, ms); 129 | } 130 | 131 | async function clock_on(aDomain) { 132 | // Starts timing for a site. 133 | // console.log('clock_on', aDomain); 134 | 135 | // Clock off should always happen before clock on, and it sets 136 | // gState.timing values to null, so we warn and redo the clock off if not. 137 | if (gState.timing.stamp && gState.timing.domain) { 138 | console.warn("Mind the Time: clock on without prior clock off"); 139 | await maybe_clock_off(gState.timing.stamp, gState.timing.domain); 140 | } 141 | Object.assign(gState.timing, { 142 | domain: aDomain, 143 | stamp: Date.now() 144 | }); 145 | }; 146 | 147 | function is_clockable_protocol(aProt) { 148 | return aProt === 'http:' || aProt === 'https:'; 149 | }; 150 | 151 | async function pre_clock_on_2_internal(aUrl) { 152 | // Maybe starts a new day, updates the ticker, and maybe clocks on. 153 | // aUrl parameter is bound to a special url in blue mode, and used for 154 | // testing new day change. 155 | try { 156 | let tab = await get_current_tab(), 157 | url = aUrl || new URL(tab.url), 158 | domain = url.host, 159 | dateNow = Date.now(), 160 | fromStorage = await STORAGE.get([ 161 | "nextDayStartsAt", 162 | "oWhitelistArray", 163 | "totalSecs", 164 | domain 165 | ]); 166 | 167 | // console.log('hours until new day:', (fromStorage.nextDayStartsAt - dateNow) / 3600000); 168 | if (dateNow > fromStorage.nextDayStartsAt) { 169 | await start_new_day(dateNow); 170 | } 171 | 172 | // Only clock on if we are not in an incognito window, 173 | // the domain has a clockable url protocol (http/https), 174 | // and the domain is not in the whitelist. 175 | if (tab.incognito) { 176 | handle_incognito_tab(tab); 177 | 178 | } else if (is_clockable_protocol(url.protocol) && 179 | !fromStorage.oWhitelistArray.includes(domain)) { 180 | 181 | let seconds = fromStorage[domain] || 0; 182 | update_ticker(seconds, fromStorage.totalSecs); 183 | clock_on(domain); 184 | restart_clock_on_timeout(fromStorage.totalSecs) 185 | 186 | } else { 187 | update_ticker(0, fromStorage.totalSecs); 188 | } 189 | } catch (e) { console.error(e); } 190 | }; 191 | 192 | 193 | // Depends on the mode (to handle blue mode). 194 | var pre_clock_on_2; 195 | 196 | function pre_clock_on() { 197 | // avoid redundant clock_on calls for the same event 198 | clearTimeout(gState.preClockOnTimeout); 199 | gState.preClockOnTimeout = setTimeout(pre_clock_on_2, 50); 200 | }; 201 | 202 | 203 | // EVENT HANDLING 204 | 205 | async function maybe_clock_off_then_pre_clock_on() { 206 | try { 207 | await maybe_clock_off(gState.timing.stamp, gState.timing.domain); 208 | pre_clock_on(); 209 | } catch (e) { console.error(e); } 210 | }; 211 | 212 | function tabs_on_updated(tabId, changeInfo, tab) { 213 | // Only handle updates to urls, not other kinds of updates. 214 | // The updated tab may not be the active tab, 215 | // so there's no point in passing a URL as an argument. 216 | if (changeInfo.url) { 217 | // console.log('! tabs.onUpdated', tabId, changeInfo, tab); 218 | maybe_clock_off_then_pre_clock_on(); 219 | } 220 | }; 221 | 222 | function tabs_on_activated(activeInfo) { 223 | // console.log('! tabs.onActivated', activeInfo); 224 | maybe_clock_off_then_pre_clock_on(); 225 | }; 226 | 227 | function tabs_on_removed(tabId, removeInfo) { 228 | // console.log('! tabs.onRemoved', removeInfo); 229 | // It may not be the active tab that was removed, 230 | // so we clock off AND back on to cover that case. 231 | maybe_clock_off_then_pre_clock_on(); 232 | }; 233 | 234 | async function windows_on_focus_changed(windowId) { 235 | // console.log('! windows.onFocusChanged', windowId); 236 | try { 237 | await maybe_clock_off(gState.timing.stamp, gState.timing.domain); 238 | if (windowId !== -1) { 239 | pre_clock_on(); 240 | } 241 | } catch (e) { console.error(e); } 242 | }; 243 | 244 | function clock_on_timeout_function() { 245 | // console.log('! clock_on_timeout_function'); 246 | maybe_clock_off_then_pre_clock_on(); 247 | }; 248 | 249 | 250 | // IDLE TIMEOUT / USER ACTIVITY DETECTION 251 | 252 | // when user is idle for IDLE_TIMEOUT_SECS we clock off, then when user becomes 253 | // active again we clock back on 254 | async function idle_handler(aIdleState) { 255 | try { 256 | let windowInfo = await browser.windows.getLastFocused(); 257 | 258 | let d = new Date; 259 | console.log('Mind the Time. idle-state:', aIdleState, '; window-focused:', windowInfo.focused, d.getHours() + ':' + d.getMinutes()); 260 | 261 | if (windowInfo.focused) { 262 | await maybe_clock_off(gState.timing.stamp, gState.timing.domain); 263 | if (aIdleState === "active") { 264 | pre_clock_on(); 265 | } 266 | // else aIdleState is 'idle' or 'locked' and we just clock off and do no more 267 | } 268 | } catch (e) { console.error(e); } 269 | }; 270 | 271 | 272 | // STORAGE CHANGE LISTENER 273 | 274 | // For logging of storage changes, to just show the new values. 275 | /* 276 | function storage_change_inspector(changes) { 277 | let keys = Object.keys(changes); 278 | let result = {}; 279 | for (let key of keys) { 280 | result[key] = changes[key].newValue; 281 | } 282 | return result; 283 | }; 284 | */ 285 | 286 | function handle_whitelist_change() { 287 | // If the whitelist changed, we clear the current domain so we don't 288 | // accidentally log a site that was added to the whitelist. 289 | maybe_clock_off_then_pre_clock_on(); 290 | }; 291 | 292 | async function handle_day_start_offset_change(aDayStartOffset) { 293 | let dateNow = Date.now(), 294 | date = get_date_with_offset(aDayStartOffset, dateNow), 295 | dayNum = get_day_number(date), 296 | next = get_next_day_starts_at(dayNum, aDayStartOffset); 297 | try { 298 | await STORAGE.set({nextDayStartsAt: next}); 299 | 300 | // Start a new day if the new day offset is moved into the past. 301 | let fromStorage = await STORAGE.get('today'); 302 | if (dayNum > fromStorage.today.dayNum) { 303 | start_new_day(dateNow); 304 | } 305 | } catch (e) { console.error(e); } 306 | }; 307 | 308 | async function handle_notifications_change() { 309 | try { 310 | let fromStorage = await STORAGE.get(["oNotificationsRate", "totalSecs"]), 311 | next = get_next_alert_at(fromStorage.oNotificationsRate, fromStorage.totalSecs); 312 | STORAGE.set({nextAlertAt: next}); 313 | 314 | } catch (e) { console.error(e); } 315 | }; 316 | 317 | async function handle_timer_mode_change(mode) { 318 | try { 319 | await maybe_clock_off(gState.timing.stamp, gState.timing.domain); 320 | set_listeners_for_timer_mode(mode); 321 | set_pre_clock_on_2_function(mode); 322 | set_ticker_update_function(); 323 | set_popup_ticker_function(mode); 324 | set_badge_for_timer_mode(mode); 325 | pre_clock_on(); 326 | 327 | } catch (e) { console.error(e); } 328 | }; 329 | 330 | // Even when a new value is the same as the old value it will fire this listener. 331 | // Note that options are typically all changed at once (but maybe not actually 332 | // changed) when save button is clicked. 333 | browser.storage.onChanged.addListener((changes, area) => { 334 | // console.log('storage changed', storage_change_inspector(changes)); 335 | 336 | // when we clear storage for delete all data everything is undefined so check for that 337 | // this is involved in initialization for the timer mode on app install / restart 338 | if (changes.timerMode && changes.timerMode.newValue) { 339 | handle_timer_mode_change(changes.timerMode.newValue); 340 | } 341 | if (changes.oButtonBadgeTotal) { 342 | set_ticker_update_function(); 343 | } 344 | if ((changes.oNotificationsOn || changes.oNotificationsRate) && 345 | (changes.oNotificationsOn.newValue || changes.oNotificationsRate.newValue)) { 346 | handle_notifications_change(); 347 | } 348 | if (changes.oDayStartOffset && 349 | // The newValue can be 0 (a JS falsy value). 350 | !is_null_or_undefined(changes.oDayStartOffset.newValue) && 351 | changes.oDayStartOffset.newValue !== changes.oDayStartOffset.oldValue) { 352 | handle_day_start_offset_change(changes.oDayStartOffset.newValue); 353 | } 354 | if (changes.oWhitelistArray && changes.oWhitelistArray.newValue) { 355 | handle_whitelist_change(); 356 | } 357 | }); 358 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/summary/index.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // Javascript for the summary page (summary/index.html) 6 | "use strict"; 7 | 8 | // remove failed-to-load message 9 | document.body.removeChild(document.getElementById("loadFailMessage")); 10 | 11 | // add click listeners for top navigation bar 12 | [['mindTheTimeNavLink', 'topnav'], 13 | ['monthsNavLink', 'rowMonths'], 14 | ['weeksNavLink', 'rowWeeks'], 15 | ['daysNavLink', 'rowDays'], 16 | ['optionsNavLink', 'rowPrefs']].forEach((pair) => { 17 | document.getElementById(pair[0]).addEventListener('click', smooth_scroll.bind(null, pair[1])); 18 | }); 19 | 20 | var gTodayStamp; 21 | 22 | // TABLE CONSTRUCTION 23 | 24 | function put_in_td(elem) { 25 | let td = document.createElement('td'), 26 | child = (typeof elem === 'string' ? document.createTextNode(elem) : elem); 27 | td.appendChild(child); 28 | return td; 29 | }; 30 | 31 | function put_tds_in_a_row(tds) { 32 | let row = document.createElement('tr'); 33 | for (let td of tds) { 34 | row.appendChild(td); 35 | } 36 | return row; 37 | }; 38 | 39 | function make_graph(secs) { 40 | let minsPerPx = 2, 41 | totalMins = Math.floor(secs / 60), 42 | hours = Math.floor(totalMins / 60), 43 | mins = Math.floor(totalMins % 60), 44 | graphUL = document.createElement('ul'); 45 | 46 | graphUL.setAttribute('class', 'graphUL'); 47 | // sets how many hours per row: 48 | // graphUL.style.maxWidth = ((hours < 5) ? (totalMins / minsPerPx) : ( 300 / minsPerPx) ) + 12 + "px"; 49 | while (hours) { 50 | let lig = document.createElement('li'); 51 | lig.setAttribute('class', 'graphLI'); 52 | // lig.style.width=((60 / minsPerPx) - 1) + "px"; 53 | graphUL.appendChild(lig); 54 | hours -= 1; 55 | } 56 | let lig = document.createElement('li'); 57 | lig.setAttribute('class', 'graphLI'); 58 | lig.style.width = (mins / minsPerPx) + "px"; 59 | graphUL.appendChild(lig); 60 | return graphUL; 61 | }; 62 | 63 | function make_header_row(header) { 64 | let h4 = document.createElement('h4'); 65 | h4.setAttribute('class', 'dateheader'); 66 | h4.appendChild( document.createTextNode(header) ); 67 | 68 | let td = put_in_td(h4); 69 | td.setAttribute('class', 'headertd'); 70 | td.colSpan = "5"; 71 | 72 | let row = document.createElement('tr'); 73 | row.setAttribute('class', 'headerrow'); 74 | row.appendChild(td); 75 | return row; 76 | }; 77 | 78 | function make_day_summary_row(boxID, c, tsecs, dayItem) { 79 | let percent = (tsecs === 0 ? "0%" : Math.round( (dayItem[1] / tsecs) * 100) + "%"), 80 | time = format_time(dayItem[1]), 81 | graph = make_graph(dayItem[1]), 82 | nodes = [" ", dayItem[0], time, percent, graph], 83 | tds = nodes.map(put_in_td); 84 | 85 | tds[1].setAttribute('class', 'domain-td'); 86 | 87 | let row = put_tds_in_a_row(tds); 88 | row.setAttribute('id', 'daysArray-trow' + boxID + c); 89 | return row; 90 | }; 91 | 92 | function make_domain_row(boxID, c, tsecs, dmnsItem, rowsShown) { 93 | let domainNode; 94 | 95 | // TODO: lowercase version introduced with first webextension version 96 | // eventually we should drop the uppercase version once it is out of 97 | // users data 98 | if (dmnsItem[0] === "o3xr2485dmmdi78177v7c33wtu7315.net" || 99 | dmnsItem[0] === "O3Xr2485dmmDi78177V7c33wtu7315.net") { 100 | domainNode = document.createTextNode("No websites logged (only time)"); 101 | } else { 102 | domainNode = document.createElement('a'); 103 | domainNode.setAttribute('href', "http://" + dmnsItem[0]); 104 | domainNode.setAttribute('class', 'domainlink'); 105 | domainNode.setAttribute('target', '_blank'); 106 | domainNode.appendChild( document.createTextNode( dmnsItem[0] )); 107 | } 108 | 109 | let percent = (tsecs === 0 ? "0%" : Math.round( (dmnsItem[1] / tsecs) * 100) + "%"), 110 | rowNumber = c + 1 + ".", 111 | nodes = [rowNumber, domainNode, format_time(dmnsItem[1]), percent, make_graph(dmnsItem[1])], 112 | tds = nodes.map(put_in_td); 113 | 114 | tds[1].setAttribute('class', 'domain-td'); 115 | 116 | let row = put_tds_in_a_row(tds); 117 | row.setAttribute('id', 'trow' + boxID + c); 118 | if (c > rowsShown - 1) { 119 | row.setAttribute('style', 'display:none'); 120 | } 121 | return row; 122 | }; 123 | 124 | function make_total_row(tsecs) { 125 | let nodes = [" ", "Total", format_time(tsecs), "100%", make_graph(tsecs)], 126 | tds = nodes.map(put_in_td), 127 | row = put_tds_in_a_row(tds); 128 | row.setAttribute('class', 'totalrow'); 129 | return row; 130 | }; 131 | 132 | function make_show_more_row(len, rowsShown, boxID) { 133 | let showLink = document.createElement('a'); 134 | showLink.setAttribute('class', 'showmore'); 135 | showLink.appendChild( document.createTextNode("Show " + (len - rowsShown) + " More") ); 136 | showLink.addEventListener("click", () => { 137 | show_or_hide_rows(boxID, len, rowsShown, true); 138 | }, false); 139 | 140 | let tds = [" ", showLink].map(put_in_td); 141 | tds[1].setAttribute('id', 'showCell' + boxID); 142 | tds[1].colSpan = "4"; 143 | tds[1].appendChild(showLink); 144 | 145 | let row = put_tds_in_a_row(tds); 146 | row.setAttribute('id', 'showrow' + boxID ); 147 | return row; 148 | }; 149 | 150 | // handles "show/hide more rows" 151 | function show_or_hide_rows(boxID, len, rowsShown, showMore) { 152 | let i, 153 | showLink = document.createElement('a'), 154 | showCell = document.getElementById('showCell' + boxID), 155 | showWhatText, 156 | displayValue; 157 | 158 | if (showMore === true) { 159 | showWhatText = document.createTextNode( "Show Only First 10" ); 160 | displayValue = null; 161 | showLink.addEventListener("click", () => { 162 | show_or_hide_rows(boxID, len, rowsShown, false); 163 | // scroll to top of table (using boxID) 164 | document.defaultView.postMessage( boxID, "*"); 165 | }, false); 166 | 167 | } else { 168 | showWhatText = document.createTextNode( "Show " + (len - rowsShown) + " More" ); 169 | displayValue = "none"; 170 | showLink.addEventListener("click", () => { 171 | show_or_hide_rows(boxID, len, rowsShown, true); 172 | }, false); 173 | } 174 | 175 | showLink.setAttribute('class', 'showmore'); 176 | showLink.appendChild(showWhatText); 177 | showCell.innerHTML = ""; 178 | showCell.appendChild(showLink); 179 | 180 | for (i = rowsShown; i < len; i += 1) { 181 | document.getElementById("trow" + boxID + i).style.display = displayValue; 182 | } 183 | }; 184 | 185 | function make_table(data, aHeaderText, boxID) { 186 | 187 | let domainCount = data.dmnsArray.length, 188 | rowsShown = 10, 189 | tab = document.createElement('table'), 190 | tbo = document.createElement('tbody'); 191 | 192 | // date header row 193 | tbo.appendChild(make_header_row(aHeaderText)); 194 | 195 | // daysArray rows 196 | if (data.daysArray) { 197 | for (let c = 0; c < data.daysArray.length; c += 1) { 198 | tbo.appendChild( make_day_summary_row( boxID, c, data.totalSecs, data.daysArray[c] )); 199 | } 200 | } 201 | 202 | // total header row 203 | tbo.appendChild( make_total_row(data.totalSecs) ); 204 | 205 | // create domain rows 206 | for (let c = 0; c < domainCount; c += 1) { 207 | tbo.appendChild( make_domain_row(boxID, c, data.totalSecs, data.dmnsArray[c], rowsShown) ); 208 | } 209 | 210 | // show more row 211 | if (domainCount > rowsShown) { 212 | tbo.appendChild( make_show_more_row( domainCount, rowsShown, boxID ) ); 213 | } 214 | 215 | tab.setAttribute('class','MTTtable'); 216 | tab.appendChild(tbo); 217 | return tab; 218 | }; 219 | 220 | function make_empty_table_message_row(text) { 221 | let td = put_in_td(text); 222 | td.colSpan = "5"; 223 | td.setAttribute('class', 'emptytableTD'); 224 | let row = document.createElement('tr'); 225 | row.appendChild(td); 226 | return row; 227 | }; 228 | 229 | function make_empty_table(header, contentText) { 230 | let headerRow = make_header_row( header), 231 | messageRow = make_empty_table_message_row(contentText), 232 | tbo = document.createElement('tbody'), 233 | tab = document.createElement('table'); 234 | tbo.appendChild(headerRow); 235 | tbo.appendChild(messageRow); 236 | tab.setAttribute('class','MTTtable'); 237 | tab.appendChild(tbo); 238 | return tab; 239 | }; 240 | 241 | async function handle_days_button_click() { 242 | try { 243 | let fromStorage = await browser.storage.local.get('days'), 244 | node = document.getElementById("showDaysButton"), 245 | days_partB = fromStorage.days.slice(8); 246 | 247 | node.parentNode.removeChild(node); 248 | add_day_big_rows(days_partB.length, 15, 29); 249 | add_day_tables(days_partB, 15, 29); 250 | 251 | } catch (e) { console.error(e); } 252 | }; 253 | 254 | function make_more_days_button() { 255 | let dayButton = document.createElement('p'), 256 | dayButtonText = document.createTextNode( "Show All Day Summaries" ); 257 | dayButton.appendChild(dayButtonText); 258 | dayButton.setAttribute('id', 'showDaysButton'); 259 | dayButton.addEventListener("click", handle_days_button_click, false); 260 | return dayButton; 261 | }; 262 | 263 | 264 | // HIGHER LEVEL MANAGEMENT 265 | 266 | function table_needed(data) { 267 | return data && data.dmnsArray.length > 0; 268 | } 269 | 270 | function pair_array(a) { 271 | // [1,2,3,4,5] --> [[1,2],[3,4],[5]] 272 | let temp = a.slice(), 273 | arr = []; 274 | while (temp.length) { 275 | arr.push(temp.splice(0,2)); 276 | } 277 | return arr; 278 | }; 279 | 280 | function make_box(boxIdNum) { 281 | let box = document.createElement('div'); 282 | box.setAttribute('class','sum-box'); 283 | box.setAttribute('id', 'box' + boxIdNum); 284 | return box; 285 | }; 286 | 287 | function make_big_row(rowIdNum) { 288 | let bigRow = document.createElement('div'); 289 | bigRow.setAttribute('class','big-row'); 290 | bigRow.setAttribute('id', 'row' + rowIdNum); 291 | return bigRow; 292 | }; 293 | 294 | function add_day_big_rows(boxCount, rowIdNum, boxIdNum) { 295 | // creates a pattern, a paired array, which solves the problem 296 | // of an odd number of days needing only one box in last big row 297 | let pattern = pair_array( Array(boxCount).fill(true) ); 298 | 299 | for (let pair of pattern) { 300 | let bigRow = make_big_row(rowIdNum); 301 | 302 | for (let item of pair) { 303 | bigRow.appendChild(make_box(boxIdNum)); 304 | boxIdNum += 1; 305 | } 306 | 307 | document.getElementById("day-rows").appendChild(bigRow); 308 | rowIdNum += 1; 309 | } 310 | }; 311 | 312 | function add_day_tables(days, rowIdNum, boxIdNum) { 313 | for (let day of days) { 314 | if (day !== null && day.totalSecs !== 0) { 315 | let boxID = "box" + boxIdNum; 316 | document.getElementById(boxID).appendChild(make_table(day, day.headerText, boxID)); 317 | boxIdNum += 1; 318 | } 319 | } 320 | }; 321 | 322 | 323 | // LOADING DATA 324 | 325 | function load_summary(aStorage) { 326 | let today = aStorage.today, 327 | domainData = gBackground.extract_domain_data(aStorage), 328 | headerText = "Today, " + today.headerText; 329 | 330 | today.dmnsArray = gBackground.get_sorted_domains(domainData); 331 | 332 | // round because today's total is still dynamic, not done yet. 333 | today.totalSecs = Math.round(aStorage.totalSecs); 334 | 335 | document.getElementById("box0").innerHTML = ""; 336 | document.getElementById("box0").appendChild(make_table(today, headerText, "box0")); 337 | 338 | // if it's a new day, reload rest of data/tables 339 | if (today.dayNum !== gTodayStamp) { 340 | gTodayStamp = today.dayNum; 341 | load_the_rest(aStorage); 342 | } 343 | }; 344 | 345 | 346 | // first load only data for previous day, current week, and previous week 347 | 348 | function load_the_rest(storage) { 349 | // clear previous day and all non-day boxes, box1 to box20 350 | let n; 351 | for (n = 20; n > 0; n -= 1) { 352 | document.getElementById("box" + n).innerHTML = ""; 353 | } 354 | document.getElementById("day-rows").innerHTML = ""; 355 | document.getElementById("rowMonths").style.display = "none"; 356 | document.getElementById("rowWeeks").style.display = "none"; 357 | document.getElementById("rowDays").style.display = "none"; 358 | 359 | 360 | // data for previous day, current week, and previous week, 361 | // n is 0 or 1, so we do not make the current week the previous week yet 362 | // if new current week will be empty 363 | n = (storage.weekSums[0] && 364 | storage.weekSums[0].dmnsArray.length === 0 && 365 | storage.weekSums[1] && 366 | storage.weekSums[1].dmnsArray.length > 0) ? 1 : 0; 367 | 368 | let prevDay = storage.days[0], 369 | thisWeek = storage.weekSums[n], 370 | prevWeek = storage.weekSums[n + 1], 371 | prevDayTable, 372 | currentWeekTable, 373 | prevWeekTable; 374 | 375 | if (table_needed(prevDay)) { 376 | prevDayTable = make_table(prevDay, "Previous Day, " + prevDay.headerText, "box1"); 377 | } else { 378 | prevDayTable = make_empty_table("Previous Day", 379 | "This table will show a summary for the previous day. Additional " + 380 | "tables covering the past 70 days will appear below."); 381 | } 382 | document.getElementById("box1").appendChild(prevDayTable); 383 | 384 | if (table_needed(thisWeek)) { 385 | currentWeekTable = make_table(thisWeek, "Current " + thisWeek.headerText, "box2"); 386 | } else { 387 | currentWeekTable = make_empty_table("Current Week", 388 | "This table will summarize the current week so far, with sub-totals for each day. " + 389 | "(Data from today is not included in the current week until today is over.)"); 390 | } 391 | document.getElementById("box2").appendChild(currentWeekTable); 392 | 393 | if (table_needed(prevWeek)) { 394 | prevWeekTable = make_table(prevWeek, "Previous " + prevWeek.headerText, "box3"); 395 | } else { 396 | prevWeekTable = make_empty_table("Previous Week", 397 | "This table will summarize the previous week, with sub-totals for each day. " + 398 | "Tables covering the past ten weeks will appear below."); 399 | } 400 | document.getElementById("box3").appendChild(prevWeekTable); 401 | 402 | 403 | // NEXT PHASE 404 | 405 | let past7daySum = storage.past7daySum, 406 | weekSums = storage.weekSums, 407 | monthSums = storage.monthSums, 408 | days_partA = storage.days.slice(0, 8); 409 | 410 | // do not move the current month/week to previous if the new current month/week will be empty. 411 | // remove current month/week from the array if it is empty and previous month/week is not. 412 | if (!table_needed(monthSums[0]) && table_needed(monthSums[1])) { 413 | monthSums.shift(); 414 | } 415 | if (!table_needed(weekSums[0]) && table_needed(weekSums[1])) { 416 | weekSums.shift(); 417 | } 418 | 419 | let past7dayTable, 420 | currentMonthTable, 421 | prevMonthTable; 422 | 423 | if (table_needed(past7daySum)) { 424 | past7dayTable = make_table(past7daySum, past7daySum.headerText, "box4"); 425 | } else { 426 | past7dayTable = make_empty_table("Past 7 Days", 427 | "This table will summarize the past seven days, including totals " + 428 | "for the time spent browsing each day."); 429 | } 430 | if (table_needed(monthSums[0])) { 431 | currentMonthTable = make_table(monthSums[0], "Current Month, " + monthSums[0].headerText, "box5"); 432 | } else { 433 | currentMonthTable = make_empty_table("Current Month", 434 | "This table will summarize the current month so far. " + 435 | "(Data from today is not included in the current month until today is over.)."); 436 | } 437 | if (table_needed(monthSums[1])) { 438 | prevMonthTable = make_table(monthSums[1], "Previous Month, " + monthSums[1].headerText, "box6"); 439 | } else { 440 | prevMonthTable = make_empty_table("Previous Month", 441 | "This table will summarize the previous month. Eventually monthly " + 442 | "summaries will be shown for the current month and the previous five months."); 443 | } 444 | 445 | document.getElementById("box4").appendChild(past7dayTable); 446 | document.getElementById("box5").appendChild(currentMonthTable); 447 | document.getElementById("box6").appendChild(prevMonthTable); 448 | 449 | // 0 is current month, 1 is previous month, 2-5 are the rest 450 | for (n = 5; n >= 2; n -= 1) { 451 | if (table_needed(monthSums[n])) { 452 | // n is 2 to 5, --> box7 to box10 453 | let boxID = "box" + (n + 5), 454 | table = make_table(monthSums[n], monthSums[n].headerText, boxID); 455 | document.getElementById(boxID).appendChild(table); 456 | } 457 | } 458 | 459 | let currentWeekTable2; 460 | if (table_needed(weekSums[0])) { 461 | currentWeekTable2 = make_table(weekSums[0], "Current, " + weekSums[0].headerText, "box11"); 462 | } else { 463 | currentWeekTable2 = make_empty_table("More Weeks", 464 | "Tables for the past ten weeks will appear here."); 465 | document.getElementById("box12").style.display = "none"; 466 | } 467 | document.getElementById("box11").appendChild(currentWeekTable2); 468 | 469 | if (table_needed(weekSums[1])) { 470 | let prevWeekTable2 = make_table(weekSums[1], weekSums[1].headerText, "box12"); 471 | document.getElementById("box12").appendChild(prevWeekTable2); 472 | document.getElementById("box12").style.display = "block"; 473 | } 474 | 475 | for (n = 9; n >= 2; n -= 1) { 476 | if (table_needed(weekSums[n])) { 477 | // n is 2 to 9, --> box13 to box20 478 | let boxID = "box" + (n + 11), 479 | table = make_table(weekSums[n], weekSums[n].headerText, boxID); 480 | document.getElementById(boxID).appendChild(table); 481 | } 482 | } 483 | 484 | // DAYS 485 | document.getElementById("day-rows").innerHTML = ""; 486 | if (!days_partA[0]) { 487 | add_day_big_rows(1, 11, 21); 488 | document.getElementById("box21").appendChild( 489 | make_empty_table("More Days", "Tables for the past 70 days will appear here.")); 490 | } else { 491 | add_day_big_rows(days_partA.length, 11, 21); 492 | add_day_tables(days_partA, 11, 21); 493 | 494 | // show the "show all days" button or not 495 | if (storage.days[8]) { 496 | document.getElementById("day-rows").appendChild(make_more_days_button()); 497 | } 498 | } 499 | 500 | document.getElementById("rowMonths").style.display = "block"; 501 | document.getElementById("rowWeeks").style.display = "block"; 502 | document.getElementById("rowDays").style.display = "block"; 503 | document.getElementById("rowPrefs").style.display = "block"; 504 | 505 | var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(storage.days)); 506 | var dlAnchorElem = document.getElementById('downloadAnchorElem'); 507 | dlAnchorElem.setAttribute("href", dataStr); 508 | dlAnchorElem.setAttribute("download", "mind-the-time.json"); 509 | }; 510 | 511 | 512 | // BUTTONS 513 | 514 | // reload button handler 515 | async function handle_reload_click() { 516 | try { 517 | await start_load(gBackground); 518 | // flicker the current day so the user knows it was updated 519 | document.getElementById("box0").style.visibility = "hidden"; 520 | setTimeout(() => { document.getElementById("box0").style.visibility = "visible"; }, 80); 521 | } catch (e) { 522 | console.error(e); 523 | window.location.reload(); 524 | } 525 | }; 526 | 527 | document.getElementById("reloadButton").addEventListener("click", handle_reload_click, false); 528 | 529 | 530 | // INITIALIZATION 531 | 532 | // gBackground is the top level document for background.js allowing access to 533 | // functions and global variables. 534 | var gBackground; 535 | 536 | async function start_load() { 537 | try { 538 | let [bg, storage] = await Promise.all([ 539 | browser.runtime.getBackgroundPage(), 540 | browser.storage.local.get() 541 | ]); 542 | gBackground = bg; 543 | 544 | let dateNow = Date.now(); 545 | if (dateNow > storage.nextDayStartsAt) { 546 | await gBackground.start_new_day(dateNow); 547 | } 548 | load_summary(storage); 549 | return true; 550 | 551 | } catch (e) { console.error(e); } 552 | }; 553 | 554 | start_load(); 555 | 556 | 557 | // FOR TESTING NEW DAY CODE 558 | // Uncomment these functions and call test_new_day on the summary page using 559 | // browser dev tools. 560 | /* 561 | async function test_new_day_2(dnow) { 562 | await gBackground.maybe_clock_off(gBackground.gState.timing.stamp, gBackground.gState.timing.domain); 563 | await handle_reload_click(); 564 | await gBackground.start_new_day(dnow); 565 | test_new_day(dnow); 566 | } 567 | 568 | function test_new_day(dnow = Date.now()) { 569 | gBackground.pre_clock_on_2(new URL(["http://mozilla.org", "http://gnu.org", "http://ubuntu.org"][dnow % 3])); 570 | setTimeout(test_new_day_2.bind(null, dnow + 86400001), 2000); 571 | }; 572 | */ 573 | --------------------------------------------------------------------------------