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.