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");
430
Submitted on 7/13/2026