Pomodoro WIdget
pomodoro timer widget

Code Snippet

1// ==============================================
2// 🍅 POMODORO BLOCKER (Widget SDK)
3// ==============================================
4
5// Paste your full base64 string back here!
6
7const logoBase64 = "data:image/png;base64,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";
8let isWidgetOpen = false;
9let pomoInterval = null;
10
11// 1. Load state from storage on boot!
12let state = await storage.get("pomo_state") || {
13    mode: "idle", // "idle" | "focus" | "break"
14    targetTime: 0
15};
16
17let displayTime = "00:00";
18
19async function saveState() {
20    await storage.set("pomo_state", state);
21}
22
23function formatTime(ms) {
24    if (ms < 0) ms = 0;
25    const mins = Math.floor(ms / 60000).toString().padStart(2, '0');
26    const secs = Math.floor((ms % 60000) / 1000).toString().padStart(2, '0');
27    return `${mins}:${secs}`;
28}
29
30// 2. Inject a gorgeous full-screen overlay into the webpage
31async function injectBlocker() {
32    await page.mutate([{ selector: "#pomo-blocker", action: "remove" }]); // Clear existing
33    
34    if (state.mode === "focus" || state.mode === "break") {
35        const isFocus = state.mode === "focus";
36        const bgColor = isFocus ? "rgba(230, 57, 70, 0.95)" : "rgba(42, 157, 143, 0.95)";
37        const title = isFocus ? "Focus Time" : "Break Time";
38        const subtitle = isFocus ? "Stop scrolling and get back to work!" : "Relax your eyes and stretch!";
39
40        const blockerHtml = `
41        <div id="pomo-blocker" style="position:fixed; top:0; left:0; width:100vw; height:100vh; background:${bgColor}; color:white; z-index:999999999; display:flex; flex-direction:column; align-items:center; justify-content:center; font-family:sans-serif; backdrop-filter: blur(10px);">
42            <h1 style="font-size: 4rem; margin-bottom: 10px; font-weight: 800;">${title}</h1>
43            <p style="font-size: 1.5rem; margin-bottom: 40px; opacity: 0.9;">${subtitle}</p>
44            <h2 id="pomo-time-display" style="font-size: 10rem; font-weight:bold; margin:0; font-variant-numeric: tabular-nums;">${displayTime}</h2>
45        </div>
46        `;
47        
48        // Appends to the page body, blocking everything behind it safely!
49        await page.mutate([{ selector: "body", action: "append", value: blockerHtml }]);
50    }
51}
52
53async function tick() {
54    if (state.mode === "idle") return;
55    
56    const now = Date.now();
57    const remaining = state.targetTime - now;
58    
59    // Timer finished!
60    if (remaining <= 0) {
61        const prevMode = state.mode;
62        state.mode = "idle";
63        state.targetTime = 0;
64        await saveState();
65        
66        displayTime = "00:00";
67        render();
68        await page.mutate([{ selector: "#pomo-blocker", action: "remove" }]);
69        
70        if (prevMode === "focus") notify.show("Focus session complete! Take a break.", "success");
71        else notify.show("Break is over! Ready to focus?", "success");
72        return;
73    }
74    
75    displayTime = formatTime(remaining);
76    render(); // Update widget UI
77    
78    // 3. Update the massive on-screen timer without re-rendering the whole overlay!
79    await page.mutate([{ selector: "#pomo-time-display", action: "replaceHtml", value: displayTime }]);
80}
81
82function startTimer(minutes, mode) {
83    state.mode = mode;
84    state.targetTime = Date.now() + (minutes * 60 * 1000);
85    saveState();
86    displayTime = formatTime(minutes * 60 * 1000);
87    injectBlocker();
88    render();
89}
90
91/** @returns {WidgetComponent[]} */
92function mainView() {
93    const isRunning = state.mode !== "idle";
94    const statusText = state.mode === "focus" ? "FOCUS MODE" : (state.mode === "break" ? "BREAK MODE" : "READY");
95    const statusColor = state.mode === "focus" ? "error" : (state.mode === "break" ? "success" : "muted");
96
97    return [
98        {
99            type: "row", align: "center",
100            components: [
101                { type: "image", src: logoBase64, width: "24px", height: "24px", rounded: true },
102                { type: "text", text: "", flex: 1 },
103                { type: "text", text: "Pomodoro", size: "lg", weight: "bold" },
104                { type: "text", text: "   ", size: "lg" } 
105            ]
106        },
107        { type: "divider" },
108        {
109            type: "column", align: "center", gap: "sm", padding: "lg", bg: "card2", rounded: true,
110            components: [
111                { type: "text", text: statusText, size: "sm", weight: "bold", color: statusColor },
112                { type: "text", text: isRunning ? displayTime : "25:00", size: "xl", weight: "bold" },
113            ]
114        },
115        { type: "divider" },
116        ...(isRunning ? [
117            { type: "button", id: "btnStop", text: "Stop Timer", variant: "ghost" }
118        ] : [
119            {
120                type: "row", gap: "sm",
121                components: [
122                    { type: "button", id: "btnFocus", text: "Focus (25m)", variant: "primary", flex: 1 },
123                    { type: "button", id: "btnBreak", text: "Break (5m)", variant: "default", flex: 1 }
124                ]
125            }
126        ])
127    ];
128}
129
130function render() {
131    if (isWidgetOpen) widget.update(mainView());
132}
133
134// --- Listeners ---
135widget.on("btnFocus", "click", () => startTimer(25, "focus"));
136widget.on("btnBreak", "click", () => startTimer(5, "break"));
137widget.on("btnStop", "click", async () => {
138    state.mode = "idle";
139    state.targetTime = 0;
140    await saveState();
141    await page.mutate([{ selector: "#pomo-blocker", action: "remove" }]);
142    render();
143});
144
145widget.on("__widget_closed__", "close", () => { 
146    isWidgetOpen = false; 
147});
148
149// --- Boot ---
150// 4. Catch up if timer was already running in storage!
151if (state.mode !== "idle") {
152    if (Date.now() < state.targetTime) {
153        displayTime = formatTime(state.targetTime - Date.now());
154        injectBlocker(); // Restore the blocker overlay on fresh boot!
155    } else {
156        state.mode = "idle";
157        state.targetTime = 0;
158        saveState();
159    }
160}
161
162widget.show(mainView(), "bottom-right");
163isWidgetOpen = true;
164pomoInterval = setInterval(tick, 1000);
165
166// Wait patiently for the user to explicitly close the widget UI
167await widget.waitForEvent("__widget_closed__", "close"); 
168
169// --- GRACEFUL SHUTDOWN ---
170isWidgetOpen = false;
171if (pomoInterval) clearInterval(pomoInterval);
172
173// IMPORTANT: We must remove the blocker when the script shuts down. 
174// Otherwise, it freezes permanently on the screen because the sandbox is dead!
175// Don't worry—the timer state is safe in `storage`, so if you run the widget again, 
176// it will perfectly resume the blocker with the correct remaining time!
177await page.mutate([{ selector: "#pomo-blocker", action: "remove" }]);
178
Submitted on 7/11/2026