AAAAAAAAAAA
This commit is contained in:
parent
fc619033b3
commit
4592f76d14
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
<!-- SEO Meta Tags -->
|
<!-- SEO Meta Tags -->
|
||||||
<meta name="description" content="Clove Twilight's live Discord presence — current status, activity, and what fae is up to right now.">
|
<meta name="description" content="Clove Twilight's live Discord presence — current status, activity, and what fae is up to right now.">
|
||||||
<meta name="keywords" content="Clove Twilight, c.stupid.cat, Discord, presence, status, Lanyard">
|
<meta name="keywords" content="Clove Twilight, c.stupid.cat, Discord, presence, status, Doughmination, Restful">
|
||||||
<meta name="author" content="doughmination">
|
<meta name="author" content="doughmination">
|
||||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
||||||
|
|
||||||
|
|
|
||||||
111
js/core.js
111
js/core.js
|
|
@ -1,6 +1,6 @@
|
||||||
// Ari was here uwu
|
/* Ari was here uwu
|
||||||
// Professional boob lover
|
* Professional boob lover
|
||||||
// girls kissing,,,
|
* girls kissing,,, */
|
||||||
console.log(`
|
console.log(`
|
||||||
⣿⣿⣿⠏⣴⣿⣿⣿⣿⡿⠟⢹⣿⣿⣿⡿⠋⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⡉⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
⣿⣿⣿⠏⣴⣿⣿⣿⣿⡿⠟⢹⣿⣿⣿⡿⠋⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⡉⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
⣿⣿⢇⣾⣿⣿⣿⡿⢋⢀⣴⣿⣿⡿⠋⠀⠘⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣀⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
⣿⣿⢇⣾⣿⣿⣿⡿⢋⢀⣴⣿⣿⡿⠋⠀⠘⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣀⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
|
|
@ -35,10 +35,10 @@ console.log(`
|
||||||
⣿⣿⣿⣿⣿⣿⣿⣿⣧⡀⢹⣿⣿⣧⢹⡄⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢇⣿⢿⣷⡄⠘⣿⣿⣿⣿⠇⣾⣿⣦⣤⡀⢸⣿⣿⣿⣿⣿⣿⣿
|
⣿⣿⣿⣿⣿⣿⣿⣿⣧⡀⢹⣿⣿⣧⢹⡄⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢇⣿⢿⣷⡄⠘⣿⣿⣿⣿⠇⣾⣿⣦⣤⡀⢸⣿⣿⣿⣿⣿⣿⣿
|
||||||
⣿⣿⣿⣷⣤⣈⡙⠻⢿⡇⠀⢿⣇⢻⡆⢿⡀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣧⡀⡜⢁⣤⡘⣿⣿⡿⢠⣿⣿⣿⣿⠁⣿⣿⣿⣿⣿⣿⣿⣿
|
⣿⣿⣿⣷⣤⣈⡙⠻⢿⡇⠀⢿⣇⢻⡆⢿⡀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣧⡀⡜⢁⣤⡘⣿⣿⡿⢠⣿⣿⣿⣿⠁⣿⣿⣿⣿⣿⣿⣿⣿
|
||||||
⣿⣿⣿⣿⡿⢿⣿⣿⣿⣿⡀⠘⣿⣄⢻⡘⡇⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣸⣿⡀⠀⣷⡘⣿⠇⣼⣿⣿⣿⡿⢸⣿⣿⣿⣿⣿⣿⣿⣿`);
|
⣿⣿⣿⣿⡿⢿⣿⣿⣿⣿⡀⠘⣿⣄⢻⡘⡇⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣸⣿⡀⠀⣷⡘⣿⠇⣼⣿⣿⣿⡿⢸⣿⣿⣿⣿⣿⣿⣿⣿`);
|
||||||
// mmmmmmmmmmmmmmmmm girls kissing,,,,,
|
/* mmmmmmmmmmmmmmmmm girls kissing,,,,, */
|
||||||
|
|
||||||
document.querySelectorAll("[data-href]").forEach((el) => {
|
document.querySelectorAll("[data-href]").forEach((el) => {
|
||||||
// cursor handled in css ([data-href] + [role="link"]) so the custom PNG isn't overridden
|
/* Cursor is handled in CSS ([data-href] + [role="link"]) so the custom PNG isn't overridden. */
|
||||||
if (!el.hasAttribute("role")) el.setAttribute("role", "link");
|
if (!el.hasAttribute("role")) el.setAttribute("role", "link");
|
||||||
if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "0");
|
if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "0");
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
|
||||||
|
|
||||||
/* ========================== flavors.js ========================= */
|
/* ========================== flavors.js ========================= */
|
||||||
(function flavors() {
|
(function flavors() {
|
||||||
// Metadata only — colors are defined in the per-flavor CSS files.
|
/* Metadata only, colors are defined in the per-flavor CSS files. */
|
||||||
const FLAVORS = {
|
const FLAVORS = {
|
||||||
mocha: { label: "Mocha", dot: "#f5c2e7" },
|
mocha: { label: "Mocha", dot: "#f5c2e7" },
|
||||||
macchiato: { label: "Macchiato", dot: "#f5bde6" },
|
macchiato: { label: "Macchiato", dot: "#f5bde6" },
|
||||||
|
|
@ -73,33 +73,33 @@ document.querySelectorAll("[data-href]").forEach((el) => {
|
||||||
|
|
||||||
function apply(name) {
|
function apply(name) {
|
||||||
const f = FLAVORS[name] || FLAVORS.mocha;
|
const f = FLAVORS[name] || FLAVORS.mocha;
|
||||||
root.setAttribute("data-flavor", name); // CSS does the rest
|
root.setAttribute("data-flavor", name); /* CSS does the rest */
|
||||||
const meta = document.querySelector('meta[name="theme-color"]');
|
const meta = document.querySelector('meta[name="theme-color"]');
|
||||||
if (meta) meta.setAttribute("content", f.dot);
|
if (meta) meta.setAttribute("content", f.dot);
|
||||||
}
|
}
|
||||||
|
|
||||||
let current = ls.getItem("ctpFlavor");
|
let current = ls.getItem("ctpFlavor");
|
||||||
if (!ORDER.includes(current)) current = "mocha";
|
if (!ORDER.includes(current)) current = "mocha";
|
||||||
apply(current); // the <head> snippet already set this to avoid a flash
|
apply(current); /* the <head> snippet already set this to avoid a flash */
|
||||||
|
|
||||||
// ---- top-right corner icon button ----
|
/* ---- top-right corner icon button ---- */
|
||||||
const bar = document.createElement("div");
|
const bar = document.createElement("div");
|
||||||
bar.className = "beta-bar";
|
bar.className = "beta-bar";
|
||||||
bar.innerHTML = `
|
bar.innerHTML = `
|
||||||
<button class="beta-btn" id="flavor-btn" type="button">
|
<button class="beta-btn" id="flavor-btn" type="button">
|
||||||
<img class="beta-icon" alt="">
|
<img class="beta-icon" alt="">
|
||||||
</button>`;
|
</button>`;
|
||||||
// Group the single-item widgets (discord + theme toggle) into one
|
/* Group the single-item widgets (discord + theme toggle) into one top
|
||||||
// top bar. On mobile they sit side by side; on desktop both stay
|
* bar. On mobile they sit side by side, on desktop both stay
|
||||||
// position:fixed, so this wrapper is zero-size and invisible.
|
* position:fixed, so this wrapper ends up zero-size and invisible. */
|
||||||
let topbar = document.querySelector(".topbar");
|
let topbar = document.querySelector(".topbar");
|
||||||
if (!topbar) {
|
if (!topbar) {
|
||||||
topbar = document.createElement("div");
|
topbar = document.createElement("div");
|
||||||
topbar.className = "topbar";
|
topbar.className = "topbar";
|
||||||
document.body.insertBefore(topbar, document.body.firstChild);
|
document.body.insertBefore(topbar, document.body.firstChild);
|
||||||
const dc = document.getElementById("discord");
|
const dc = document.getElementById("discord");
|
||||||
// Don't hijack the presence card when it's the centerpiece of the
|
/* Don't hijack the presence card when it's the centerpiece of the
|
||||||
// dedicated /discord page (it lives inside .presence-stage there).
|
* dedicated /discord page (it lives inside .presence-stage there). */
|
||||||
if (dc && !dc.closest(".presence-stage")) topbar.appendChild(dc);
|
if (dc && !dc.closest(".presence-stage")) topbar.appendChild(dc);
|
||||||
}
|
}
|
||||||
topbar.appendChild(bar);
|
topbar.appendChild(bar);
|
||||||
|
|
@ -109,7 +109,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
|
||||||
|
|
||||||
function paintBtn() {
|
function paintBtn() {
|
||||||
const f = FLAVORS[current];
|
const f = FLAVORS[current];
|
||||||
icon.src = `/assets/theme/${current}.png`; // e.g. /assets/theme/mocha.png
|
icon.src = `/assets/theme/${current}.png`; /* e.g. /assets/theme/mocha.png */
|
||||||
icon.alt = f.label;
|
icon.alt = f.label;
|
||||||
btn.title = `Theme: ${f.label} (click to cycle)`;
|
btn.title = `Theme: ${f.label} (click to cycle)`;
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +122,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
|
||||||
paintBtn();
|
paintBtn();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- cat collection button (sits next to the theme button) ----
|
/* ---- cat collection button (sits next to the theme button) ---- */
|
||||||
const catBtn = document.createElement("button");
|
const catBtn = document.createElement("button");
|
||||||
catBtn.className = "beta-btn";
|
catBtn.className = "beta-btn";
|
||||||
catBtn.id = "cat-btn";
|
catBtn.id = "cat-btn";
|
||||||
|
|
@ -138,7 +138,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/* ===================== cat.js (oneko.js) ======================= */
|
/* ===================== cat.js (oneko.js) ======================= */
|
||||||
// oneko.js: https://github.com/adryd325/oneko.js
|
/* oneko.js: https://github.com/adryd325/oneko.js */
|
||||||
|
|
||||||
(function oneko() {
|
(function oneko() {
|
||||||
const isReducedMotion =
|
const isReducedMotion =
|
||||||
|
|
@ -296,7 +296,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
|
||||||
let lastFrameTimestamp;
|
let lastFrameTimestamp;
|
||||||
|
|
||||||
function onAnimationFrame(timestamp) {
|
function onAnimationFrame(timestamp) {
|
||||||
// Stops execution if the neko element is removed from DOM
|
/* Stop running if the neko element is removed from the DOM. */
|
||||||
if (!nekoEl.isConnected) {
|
if (!nekoEl.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -323,7 +323,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
|
||||||
function idle() {
|
function idle() {
|
||||||
idleTime += 1;
|
idleTime += 1;
|
||||||
|
|
||||||
// every ~ 20 seconds
|
/* Roughly every 20 seconds. */
|
||||||
if (
|
if (
|
||||||
idleTime > 10 &&
|
idleTime > 10 &&
|
||||||
Math.floor(Math.random() * 200) == 0 &&
|
Math.floor(Math.random() * 200) == 0 &&
|
||||||
|
|
@ -392,7 +392,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
|
||||||
|
|
||||||
if (idleTime > 1) {
|
if (idleTime > 1) {
|
||||||
setSprite("alert", 0);
|
setSprite("alert", 0);
|
||||||
// count down after being alerted before moving
|
/* Count down after being alerted, before moving. */
|
||||||
idleTime = Math.min(idleTime, 7);
|
idleTime = Math.min(idleTime, 7);
|
||||||
idleTime -= 1;
|
idleTime -= 1;
|
||||||
return;
|
return;
|
||||||
|
|
@ -422,23 +422,23 @@ const BASE_SPRITE = "/assets/oneko/classics/classic.png";
|
||||||
|
|
||||||
let CAT_MODES = [];
|
let CAT_MODES = [];
|
||||||
|
|
||||||
// Order the category sections appear in the menu
|
/* Order the category sections appear in within the menu. */
|
||||||
const CATEGORY_ORDER = ["Classics", "Pride", "Cats", "Romance", "Gaming", "Pokémon", "Other Animals", "Things", "Rare"];
|
const CATEGORY_ORDER = ["Classics", "Pride", "Cats", "Romance", "Gaming", "Pokémon", "Other Animals", "Things", "Rare"];
|
||||||
|
|
||||||
// click-count goals (total clicks on the cat)
|
/* Click-count goals (total clicks on the cat). */
|
||||||
const CLICK_GOALS = { filter: 13, romance: 69, weed: 420 };
|
const CLICK_GOALS = { filter: 13, romance: 69, weed: 420 };
|
||||||
const SPRITE = BASE_SPRITE; // base sprite used for filter modes + previews
|
const SPRITE = BASE_SPRITE; /* base sprite used for filter modes + previews */
|
||||||
const IDLE_POS = "-97px -97px"; // idle frame, inset 1px to avoid neighbour-frame bleed
|
const IDLE_POS = "-97px -97px"; /* idle frame, inset 1px to avoid neighbour-frame bleed */
|
||||||
const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
|
|
||||||
(async function catModes() {
|
(async function catModes() {
|
||||||
try {
|
try {
|
||||||
// index.json lists which per-folder configs to load (one per oneko folder)
|
/* index.json lists which per-folder configs to load, one per oneko folder. */
|
||||||
const index = await fetch("/js/on/index.json").then((r) => {
|
const index = await fetch("/js/on/index.json").then((r) => {
|
||||||
if (!r.ok) throw new Error(`index.json (${r.status})`);
|
if (!r.ok) throw new Error(`index.json (${r.status})`);
|
||||||
return r.json();
|
return r.json();
|
||||||
});
|
});
|
||||||
// load every /js/on/<folder>.json and merge them into one list
|
/* Load every /js/on/<folder>.json and merge them into one list. */
|
||||||
const lists = await Promise.all(
|
const lists = await Promise.all(
|
||||||
index.map((name) =>
|
index.map((name) =>
|
||||||
fetch(`/js/on/${name}.json`)
|
fetch(`/js/on/${name}.json`)
|
||||||
|
|
@ -455,19 +455,19 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
if (!oneko) return;
|
if (!oneko) return;
|
||||||
|
|
||||||
oneko.style.pointerEvents = "auto";
|
oneko.style.pointerEvents = "auto";
|
||||||
// cursor handled in css (#oneko) so the custom pointer PNG isn't overridden
|
/* Cursor is handled in CSS (#oneko) so the custom pointer PNG isn't overridden. */
|
||||||
|
|
||||||
const ls = window.localStorage;
|
const ls = window.localStorage;
|
||||||
let clicks = parseInt(ls.getItem("onekoClicks") || "0", 10);
|
let clicks = parseInt(ls.getItem("onekoClicks") || "0", 10);
|
||||||
let mode = parseInt(ls.getItem("onekoMode") || "0", 10);
|
let mode = parseInt(ls.getItem("onekoMode") || "0", 10);
|
||||||
|
|
||||||
// permanently-earned methods (konami, gold, pokemon, + any click goal hit)
|
/* Permanently-earned methods (konami, gold, pokemon, plus any click goal hit). */
|
||||||
let unlocks;
|
let unlocks;
|
||||||
try { unlocks = new Set(JSON.parse(ls.getItem("onekoUnlocks") || "[]")); }
|
try { unlocks = new Set(JSON.parse(ls.getItem("onekoUnlocks") || "[]")); }
|
||||||
catch (e) { unlocks = new Set(); }
|
catch (e) { unlocks = new Set(); }
|
||||||
const saveUnlocks = () => ls.setItem("onekoUnlocks", JSON.stringify([...unlocks]));
|
const saveUnlocks = () => ls.setItem("onekoUnlocks", JSON.stringify([...unlocks]));
|
||||||
|
|
||||||
// Returns true if a method was newly unlocked (false if already had it)
|
/* Returns true if a method was newly unlocked, false if already had it. */
|
||||||
function unlockMethod(key) {
|
function unlockMethod(key) {
|
||||||
if (unlocks.has(key)) return false;
|
if (unlocks.has(key)) return false;
|
||||||
unlocks.add(key);
|
unlocks.add(key);
|
||||||
|
|
@ -481,7 +481,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
const key = methodOf(CAT_MODES[i]);
|
const key = methodOf(CAT_MODES[i]);
|
||||||
if (key === "gay") return true;
|
if (key === "gay") return true;
|
||||||
if (key in CLICK_GOALS) return clicks >= CLICK_GOALS[key] || unlocks.has(key);
|
if (key in CLICK_GOALS) return clicks >= CLICK_GOALS[key] || unlocks.has(key);
|
||||||
return unlocks.has(key); // konami / gold / pokemon
|
return unlocks.has(key); /* konami / gold / pokemon */
|
||||||
};
|
};
|
||||||
const unlockedIndices = () =>
|
const unlockedIndices = () =>
|
||||||
CAT_MODES.map((_, i) => i).filter(isUnlocked);
|
CAT_MODES.map((_, i) => i).filter(isUnlocked);
|
||||||
|
|
@ -492,7 +492,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
oneko.style.filter = c.filter || "none";
|
oneko.style.filter = c.filter || "none";
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---------- picker overlay (no visible trigger — press C to find it) ---------- */
|
/* ---- picker overlay (no visible trigger, press C to find it) ---- */
|
||||||
const overlay = document.createElement("div");
|
const overlay = document.createElement("div");
|
||||||
overlay.className = "cat-picker";
|
overlay.className = "cat-picker";
|
||||||
overlay.hidden = true;
|
overlay.hidden = true;
|
||||||
|
|
@ -526,14 +526,14 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
function renderGrid() {
|
function renderGrid() {
|
||||||
grid.innerHTML = "";
|
grid.innerHTML = "";
|
||||||
|
|
||||||
// bucket cat indices by category
|
/* Bucket cat indices by category. */
|
||||||
const byCat = {};
|
const byCat = {};
|
||||||
CAT_MODES.forEach((c, i) => {
|
CAT_MODES.forEach((c, i) => {
|
||||||
const cat = c.category || "Classics";
|
const cat = c.category || "Classics";
|
||||||
(byCat[cat] = byCat[cat] || []).push(i);
|
(byCat[cat] = byCat[cat] || []).push(i);
|
||||||
});
|
});
|
||||||
|
|
||||||
// known categories first (in order), then any stragglers
|
/* Known categories first, in order, then any stragglers. */
|
||||||
const order = CATEGORY_ORDER.filter((c) => byCat[c])
|
const order = CATEGORY_ORDER.filter((c) => byCat[c])
|
||||||
.concat(Object.keys(byCat).filter((c) => !CATEGORY_ORDER.includes(c)));
|
.concat(Object.keys(byCat).filter((c) => !CATEGORY_ORDER.includes(c)));
|
||||||
|
|
||||||
|
|
@ -569,7 +569,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
const closePicker = () => (overlay.hidden = true);
|
const closePicker = () => (overlay.hidden = true);
|
||||||
const togglePicker = () => (overlay.hidden ? openPicker() : closePicker());
|
const togglePicker = () => (overlay.hidden ? openPicker() : closePicker());
|
||||||
|
|
||||||
// let other scripts (e.g. the theme-bar button) open the cat menu
|
/* Let other scripts (e.g. the theme-bar button) open the cat menu. */
|
||||||
window.toggleCatPicker = togglePicker;
|
window.toggleCatPicker = togglePicker;
|
||||||
|
|
||||||
overlay
|
overlay
|
||||||
|
|
@ -579,7 +579,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
if (e.target === overlay) closePicker();
|
if (e.target === overlay) closePicker();
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
// ignore while typing in a field or with modifier keys held
|
/* Ignore while typing in a field or with a modifier key held. */
|
||||||
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName || "");
|
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName || "");
|
||||||
if (e.key === "Escape" && !overlay.hidden) {
|
if (e.key === "Escape" && !overlay.hidden) {
|
||||||
closePicker();
|
closePicker();
|
||||||
|
|
@ -596,7 +596,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ---------- toast ---------- */
|
/* ---- toast ---- */
|
||||||
let toastEl, toastTimer;
|
let toastEl, toastTimer;
|
||||||
function toast(msg) {
|
function toast(msg) {
|
||||||
if (!toastEl) {
|
if (!toastEl) {
|
||||||
|
|
@ -612,22 +612,23 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 1700);
|
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 1700);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- squeak / boop sound on click ---------- */
|
/* ---- squeak / boop sound on click ---- */
|
||||||
const boop = new Audio("/assets/oneko/boop.mp3");
|
const boop = new Audio("/assets/oneko/boop.mp3");
|
||||||
boop.preload = "auto";
|
boop.preload = "auto";
|
||||||
function playBoop() {
|
function playBoop() {
|
||||||
try {
|
try {
|
||||||
boop.currentTime = 0; // rewind so rapid clicks each squeak
|
boop.currentTime = 0; /* rewind so rapid clicks each squeak */
|
||||||
boop.play().catch(() => { }); // ignore autoplay/missing-file errors
|
boop.play().catch(() => { }); /* ignore autoplay/missing-file errors */
|
||||||
} catch (e) { /* no-op */ }
|
} catch (e) { /* no-op */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- init + cat click ---------- */
|
/* ---- init + cat click ---- */
|
||||||
if (!isUnlocked(mode)) mode = 0; // fall back to Classic if current is locked
|
if (!isUnlocked(mode)) mode = 0; /* fall back to Classic if current is locked */
|
||||||
apply(mode);
|
apply(mode);
|
||||||
|
|
||||||
// Clicking the cat no longer changes its look — it only counts toward
|
/* Clicking the cat no longer changes its look, it only counts toward
|
||||||
// the click-based unlocks (13 / 69 / 420). Pick a cat from the menu.
|
* the click-based unlocks (13 / 69 / 420). Pick a cat from the menu
|
||||||
|
* instead. */
|
||||||
oneko.addEventListener("click", (e) => {
|
oneko.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -635,20 +636,20 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
clicks += 1;
|
clicks += 1;
|
||||||
ls.setItem("onekoClicks", String(clicks));
|
ls.setItem("onekoClicks", String(clicks));
|
||||||
|
|
||||||
// Did this click hit a click-count goal exactly? (13 / 69 / 420)
|
/* Did this click hit a click-count goal exactly? (13 / 69 / 420) */
|
||||||
for (const key in CLICK_GOALS) {
|
for (const key in CLICK_GOALS) {
|
||||||
if (clicks === CLICK_GOALS[key]) {
|
if (clicks === CLICK_GOALS[key]) {
|
||||||
unlocks.add(key);
|
unlocks.add(key);
|
||||||
saveUnlocks();
|
saveUnlocks();
|
||||||
const idx = CAT_MODES.findIndex((c) => methodOf(c) === key);
|
const idx = CAT_MODES.findIndex((c) => methodOf(c) === key);
|
||||||
const name = idx >= 0 ? CAT_MODES[idx].name : key;
|
const name = idx >= 0 ? CAT_MODES[idx].name : key;
|
||||||
toast(`✨ Unlocked: ${name}! — open the cat menu 🐱`);
|
toast(`✨ Unlocked: ${name}! Open the cat menu 🐱`);
|
||||||
if (!overlay.hidden) renderGrid();
|
if (!overlay.hidden) renderGrid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ---------- Konami code → press Enter to confirm ---------- */
|
/* ---- Konami code, press Enter to confirm ---- */
|
||||||
const KONAMI = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown",
|
const KONAMI = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown",
|
||||||
"ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"];
|
"ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"];
|
||||||
let kProg = 0, kArmed = false;
|
let kProg = 0, kArmed = false;
|
||||||
|
|
@ -671,11 +672,11 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
toast("Konami code… press Enter ↵");
|
toast("Konami code… press Enter ↵");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
kProg = key === KONAMI[0] ? 1 : 0; // allow a fresh start on ↑
|
kProg = key === KONAMI[0] ? 1 : 0; /* allow a fresh start on ↑ */
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ---------- Gold → opened the site while Discord status is Idle ---------- */
|
/* ---- Gold: opened the site while Discord status is Idle ---- */
|
||||||
const dc = document.getElementById("discord");
|
const dc = document.getElementById("discord");
|
||||||
if (dc) {
|
if (dc) {
|
||||||
const checkIdle = () => {
|
const checkIdle = () => {
|
||||||
|
|
@ -688,7 +689,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
.observe(dc, { attributes: true, attributeFilter: ["data-status"] });
|
.observe(dc, { attributes: true, attributeFilter: ["data-status"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Pokémon → find & click the hidden pokéball ---------- */
|
/* ---- Pokémon: find and click the hidden pokéball ---- */
|
||||||
const poke = document.getElementById("pokeball-secret");
|
const poke = document.getElementById("pokeball-secret");
|
||||||
if (poke) {
|
if (poke) {
|
||||||
poke.addEventListener("click", (e) => {
|
poke.addEventListener("click", (e) => {
|
||||||
|
|
@ -698,10 +699,10 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Timer → keep the page open for a while ---------- */
|
/* ---- Timer: keep the page open for a while ----
|
||||||
// Counts only while the tab is visible; resets each visit, but once the
|
* Counts only while the tab is visible, and resets each visit, but
|
||||||
// goal is reached the unlock is saved for good.
|
* once the goal is reached the unlock is saved for good. */
|
||||||
const TIMER_GOAL_MS = 5 * 60 * 1000; // 5 minutes
|
const TIMER_GOAL_MS = 5 * 60 * 1000; /* 5 minutes */
|
||||||
if (!unlocks.has("timer")) {
|
if (!unlocks.has("timer")) {
|
||||||
let elapsed = 0;
|
let elapsed = 0;
|
||||||
let last = Date.now();
|
let last = Date.now();
|
||||||
|
|
@ -712,8 +713,8 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
|
||||||
last = now;
|
last = now;
|
||||||
if (elapsed >= TIMER_GOAL_MS) {
|
if (elapsed >= TIMER_GOAL_MS) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
if (unlockMethod("timer")) toast("✨ Patience pays off — timer cats unlocked!");
|
if (unlockMethod("timer")) toast("✨ Patience pays off, timer cats unlocked!");
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// ====== PASTE YOUR EMBEDDABLE JSON SHARE URLS HERE ===================
|
/* Paste your embeddable JSON share URLs here. */
|
||||||
var WAKATIME = {
|
var WAKATIME = {
|
||||||
codingActivity: "https://wakatime.com/share/@doughmination/9dcc5b5c-ed3d-4896-bfa3-87737fa70930.json",
|
codingActivity: "https://wakatime.com/share/@doughmination/9dcc5b5c-ed3d-4896-bfa3-87737fa70930.json",
|
||||||
languages: "https://wakatime.com/share/@doughmination/8354e3f8-b458-452b-aa06-839f303d4904.json",
|
languages: "https://wakatime.com/share/@doughmination/8354e3f8-b458-452b-aa06-839f303d4904.json",
|
||||||
|
|
@ -9,11 +9,10 @@
|
||||||
editors: "https://wakatime.com/share/@doughmination/38dba24b-d2de-4d50-9b09-83642c01c33e.json",
|
editors: "https://wakatime.com/share/@doughmination/38dba24b-d2de-4d50-9b09-83642c01c33e.json",
|
||||||
operatingSystems: "https://wakatime.com/share/@doughmination/a69f00cb-e38e-4de1-aa42-eec71dc6d658.json"
|
operatingSystems: "https://wakatime.com/share/@doughmination/a69f00cb-e38e-4de1-aa42-eec71dc6d658.json"
|
||||||
};
|
};
|
||||||
// How many rows to show in each ranked list.
|
/* How many rows to show in each ranked list. */
|
||||||
var MAX_ROWS = 8;
|
var MAX_ROWS = 8;
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
// ---- JSONP loader ---------------------------------------------------
|
/* ---- JSONP loader ---- */
|
||||||
var seq = 0;
|
var seq = 0;
|
||||||
function jsonp(url, timeoutMs) {
|
function jsonp(url, timeoutMs) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
|
|
@ -36,7 +35,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- helpers --------------------------------------------------------
|
/* ---- helpers ---- */
|
||||||
function fmt(seconds) {
|
function fmt(seconds) {
|
||||||
seconds = Math.max(0, Math.round(seconds || 0));
|
seconds = Math.max(0, Math.round(seconds || 0));
|
||||||
var h = Math.floor(seconds / 3600);
|
var h = Math.floor(seconds / 3600);
|
||||||
|
|
@ -50,8 +49,8 @@
|
||||||
return (p < 10 ? Math.round(p * 10) / 10 : Math.round(p)) + "%";
|
return (p < 10 ? Math.round(p * 10) / 10 : Math.round(p)) + "%";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Value shown at the end of a bar: real time when the embed provides it,
|
/* Value shown at the end of a bar: real time when the embed provides it,
|
||||||
// otherwise the percentage share.
|
* otherwise the percentage share. */
|
||||||
function valueLabel(d, hasSeconds) {
|
function valueLabel(d, hasSeconds) {
|
||||||
if (hasSeconds && (d.total_seconds || 0) > 0) return d.text || fmt(d.total_seconds);
|
if (hasSeconds && (d.total_seconds || 0) > 0) return d.text || fmt(d.total_seconds);
|
||||||
if (typeof d.percent === "number") return pctLabel(d.percent);
|
if (typeof d.percent === "number") return pctLabel(d.percent);
|
||||||
|
|
@ -66,15 +65,15 @@
|
||||||
if (m && text) { m.textContent = text; m.hidden = false; }
|
if (m && text) { m.textContent = text; m.hidden = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render a ranked list of horizontal bars into a container.
|
/* Render a ranked list of horizontal bars into a container. */
|
||||||
function renderBars(containerId, items) {
|
function renderBars(containerId, items) {
|
||||||
var box = el(containerId);
|
var box = el(containerId);
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
if (!items || !items.length) { failSection(box, "No data yet."); return; }
|
if (!items || !items.length) { failSection(box, "No data yet."); return; }
|
||||||
|
|
||||||
// Share embeds for languages/categories/editors/OS often return only
|
/* Share embeds for languages/categories/editors/OS often return only
|
||||||
// {name, percent, color} with no seconds — so fall back to percent for
|
* {name, percent, color} with no seconds, so we fall back to percent
|
||||||
// both the bar width and the value label when time isn't provided.
|
* for both the bar width and the value label when time isn't given. */
|
||||||
var hasSeconds = items.some(function (d) { return d && (d.total_seconds || 0) > 0; });
|
var hasSeconds = items.some(function (d) { return d && (d.total_seconds || 0) > 0; });
|
||||||
|
|
||||||
var rows = items
|
var rows = items
|
||||||
|
|
@ -122,7 +121,7 @@
|
||||||
showSection(box);
|
showSection(box);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the 7-day vertical bar chart + headline total.
|
/* Render the 7 day vertical bar chart and the headline total. */
|
||||||
function renderWeek(days) {
|
function renderWeek(days) {
|
||||||
var box = el("waka-week");
|
var box = el("waka-week");
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
|
|
@ -171,12 +170,15 @@
|
||||||
showSection(box);
|
showSection(box);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- shape parsers (defensive: WakaTime embed shapes vary) ----------
|
/* ---- shape parsers ----
|
||||||
// Categorical embeds (languages/categories/editors/OS) -> data:[{name,total_seconds,percent,color,text}]
|
* Defensive, because WakaTime's embed shapes vary between endpoints. */
|
||||||
|
|
||||||
|
/* Categorical embeds (languages/categories/editors/OS) ->
|
||||||
|
* data: [{name, total_seconds, percent, color, text}] */
|
||||||
function asCategorical(json) {
|
function asCategorical(json) {
|
||||||
var data = json && json.data;
|
var data = json && json.data;
|
||||||
if (!Array.isArray(data)) return [];
|
if (!Array.isArray(data)) return [];
|
||||||
// Some embeds nest under data.<key>; flatten the first array we find.
|
/* Some embeds nest under data.<key>, so flatten the first array we find. */
|
||||||
if (data.length && data[0] && data[0].name === undefined && Array.isArray(data[0])) {
|
if (data.length && data[0] && data[0].name === undefined && Array.isArray(data[0])) {
|
||||||
data = data[0];
|
data = data[0];
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +193,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coding-activity embed -> array of {label, short, total}
|
/* Coding activity embed -> array of {label, short, total} */
|
||||||
function asDays(json) {
|
function asDays(json) {
|
||||||
var data = json && json.data;
|
var data = json && json.data;
|
||||||
if (!Array.isArray(data)) return [];
|
if (!Array.isArray(data)) return [];
|
||||||
|
|
@ -199,17 +201,18 @@
|
||||||
data.forEach(function (d) {
|
data.forEach(function (d) {
|
||||||
var seconds = 0, dateStr = "";
|
var seconds = 0, dateStr = "";
|
||||||
if (d.grand_total && typeof d.grand_total.total_seconds === "number") {
|
if (d.grand_total && typeof d.grand_total.total_seconds === "number") {
|
||||||
seconds = d.grand_total.total_seconds; // daily-summaries shape
|
seconds = d.grand_total.total_seconds; /* daily-summaries shape */
|
||||||
} else if (typeof d.total_seconds === "number") {
|
} else if (typeof d.total_seconds === "number") {
|
||||||
seconds = d.total_seconds; // flat shape
|
seconds = d.total_seconds; /* flat shape */
|
||||||
}
|
}
|
||||||
if (d.range && (d.range.date || d.range.text)) {
|
if (d.range && (d.range.date || d.range.text)) {
|
||||||
dateStr = d.range.date || d.range.text;
|
dateStr = d.range.date || d.range.text;
|
||||||
} else if (d.date) {
|
} else if (d.date) {
|
||||||
dateStr = d.date;
|
dateStr = d.date;
|
||||||
}
|
}
|
||||||
// Anchor bare YYYY-MM-DD to local noon so the weekday label doesn't
|
/* Anchor a bare YYYY-MM-DD to local noon so the weekday label doesn't
|
||||||
// slip a day in timezones west of UTC (where it'd parse as UTC midnight).
|
* slip a day in timezones west of UTC, where it would otherwise parse
|
||||||
|
* as UTC midnight. */
|
||||||
var dateForParse = /^\d{4}-\d{2}-\d{2}$/.test(dateStr) ? dateStr + "T12:00:00" : dateStr;
|
var dateForParse = /^\d{4}-\d{2}-\d{2}$/.test(dateStr) ? dateStr + "T12:00:00" : dateStr;
|
||||||
var dt = dateStr ? new Date(dateForParse) : null;
|
var dt = dateStr ? new Date(dateForParse) : null;
|
||||||
var label = dt && !isNaN(dt) ? dt.toDateString() : (dateStr || "");
|
var label = dt && !isNaN(dt) ? dt.toDateString() : (dateStr || "");
|
||||||
|
|
@ -221,7 +224,7 @@
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- orchestration --------------------------------------------------
|
/* ---- orchestration ---- */
|
||||||
function load(url, onData, fallbackBoxId) {
|
function load(url, onData, fallbackBoxId) {
|
||||||
if (!url) return Promise.resolve(null);
|
if (!url) return Promise.resolve(null);
|
||||||
return jsonp(url).then(function (json) {
|
return jsonp(url).then(function (json) {
|
||||||
|
|
@ -277,7 +280,7 @@
|
||||||
}, "waka-os"));
|
}, "waka-os"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no coding-activity embed was set, hide the headline total card.
|
/* If no coding-activity embed was set, hide the headline total card. */
|
||||||
if (!WAKATIME.codingActivity) {
|
if (!WAKATIME.codingActivity) {
|
||||||
var totEl = el("waka-total");
|
var totEl = el("waka-total");
|
||||||
if (totEl) totEl.hidden = true;
|
if (totEl) totEl.hidden = true;
|
||||||
|
|
@ -292,10 +295,11 @@
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- anchor links: open the targeted <details> and scroll to it --------
|
/* ---- anchor links ----
|
||||||
// Sections are collapsible (and some are populated/un-hidden async), so a
|
* Open the targeted <details> and scroll to it. Sections are collapsible
|
||||||
// plain #hash won't reliably reveal them. Open + scroll on load and on
|
* (and some get populated/unhidden async), so a plain #hash won't reliably
|
||||||
// hashchange, retrying briefly while late content settles in.
|
* reveal them. We open and scroll on load and on hashchange, retrying
|
||||||
|
* briefly while late content settles in. */
|
||||||
function openFromHash() {
|
function openFromHash() {
|
||||||
var id = (location.hash || "").slice(1);
|
var id = (location.hash || "").slice(1);
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
@ -316,4 +320,4 @@
|
||||||
openFromHash();
|
openFromHash();
|
||||||
}
|
}
|
||||||
window.addEventListener("hashchange", openFromHash);
|
window.addEventListener("hashchange", openFromHash);
|
||||||
})();
|
})();
|
||||||
|
|
@ -745,7 +745,6 @@
|
||||||
|
|
||||||
// ---- data source: Doughmination Restful API (sole source) ---------------
|
// ---- data source: Doughmination Restful API (sole source) ---------------
|
||||||
// Returns presence + full profile (incl. theme_colors + display_name_styles)
|
// Returns presence + full profile (incl. theme_colors + display_name_styles)
|
||||||
// in a single call. Lanyard + dstn.to were removed.
|
|
||||||
const SELF_BASE = "https://restful.doughmination.uk/v1/users/";
|
const SELF_BASE = "https://restful.doughmination.uk/v1/users/";
|
||||||
const SELF_POLL_MS = opts.pollMs || 20000; // presence refresh cadence
|
const SELF_POLL_MS = opts.pollMs || 20000; // presence refresh cadence
|
||||||
let selfTimer = null;
|
let selfTimer = null;
|
||||||
|
|
@ -864,12 +863,12 @@
|
||||||
(function friends() {
|
(function friends() {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// Each friend is rendered as a full — but smaller — presence card, built by
|
/* Each friend is rendered as a full — but smaller — presence card, built by
|
||||||
// the shared factory above (window.PresenceCard). Cards pull live
|
* the shared factory above (window.PresenceCard). Cards pull live
|
||||||
// presence (status, activity, badges, banner, bio, connections, wishlist…)
|
* presence (status, activity, badges, banner, bio, connections, wishlist…)
|
||||||
// from the same Doughmination Restful API the main card uses.
|
* from the same Doughmination Restful API the main card uses.
|
||||||
// NOTE: now lives in the same file as the factory (formerly discord.js),
|
* NOTE: now lives in the same file as the factory (formerly discord.js),
|
||||||
// so load order is no longer a concern — this IIFE just runs second.
|
* so load order is no longer a concern — this IIFE just runs second. */
|
||||||
|
|
||||||
var FRIENDS = [
|
var FRIENDS = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
/* =====================================================================
|
/* fronting.js
|
||||||
* fronting.js — homepage "who's fronting" box.
|
|
||||||
*
|
*
|
||||||
* Polls the system's PluralKit-style API for the current fronter(s) and
|
* Homepage "who's fronting" box. Polls the system's PluralKit-style API
|
||||||
* renders a small card. Each member links to their page on the system
|
* for the current fronter(s) and renders a small card. Each member links
|
||||||
* site. Refreshes every 30s so switches show without a reload.
|
* to their page on the system site. Refreshes every 30s so switches show
|
||||||
* ===================================================================== */
|
* up without needing a reload. */
|
||||||
(function fronting() {
|
(function fronting() {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
|
@ -18,12 +17,12 @@
|
||||||
return String(s == null ? "" : s)
|
return String(s == null ? "" : s)
|
||||||
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||||
}
|
}
|
||||||
// member.color is a 6-char hex string (no leading #), may be null.
|
/* member.color is a 6-char hex string with no leading #, and may be null. */
|
||||||
function colorHex(c) {
|
function colorHex(c) {
|
||||||
return /^[0-9a-fA-F]{6}$/.test(c || "") ? "#" + c : null;
|
return /^[0-9a-fA-F]{6}$/.test(c || "") ? "#" + c : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- build the shell ----------------------------------------------------
|
/* ---- build the shell ---- */
|
||||||
const card = document.createElement("section");
|
const card = document.createElement("section");
|
||||||
card.id = "fronting";
|
card.id = "fronting";
|
||||||
card.className = "fronting-card";
|
card.className = "fronting-card";
|
||||||
|
|
@ -59,7 +58,7 @@
|
||||||
|
|
||||||
function render(members) {
|
function render(members) {
|
||||||
if (!Array.isArray(members) || !members.length) {
|
if (!Array.isArray(members) || !members.length) {
|
||||||
// No one registered as fronting — keep the box but say so.
|
/* No one registered as fronting, keep the box up but say so. */
|
||||||
membersEl.innerHTML = '<span class="fr-empty">no one is currently fronting</span>';
|
membersEl.innerHTML = '<span class="fr-empty">no one is currently fronting</span>';
|
||||||
card.hidden = false;
|
card.hidden = false;
|
||||||
return;
|
return;
|
||||||
|
|
@ -78,8 +77,9 @@
|
||||||
failed = false;
|
failed = false;
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
// On first failure hide the box quietly; if it was already showing,
|
/* On the first failure, hide the box quietly. If it was already
|
||||||
// leave the last-known fronters up rather than flashing an error.
|
* showing, leave the last known fronters up instead of flashing
|
||||||
|
* an error. */
|
||||||
if (!failed && card.hidden) card.hidden = true;
|
if (!failed && card.hidden) card.hidden = true;
|
||||||
failed = true;
|
failed = true;
|
||||||
});
|
});
|
||||||
|
|
@ -87,9 +87,9 @@
|
||||||
|
|
||||||
load();
|
load();
|
||||||
const timer = setInterval(load, POLL_MS);
|
const timer = setInterval(load, POLL_MS);
|
||||||
// Refresh immediately when the tab becomes visible again.
|
/* Refresh immediately when the tab becomes visible again. */
|
||||||
document.addEventListener("visibilitychange", function () {
|
document.addEventListener("visibilitychange", function () {
|
||||||
if (!document.hidden) load();
|
if (!document.hidden) load();
|
||||||
});
|
});
|
||||||
window.addEventListener("beforeunload", function () { clearInterval(timer); });
|
window.addEventListener("beforeunload", function () { clearInterval(timer); });
|
||||||
})();
|
})();
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
(function (global) {
|
(function (global) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// ---- config (from the <script> data-* attributes) ------------------
|
/* ---- config (from the <script> data-* attributes) ---- */
|
||||||
var script =
|
var script =
|
||||||
document.currentScript ||
|
document.currentScript ||
|
||||||
document.querySelector('script[src*="guestbook.js"]');
|
document.querySelector('script[src*="guestbook.js"]');
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
var STYLE_ID = "guestbook-styles";
|
var STYLE_ID = "guestbook-styles";
|
||||||
|
|
||||||
// ---- styles (injected, mirrors visitor-counter.js pattern) ----------
|
/* ---- styles (injected, mirrors the visitor-counter.js pattern) ---- */
|
||||||
function injectStyles() {
|
function injectStyles() {
|
||||||
if (document.getElementById(STYLE_ID)) return;
|
if (document.getElementById(STYLE_ID)) return;
|
||||||
var s = document.createElement("style");
|
var s = document.createElement("style");
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
".gb-status { font-size: 0.82rem; color: var(--subtext-0); }",
|
".gb-status { font-size: 0.82rem; color: var(--subtext-0); }",
|
||||||
".gb-status.gb-err { color: var(--red); }",
|
".gb-status.gb-err { color: var(--red); }",
|
||||||
".gb-status.gb-ok { color: var(--green); }",
|
".gb-status.gb-ok { color: var(--green); }",
|
||||||
// honeypot: visually hidden but still in the DOM for bots
|
/* honeypot: visually hidden but still in the DOM for bots */
|
||||||
".gb-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }",
|
".gb-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }",
|
||||||
".gb-entries { display: flex; flex-direction: column; gap: 0.9rem; padding-bottom: 4.5rem; }",
|
".gb-entries { display: flex; flex-direction: column; gap: 0.9rem; padding-bottom: 4.5rem; }",
|
||||||
".gb-empty { color: var(--subtext-0); text-align: center; font-size: 0.9rem; }",
|
".gb-empty { color: var(--subtext-0); text-align: center; font-size: 0.9rem; }",
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
document.head.appendChild(s);
|
document.head.appendChild(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- helpers --------------------------------------------------------
|
/* ---- helpers ---- */
|
||||||
function esc(str) {
|
function esc(str) {
|
||||||
return String(str == null ? "" : str)
|
return String(str == null ? "" : str)
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
|
|
@ -111,7 +111,7 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- rendering ------------------------------------------------------
|
/* ---- rendering ---- */
|
||||||
var entriesEl, formEl, statusEl, submitEl, counterEl, msgEl;
|
var entriesEl, formEl, statusEl, submitEl, counterEl, msgEl;
|
||||||
|
|
||||||
function setStatus(text, kind) {
|
function setStatus(text, kind) {
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
function renderEntries(list) {
|
function renderEntries(list) {
|
||||||
if (!entriesEl) return;
|
if (!entriesEl) return;
|
||||||
if (!list || !list.length) {
|
if (!list || !list.length) {
|
||||||
entriesEl.innerHTML = '<p class="gb-empty">No messages yet — be the first to sign!</p>';
|
entriesEl.innerHTML = '<p class="gb-empty">No messages yet, be the first to sign!</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
entriesEl.innerHTML = list.map(entryHTML).join("");
|
entriesEl.innerHTML = list.map(entryHTML).join("");
|
||||||
|
|
@ -146,14 +146,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Turnstile (optional) ------------------------------------------
|
/* ---- Turnstile (optional) ---- */
|
||||||
function turnstileToken() {
|
function turnstileToken() {
|
||||||
try {
|
try {
|
||||||
if (global.turnstile && typeof global.turnstile.getResponse === "function") {
|
if (global.turnstile && typeof global.turnstile.getResponse === "function") {
|
||||||
return global.turnstile.getResponse() || "";
|
return global.turnstile.getResponse() || "";
|
||||||
}
|
}
|
||||||
} catch (_) { }
|
} catch (_) { }
|
||||||
// Fallback: the widget injects a hidden input named cf-turnstile-response
|
/* Fallback: the widget injects a hidden input named cf-turnstile-response. */
|
||||||
var input = document.querySelector('[name="cf-turnstile-response"]');
|
var input = document.querySelector('[name="cf-turnstile-response"]');
|
||||||
return input ? input.value : "";
|
return input ? input.value : "";
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +173,7 @@
|
||||||
document.head.appendChild(s);
|
document.head.appendChild(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- submit ---------------------------------------------------------
|
/* ---- submit ---- */
|
||||||
async function onSubmit(ev) {
|
async function onSubmit(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (!API) {
|
if (!API) {
|
||||||
|
|
@ -185,7 +185,7 @@
|
||||||
name: formEl.name.value,
|
name: formEl.name.value,
|
||||||
website: formEl.website.value,
|
website: formEl.website.value,
|
||||||
message: formEl.message.value,
|
message: formEl.message.value,
|
||||||
url2: formEl.url2.value, // honeypot
|
url2: formEl.url2.value, /* honeypot */
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!payload.name.trim() || !payload.message.trim()) {
|
if (!payload.name.trim() || !payload.message.trim()) {
|
||||||
|
|
@ -225,13 +225,13 @@
|
||||||
await loadEntries();
|
await loadEntries();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[guestbook] submit failed", err);
|
console.error("[guestbook] submit failed", err);
|
||||||
setStatus("Network error — please try again.", "err");
|
setStatus("Network error, please try again.", "err");
|
||||||
} finally {
|
} finally {
|
||||||
submitEl.disabled = false;
|
submitEl.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- init -----------------------------------------------------------
|
/* ---- init ---- */
|
||||||
function init() {
|
function init() {
|
||||||
injectStyles();
|
injectStyles();
|
||||||
entriesEl = document.getElementById("gb-entries");
|
entriesEl = document.getElementById("gb-entries");
|
||||||
|
|
@ -262,4 +262,4 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
global.Guestbook = { reload: loadEntries };
|
global.Guestbook = { reload: loadEntries };
|
||||||
})(typeof window !== "undefined" ? window : this);
|
})(typeof window !== "undefined" ? window : this);
|
||||||
|
|
@ -96,7 +96,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- pride palettes: [empty, level1, level2, level3, level4] ---- */
|
/* ---- pride palettes: [empty, level1, level2, level3, level4] ---- */
|
||||||
const N = "#20262e"; // shared neutral "empty" so lit cells pop on a dark card
|
const N = "#20262e"; /* shared neutral "empty" so lit cells pop on a dark card */
|
||||||
const THEMES = {
|
const THEMES = {
|
||||||
forest: ["#232a33", "#173f2c", "#1e7349", "#34ab68", "#5ce897"],
|
forest: ["#232a33", "#173f2c", "#1e7349", "#34ab68", "#5ce897"],
|
||||||
rainbow: [N, "#e40303", "#ff8c00", "#ffed00", "#2ecc40"],
|
rainbow: [N, "#e40303", "#ff8c00", "#ffed00", "#2ecc40"],
|
||||||
|
|
@ -113,14 +113,14 @@
|
||||||
|
|
||||||
function resolveTheme(theme) {
|
function resolveTheme(theme) {
|
||||||
if (Array.isArray(theme)) {
|
if (Array.isArray(theme)) {
|
||||||
return theme.length === 4 ? [N].concat(theme) : theme; // allow 4 lit colors
|
return theme.length === 4 ? [N].concat(theme) : theme; /* allow 4 lit colors */
|
||||||
}
|
}
|
||||||
return THEMES[theme] || THEMES.rainbow;
|
return THEMES[theme] || THEMES.rainbow;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
|
|
||||||
// track in-flight fetch per container so re-renders cancel cleanly
|
/* Track the in-flight fetch per container so re-renders cancel cleanly. */
|
||||||
const controllers = new WeakMap();
|
const controllers = new WeakMap();
|
||||||
const observers = new WeakMap();
|
const observers = new WeakMap();
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@
|
||||||
months: true, weekdays: true, legend: true, count: true,
|
months: true, weekdays: true, legend: true, count: true,
|
||||||
fit: false, minCell: 8, maxCell: 13, gap: 3,
|
fit: false, minCell: 8, maxCell: 13, gap: 3,
|
||||||
}, options);
|
}, options);
|
||||||
opts.cell = opts.cell || opts.maxCell; // initial size before auto-fit kicks in
|
opts.cell = opts.cell || opts.maxCell; /* initial size before auto-fit kicks in */
|
||||||
|
|
||||||
const root = typeof target === "string" ? document.querySelector(target) : target;
|
const root = typeof target === "string" ? document.querySelector(target) : target;
|
||||||
if (!root) {
|
if (!root) {
|
||||||
|
|
@ -141,7 +141,7 @@
|
||||||
|
|
||||||
injectStyles();
|
injectStyles();
|
||||||
|
|
||||||
// cancel a previous render on this element (route change / re-mount)
|
/* Cancel a previous render on this element (route change or re-mount). */
|
||||||
const prev = controllers.get(root);
|
const prev = controllers.get(root);
|
||||||
if (prev) prev.abort();
|
if (prev) prev.abort();
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@ -150,7 +150,7 @@
|
||||||
const prevObs = observers.get(root);
|
const prevObs = observers.get(root);
|
||||||
if (prevObs) { prevObs.disconnect(); observers.delete(root); }
|
if (prevObs) { prevObs.disconnect(); observers.delete(root); }
|
||||||
|
|
||||||
// (re)build the shell
|
/* (Re)build the shell. */
|
||||||
root.classList.add("ch-root");
|
root.classList.add("ch-root");
|
||||||
root.style.setProperty("--ch-cell", opts.cell + "px");
|
root.style.setProperty("--ch-cell", opts.cell + "px");
|
||||||
root.style.setProperty("--ch-gap", opts.gap + "px");
|
root.style.setProperty("--ch-gap", opts.gap + "px");
|
||||||
|
|
@ -227,8 +227,8 @@
|
||||||
const since = weeks.length ? weeks[0][0].date : null;
|
const since = weeks.length ? weeks[0][0].date : null;
|
||||||
if (countEl) countEl.textContent = `${total} contributions since ${since}`;
|
if (countEl) countEl.textContent = `${total} contributions since ${since}`;
|
||||||
|
|
||||||
// scale day squares so the full span fits the container width
|
/* Scale the day squares so the full span fits the container width,
|
||||||
// (instead of overflowing past the page's content column)
|
* instead of overflowing past the page's content column. */
|
||||||
if (opts.fit && weeks.length) {
|
if (opts.fit && weeks.length) {
|
||||||
const fitCells = () => {
|
const fitCells = () => {
|
||||||
const w = parseFloat(getComputedStyle(root).getPropertyValue("--ch-weekday-w")) || 0;
|
const w = parseFloat(getComputedStyle(root).getPropertyValue("--ch-weekday-w")) || 0;
|
||||||
|
|
@ -248,7 +248,7 @@
|
||||||
return { total, since, weeks };
|
return { total, since, weeks };
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (err.name === "AbortError") return null; // superseded by a newer render
|
if (err.name === "AbortError") return null; /* superseded by a newer render */
|
||||||
if (countEl) countEl.textContent = "Couldn't load contributions.";
|
if (countEl) countEl.textContent = "Couldn't load contributions.";
|
||||||
console.error("[heatmap]", err);
|
console.error("[heatmap]", err);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -379,7 +379,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// PRESENCE — Doughmination Restful API (replaces the Lanyard socket)
|
// PRESENCE — Doughmination Restful API
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// Same now-playing data, pulled from the self-hosted API the rest of the
|
// Same now-playing data, pulled from the self-hosted API the rest of the
|
||||||
// site uses. It's request/response (not a socket), so we poll; the tick()
|
// site uses. It's request/response (not a socket), so we poll; the tick()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
/* ============================================================
|
/* selfies.js
|
||||||
selfies.js — renders the /selfies gallery from a manifest
|
*
|
||||||
Manifest: /assets/selfies/selfies.json
|
* Renders the /selfies gallery from a manifest at
|
||||||
- array of filename strings, or { "src", "alt", "caption" } objects
|
* /assets/selfies/selfies.json. The manifest is either an array of
|
||||||
- shown in list order (newest first = top of the list)
|
* filename strings, or an array of {"src", "alt", "caption"} objects,
|
||||||
"alt" is for screen readers; "caption" (optional) is shown on the page.
|
* and is shown in list order (newest first goes at the top of the list).
|
||||||
Click any thumbnail to open it full-size in a lightbox.
|
* "alt" is for screen readers, "caption" is optional and shown on the
|
||||||
============================================================ */
|
* page. Click any thumbnail to open it full size in a lightbox. */
|
||||||
(function selfies() {
|
(function selfies() {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
"(prefers-reduced-motion: reduce)"
|
"(prefers-reduced-motion: reduce)"
|
||||||
).matches;
|
).matches;
|
||||||
|
|
||||||
/* ---------- helpers ---------- */
|
/* ---- helpers ---- */
|
||||||
// Resolve a manifest src to a usable URL.
|
/* Resolve a manifest src to a usable URL. */
|
||||||
function resolveSrc(s) {
|
function resolveSrc(s) {
|
||||||
if (typeof s !== "string") return "";
|
if (typeof s !== "string") return "";
|
||||||
s = s.trim();
|
s = s.trim();
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
return FOLDER + s.replace(/^\.?\//, "");
|
return FOLDER + s.replace(/^\.?\//, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalise a manifest entry into { src, alt, caption } or null if unusable.
|
/* Normalise a manifest entry into { src, alt, caption }, or null if unusable. */
|
||||||
function normalize(entry, i) {
|
function normalize(entry, i) {
|
||||||
let raw = "";
|
let raw = "";
|
||||||
let alt = "";
|
let alt = "";
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
root.appendChild(p);
|
root.appendChild(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- lightbox ---------- */
|
/* ---- lightbox ---- */
|
||||||
let items = [];
|
let items = [];
|
||||||
let current = 0;
|
let current = 0;
|
||||||
let lastFocus = null;
|
let lastFocus = null;
|
||||||
|
|
@ -88,7 +88,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(i) {
|
function render(i) {
|
||||||
current = (i + items.length) % items.length; // wrap around
|
current = (i + items.length) % items.length; /* wrap around */
|
||||||
const it = items[current];
|
const it = items[current];
|
||||||
lbImg.src = it.src;
|
lbImg.src = it.src;
|
||||||
lbImg.alt = it.alt;
|
lbImg.alt = it.alt;
|
||||||
|
|
@ -131,7 +131,7 @@
|
||||||
btnClose.addEventListener("click", close);
|
btnClose.addEventListener("click", close);
|
||||||
btnNext.addEventListener("click", next);
|
btnNext.addEventListener("click", next);
|
||||||
btnPrev.addEventListener("click", prev);
|
btnPrev.addEventListener("click", prev);
|
||||||
// Click on the dim backdrop (but not the image/caption or buttons) closes.
|
/* Click on the dim backdrop (but not the image, caption, or buttons) closes. */
|
||||||
lb.addEventListener("click", (e) => {
|
lb.addEventListener("click", (e) => {
|
||||||
if (e.target === lb) close();
|
if (e.target === lb) close();
|
||||||
});
|
});
|
||||||
|
|
@ -142,7 +142,7 @@
|
||||||
else if (e.key === "ArrowLeft") prev();
|
else if (e.key === "ArrowLeft") prev();
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ---------- grid ---------- */
|
/* ---- grid ---- */
|
||||||
function buildGrid(list) {
|
function buildGrid(list) {
|
||||||
root.innerHTML = "";
|
root.innerHTML = "";
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
|
|
@ -160,7 +160,7 @@
|
||||||
img.alt = it.alt;
|
img.alt = it.alt;
|
||||||
img.loading = i < 4 ? "eager" : "lazy";
|
img.loading = i < 4 ? "eager" : "lazy";
|
||||||
img.decoding = "async";
|
img.decoding = "async";
|
||||||
// If an image fails to load, drop its tile so the grid stays clean.
|
/* If an image fails to load, drop its tile so the grid stays clean. */
|
||||||
img.addEventListener("error", () => fig.remove());
|
img.addEventListener("error", () => fig.remove());
|
||||||
|
|
||||||
btn.appendChild(img);
|
btn.appendChild(img);
|
||||||
|
|
@ -179,7 +179,7 @@
|
||||||
root.appendChild(frag);
|
root.appendChild(frag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- load ---------- */
|
/* ---- load ---- */
|
||||||
root.setAttribute("aria-busy", "true");
|
root.setAttribute("aria-busy", "true");
|
||||||
fetch(MANIFEST, { cache: "no-cache" })
|
fetch(MANIFEST, { cache: "no-cache" })
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
|
|
@ -190,7 +190,7 @@
|
||||||
if (!Array.isArray(data)) throw new Error("manifest is not an array");
|
if (!Array.isArray(data)) throw new Error("manifest is not an array");
|
||||||
items = data.map(normalize).filter(Boolean);
|
items = data.map(normalize).filter(Boolean);
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
showMessage("No selfies yet — check back soon! 📸");
|
showMessage("No selfies yet, check back soon! 📸");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
buildGrid(items);
|
buildGrid(items);
|
||||||
|
|
@ -200,4 +200,4 @@
|
||||||
showMessage("Couldn't load the selfies right now.");
|
showMessage("Couldn't load the selfies right now.");
|
||||||
})
|
})
|
||||||
.finally(() => root.removeAttribute("aria-busy"));
|
.finally(() => root.removeAttribute("aria-busy"));
|
||||||
})();
|
})();
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
/* =====================================================================
|
/* terminal.js
|
||||||
* terminal.js — the homepage's interactive terminal.
|
|
||||||
*
|
*
|
||||||
* Flow: a short boot log streams in, the side chrome fades in alongside
|
* The homepage's interactive terminal. Flow: a short boot log streams
|
||||||
* it, then the banner + a pinned prompt appear. You type a command and
|
* in, the side chrome fades in alongside it, then the banner and a
|
||||||
* the output is appended to the scrollback BELOW the input — the input
|
* pinned prompt appear. You type a command and the output gets
|
||||||
* itself never moves.
|
* appended to the scrollback below the input, the input itself never
|
||||||
* ===================================================================== */
|
* moves. */
|
||||||
(function terminal() {
|
(function terminal() {
|
||||||
const root = document.getElementById("terminal");
|
const root = document.getElementById("terminal");
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
|
|
||||||
// arch.ascii (hyfetch format) is fetched once at startup for `hyfetch`.
|
/* arch.ascii (hyfetch format) is fetched once at startup for `hyfetch`. */
|
||||||
let archLines = null;
|
let archLines = null;
|
||||||
function loadArt() {
|
function loadArt() {
|
||||||
fetch("/arch.ascii").then(function (r) { return r.ok ? r.text() : ""; }).then(function (t) {
|
fetch("/arch.ascii").then(function (r) { return r.ok ? r.text() : ""; }).then(function (t) {
|
||||||
|
|
@ -23,7 +22,7 @@
|
||||||
}).catch(function () { });
|
}).catch(function () { });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- ascii banner -------------------------------------------------------
|
/* ---- ascii banner ---- */
|
||||||
const BANNER = [
|
const BANNER = [
|
||||||
" ██████╗██╗ ██████╗ ██╗ ██╗███████╗",
|
" ██████╗██╗ ██████╗ ██╗ ██╗███████╗",
|
||||||
"██╔════╝██║ ██╔═══██╗██║ ██║██╔════╝",
|
"██╔════╝██║ ██╔═══██╗██║ ██║██╔════╝",
|
||||||
|
|
@ -33,7 +32,7 @@
|
||||||
" ╚═════╝╚══════╝ ╚═════╝ ╚═══╝ ╚══════╝"
|
" ╚═════╝╚══════╝ ╚═════╝ ╚═══╝ ╚══════╝"
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
// ---- boot log -----------------------------------------------------------
|
/* ---- boot log ---- */
|
||||||
const BOOT = [
|
const BOOT = [
|
||||||
["info", "starting clovesh..."],
|
["info", "starting clovesh..."],
|
||||||
["info", "mounting /dev/estrogen..."],
|
["info", "mounting /dev/estrogen..."],
|
||||||
|
|
@ -42,15 +41,15 @@
|
||||||
["ok", "modules loaded"],
|
["ok", "modules loaded"],
|
||||||
["info", "summoning cats..."],
|
["info", "summoning cats..."],
|
||||||
["ok", "oneko ready"],
|
["ok", "oneko ready"],
|
||||||
["info", "connecting to discord via lanyard..."],
|
["info", "connecting to discord via restful..."],
|
||||||
["ok", "presence online"],
|
["ok", "presence online"],
|
||||||
["info", "mounting button wall..."],
|
["info", "mounting button wall..."],
|
||||||
["ok", "88x31 buttons hung"],
|
["ok", "88x31 buttons hung"],
|
||||||
["info", "starting terminal..."],
|
["info", "starting terminal..."],
|
||||||
["ok", "ready — type 'help'"]
|
["ok", "ready, type 'help'"]
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---- build DOM ----------------------------------------------------------
|
/* ---- build DOM ---- */
|
||||||
root.innerHTML =
|
root.innerHTML =
|
||||||
'<pre class="t-boot" id="t-boot" aria-hidden="true"></pre>' +
|
'<pre class="t-boot" id="t-boot" aria-hidden="true"></pre>' +
|
||||||
'<div class="t-main" id="t-main" hidden>' +
|
'<div class="t-main" id="t-main" hidden>' +
|
||||||
|
|
@ -76,7 +75,7 @@
|
||||||
return new Date().toLocaleTimeString("en-GB", { hour12: false });
|
return new Date().toLocaleTimeString("en-GB", { hour12: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- command handlers ---------------------------------------------------
|
/* ---- command handlers ---- */
|
||||||
const COMMANDS = {
|
const COMMANDS = {
|
||||||
help() {
|
help() {
|
||||||
const rows = [
|
const rows = [
|
||||||
|
|
@ -123,7 +122,7 @@
|
||||||
about() {
|
about() {
|
||||||
return {
|
return {
|
||||||
text:
|
text:
|
||||||
"Clove Twilight — fae/faer\n" +
|
"Clove Twilight, fae/faer\n" +
|
||||||
"Transfem developer from Southampton, UK. I make Projects,\n" +
|
"Transfem developer from Southampton, UK. I make Projects,\n" +
|
||||||
"personal-site nonsense, and run a small corner of the internet\n" +
|
"personal-site nonsense, and run a small corner of the internet\n" +
|
||||||
"under the trade mark 'doughmination system'. Big on Linux, Catppuccin, and cats."
|
"under the trade mark 'doughmination system'. Big on Linux, Catppuccin, and cats."
|
||||||
|
|
@ -160,7 +159,7 @@
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- runtime ------------------------------------------------------------
|
/* ---- runtime ---- */
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
function uptime() {
|
function uptime() {
|
||||||
let s = Math.floor((Date.now() - startedAt) / 1000);
|
let s = Math.floor((Date.now() - startedAt) / 1000);
|
||||||
|
|
@ -213,7 +212,7 @@
|
||||||
showResult({ text: "clovesh: command not found: " + name + "\nType 'help' for a list.", error: true });
|
showResult({ text: "clovesh: command not found: " + name + "\nType 'help' for a list.", error: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- tab-complete + history --------------------------------------------
|
/* ---- tab-complete + history ---- */
|
||||||
const COMPLETIONS = Object.keys(COMMANDS);
|
const COMPLETIONS = Object.keys(COMMANDS);
|
||||||
function complete(prefix) {
|
function complete(prefix) {
|
||||||
if (!prefix) return null;
|
if (!prefix) return null;
|
||||||
|
|
@ -250,7 +249,7 @@
|
||||||
if ((window.getSelection() + "") === "") input.focus();
|
if ((window.getSelection() + "") === "") input.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- boot then reveal ---------------------------------------------------
|
/* ---- boot then reveal ---- */
|
||||||
document.body.classList.add("term-booting");
|
document.body.classList.add("term-booting");
|
||||||
|
|
||||||
let booted = false;
|
let booted = false;
|
||||||
|
|
@ -285,4 +284,4 @@
|
||||||
loadArt();
|
loadArt();
|
||||||
requestAnimationFrame(() => document.body.classList.add("term-chrome-in"));
|
requestAnimationFrame(() => document.body.classList.add("term-chrome-in"));
|
||||||
streamBoot(0);
|
streamBoot(0);
|
||||||
})();
|
})();
|
||||||
|
|
@ -44,17 +44,16 @@
|
||||||
document.head.appendChild(s);
|
document.head.appendChild(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/* Returns the cached visitor count for this session, or null if not
|
||||||
* Returns the cached visitor count for this session, or null if not cached.
|
* cached. The cache key is scoped to namespace + key so multiple
|
||||||
* The cache key is scoped to namespace+key so multiple counters don't collide.
|
* counters don't collide. */
|
||||||
*/
|
|
||||||
function getCached(namespace, key) {
|
function getCached(namespace, key) {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem("vc:" + namespace + ":" + key);
|
const raw = localStorage.getItem("vc:" + namespace + ":" + key);
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const { count, session } = JSON.parse(raw);
|
const { count, session } = JSON.parse(raw);
|
||||||
// Use sessionStorage token to detect new tabs vs. refreshes.
|
/* Use a sessionStorage token to tell a new tab apart from a refresh.
|
||||||
// A refresh keeps the same sessionStorage; a new tab starts fresh.
|
* A refresh keeps the same sessionStorage, a new tab starts fresh. */
|
||||||
const token = sessionStorage.getItem("vc-session");
|
const token = sessionStorage.getItem("vc-session");
|
||||||
if (token && token === session) return count;
|
if (token && token === session) return count;
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -65,7 +64,7 @@
|
||||||
|
|
||||||
function setCached(namespace, key, count) {
|
function setCached(namespace, key, count) {
|
||||||
try {
|
try {
|
||||||
// Create (or reuse) a session token so this count is tied to the tab session.
|
/* Create (or reuse) a session token so this count is tied to the tab session. */
|
||||||
let token = sessionStorage.getItem("vc-session");
|
let token = sessionStorage.getItem("vc-session");
|
||||||
if (!token) {
|
if (!token) {
|
||||||
token = Math.random().toString(36).slice(2);
|
token = Math.random().toString(36).slice(2);
|
||||||
|
|
@ -83,7 +82,7 @@
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) throw new Error("Abacus HTTP " + res.status);
|
if (!res.ok) throw new Error("Abacus HTTP " + res.status);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
// Abacus returns { value: <number> }
|
/* Abacus returns { value: <number> }. */
|
||||||
if (typeof data.value !== "number") throw new Error("Unexpected response shape");
|
if (typeof data.value !== "number") throw new Error("Unexpected response shape");
|
||||||
return data.value;
|
return data.value;
|
||||||
}
|
}
|
||||||
|
|
@ -135,14 +134,15 @@
|
||||||
root.appendChild(labelEl);
|
root.appendChild(labelEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try cache first — avoids hitting the API (and incrementing) on refresh
|
/* Try the cache first, this avoids hitting the API (and incrementing
|
||||||
|
* the count) on every refresh. */
|
||||||
const cached = getCached(opts.namespace, opts.key);
|
const cached = getCached(opts.namespace, opts.key);
|
||||||
if (cached !== null) {
|
if (cached !== null) {
|
||||||
renderDigits(digitsEl, cached, opts.imgPath, opts.imgExt);
|
renderDigits(digitsEl, cached, opts.imgPath, opts.imgExt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First visit in this session: hit the API
|
/* First visit in this session, so hit the API. */
|
||||||
try {
|
try {
|
||||||
const count = await fetchCount(opts.namespace, opts.key);
|
const count = await fetchCount(opts.namespace, opts.key);
|
||||||
setCached(opts.namespace, opts.key, count);
|
setCached(opts.namespace, opts.key, count);
|
||||||
|
|
@ -151,7 +151,7 @@
|
||||||
console.error("[visitor-counter]", err);
|
console.error("[visitor-counter]", err);
|
||||||
const errEl = document.createElement("span");
|
const errEl = document.createElement("span");
|
||||||
errEl.className = "vc-error";
|
errEl.className = "vc-error";
|
||||||
errEl.textContent = "— visitors";
|
errEl.textContent = "?? visitors";
|
||||||
digitsEl.replaceWith(errEl);
|
digitsEl.replaceWith(errEl);
|
||||||
if (opts.label && root.querySelector(".vc-label")) {
|
if (opts.label && root.querySelector(".vc-label")) {
|
||||||
root.querySelector(".vc-label").remove();
|
root.querySelector(".vc-label").remove();
|
||||||
|
|
@ -159,7 +159,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-init from script tag attributes
|
/* Auto-init from script tag attributes. */
|
||||||
function autoInit() {
|
function autoInit() {
|
||||||
const script =
|
const script =
|
||||||
document.currentScript ||
|
document.currentScript ||
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue