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" }]);
178Submitted on 7/11/2026