PageSourceSearch

https://polygonhunter.com/js/downloads.js

js polygonhunter.com collected 2026-10-04 22:10:19 UTC 4,461 bytes, 133 lines download raw bytes

1/* ============================================================
2   Download counters: official Obsidian community stats
3   Fetches community-plugin-stats.json (same numbers the plugin
4   browser shows), caches the four counts for 6 h, reveals the
5   .dl-badge pills and appends the herd total to the hero eyebrow.
6   ============================================================ */
7(() => {
8  "use strict";
9
10  const STATS_URL =
11    "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/master/community-plugin-stats.json";
12  const CACHE_KEY = "dino-dl-stats";
13  const CACHE_TTL = 6 * 60 * 60 * 1000; // 6 h
14
15  const QA = new URLSearchParams(location.search).has("qa");
16  const REDUCED = window.matchMedia("(prefers-reduced-motion: reduce)").matches || QA;
17
18  const PLUGIN_IDS = ["autolink-keywords", "scalosaurus", "searchosaurus", "slashosaurus", "barosaurus"];
19
20  /* ---------- cache (localStorage may be unavailable in private mode) ---------- */
21  function readCache() {
22    try {
23      const raw = localStorage.getItem(CACHE_KEY);
24      if (!raw) return null;
25      const data = JSON.parse(raw);
26      if (!data || typeof data.ts !== "number" || typeof data.counts !== "object") return null;
27      return data;
28    } catch {
29      return null;
30    }
31  }
32
33  function writeCache(counts) {
34    try {
35      localStorage.setItem(CACHE_KEY, JSON.stringify({ ts: Date.now(), counts }));
36    } catch {
37      /* fine - we just fetch again next visit */
38    }
39  }
40
41  /* ---------- rendering ---------- */
42  function countUp(el, target) {
43    const DURATION = 900;
44    let start = null;
45    const step = (now) => {
46      if (start === null) start = now;
47      const t = Math.min((now - start) / DURATION, 1);
48      const eased = 1 - Math.pow(1 - t, 3); // ease-out cubic
49      el.textContent = Math.round(target * eased).toLocaleString("en-US");
50      if (t < 1) requestAnimationFrame(step);
51    };
52    requestAnimationFrame(step);
53  }
54
55  function animateWhenVisible(el, numEl, target) {
56    if (REDUCED || !("IntersectionObserver" in window)) {
57      numEl.textContent = target.toLocaleString("en-US");
58      return;
59    }
60    numEl.textContent = "0";
61    const io = new IntersectionObserver(
62      (entries) => {
63        for (const entry of entries) {
64          if (!entry.isIntersecting) continue;
65          io.disconnect();
66          countUp(numEl, target);
67        }
68      },
69      { threshold: 0.4 }
70    );
71    io.observe(el);
72  }
73
74  function render(counts) {
75    // total over unique plugin ids - the same id may appear in several badges
76    let total = 0;
77    let any = false;
78    for (const id of PLUGIN_IDS) {
79      if (typeof counts[id] === "number") {
80        total += counts[id];
81        any = true;
82      }
83    }
84
85    for (const badge of document.querySelectorAll(".dl-badge[data-plugin-id], .herd-dl[data-plugin-id]")) {
86      const count = counts[badge.dataset.pluginId];
87      if (typeof count !== "number") continue; // missing id → badge stays hidden
88      const numEl = badge.querySelector(".dl-num");
89      if (!numEl) continue;
90      badge.hidden = false;
91      animateWhenVisible(badge, numEl, count);
92    }
93
94    const eyebrow = document.querySelector(".hero-eyebrow");
95    if (any && eyebrow && !eyebrow.querySelector(".dl-total")) {
96      const sep = document.createTextNode(" · ");
97      const span = document.createElement("span");
98      span.className = "dl-total";
99      const num = document.createElement("span");
100      num.className = "dl-num";
101      num.textContent = total.toLocaleString("en-US");
102      span.append(num, " downloads");
103      eyebrow.append(sep, span);
104      animateWhenVisible(eyebrow, num, total);
105    }
106  }
107
108  /* ---------- flow: fresh cache → fetch → stale cache fallback ---------- */
109  async function init() {
110    const cached = readCache();
111    if (cached && Date.now() - cached.ts < CACHE_TTL) {
112      render(cached.counts);
113      return;
114    }
115    try {
116      const res = await fetch(STATS_URL);
117      if (!res.ok) throw new Error("HTTP " + res.status);
118      const stats = await res.json();
119      const counts = {};
120      for (const id of PLUGIN_IDS) {
121        const dl = stats[id] && stats[id].downloads;
122        if (typeof dl === "number") counts[id] = dl;
123      }
124      writeCache(counts);
125      render(counts);
126    } catch {
127      if (cached) render(cached.counts); // stale beats nothing
128      /* otherwise: badges stay hidden - visitors never see an error */
129    }
130  }
131
132  init();
133})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.