// ==UserScript== // @name Social Pomodoro Reminder (15min, gentle) + Segment Timer (Cookie) // @namespace local.maxxie.social.pomodoro // @version 1.3.0 // @description Gentle pomodoro reminder every 15 min of active browsing time + visible segment timer; state stored in cookies // @match *://*.twitter.com/* // @match *://*.x.com/* // @match *://*.facebook.com/* // @match *://*.instagram.com/* // @match *://*.youtube.com/* // @match *://*.tiktok.com/* // @match *://*.reddit.com/* // @grant none // @downloadURL https://update.greasyfork.icu/scripts/561724/Social%20Pomodoro%20Reminder%20%2815min%2C%20gentle%29%20%2B%20Segment%20Timer%20%28Cookie%29.user.js // @updateURL https://update.greasyfork.icu/scripts/561724/Social%20Pomodoro%20Reminder%20%2815min%2C%20gentle%29%20%2B%20Segment%20Timer%20%28Cookie%29.meta.js // ==/UserScript== (() => { "use strict"; // ===== Config ===== const SEGMENT_MIN = 15; const TICK_MS = 1000; const REQUIRE_ACTIVE_TAB = true; const SHOW_BADGE = true; const SHOW_SEGMENT_TIMER = true; // 用 cookie 存(每個網域各存一份) const COOKIE_NAME = "social_pomodoro_state_v1"; const COOKIE_DAYS = 365; // cookie 保存多久(天) // ===== Cookie helpers ===== function setCookie(name, value, days) { const expires = new Date(Date.now() + days * 864e5).toUTCString(); // SameSite=Lax 對一般瀏覽最安全、也常見;path=/ 確保整站可讀 document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; ` + `expires=${expires}; path=/; SameSite=Lax`; } function getCookie(name) { const key = encodeURIComponent(name) + "="; const parts = document.cookie.split("; "); for (const p of parts) { if (p.startsWith(key)) return decodeURIComponent(p.slice(key.length)); } return null; } // ===== Helpers ===== const now = () => Date.now(); const todayKey = () => new Date().toISOString().slice(0, 10); // YYYY-MM-DD function loadState() { try { const raw = getCookie(COOKIE_NAME); if (!raw) return { day: todayKey(), activeMs: 0, lastSegmentNotified: 0 }; const s = JSON.parse(raw); if (s.day !== todayKey()) return { day: todayKey(), activeMs: 0, lastSegmentNotified: 0 }; return { day: s.day, activeMs: Number(s.activeMs) || 0, lastSegmentNotified: Number(s.lastSegmentNotified) || 0, }; } catch { return { day: todayKey(), activeMs: 0, lastSegmentNotified: 0 }; } } function saveState(s) { // 只存最小必要欄位,避免 cookie 過大 const payload = JSON.stringify({ day: s.day, activeMs: Math.floor(s.activeMs), lastSegmentNotified: Math.floor(s.lastSegmentNotified), }); setCookie(COOKIE_NAME, payload, COOKIE_DAYS); } function isActive() { if (!REQUIRE_ACTIVE_TAB) return true; return document.visibilityState === "visible" && document.hasFocus(); } function formatMs(ms) { const totalSec = Math.floor(ms / 1000); const m = Math.floor(totalSec / 60); const s = totalSec % 60; return `${m}m ${String(s).padStart(2, "0")}s`; } function formatMMSS(ms) { const totalSec = Math.floor(ms / 1000); const m = Math.floor(totalSec / 60); const s = totalSec % 60; return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; } async function ensureNotificationPermission() { if (!("Notification" in window)) return false; if (Notification.permission === "granted") return true; if (Notification.permission === "denied") return false; try { const p = await Notification.requestPermission(); return p === "granted"; } catch { return false; } } function notify(title, body) { ensureNotificationPermission().then((ok) => { if (ok) new Notification(title, { body }); else alert(`${title}\n\n${body}`); }); } // ===== UI: Badge (small) ===== let badgeEl = null; function mountBadge() { if (!SHOW_BADGE) return; badgeEl = document.createElement("div"); badgeEl.style.cssText = ` position: fixed; right: 12px; bottom: 12px; z-index: 999999; font: 12px/1.25 -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; background: rgba(0,0,0,0.72); color: #fff; padding: 8px 10px; border-radius: 10px; box-shadow: 0 6px 18px rgba(0,0,0,0.25); user-select: none; white-space: pre; `; badgeEl.title = "Counts only when tab is visible & focused"; document.documentElement.appendChild(badgeEl); } function updateBadge(activeMs) { if (!badgeEl) return; const segmentMs = SEGMENT_MIN * 60 * 1000; const segDone = Math.floor(activeMs / segmentMs); const within = activeMs % segmentMs; const left = segmentMs - within; badgeEl.textContent = `🍅 Social Pomodoro\n` + `Today: ${segDone} segments + ${formatMMSS(within)}\n` + `Next remind in: ${formatMMSS(left)}`; } // ===== UI: Segment Timer (big, draggable) ===== let timerEl = null; function mountSegmentTimer() { if (!SHOW_SEGMENT_TIMER) return; timerEl = document.createElement("div"); timerEl.style.cssText = ` position: fixed; right: 12px; top: 12px; z-index: 999999; font: 14px/1.2 -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; background: rgba(20,20,20,0.78); color: #fff; padding: 10px 12px; border-radius: 12px; box-shadow: 0 10px 24px rgba(0,0,0,0.30); user-select: none; min-width: 180px; cursor: grab; `; timerEl.innerHTML = `