◓ Today

A visual birds eye view of today. Uses top of Browser as a timline of year. | NOTE: Select icon toggles outline.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         ◓ Today
// @namespace    qp5.progress.weekdot
// @version      0.2
// @description  A visual birds eye view of today. Uses top of Browser as a timline of year. | NOTE: Select icon toggles outline.
// @icon         data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 60'><path d='M10 50 A40 40 0 0 1 90 50' fill='none' stroke='white' stroke-width='6'/><line x1='10' y1='50' x2='90' y2='50' stroke='white' stroke-width='6'/></svg>
// @match        *://*/*
// @run-at       document-idle
// @license      free use
// @grant        none
// ==/UserScript==

// My other scripts:
// https://greasyfork.org/en/scripts/531444-tampermonkey-hide-header
// https://greasyfork.org/en/scripts/534417-refresh-script-qa-tool

(function () {
  const SIZE = 14; // px
  const DOT_ID = "__weekProgressDot__";

  if (document.getElementById(DOT_ID)) return;

  const dot = document.createElement("div");
  dot.id = DOT_ID;
  Object.assign(dot.style, {
    position: "fixed",
    top: "0px",
    left: "0px",
    width: SIZE + "px",
    height: Math.ceil(SIZE / 2) + "px",
    background: "#fff",
    borderTopLeftRadius: SIZE + "px",
    borderTopRightRadius: SIZE + "px",
    borderBottomLeftRadius: "0",
    borderBottomRightRadius: "0",
    boxSizing: "border-box",
    border: "1px solid #000",
    transform: "translateX(-50%)",
    zIndex: "2147483647",
    pointerEvents: "auto",
  });

  // toggle outline on click
  dot.addEventListener("click", (e) => {
    e.stopPropagation();
    dot.style.border = dot.style.border ? "" : "1px solid #000";
  });

  // place after body is ready
  const place = () => document.body && document.body.appendChild(dot);
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", place);
  } else {
    place();
  }

  // helper: same calendar day
  function sameDay(a, b) {
    return (
      a.getFullYear() === b.getFullYear() &&
      a.getMonth() === b.getMonth() &&
      a.getDate() === b.getDate()
    );
  }

  // helper: US Thanksgiving (4th Thursday in November)
  function thanksgivingDate(year) {
    const d = new Date(year, 10, 1); // November 1
    const day = d.getDay(); // 0 = Sun ... 6 = Sat
    const offsetToThu = (4 - day + 7) % 7; // 4 = Thu
    d.setDate(1 + offsetToThu + 3 * 7); // 4th Thursday
    return d;
  }

  // list of holidays for a given year. //Month minuses 1 month
  function getHolidays(year) {
    return [
      new Date(year, 0, 1), // New Year’s
      new Date(year, 1, 14), // Valentine day
      new Date(year, 2, 17), // St. Patricks
      new Date(year, 3, 20), // Easter Sunday
      new Date(year, 4, 11), // Mother's Day
      new Date(year, 4, 26), // Mem Day
      new Date(year, 5, 15), // Father's Day
      new Date(year, 4, 26), // Mem Day
      new Date(year, 6, 4), //  Indepen Day
      new Date(year, 9, 31), // Halloween
      new Date(year, 10, 27), // Indepen Day
      new Date(year, 11, 25), // Christmas
    ];
  }

  // return info about holiday status for day 'd'
    // { color: "green"|"yellow"|null, kind: "holiday"|"pre"|null }
  function holidayInfo(d) {
    const year = d.getFullYear();
    const holidays = getHolidays(year);

    for (const h of holidays) {
      if (sameDay(d, h)) {
        return { color: "green", kind: "holiday" };
      }
      const prev = new Date(h);
      prev.setDate(prev.getDate() - 1);
      if (sameDay(d, prev)) {
        return { color: "yellow", kind: "pre" };
      }
    }

    return { color: null, kind: null };
  }

  // days until next holiday (this year or next)
  function daysUntilNextHoliday(d) {
    const start = new Date(d.getFullYear(), d.getMonth(), d.getDate());
    const msPerDay = 86400000;

    function scan(year) {
      let best = null;
      for (const h of getHolidays(year)) {
        const diff = h - start;
        if (diff >= 0 && (best === null || diff < best)) {
          best = diff;
        }
      }
      return best;
    }

    let diff = scan(start.getFullYear());
    if (diff === null) {
      diff = scan(start.getFullYear() + 1);
    }

    return diff === null ? null : Math.round(diff / msPerDay);
  }




  function weekOfYear(d) {
    // ISO-like: week starts Monday; simpler: use UTC to be stable
    const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
    const dayNum = t.getUTCDay() || 7;
    t.setUTCDate(t.getUTCDate() + 4 - dayNum);
    const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
    const week = Math.floor(((t - yearStart) / 86400000 + 1) / 7) + 1;
    return Math.max(1, Math.min(week, 52)); // clamp to 1..52
  }

  function positionDot() {
    const today = new Date();
    const w = Math.max(window.innerWidth, 1);
    const wk = weekOfYear(today); // 1..52
    const frac = (wk - 0.5) / 52; // middle of each week slot
    const x = Math.round(frac * w);
    dot.style.left = x + "px";

    const info = holidayInfo(today);

    if (info.color) {
      // holiday or day before
      dot.style.background = info.color;
      if (info.kind === "Holiday") {    //yellow
        dot.title = "Holiday today";    //green
      } else {
        dot.title = "Holiday tomorrow!";
      }
    } else {
      // normal day: show countdown
      dot.style.background = "#fff";
      const days = daysUntilNextHoliday(today);
      if (days === null) {
        dot.title = "No holidays found";
      } else if (days === 0) {
        dot.title = "Holiday";
      } else if (days === 1) {
        dot.title = "1 day before holiday";
      } else {
        dot.title = days + "Days before next holiday";
      }
    }
  }

  function scheduleNextDay() {
    const now = new Date();
    const next = new Date(now);
    next.setHours(24, 0, 2, 0);
    setTimeout(() => {
      positionDot();
      scheduleNextDay();
    }, next - now);
  }

  positionDot();
  scheduleNextDay();
})();