Marketplace
Supercharge your workflow
Discover and install custom actions for the tee extension. Browse automation tools built by the community or publish your own powerful workflows.
// This is an action group containing 2 actions.
// Install to import the entire group:
// - Codeforces Blocker / UI
// - Codeforces Blocker / backendDigital Wellbeing
byAnonymous
A collection of 2 actions bundled together.
// This is an action group containing 2 actions.
// Install to import the entire group:
// - Digital Wellbeing/Tracker
// - Digital Wellbeing/DashboardWeblock
byAnonymous
Applock but for websites
// ==========================================
// 🔒 WEBSITE APPLOCK SCRIPT (Ultra-Minimal)
// ==========================================
// Add the domains you want to lock down right here!
const TARGET_DOMAINS = [
"youtube.com"
];
// Rely on the guaranteed selection context instead of async tabs
const currentDomain = new URL(selection.pageUrl).hostname;
// Check if the current domain matches (handles subdomains like www.youtube.com seamlessly)
const shouldLock = TARGET_DOMAINS.some(domain =>
currentDomain === domain || currentDomain.endsWith("." + domain)
);
// If it's not a target site, exit immediately without doing anything
if (!shouldLock) {
return;
}
// 1. Instantly paralyze and blur the page!
const cssId = await page.injectCSS(`
html, body { overflow: hidden !important; }
body > * {
filter: blur(25px) grayscale(100%) !important;
pointer-events: none !important;
user-select: none !important;
opacity: 0.1 !important;
}
body { background: #000 !important; }
`);
try {
let masterPin = await storage.get("applock_pin");
// 2. FIRST-TIME SETUP
while (!masterPin) {
const setupPin = await input.prompt({
title: "Setup AppLock: Create a 4-digit PIN",
placeholder: "••••",
position: "center"
});
// If they hit Esc or click outside, block them out permanently (until refresh)
if (setupPin === null || setupPin === undefined) {
return;
}
if (setupPin.trim().length >= 4) {
masterPin = setupPin.trim();
await storage.set("applock_pin", masterPin);
break;
}
}
// 3. SNAPPY VERIFICATION LOOP
while (true) {
const enteredPin = await input.prompt({
title: `Unlock ${currentDomain}`,
placeholder: "Enter PIN...",
position: "center"
});
// If they hit Esc or click outside the unlock prompt, block them out permanently (until refresh)
if (enteredPin === null || enteredPin === undefined) {
return;
}
if (enteredPin === masterPin) {
// Correct PIN! The loop breaks.
break;
}
}
} finally {
// 4. Instantly strip the blur CSS when they unlock!
await page.removeCSS(cssId);
}
Test Suite
byAnonymous
Tests all sdk methods to make sure they are operational
// ==========================================
// 1. CONSOLE SDK
// ==========================================
console.log("🚀 Starting the Ultimate Mega SDK Integration Test...");
console.warn("Please do not interact with the page while the test runs.");
console.error("This is a simulated error log to test console.error.");
// ==========================================
// 2. SELECTION (Global Context)
// ==========================================
console.log("Context Data:", {
text: selection.text,
url: selection.pageUrl,
title: selection.pageTitle
});
// ==========================================
// 3. NOTIFY SDK
// ==========================================
await notify.show("Starting Ultimate Mega SDK Test...", "info");
await new Promise(r => setTimeout(r, 500));
await notify.show("Testing all methods sequentially...", "warning");
await new Promise(r => setTimeout(r, 500));
// ==========================================
// 4. SPINNER SDK
// ==========================================
await spinner.show("Testing Storage SDK...");
// ==========================================
// 5. STORAGE SDK
// ==========================================
await storage.set("mega_test_key", { count: 1 });
let storageResult = await storage.get("mega_test_key");
await storage.update("mega_test_key", (val) => ({ count: val.count + 1 }));
const updatedStorage = await storage.get("mega_test_key");
const allKeys = await storage.keys();
await storage.delete("mega_test_key");
console.log("Storage Test:", (updatedStorage.count === 2 && allKeys.includes("mega_test_key")) ? "✅" : "❌");
// ==========================================
// 6. CLIPBOARD SDK
// ==========================================
await spinner.update("Testing Clipboard SDK...");
const originalClipboard = await clipboard.read().catch(() => "");
await clipboard.write("tee_test_string");
const clipboardResult = await clipboard.read();
await clipboard.write(originalClipboard);
console.log("Clipboard Test:", clipboardResult === "tee_test_string" ? "✅" : "❌");
// ==========================================
// 7. HTTP SDK
// ==========================================
await spinner.update("Testing HTTP SDK...");
const httpResult = await http.fetch("https://jsonplaceholder.typicode.com/todos/1", { method: "GET" });
console.log("HTTP Test:", httpResult?.id === 1 ? "✅" : "❌");
// ==========================================
// 8. AI SDK
// ==========================================
await spinner.update("Testing AI SDK (Prompt, Chat, JSON)...");
const aiPromptRes = await ai.prompt("What is 2+2? Reply with ONLY the number.", "You are a calculator.");
const aiChatRes = await ai.chat([
{ role: "system", content: "You are a pirate." },
{ role: "user", content: "Say the word 'Ahoy' and nothing else." }
]);
const aiJsonRes = await ai.json("Extract the name from: John Smith", "Output JSON with a 'name' field");
console.log("AI Test (Prompt):", aiPromptRes ? "✅" : "❌");
console.log("AI Test (Chat):", aiChatRes ? "✅" : "❌");
console.log("AI Test (JSON):", (typeof aiJsonRes === 'string' ? JSON.parse(aiJsonRes).name : aiJsonRes.name) ? "✅" : "❌");
// ==========================================
// 9. NOTES SDK
// ==========================================
await spinner.update("Testing Notes SDK...");
const newNote = await notes.create({ title: "Mega Test Note", content: "Temporary note." });
let fetchedNote = await notes.get(newNote.id);
await notes.update(newNote.id, { content: "Updated note content." });
const allNotes = await notes.list();
await notes.delete(newNote.id);
console.log("Notes Test:", (fetchedNote?.id === newNote.id && allNotes.length > 0) ? "✅" : "❌");
// ==========================================
// 10. PAGE SDK
// ==========================================
await spinner.update("Testing Page SDK...");
const hasFocus = await page.hasFocus();
const pageText = await page.getText();
const pageSelection = await page.getSelection();
const metadata = await page.getMetadata();
await page.scrollTo(0, 100);
await new Promise(r => setTimeout(r, 500));
await page.scrollTo(0, 0);
// CSS Injection Test
const cssId = await page.injectCSS("body { border: 5px solid red !important; }");
await new Promise(r => setTimeout(r, 500));
await page.removeCSS(cssId);
// Text Node Patching & Highlighting
await page.highlightSelection("rgba(200, 255, 0, 0.3)");
const textNodes = await page.getVisibleTextNodes({ limit: 1 });
if (textNodes && textNodes.length > 0) {
await page.patchTextNodes([{ id: textNodes[0].id, text: "TEE WAS HERE!" }]);
}
const screenshot = await page.screenshot();
// Skip mutate/click/fill/extract as they require specific DOM structures that might not exist on the current page.
console.log("Page SDK Base Tests:", (pageText && screenshot) ? "✅" : "❌");
// ==========================================
// 11. BROWSER SDK
// ==========================================
await spinner.update("Testing Browser SDK...");
// Tabs
const activeTabs = await browser.tabs.query({ active: true, currentWindow: true });
const currentTab = await browser.tabs.getCurrent();
const newTab = await browser.tabs.create({ url: "https://example.com", active: false });
await browser.tabs.update(newTab.id, { muted: true });
await browser.tabs.remove(newTab.id);
// Bookmarks & History (May return empty depending on browser state, just testing they don't crash)
const tree = await browser.bookmarks.getTree();
const newBookmark = await browser.bookmarks.create({ title: "Tee Test", url: "https://example.com" });
await browser.bookmarks.remove(newBookmark.id).catch(() => {}); // cleanup if supported
const history = await browser.history.search({ text: "google", maxResults: 1 });
console.log("Browser SDK Tests:", activeTabs.length > 0 ? "✅" : "❌");
// ==========================================
// 12. INPUT SDK
// ==========================================
await spinner.hide();
const selectedOption = await input.select("Choose a difficulty level:", ["Easy", "Medium", "Hard", "Ultimate Mega"]);
console.log("Input SDK (Select):", selectedOption);
const userInput = await input.text("Enter your name to generate the final report:", "Developer");
console.log("Input SDK (Text):", userInput);
const userConfirmed = await input.confirm("Would you like to render the final results widget?");
console.log("Input SDK (Confirm):", userConfirmed);
if (!userConfirmed) {
await notify.show("Test aborted by user.", "error");
return;
}
// ==========================================
// 13. POPUP SDK (Toasts)
// ==========================================
await popup.show("Generating Widget UI...");
await new Promise(r => setTimeout(r, 1000));
await popup.html("<b>Applying themes...</b>");
await new Promise(r => setTimeout(r, 1000));
await popup.success("Widget Ready!");
await popup.error("Just kidding, this is a simulated error toast to test popup.error.");
// ==========================================
// 14. WIDGET SDK
// ==========================================
await widget.show([
{ type: "text", text: "Ultimate Mega Test Results", size: "xl", weight: "bold", color: "accent" },
{ type: "text", text: `Triggered by: ${userInput} on ${selection.pageTitle}`, size: "sm", color: "muted" },
{ type: "divider" },
{ type: "input", id: "test-input", placeholder: "Type here to test widget events..." },
{ type: "text", id: "event-logger", text: "Waiting for input...", size: "sm", color: "warning" },
{ type: "divider" },
{ type: "button", id: "btn-finish", text: "Acknowledge & Close", variant: "primary" }
], "center", { theme: "dark" });
// Test widget.on (Live event listeners)
widget.on("test-input", "change", async (data) => {
console.log("Widget Input Changed:", data);
const currentValue = await widget.getValue("test-input");
await widget.update([
{ type: "text", text: "Ultimate Mega Test Results", size: "xl", weight: "bold", color: "accent" },
{ type: "text", text: `Triggered by: ${userInput} on ${selection.pageTitle}`, size: "sm", color: "muted" },
{ type: "divider" },
{ type: "input", id: "test-input", placeholder: "Type here to test widget events...", value: currentValue },
{ type: "text", id: "event-logger", text: `You typed: ${currentValue}`, size: "sm", color: "success" },
{ type: "divider" },
{ type: "button", id: "btn-finish", text: "Acknowledge & Close", variant: "primary" }
]);
});
// Wait for the user to click the finish button
await widget.waitForEvent("btn-finish", "click");
// Cleanup
await widget.hide();
await popup.success("All systems operational!");
Weather Box
byAnonymous
Widget to track weather across cities
let view = "dashboard";
let savedCities = await storage.get("weather_cities_v3") || [
{ name: "London", lat: 51.5085, lon: -0.1257, state: "England", country: "United Kingdom" },
{ name: "New York", lat: 40.7143, lon: -74.006, state: "New York", country: "United States" }
];
let searchResults = [];
let isSearching = false;
let currentQuery = "";
let searchTimeout = null;
let weatherCache = {};
// Highly accurate, Day/Night aware Emoji mapping!
function getWeatherEmoji(code, isDay = 1) {
const clear = isDay ? "☀️" : "🌙";
if (code === 0) return clear; // Clear sky
if (code === 1) return isDay ? "🌤️" : "☁️"; // Mainly clear
if (code === 2) return isDay ? "⛅" : "☁️"; // Partly cloudy
if (code === 3) return "☁️"; // Overcast
if (code >= 45 && code <= 48) return "🌫️"; // Fog
if (code === 51 || code === 53 || code === 55) return "🌧️"; // Drizzle
if (code === 56 || code === 57) return "🌨️"; // Freezing Drizzle
if (code === 61 || code === 63 || code === 65) return "🌧️"; // Rain
if (code === 66 || code === 67) return "🌨️"; // Freezing Rain
if (code === 71 || code === 73 || code === 75) return "❄️"; // Snow fall
if (code === 77) return "❄️"; // Snow grains
if (code === 80 || code === 81 || code === 82) return isDay ? "🌦️" : "🌧️"; // Rain showers
if (code === 85 || code === 86) return "🌨️"; // Snow showers
if (code >= 95 && code <= 99) return "⛈️"; // Thunderstorm
return "🌡️";
}
async function renderDashboard(isUpdate = false) {
const cityCards = savedCities.map((city, index) => {
const cached = weatherCache[city.name];
const temp = cached ? `${cached.temp}°C` : "-";
const emoji = cached ? cached.emoji : "-";
return {
type: "row",
bg: "card2",
rounded: true,
padding: "md",
align: "center",
justify: "between",
components: [
{
type: "column",
gap: "sm",
align: "start",
components: [
{ type: "row", gap: "md", padding: "sm", align: "center", justify: "start", components: [
{ type: "text", text: emoji, size: "md" },
{ type: "text", text: city.name, weight: "bold", size: "md" },
]},
]
},
{
type: "row",
gap: "md",
align: "center",
justify: "end",
padding: "sm",
components: [
{ type: "text", text: temp, weight: "bold", size: "md" },
{ type: "button", id: `del_${index}`, icon: "X", text: "", variant: "ghost", size: "md" }
]
}
]
};
});
const logoBase64 = "data:image/png;base64,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";
const componentTree = [
{
type: "column",
gap: "sm",
bg: "card",
padding: "md",
rounded: true,
components: [
{ type: "row", justify: "between", align: "center", components: [
{ type: "image", src: logoBase64, width: "24px", height: "24px", rounded: true },
{ type: "text", text: "Weatherbox", size: "md", weight: "bold" }
]},
{ type: "divider" },
{
type: "column",
gap: "sm",
maxHeight: "250px",
scroll: true,
components: cityCards.length > 0 ? cityCards : [{ type: "text", text: "No cities added.", color: "muted", size: "sm", align: "center" }]
},
{ type: "button", id: "open_search", icon: "Plus", text: "New City", variant: "primary", size: "md" }
]
}
];
if (isUpdate) {
await widget.update(componentTree);
} else {
await widget.show(componentTree, "bottom-right");
}
await widget.on("open_search", "click", async () => {
view = "search";
searchResults = [];
currentQuery = "";
await renderSearch(false);
});
for (let i = 0; i < savedCities.length; i++) {
await widget.on(`del_${i}`, "click", async () => {
savedCities.splice(i, 1);
await storage.set("weather_cities_v3", savedCities);
await renderDashboard(true);
});
}
const needsFetch = savedCities.some(c => !weatherCache[c.name]);
if (needsFetch) {
await Promise.all(savedCities.map(async (city) => {
if (weatherCache[city.name]) return;
try {
const data = await http.fetch(`https://api.open-meteo.com/v1/forecast?latitude=${city.lat}&longitude=${city.lon}¤t_weather=true`);
if (!data.error) {
const current = data.current_weather;
weatherCache[city.name] = {
temp: Math.round(current.temperature),
// Pass the is_day parameter into the emoji generator!
emoji: getWeatherEmoji(current.weathercode, current.is_day)
};
} else {
weatherCache[city.name] = { temp: "?", emoji: "❌" };
}
} catch(e) {
weatherCache[city.name] = { temp: "?", emoji: "❌" };
}
}));
if (view === "dashboard") {
await renderDashboard(true);
}
}
}
async function renderSearch(isUpdate = true) {
let resultCards = [];
if (isSearching) {
resultCards = [
{ type: "row", justify: "center", gap: "sm", padding: "md", components: [
{ type: "spinner" },
{ type: "text", text: "Searching...", color: "muted", size: "sm" }
]}
];
} else if (searchResults.length > 0) {
resultCards = searchResults.map((r, index) => ({
type: "row",
bg: "card2",
rounded: true,
padding: "md",
align: "center",
justify: "between",
components: [
{
type: "column",
gap: "sm",
align: "start",
components: [
{ type: "text", text: r.name, weight: "bold", size: "sm" },
{ type: "text", text: `${r.admin1 ? r.admin1 + ', ' : ''}${r.country}`, color: "muted", size: "xs" }
]
},
{ type: "button", id: `add_res_${index}`, icon: "Plus", variant: "outline", size: "sm" }
]
}));
} else if (currentQuery.length > 0) {
resultCards = [{ type: "text", text: "No results found.", color: "muted", size: "sm", align: "center" }];
}
const componentTree = [
{
type: "column",
gap: "sm",
bg: "card",
padding: "md",
rounded: true,
components: [
{
type: "row",
align: "center",
justify: "between",
components: [
{ type: "button", id: "go_back", icon: "ArrowLeft", text: "", variant: "ghost", size: "sm" },
{ type: "text", text: "Search Cities", size: "md", weight: "bold" }
]
},
{ type: "divider" },
{
type: "input", id: "search_input", placeholder: "Start typing (e.g. Paris)...", value: currentQuery, size: "sm"
},
{
type: "column",
gap: "sm",
maxHeight: "200px",
scroll: true,
components: resultCards
}
]
}
];
if (isUpdate) {
await widget.update(componentTree);
} else {
await widget.show(componentTree, "bottom-right");
}
await widget.on("go_back", "click", async () => {
view = "dashboard";
await renderDashboard(true);
});
await widget.on("search_input", "change", async (val) => {
currentQuery = val;
if (searchTimeout) clearTimeout(searchTimeout);
if (currentQuery.trim().length === 0) {
searchResults = [];
isSearching = false;
await renderSearch(true);
return;
}
isSearching = true;
await renderSearch(true);
searchTimeout = setTimeout(async () => {
try {
const data = await http.fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(currentQuery)}&count=5&language=en&format=json`);
searchResults = data.results || [];
} catch(err) {
searchResults = [];
}
isSearching = false;
await renderSearch(true);
}, 400);
});
for (let i = 0; i < searchResults.length; i++) {
await widget.on(`add_res_${i}`, "click", async () => {
const cityToAdd = searchResults[i];
savedCities.push({
name: cityToAdd.name,
lat: cityToAdd.latitude,
lon: cityToAdd.longitude,
state: cityToAdd.admin1 || "",
country: cityToAdd.country || "Unknown"
});
await storage.set("weather_cities_v3", savedCities);
view = "dashboard";
await renderDashboard(true);
});
}
}
await renderDashboard();
await widget.waitForEvent("__widget_closed__", "close");
Clock
byAnonymous
Classic Clock Widget with stopwatch and timer
// Initial State
let mode = "clock"; // Modes: "clock", "stopwatch", "timer"
// Stopwatch state
let swStartTime = 0;
let swElapsed = 0;
let swRunning = false;
// Timer state
let timerDuration = 5 * 60; // default 5 minutes
let timerRemaining = timerDuration;
let timerRunning = false;
let timerEndTime = 0;
// Formatting Utilities
function pad(num) {
return num.toString().padStart(2, '0');
}
function formatTime(ms) {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const millis = Math.floor((ms % 1000) / 10);
return `${pad(minutes)}:${pad(seconds)}.${pad(millis)}`;
}
function formatTimer(sec) {
const m = Math.floor(sec / 60);
const s = sec % 60;
return `${pad(m)}:${pad(s)}`;
}
// UI Rendering Function
async function render() {
let content = [];
// Top Navigation Tabs
const tabs = {
type: "row",
justify: "center",
gap: "md",
components: [
{ type: "button", id: "cw_tab_clock", text: "Clock", variant: mode === "clock" ? "primary" : "ghost", size: "sm" },
{ type: "button", id: "cw_tab_sw", text: "Stopwatch", variant: mode === "stopwatch" ? "primary" : "ghost", size: "sm" },
{ type: "button", id: "cw_tab_timer", text: "Timer", variant: mode === "timer" ? "primary" : "ghost", size: "sm" },
]
};
content.push(tabs);
content.push({ type: "divider" });
// Mode-specific UI
if (mode === "clock") {
const now = new Date();
const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const dateString = now.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' });
content.push({
type: "column",
align: "center",
gap: "sm",
components: [
{ type: "text", text: timeString, size: "xl", weight: "bold", color: "accent" },
{ type: "text", text: dateString, size: "sm", color: "muted" }
]
});
} else if (mode === "stopwatch") {
let display = "00:00.00";
if (swRunning) {
display = formatTime(swElapsed + (Date.now() - swStartTime));
} else {
display = formatTime(swElapsed);
}
content.push({
type: "column",
align: "center",
gap: "md",
components: [
{ type: "text", text: display, size: "xl", weight: "bold", color: "default" },
{
type: "row",
justify: "center",
gap: "md",
components: [
{ type: "button", id: "cw_sw_toggle", text: swRunning ? "Stop" : "Start", variant: swRunning ? "danger" : "success" },
{ type: "button", id: "cw_sw_reset", text: "Reset", variant: "ghost", disabled: swRunning }
]
}
]
});
} else if (mode === "timer") {
let display = formatTimer(timerRemaining);
if (timerRunning) {
const left = Math.max(0, Math.ceil((timerEndTime - Date.now()) / 1000));
display = formatTimer(left);
}
content.push({
type: "column",
align: "center",
gap: "md",
components: [
{ type: "text", text: display, size: "xl", weight: "bold", color: timerRemaining <= 10 && timerRemaining > 0 ? "warning" : "default" },
{
type: "row",
justify: "center",
gap: "md",
components: [
{ type: "button", id: "cw_timer_toggle", text: timerRunning ? "Pause" : "Start", variant: timerRunning ? "danger" : "success" },
{ type: "button", id: "cw_timer_reset", text: "Reset", variant: "ghost" }
]
},
(!timerRunning ? {
type: "row",
justify: "center",
align: "center",
gap: "sm",
components: [
{ type: "input", id: "cw_timer_input_m", placeholder: "Min", value: Math.floor(timerDuration/60).toString(), size: "sm" },
{ type: "text", text: ":", weight: "bold" },
{ type: "input", id: "cw_timer_input_s", placeholder: "Sec", value: (timerDuration%60).toString(), size: "sm" },
{ type: "button", id: "cw_timer_set", text: "Set", variant: "ghost", size: "md" }
]
} : null)
].filter(Boolean)
});
}
// Push the updated tree to the widget API
await widget.update([{
type: "column",
bg: "card",
rounded: true,
padding: "lg",
gap: "lg",
components: content
}]);
}
// 1. Initial render
await widget.show([{ type: "text", text: "Loading clock..." }], "bottom-right", { theme: "dark" });
await render();
// 2. Set up asynchronous listeners
widget.on("cw_tab_clock", "click", async () => { mode = "clock"; await render(); });
widget.on("cw_tab_sw", "click", async () => { mode = "stopwatch"; await render(); });
widget.on("cw_tab_timer", "click", async () => { mode = "timer"; await render(); });
// Stopwatch controls
widget.on("cw_sw_toggle", "click", async () => {
if (swRunning) {
swRunning = false;
swElapsed += Date.now() - swStartTime;
} else {
swRunning = true;
swStartTime = Date.now();
}
await render();
});
widget.on("cw_sw_reset", "click", async () => {
if (!swRunning) {
swElapsed = 0;
await render();
}
});
// Timer controls
widget.on("cw_timer_toggle", "click", async () => {
if (timerRunning) {
timerRunning = false;
timerRemaining = Math.max(0, Math.ceil((timerEndTime - Date.now()) / 1000));
} else {
if (timerRemaining > 0) {
timerRunning = true;
timerEndTime = Date.now() + (timerRemaining * 1000);
}
}
await render();
});
widget.on("cw_timer_reset", "click", async () => {
timerRunning = false;
timerRemaining = timerDuration;
await render();
});
widget.on("cw_timer_set", "click", async () => {
const valM = await widget.getValue("cw_timer_input_m");
const valS = await widget.getValue("cw_timer_input_s");
// Parse the inputs, defaulting to 0 if left empty
const mins = parseInt(valM, 10) || 0;
const secs = parseInt(valS, 10) || 0;
if (mins >= 0 && secs >= 0 && (mins > 0 || secs > 0)) {
timerDuration = (mins * 60) + secs;
timerRemaining = timerDuration;
notify.show(`Timer set to ${mins}m ${secs}s.`, "success");
await render();
} else {
notify.show("Please enter a valid time.", "error");
}
});
// 3. Main Update Loop (Ticks every 100ms for stopwatch accuracy)
const timerInterval = setInterval(async () => {
if (mode === "clock" || mode === "stopwatch") {
await render();
} else if (mode === "timer" && timerRunning) {
const left = Math.max(0, Math.ceil((timerEndTime - Date.now()) / 1000));
if (left <= 0) {
timerRunning = false;
timerRemaining = 0;
notify.show("Timer finished!", "success");
} else {
timerRemaining = left;
}
await render();
}
}, 100);
// 4. Shut down the script cleanly when the user closes the Widget UI!
await widget.waitForEvent("__widget_closed__", "close");
// Critical: Stop the background loop so it doesn't keep updating a dead widget!
clearInterval(timerInterval);
Pomodoro WIdget
byAnonymous
pomodoro timer widget
// ==============================================
// 🍅 POMODORO BLOCKER (Widget SDK)
// ==============================================
// Paste your full base64 string back here!
const logoBase64 = "data:image/png;base64,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";
let isWidgetOpen = false;
let pomoInterval = null;
// 1. Load state from storage on boot!
let state = await storage.get("pomo_state") || {
mode: "idle", // "idle" | "focus" | "break"
targetTime: 0
};
let displayTime = "00:00";
async function saveState() {
await storage.set("pomo_state", state);
}
function formatTime(ms) {
if (ms < 0) ms = 0;
const mins = Math.floor(ms / 60000).toString().padStart(2, '0');
const secs = Math.floor((ms % 60000) / 1000).toString().padStart(2, '0');
return `${mins}:${secs}`;
}
// 2. Inject a gorgeous full-screen overlay into the webpage
async function injectBlocker() {
await page.mutate([{ selector: "#pomo-blocker", action: "remove" }]); // Clear existing
if (state.mode === "focus" || state.mode === "break") {
const isFocus = state.mode === "focus";
const bgColor = isFocus ? "rgba(230, 57, 70, 0.95)" : "rgba(42, 157, 143, 0.95)";
const title = isFocus ? "Focus Time" : "Break Time";
const subtitle = isFocus ? "Stop scrolling and get back to work!" : "Relax your eyes and stretch!";
const blockerHtml = `
<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);">
<h1 style="font-size: 4rem; margin-bottom: 10px; font-weight: 800;">${title}</h1>
<p style="font-size: 1.5rem; margin-bottom: 40px; opacity: 0.9;">${subtitle}</p>
<h2 id="pomo-time-display" style="font-size: 10rem; font-weight:bold; margin:0; font-variant-numeric: tabular-nums;">${displayTime}</h2>
</div>
`;
// Appends to the page body, blocking everything behind it safely!
await page.mutate([{ selector: "body", action: "append", value: blockerHtml }]);
}
}
async function tick() {
if (state.mode === "idle") return;
const now = Date.now();
const remaining = state.targetTime - now;
// Timer finished!
if (remaining <= 0) {
const prevMode = state.mode;
state.mode = "idle";
state.targetTime = 0;
await saveState();
displayTime = "00:00";
render();
await page.mutate([{ selector: "#pomo-blocker", action: "remove" }]);
if (prevMode === "focus") notify.show("Focus session complete! Take a break.", "success");
else notify.show("Break is over! Ready to focus?", "success");
return;
}
displayTime = formatTime(remaining);
render(); // Update widget UI
// 3. Update the massive on-screen timer without re-rendering the whole overlay!
await page.mutate([{ selector: "#pomo-time-display", action: "replaceHtml", value: displayTime }]);
}
function startTimer(minutes, mode) {
state.mode = mode;
state.targetTime = Date.now() + (minutes * 60 * 1000);
saveState();
displayTime = formatTime(minutes * 60 * 1000);
injectBlocker();
render();
}
/** @returns {WidgetComponent[]} */
function mainView() {
const isRunning = state.mode !== "idle";
const statusText = state.mode === "focus" ? "FOCUS MODE" : (state.mode === "break" ? "BREAK MODE" : "READY");
const statusColor = state.mode === "focus" ? "error" : (state.mode === "break" ? "success" : "muted");
return [
{
type: "row", align: "center",
components: [
{ type: "image", src: logoBase64, width: "24px", height: "24px", rounded: true },
{ type: "text", text: "", flex: 1 },
{ type: "text", text: "Pomodoro", size: "lg", weight: "bold" },
{ type: "text", text: " ", size: "lg" }
]
},
{ type: "divider" },
{
type: "column", align: "center", gap: "sm", padding: "lg", bg: "card2", rounded: true,
components: [
{ type: "text", text: statusText, size: "sm", weight: "bold", color: statusColor },
{ type: "text", text: isRunning ? displayTime : "25:00", size: "xl", weight: "bold" },
]
},
{ type: "divider" },
...(isRunning ? [
{ type: "button", id: "btnStop", text: "Stop Timer", variant: "ghost" }
] : [
{
type: "row", gap: "sm",
components: [
{ type: "button", id: "btnFocus", text: "Focus (25m)", variant: "primary", flex: 1 },
{ type: "button", id: "btnBreak", text: "Break (5m)", variant: "default", flex: 1 }
]
}
])
];
}
function render() {
if (isWidgetOpen) widget.update(mainView());
}
// --- Listeners ---
widget.on("btnFocus", "click", () => startTimer(25, "focus"));
widget.on("btnBreak", "click", () => startTimer(5, "break"));
widget.on("btnStop", "click", async () => {
state.mode = "idle";
state.targetTime = 0;
await saveState();
await page.mutate([{ selector: "#pomo-blocker", action: "remove" }]);
render();
});
widget.on("__widget_closed__", "close", () => {
isWidgetOpen = false;
});
// --- Boot ---
// 4. Catch up if timer was already running in storage!
if (state.mode !== "idle") {
if (Date.now() < state.targetTime) {
displayTime = formatTime(state.targetTime - Date.now());
injectBlocker(); // Restore the blocker overlay on fresh boot!
} else {
state.mode = "idle";
state.targetTime = 0;
saveState();
}
}
widget.show(mainView(), "bottom-right");
isWidgetOpen = true;
pomoInterval = setInterval(tick, 1000);
// Wait patiently for the user to explicitly close the widget UI
await widget.waitForEvent("__widget_closed__", "close");
// --- GRACEFUL SHUTDOWN ---
isWidgetOpen = false;
if (pomoInterval) clearInterval(pomoInterval);
// IMPORTANT: We must remove the blocker when the script shuts down.
// Otherwise, it freezes permanently on the screen because the sandbox is dead!
// Don't worry—the timer state is safe in `storage`, so if you run the widget again,
// it will perfectly resume the blocker with the correct remaining time!
await page.mutate([{ selector: "#pomo-blocker", action: "remove" }]);
Web Editor
byAnonymous
A collection of 2 actions bundled together.
// This is an action group containing 2 actions.
// Install to import the entire group:
// - Web Editor/Editor
// - Web Editor/AutoloaderTee Widget Launcher
byAnonymous
A persistent launcher to manage and execute your widgets.
// ==============================================
// 🚀 TEE WIDGET LAUNCHER
// ==============================================
const logoBase64 = "data:image/png;base64,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";
// --- STATE & STORAGE ---
let apps = await storage.get("launcher_apps");
if (!apps) {
apps = [{ name: "Page Chatbot", id: "default-page-chatbot" }];
await storage.set("launcher_apps", apps);
}
let viewMode = "minimized"; // Can be "main", "add", "minimized", or "delete"
let deletingIndex = -1;
async function saveApps() {
await storage.set("launcher_apps", apps);
}
// --- UI VIEWS ---
/** @returns {WidgetComponent[]} */
function minimizedView() {
return [
{ type: "image", id: "openBtn", src: logoBase64, width: "45px", height: "45px" }
];
}
/** @returns {WidgetComponent[]} */
function mainView() {
/** @type {WidgetComponent[]} */
const appCards = apps.length > 0 ? apps.map((app, idx) => ({
type: "row", bg: "card2", padding: "md", rounded: true, align: "center", justify: "between",
components: [
{
type: "column", gap: "sm",
components: [
{ type: "text", text: app.name, size: "md", weight: "bold" },
{ type: "text", text: app.id.substring(0, 20) + "…", size: "xs", color: "muted" }
]
},
{
type: "row", gap: "sm", align: "center",justify:"end",
components: [
{ type: "button", icon: "Power", id: `launch_${idx}`, variant: "ghost" },
{ type: "button", icon: "Trash2", id: `delete_${idx}`, variant: "danger" }
]
}
]
})) : [
{
type: "column", align: "center", padding: "lg", gap: "sm",
components: [
{ type: "image", src: logoBase64, width: "32px", height: "32px", rounded: true },
{ type: "text", text: "No widgets yet", size: "md", weight: "medium", color: "muted", align: "center" },
{ type: "text", text: "Add your first widget to get started.", size: "xs", color: "muted", align: "center" }
]
}
];
return [
{
type: "row", justify: "between", align: "center",
components: [
{
type: "row", gap: "sm", align: "center",
components: [
{ type: "image", src: logoBase64, width: "24px", height: "24px", rounded: true },
]
},
{ type: "button", icon: "Minimize", id: "closeBtn", variant: "ghost" }
]
},
{ type: "divider" },
{ type: "column", gap: "sm", scroll: true, maxHeight: "320px", components: appCards },
{ type: "divider" },
{ type: "button", text: "Add Widget", id: "addBtn", variant: "primary" }
];
}
/** @returns {WidgetComponent[]} */
function addView() {
return [
{
type: "row", align: "center", justify: "between",
components: [
{ type: "image", src: logoBase64, width: "24px", height: "24px", rounded: true },
{ type: "text", text: "Add Widget", size: "lg", weight: "bold" }
]
},
{ type: "divider" },
{
type: "column", gap: "md",
components: [
{
type: "column", gap: "sm",
components: [
{ type: "text", text: "Widget Name", size: "sm", weight: "medium", color: "muted" },
{ type: "input", id: "appName", placeholder: "e.g. Gmail Client" }
]
},
{
type: "column", gap: "md",
components: [
{ type: "text", text: "Action ID", size: "sm", weight: "medium", color: "muted" },
{ type: "input", id: "appId", placeholder: "Paste action ID here..." }
]
}
]
},
{ type: "divider" },
{
type: "row", gap: "sm", justify: "end",
components: [
{ type: "button", text: "Cancel", id: "cancelAdd", variant: "ghost" },
{ type: "button", text: "Save Widget", id: "saveApp", variant: "primary" }
]
}
];
}
/** @returns {WidgetComponent[]} */
function deleteView() {
const app = apps[deletingIndex];
return [
{
type: "column", align: "center", padding: "lg", gap: "md",
components: [
{ type: "icon", icon: "AlertTriangle", size: "lg", color: "error" },
{ type: "text", text: "Delete Widget?", size: "lg", weight: "bold", align: "center" },
{ type: "text", text: `Are you sure you want to remove "${app?.name}" from your launcher?`, size: "md", color: "muted", align: "center" }
]
},
{ type: "divider" },
{
type: "row", gap: "sm", justify: "end",
components: [
{ type: "button", text: "Cancel", id: "cancelDelete", variant: "ghost" },
{ type: "button", text: "Delete", id: "confirmDelete", variant: "danger" }
]
}
];
}
function render() {
if (viewMode === "minimized") {
widget.show(minimizedView(), "bottom-right", { hideCloseButton: true, transparent: true });
} else if (viewMode === "add") {
widget.show(addView(), "bottom-right", { hideCloseButton: true });
} else if (viewMode === "delete") {
widget.show(deleteView(), "bottom-right", { hideCloseButton: true });
} else {
widget.show(mainView(), "bottom-right", { hideCloseButton: true });
}
}
// --- EVENT ROUTING ---
function bindAppListeners() {
apps.forEach((app, idx) => {
widget.on(`launch_${idx}`, "click", async () => {
widget.hide();
await actions.run(app.id);
render();
bindAppListeners();
});
widget.on(`delete_${idx}`, "click", async () => {
deletingIndex = idx;
viewMode = "delete";
render();
});
});
}
// Add/Cancel views
widget.on("addBtn", "click", () => { viewMode = "add"; render(); });
widget.on("cancelAdd", "click", () => { viewMode = "main"; render(); });
// Delete Confirm views
widget.on("cancelDelete", "click", () => { viewMode = "main"; render(); });
widget.on("confirmDelete", "click", async () => {
if (deletingIndex >= 0 && deletingIndex < apps.length) {
apps.splice(deletingIndex, 1);
await saveApps();
}
viewMode = "main";
render();
bindAppListeners();
});
// Minimizing and Re-opening
widget.on("closeBtn", "click", () => { viewMode = "minimized"; render(); });
widget.on("openBtn", "click", () => { viewMode = "main"; render(); });
widget.on("saveApp", "click", async (data) => {
const name = data.appName?.trim();
const id = data.appId?.trim();
if (name && id) {
apps.push({ name, id });
await saveApps();
viewMode = "main";
render();
bindAppListeners();
} else {
notify.error("Please enter both a Name and an Action ID.");
}
});
// --- BOOT ---
render();
bindAppListeners();
await widget.waitForEvent("FOREVER_EVENT", "click");