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 |
--------------------------------------------------------------------------------