Digital Wellbeing
A collection of 2 actions bundled together.
Code Snippet
1// ==========================================
2// ACTION GROUP (2 Actions)
3// ==========================================
4
5// ------------------------------------------
6// Action: Digital Wellbeing/Tracker
7// Description: The background tracker that tracks your website usage.
8// ------------------------------------------
9
10// ==============================================
11// ⏱️ DIGITAL WELLBEING TRACKER (Background) — FIXED
12// ==============================================
13
14const currentTab = await browser.tabs.getCurrent();
15let currentDomain = new URL(currentTab.url).hostname;
16let today = new Date().toISOString().split('T')[0];
17
18let currentUsageSecs = 0;
19let sessionStartTime = null;
20let currentLimitMins = 0;
21let isBlocked = false;
22
23const FLUSH_INTERVAL_SECS = 5;
24
25function todayString() {
26 return new Date().toISOString().split('T')[0];
27}
28
29async function flushSession(domain, dateKey, elapsedSecs) {
30 if (elapsedSecs <= 0) return null;
31 const freshData = await storage.update("dw_data", (d) => {
32 if (!d) d = {};
33 if (!d[dateKey]) d[dateKey] = { usage: {} };
34 if (!d[dateKey].usage) d[dateKey].usage = {};
35 d[dateKey].usage[domain] = (d[dateKey].usage[domain] || 0) + elapsedSecs;
36 return d;
37 });
38 return freshData;
39}
40
41async function showBlocker() {
42 isBlocked = true;
43
44 // Save final seconds safely
45 if (sessionStartTime) {
46 const elapsedSecs = (Date.now() - sessionStartTime) / 1000;
47 sessionStartTime = null;
48 await flushSession(currentDomain, today, elapsedSecs);
49 }
50
51 // Completely Native HTML Blocker Overlay
52 const blockerHtml = `
53 <div id="dw-blocker" style="position:fixed; top:0; left:0; width:100vw; height:100vh; background:rgba(5, 5, 5, 0.98); color:white; z-index:999999998; display:flex; flex-direction:column; align-items:center; justify-content:center; font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; backdrop-filter: blur(20px);">
54 <h1 style="font-size: 4rem; color: #FF3366; margin-bottom: 10px; font-weight: 800; letter-spacing: -1px;">Limit Reached</h1>
55 <p style="font-size: 1.5rem; margin-bottom: 40px; opacity: 0.9;">You've hit your daily allowance of ${currentLimitMins} minutes for ${currentDomain}.</p>
56
57 <button id="btn_add_time_overlay" style="background:#FFF; color:#000; border:none; padding:12px 32px; font-size:16px; font-weight:700; border-radius:8px; cursor:pointer; margin-bottom: 16px;">
58 + Add 10 Minutes
59 </button>
60
61 <div style="color:#888; font-size:14px;">(Page refresh required)</div>
62 </div>
63 `;
64
65 // Inject via the new Page SDK batch mutation engine
66 await page.mutate([
67 { selector: "#dw-blocker", action: "remove" },
68 { selector: "body", action: "append", value: blockerHtml }
69 ]);
70
71 // Natively mute the tab at the browser level!
72 await browser.tabs.update(currentTab.id, { muted: true });
73
74 // Wait for the native HTML button to be clicked using page SDK events
75 await new Promise(async (resolve) => {
76 await page.mutate([
77 { selector: "#btn_add_time_overlay", action: "listen", event: "click", id: "btn_click" }
78 ], (e) => {
79 if (e.id === "btn_click") resolve();
80 });
81 });
82
83 // Add time to storage upon click — scoped to TODAY's date
84 await storage.update("dw_data", (d) => {
85 if (!d) d = {};
86 if (!d.limits) d.limits = {};
87 if (!d.limits[today]) d.limits[today] = {};
88 d.limits[today][currentDomain] = (d.limits[today][currentDomain] || 0) + 10;
89 return d;
90 });
91
92 const successHtml = `
93 <h1 style="font-size: 3rem; color: #C8FF00; margin-bottom: 16px; font-weight: 800; letter-spacing: -1px;">Time Added</h1>
94 <p style="font-size: 1.2rem; color: #888; margin-bottom: 32px;">We've added 10 minutes to your limit.</p>
95 <p style="font-size: 1.2rem; color: #FFF; font-weight: 600;">Please refresh the page to continue browsing.</p>
96 `;
97
98 // Swap to success screen natively
99 await page.mutate([
100 { selector: "#dw-blocker", action: "replaceHtml", value: successHtml }
101 ]);
102}
103
104async function loadLimitAndUsage(domain, dateKey) {
105 const data = await storage.get("dw_data");
106 let usageSecs = 0;
107 let limitMins = 0;
108
109 if (data) {
110 if (data[dateKey] && data[dateKey].usage) {
111 usageSecs = data[dateKey].usage[domain] || 0;
112 }
113
114 // 1. Get base global limit from the UI Dashboard
115 if (data.limits && data.limits[domain] !== undefined) {
116 limitMins = data.limits[domain];
117 }
118
119 // 2. Add daily bonus limit on top of base limit
120 if (data.limits && data.limits[dateKey] && data.limits[dateKey][domain]) {
121 limitMins += data.limits[dateKey][domain];
122 }
123 }
124
125 return { usageSecs, limitMins };
126}
127
128{
129 const initial = await loadLimitAndUsage(currentDomain, today);
130 currentUsageSecs = initial.usageSecs;
131 currentLimitMins = initial.limitMins;
132}
133
134if (currentLimitMins > 0 && currentUsageSecs >= currentLimitMins * 60) {
135 await showBlocker();
136} else {
137 if (await page.hasFocus()) sessionStartTime = Date.now();
138}
139
140async function tick() {
141 if (isBlocked) return;
142
143 try {
144 const liveTab = await browser.tabs.getCurrent();
145 const liveDomain = new URL(liveTab.url).hostname;
146 const liveDate = todayString();
147
148 const domainChanged = liveDomain !== currentDomain;
149 const dateChanged = liveDate !== today;
150
151 if (domainChanged || dateChanged) {
152 if (sessionStartTime) {
153 const elapsedSecs = (Date.now() - sessionStartTime) / 1000;
154 await flushSession(currentDomain, today, elapsedSecs);
155 }
156
157 currentDomain = liveDomain;
158 today = liveDate;
159 sessionStartTime = (await page.hasFocus()) ? Date.now() : null;
160
161 const fresh = await loadLimitAndUsage(currentDomain, today);
162 currentUsageSecs = fresh.usageSecs;
163 currentLimitMins = fresh.limitMins;
164
165 console.log(`[Wellbeing Tracker] Switched tracking to ${currentDomain} (${today}).`);
166 }
167
168 const isFocused = await page.hasFocus();
169
170 if (isFocused) {
171 if (!sessionStartTime) sessionStartTime = Date.now();
172
173 const elapsedSecs = (Date.now() - sessionStartTime) / 1000;
174 const realtimeSecs = currentUsageSecs + elapsedSecs;
175
176 if (currentLimitMins > 0 && realtimeSecs >= currentLimitMins * 60) {
177 console.log(`[Wellbeing Tracker] Limit reached for ${currentDomain}! Blocking...`);
178 sessionStartTime = null;
179 await flushSession(currentDomain, today, elapsedSecs);
180 await showBlocker();
181 return;
182 }
183
184 if (elapsedSecs >= FLUSH_INTERVAL_SECS) {
185 sessionStartTime = Date.now();
186 const freshData = await flushSession(currentDomain, today, elapsedSecs);
187
188 if (freshData) {
189 currentUsageSecs = freshData[today].usage[currentDomain];
190
191 // Recompute total limit (base + daily bonus) on flush
192 let updatedLimit = 0;
193 if (freshData.limits && freshData.limits[currentDomain] !== undefined) {
194 updatedLimit = freshData.limits[currentDomain];
195 }
196 if (freshData.limits && freshData.limits[today] && freshData.limits[today][currentDomain]) {
197 updatedLimit += freshData.limits[today][currentDomain];
198 }
199
200 // If no limit is found in fresh data, preserve the memory state (edge-case fallback)
201 if (updatedLimit > 0) currentLimitMins = updatedLimit;
202 }
203
204 console.log(`[Wellbeing Tracker] Saved chunk for ${currentDomain}. Total today: ${Math.round(currentUsageSecs)}s`);
205 }
206 } else {
207 if (sessionStartTime) {
208 const elapsedSecs = (Date.now() - sessionStartTime) / 1000;
209 sessionStartTime = null;
210 await flushSession(currentDomain, today, elapsedSecs);
211 }
212 }
213 } catch (err) {
214 console.log(`[Wellbeing Tracker] Tick error (continuing): ${err && err.message}`);
215 } finally {
216 if (!isBlocked) setTimeout(tick, 1000);
217 }
218}
219
220setTimeout(tick, 1000);
221
222await new Promise(() => {});
223
224
225
226// ------------------------------------------
227// Action: Digital Wellbeing/Dashboard
228// Description: This is the dashboard to show the website usages, to be used in conjunction with the wellbeing tracker
229// ------------------------------------------
230
231// ==============================================
232// 📊 DIGITAL WELLBEING DASHBOARD (Manual UI)
233// ==============================================
234
235const currentDomain = new URL(selection.pageUrl).hostname;
236let today = new Date().toISOString().split('T')[0];
237
238function formatTime(secs) {
239 const mins = Math.floor(secs / 60);
240 if (mins === 0) return "< 1m";
241 if (mins >= 60) {
242 const hrs = Math.floor(mins / 60);
243 const remMins = mins % 60;
244 return `${hrs}h ${remMins}m`;
245 }
246 return `${mins}m`;
247}
248
249async function renderDashboard(isUpdate = false) {
250 let d = await storage.get("dw_data") || {};
251 if (!d[today]) d[today] = { usage: {} };
252 if (!d.limits) d.limits = {};
253
254 const allDomains = Object.keys(d[today].usage);
255 allDomains.sort((a, b) => (d[today].usage[b] || 0) - (d[today].usage[a] || 0));
256
257 const colors = ['#C8FF00', '#00E5FF', '#FF3366', '#9D00FF', '#FF9900'];
258 let listHtml = "";
259 let validUsageSecs = [];
260 let otherSecs = 0;
261 let otherCount = 0;
262 let totalUsageSecs = 0;
263
264 allDomains.forEach((dom, idx) => {
265 let uSecs = d[today].usage[dom] || 0;
266 if (uSecs === 0) return;
267 totalUsageSecs += uSecs;
268
269 if (idx < 4) {
270 let color = colors[idx];
271 validUsageSecs.push(uSecs);
272 listHtml += `
273 <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #2A2A2A;">
274 <div style="display: flex; align-items: center; gap: 8px; overflow: hidden;">
275 <div style="width: 10px; height: 10px; border-radius: 50%; background: ${color}; flex-shrink: 0;"></div>
276 <span style="color: #FFF; font-weight: 500; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${dom === currentDomain ? `<b>${dom}</b>` : dom}</span>
277 </div>
278 <span style="color: #888; font-size: 13px; font-weight: 600; flex-shrink: 0; margin-left: 12px;">${formatTime(uSecs)}</span>
279 </div>
280 `;
281 } else {
282 otherSecs += uSecs;
283 otherCount++;
284 }
285 });
286
287 if (otherCount > 0) {
288 if (otherSecs > 0) validUsageSecs.push(otherSecs);
289 listHtml += `
290 <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #2A2A2A;">
291 <div style="display: flex; align-items: center; gap: 8px; overflow: hidden;">
292 <div style="width: 10px; height: 10px; border-radius: 50%; background: ${colors[4]}; flex-shrink: 0;"></div>
293 <span style="color: #FFF; font-weight: 500; font-size: 13px;">Others (${otherCount})</span>
294 </div>
295 <span style="color: #888; font-size: 13px; font-weight: 600; flex-shrink: 0; margin-left: 12px;">${formatTime(otherSecs)}</span>
296 </div>
297 `;
298 }
299
300 // ADDED: Current Website block styled exactly like the list items above
301 const currentSiteUsageSecs = d[today].usage[currentDomain] || 0;
302 listHtml += `
303 <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-top: 1px solid rgba(255, 255, 255, 0.1); margin-top: auto;">
304 <div style="display: flex; align-items: center; gap: 8px; overflow: hidden;">
305 <div style="width: 10px; height: 10px; border-radius: 50%; background: #C8FF00; flex-shrink: 0; opacity: 0.7;"></div>
306 <span style="color: #FFF; font-weight: 500; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">Current: <b>${currentDomain}</b></span>
307 </div>
308 <span style="color: #888; font-size: 13px; font-weight: 600; flex-shrink: 0; margin-left: 12px;">${formatTime(currentSiteUsageSecs)}</span>
309 </div>
310 `;
311
312 const radius = 60;
313 const strokeWidth = 14;
314 const circumference = 2 * Math.PI * radius;
315 let currentOffset = 0;
316 let svgSlices = '';
317
318 if (totalUsageSecs === 0) {
319 svgSlices = `<circle cx="70" cy="70" r="${radius}" fill="none" stroke="#2A2A2A" stroke-width="${strokeWidth}" />`;
320 } else {
321 validUsageSecs.forEach((val, i) => {
322 if (val === 0) return;
323 const slicePct = val / totalUsageSecs;
324 const sliceLength = slicePct * circumference;
325 const dashLength = Math.max(0, sliceLength - (validUsageSecs.length > 1 ? 4 : 0));
326
327 svgSlices += `<circle cx="70" cy="70" r="${radius}" fill="none" stroke="${colors[i]}" stroke-width="${strokeWidth}" stroke-dasharray="${dashLength} ${circumference}" stroke-dashoffset="${-currentOffset}" stroke-linecap="round" />`;
328
329 currentOffset += sliceLength;
330 });
331 }
332
333 const chartHtml = `
334 <div style="display: flex; flex-direction: column; padding: 0 4px 12px 4px;">
335 <h2 style="margin: -2px 0 16px 0; font-size: 18px; color: #FFF; letter-spacing: -0.5px; font-weight: 700; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">Digital Wellbeing</h2>
336
337 <div style="display: flex; gap: 24px; align-items: flex-start; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
338 <div style="position: relative; width: 140px; height: 140px; flex-shrink: 0;">
339 <svg width="140" height="140" viewBox="0 0 140 140">
340 <g transform="rotate(-90 70 70)">
341 ${svgSlices}
342 </g>
343 </svg>
344 <div style="position: absolute; top:0; left:0; width:100%; height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center;">
345 <span style="font-size: 18px; font-weight: 700; color: #FFF; line-height: 1;">${formatTime(totalUsageSecs)}</span>
346 <span style="font-size: 10px; color: #888; font-weight: 600; margin-top: 4px; letter-spacing: 0.5px;">TOTAL</span>
347 </div>
348 </div>
349 <div style="flex: 1; display: flex; flex-direction: column; max-height: 220px; overflow-y: auto; padding-right: 8px;">
350 ${listHtml || '<div style="color:#888; font-size:13px; padding:10px 0;">No usage yet today.</div>'}
351 </div>
352 </div>
353 </div>
354 `;
355
356 let baseLimit = d.limits[currentDomain] || 0;
357 let bonusLimit = (d.limits[today] && d.limits[today][currentDomain]) ? d.limits[today][currentDomain] : 0;
358 let currentLimitMins = baseLimit + bonusLimit;
359
360 const components = [
361 {
362 type: "column",
363 gap: "sm",
364 components: [
365 { type: "iframe", srcDoc: chartHtml, height: "260px" },
366 {
367 type: "column",
368 gap: "sm",
369 components: [
370 { type: "text", text: "Daily Limit for this site:", size: "sm", color: "muted" },
371 {
372 type: "row",
373 gap: "sm",
374 components: [
375 { type: "input", id: "limit_input", placeholder: "Mins", value: currentLimitMins > 0 ? currentLimitMins.toString() : "" },
376 { type: "button", id: "btn_save_limit", text: "Save", variant: "primary", size: "md" }
377 ]
378 },
379 { type: "divider" },
380 { type: "button", id: "btn_clear_data", text: "Clear All Data", variant: "danger", size: "sm" }
381 ]
382 }
383 ]
384 }
385 ];
386
387 if (isUpdate) {
388 await widget.update(components);
389 } else {
390 await widget.show(components, "bottom-right");
391 }
392}
393
394widget.on("btn_save_limit", "click", async (reqData) => {
395 const newLimit = parseInt(reqData.limit_input || "0") || 0;
396
397 await storage.update("dw_data", (d) => {
398 if (!d) d = { limits: {} };
399 if (!d.limits) d.limits = {};
400
401 d.limits[currentDomain] = newLimit;
402
403 if (d.limits[today]) {
404 d.limits[today][currentDomain] = 0;
405 }
406
407 return d;
408 });
409
410 await notify.show(`Limit set to ${newLimit} mins!`, "success");
411 await renderDashboard(true);
412});
413
414widget.on("btn_clear_data", "click", async () => {
415 const isConfirmed = await input.prompt({
416 type: "confirm",
417 title: "Are you sure you want to clear all tracked data? This cannot be undone."
418 });
419
420 if (isConfirmed) {
421 await storage.delete("dw_data");
422 await notify.show("All tracked data cleared.", "success");
423 await renderDashboard(true);
424 }
425});
426
427// Render the UI initially
428await renderDashboard(false);
429await widget.waitForEvent("FOREVER", "click");
430Submitted on 7/13/2026