Static site
Browse files- index.html +1 -1
- static/hub.js +206 -204
index.html
CHANGED
|
@@ -29,7 +29,7 @@
|
|
| 29 |
|
| 30 |
<nav id="nav">
|
| 31 |
<button class="logo" data-go="home" aria-label="VOID home">VOID</button>
|
| 32 |
-
<span class="credit">Made by Legion</span>
|
| 33 |
<div class="right">
|
| 34 |
<button id="snd" class="pill" aria-pressed="false" title="Music and engine sound">Sound off</button>
|
| 35 |
<button id="exit" class="pill" title="Back home (Esc)">✕</button>
|
|
|
|
| 29 |
|
| 30 |
<nav id="nav">
|
| 31 |
<button class="logo" data-go="home" aria-label="VOID home">VOID</button>
|
| 32 |
+
<span class="credit"><img src="data:image/png;base64,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" alt="" width="14" height="14" style="width:14px;height:14px;border-radius:3px;vertical-align:-3px;margin-right:5px">Made by Legion</span>
|
| 33 |
<div class="right">
|
| 34 |
<button id="snd" class="pill" aria-pressed="false" title="Music and engine sound">Sound off</button>
|
| 35 |
<button id="exit" class="pill" title="Back home (Esc)">✕</button>
|
static/hub.js
CHANGED
|
@@ -1,204 +1,206 @@
|
|
| 1 |
-
/* Universal layer, client side. One Cramly account works in every app, and the owner's switches reach every app:
|
| 2 |
-
off / ban / time-out screens, messages, and harmless trolls. Add to a page with:
|
| 3 |
-
<script src="/static/hub.js" data-app="alibi" defer></script>
|
| 4 |
-
It fails open: if Cramly cannot be reached the app simply works as normal. */
|
| 5 |
-
(() => {
|
| 6 |
-
"use strict";
|
| 7 |
-
const me = document.currentScript;
|
| 8 |
-
const APP = (me && me.dataset.app) || "app";
|
| 9 |
-
const HUB = ((me && me.dataset.hub) || "https://cramly-twz5.onrender.com").replace(/\/$/, "");
|
| 10 |
-
const store = {
|
| 11 |
-
get: k => { try { return localStorage.getItem("hub." + k) || ""; } catch (e) { return ""; } },
|
| 12 |
-
set: (k, v) => { try { v ? localStorage.setItem("hub." + k, v) : localStorage.removeItem("hub." + k); } catch (e) { /* private mode */ } },
|
| 13 |
-
};
|
| 14 |
-
const keyOf = () => { // inside Cramly the signed-in account is used as it is
|
| 15 |
-
const k = store.get("key");
|
| 16 |
-
if (k || APP !== "cramly") return k;
|
| 17 |
-
try { return JSON.parse(localStorage.getItem("cramly.key") || "null") || ""; } catch (e) { return ""; }
|
| 18 |
-
};
|
| 19 |
-
let dev = store.get("dev");
|
| 20 |
-
if (!dev) { dev = (crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2) + Date.now().toString(36)).replace(/-/g, "").slice(0, 24); store.set("dev", dev); }
|
| 21 |
-
window.HUB = { dev: () => dev, key: keyOf, app: APP };
|
| 22 |
-
|
| 23 |
-
/* every same-origin request carries who is asking, so the server can enforce a ban too */
|
| 24 |
-
const realFetch = window.fetch.bind(window);
|
| 25 |
-
window.fetch = (input, init = {}) => {
|
| 26 |
-
try {
|
| 27 |
-
const url = new URL(typeof input === "string" ? input : input.url, location.href);
|
| 28 |
-
if (url.origin === location.origin) { init = { ...init, headers: { ...(init.headers instanceof Headers ? Object.fromEntries(init.headers) : init.headers || {}), "X-Hub-Dev": dev, ...(keyOf() ? { "X-Hub-Key": keyOf() } : {}) } }; }
|
| 29 |
-
} catch (e) { /* leave it alone */ }
|
| 30 |
-
return realFetch(input, init);
|
| 31 |
-
};
|
| 32 |
-
|
| 33 |
-
const css = `
|
| 34 |
-
.hub-ov{position:fixed;inset:0;z-index:2147483000;display:grid;place-items:center;background:#0a0a0b;color:#ececf0;font:16px/1.5 system-ui,-apple-system,Segoe UI,sans-serif;padding:24px;text-align:center}
|
| 35 |
-
.hub-ov h2{margin:0 0 8px;font-size:26px;font-weight:700;letter-spacing:-.01em}.hub-ov p{margin:0 auto 14px;max-width:34ch;color:#9a9aa3}.hub-ov .em{font-size:48px;margin-bottom:10px}
|
| 36 |
-
.hub-ov small{color:#6c6c75}
|
| 37 |
-
.hub-chip{position:fixed;left:12px;bottom:12px;z-index:2147482000;width:34px;height:34px;border-radius:50%;border:1px solid #2a2a30;background:#121214;color:#b9b9c2;font:600 13px system-ui;cursor:pointer;opacity:.55;display:grid;place-items:center;padding:0}
|
| 38 |
-
.hub-chip:hover,.hub-chip:focus-visible{opacity:1;outline:2px solid #5b93ff}
|
| 39 |
-
.hub-box{position:fixed;left:12px;bottom:54px;z-index:2147482001;width:min(320px,calc(100vw - 24px));background:#121214;border:1px solid #2a2a30;border-radius:16px;padding:16px;color:#ececf0;font:14px/1.45 system-ui,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.6)}
|
| 40 |
-
.hub-box h3{margin:0 0 6px;font-size:16px}.hub-box p{margin:0 0 10px;color:#9a9aa3}.hub-box input{width:100%;box-sizing:border-box;padding:10px 12px;border-radius:10px;border:1px solid #2a2a30;background:#0a0a0b;color:#ececf0;font:600 18px ui-monospace,monospace;letter-spacing:.2em;text-align:center}
|
| 41 |
-
.hub-box button{margin-top:10px;width:100%;padding:10px;border-radius:10px;border:0;background:#5b93ff;color:#08101f;font:700 14px system-ui;cursor:pointer}.hub-box button.alt{background:#1b1b1f;color:#ececf0}
|
| 42 |
-
.hub-box .err{color:#ff6b74;margin-top:8px;min-height:1em}
|
| 43 |
-
.hub-box input.hub-in{font:15px system-ui,sans-serif;letter-spacing:0;text-align:left;margin-top:8px}.hub-box button:disabled{opacity:.6}
|
| 44 |
-
.hub-links{margin:10px 0 0!important;font-size:12.5px}.hub-links a{color:#8fb4ff}.hub-small{margin:10px 0 0!important;font-size:11.5px;color:#6c6c75!important}
|
| 45 |
-
.hub-toast{position:fixed;left:50%;top:14px;transform:translateX(-50%);z-index:2147483100;max-width:min(92vw,420px);background:#121214;border:1px solid #2a2a30;color:#ececf0;padding:12px 16px;border-radius:14px;font:14px/1.45 system-ui,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.6);animation:hubIn .3s ease}
|
| 46 |
-
.hub-toast b{display:block;font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:#8d8d96;margin-bottom:2px}
|
| 47 |
-
@keyframes hubIn{from{opacity:0;transform:translate(-50%,-10px)}}
|
| 48 |
-
@keyframes hubFlip{to{transform:rotate(180deg)}}@keyframes hubShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-8px)}75%{transform:translateX(8px)}}
|
| 49 |
-
@keyframes hubHue{to{filter:hue-rotate(360deg)}}@keyframes hubSpin{to{transform:rotate(360deg)}}@keyframes hubFall{to{transform:translateY(110vh) rotate(540deg)}}
|
| 50 |
-
@keyframes hubDisco{0%{background:#f0b429}33%{background:#3dd49b}66%{background:#5b93ff}100%{background:#ff6b74}}
|
| 51 |
-
.hub-credit{position:fixed;right:10px;bottom:calc(8px + env(safe-area-inset-bottom,0px));z-index:2147481000;text-decoration:none;font:600 10.5px/1 system-ui,-apple-system,Segoe UI,sans-serif;letter-spacing:.06em;color:#9a9aa3;background:rgba(14,14,16,.62);border:1px solid rgba(255,255,255,.08);padding:5px 8px;border-radius:999px;opacity:.75;backdrop-filter:blur(6px)}
|
| 52 |
-
.hub-credit
|
| 53 |
-
.hub-credit
|
| 54 |
-
|
| 55 |
-
.hub-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
const
|
| 59 |
-
const
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
overlay
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
s
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
const
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
chip.
|
| 109 |
-
chip.
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
const
|
| 114 |
-
|
| 115 |
-
if (view === "
|
| 116 |
-
if (view === "
|
| 117 |
-
if (view === "
|
| 118 |
-
if (view === "
|
| 119 |
-
return `<h3>
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
if (
|
| 124 |
-
if (
|
| 125 |
-
|
| 126 |
-
box.
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
const
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
$("
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
$("
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
$("
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
window.HUB.
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
const
|
| 169 |
-
const
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
else
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
if (g.
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
if (
|
| 197 |
-
const
|
| 198 |
-
|
| 199 |
-
c
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Universal layer, client side. One Cramly account works in every app, and the owner's switches reach every app:
|
| 2 |
+
off / ban / time-out screens, messages, and harmless trolls. Add to a page with:
|
| 3 |
+
<script src="/static/hub.js" data-app="alibi" defer></script>
|
| 4 |
+
It fails open: if Cramly cannot be reached the app simply works as normal. */
|
| 5 |
+
(() => {
|
| 6 |
+
"use strict";
|
| 7 |
+
const me = document.currentScript;
|
| 8 |
+
const APP = (me && me.dataset.app) || "app";
|
| 9 |
+
const HUB = ((me && me.dataset.hub) || "https://cramly-twz5.onrender.com").replace(/\/$/, "");
|
| 10 |
+
const store = {
|
| 11 |
+
get: k => { try { return localStorage.getItem("hub." + k) || ""; } catch (e) { return ""; } },
|
| 12 |
+
set: (k, v) => { try { v ? localStorage.setItem("hub." + k, v) : localStorage.removeItem("hub." + k); } catch (e) { /* private mode */ } },
|
| 13 |
+
};
|
| 14 |
+
const keyOf = () => { // inside Cramly the signed-in account is used as it is
|
| 15 |
+
const k = store.get("key");
|
| 16 |
+
if (k || APP !== "cramly") return k;
|
| 17 |
+
try { return JSON.parse(localStorage.getItem("cramly.key") || "null") || ""; } catch (e) { return ""; }
|
| 18 |
+
};
|
| 19 |
+
let dev = store.get("dev");
|
| 20 |
+
if (!dev) { dev = (crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2) + Date.now().toString(36)).replace(/-/g, "").slice(0, 24); store.set("dev", dev); }
|
| 21 |
+
window.HUB = { dev: () => dev, key: keyOf, app: APP };
|
| 22 |
+
|
| 23 |
+
/* every same-origin request carries who is asking, so the server can enforce a ban too */
|
| 24 |
+
const realFetch = window.fetch.bind(window);
|
| 25 |
+
window.fetch = (input, init = {}) => {
|
| 26 |
+
try {
|
| 27 |
+
const url = new URL(typeof input === "string" ? input : input.url, location.href);
|
| 28 |
+
if (url.origin === location.origin) { init = { ...init, headers: { ...(init.headers instanceof Headers ? Object.fromEntries(init.headers) : init.headers || {}), "X-Hub-Dev": dev, ...(keyOf() ? { "X-Hub-Key": keyOf() } : {}) } }; }
|
| 29 |
+
} catch (e) { /* leave it alone */ }
|
| 30 |
+
return realFetch(input, init);
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
const css = `
|
| 34 |
+
.hub-ov{position:fixed;inset:0;z-index:2147483000;display:grid;place-items:center;background:#0a0a0b;color:#ececf0;font:16px/1.5 system-ui,-apple-system,Segoe UI,sans-serif;padding:24px;text-align:center}
|
| 35 |
+
.hub-ov h2{margin:0 0 8px;font-size:26px;font-weight:700;letter-spacing:-.01em}.hub-ov p{margin:0 auto 14px;max-width:34ch;color:#9a9aa3}.hub-ov .em{font-size:48px;margin-bottom:10px}
|
| 36 |
+
.hub-ov small{color:#6c6c75}
|
| 37 |
+
.hub-chip{position:fixed;left:12px;bottom:12px;z-index:2147482000;width:34px;height:34px;border-radius:50%;border:1px solid #2a2a30;background:#121214;color:#b9b9c2;font:600 13px system-ui;cursor:pointer;opacity:.55;display:grid;place-items:center;padding:0}
|
| 38 |
+
.hub-chip:hover,.hub-chip:focus-visible{opacity:1;outline:2px solid #5b93ff}
|
| 39 |
+
.hub-box{position:fixed;left:12px;bottom:54px;z-index:2147482001;width:min(320px,calc(100vw - 24px));background:#121214;border:1px solid #2a2a30;border-radius:16px;padding:16px;color:#ececf0;font:14px/1.45 system-ui,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.6)}
|
| 40 |
+
.hub-box h3{margin:0 0 6px;font-size:16px}.hub-box p{margin:0 0 10px;color:#9a9aa3}.hub-box input{width:100%;box-sizing:border-box;padding:10px 12px;border-radius:10px;border:1px solid #2a2a30;background:#0a0a0b;color:#ececf0;font:600 18px ui-monospace,monospace;letter-spacing:.2em;text-align:center}
|
| 41 |
+
.hub-box button{margin-top:10px;width:100%;padding:10px;border-radius:10px;border:0;background:#5b93ff;color:#08101f;font:700 14px system-ui;cursor:pointer}.hub-box button.alt{background:#1b1b1f;color:#ececf0}
|
| 42 |
+
.hub-box .err{color:#ff6b74;margin-top:8px;min-height:1em}
|
| 43 |
+
.hub-box input.hub-in{font:15px system-ui,sans-serif;letter-spacing:0;text-align:left;margin-top:8px}.hub-box button:disabled{opacity:.6}
|
| 44 |
+
.hub-links{margin:10px 0 0!important;font-size:12.5px}.hub-links a{color:#8fb4ff}.hub-small{margin:10px 0 0!important;font-size:11.5px;color:#6c6c75!important}
|
| 45 |
+
.hub-toast{position:fixed;left:50%;top:14px;transform:translateX(-50%);z-index:2147483100;max-width:min(92vw,420px);background:#121214;border:1px solid #2a2a30;color:#ececf0;padding:12px 16px;border-radius:14px;font:14px/1.45 system-ui,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.6);animation:hubIn .3s ease}
|
| 46 |
+
.hub-toast b{display:block;font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:#8d8d96;margin-bottom:2px}
|
| 47 |
+
@keyframes hubIn{from{opacity:0;transform:translate(-50%,-10px)}}
|
| 48 |
+
@keyframes hubFlip{to{transform:rotate(180deg)}}@keyframes hubShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-8px)}75%{transform:translateX(8px)}}
|
| 49 |
+
@keyframes hubHue{to{filter:hue-rotate(360deg)}}@keyframes hubSpin{to{transform:rotate(360deg)}}@keyframes hubFall{to{transform:translateY(110vh) rotate(540deg)}}
|
| 50 |
+
@keyframes hubDisco{0%{background:#f0b429}33%{background:#3dd49b}66%{background:#5b93ff}100%{background:#ff6b74}}
|
| 51 |
+
.hub-credit{position:fixed;right:10px;bottom:calc(8px + env(safe-area-inset-bottom,0px));z-index:2147481000;text-decoration:none;font:600 10.5px/1 system-ui,-apple-system,Segoe UI,sans-serif;letter-spacing:.06em;color:#9a9aa3;background:rgba(14,14,16,.62);border:1px solid rgba(255,255,255,.08);padding:5px 8px;border-radius:999px;opacity:.75;backdrop-filter:blur(6px)}
|
| 52 |
+
.hub-credit img{width:14px;height:14px;border-radius:3px;vertical-align:-3px;margin-right:5px}
|
| 53 |
+
.hub-credit u{text-decoration:none;color:#c9c9d1}.hub-credit:hover,.hub-credit:focus-visible{opacity:1;color:#ececf0}
|
| 54 |
+
.hub-credit.l{right:auto;left:54px}.hub-credit.t{bottom:auto;top:calc(8px + env(safe-area-inset-top,0px))}
|
| 55 |
+
@media print{.hub-credit{display:none}}
|
| 56 |
+
.hub-fall{position:fixed;top:-30px;z-index:2147483050;pointer-events:none;animation:hubFall linear forwards}
|
| 57 |
+
@media (prefers-reduced-motion:reduce){.hub-fall{display:none}}`;
|
| 58 |
+
const style = document.createElement("style"); style.textContent = css; document.head.append(style);
|
| 59 |
+
const el = (tag, cls, html) => { const e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; };
|
| 60 |
+
const esc = s => String(s == null ? "" : s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
| 61 |
+
|
| 62 |
+
/* full-screen states: off, banned, on a break */
|
| 63 |
+
let overlay = null, tick = null;
|
| 64 |
+
function block(emoji, title, body, extra) {
|
| 65 |
+
if (!overlay) { overlay = el("div", "hub-ov"); overlay.setAttribute("role", "alert"); document.body.append(overlay); }
|
| 66 |
+
overlay.innerHTML = `<div><div class="em">${emoji}</div><h2>${esc(title)}</h2><p>${esc(body)}</p>${extra || ""}</div>`;
|
| 67 |
+
}
|
| 68 |
+
function unblock() { if (overlay) { overlay.remove(); overlay = null; } if (tick) { clearInterval(tick); tick = null; } }
|
| 69 |
+
|
| 70 |
+
/* harmless trolls: all end by themselves */
|
| 71 |
+
const toast = (label, text, ms = 9000) => { const t = el("div", "hub-toast", `<b>${esc(label)}</b>${esc(text)}`); t.setAttribute("role", "status"); document.body.append(t); setTimeout(() => t.remove(), ms); t.onclick = () => t.remove(); };
|
| 72 |
+
function rain(chars, n = 60) {
|
| 73 |
+
for (let i = 0; i < n; i++) {
|
| 74 |
+
const s = el("div", "hub-fall", chars[i % chars.length]); const d = 2.5 + Math.random() * 3;
|
| 75 |
+
s.style.cssText = `left:${Math.random() * 100}vw;font-size:${18 + Math.random() * 22}px;animation-duration:${d}s;animation-delay:${Math.random() * 2}s`;
|
| 76 |
+
document.body.append(s); setTimeout(() => s.remove(), (d + 2.5) * 1000);
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
const onRoot = (anim, ms) => { const r = document.documentElement; r.style.animation = anim; setTimeout(() => { r.style.animation = ""; r.style.filter = ""; r.style.transform = ""; }, ms); };
|
| 80 |
+
const EFFECTS = {
|
| 81 |
+
confetti: () => rain(["🎉", "🎊", "✨", "🟦", "🟨", "🟥", "🟩"], 70),
|
| 82 |
+
emoji: () => rain(["😂", "🤪", "🫠", "🥳", "😜", "🤡"], 60),
|
| 83 |
+
flip: () => { const r = document.documentElement; r.style.transition = "transform 1s"; r.style.transform = "rotate(180deg)"; setTimeout(() => { r.style.transform = ""; setTimeout(() => r.style.transition = "", 1000); }, 7000); },
|
| 84 |
+
shake: () => onRoot("hubShake .35s linear 14", 5000),
|
| 85 |
+
rainbow: () => onRoot("hubHue 2s linear 4", 8000),
|
| 86 |
+
spin: () => onRoot("hubSpin 2s ease-in-out 1", 2200),
|
| 87 |
+
blur: () => { const r = document.documentElement; r.style.transition = "filter 1s"; r.style.filter = "blur(6px)"; setTimeout(() => { r.style.filter = ""; }, 5000); },
|
| 88 |
+
disco: () => { const d = el("div"); d.style.cssText = "position:fixed;inset:0;z-index:2147483040;opacity:.25;pointer-events:none;animation:hubDisco .5s steps(1) infinite"; document.body.append(d); setTimeout(() => d.remove(), 6000); },
|
| 89 |
+
fakecrash: () => { block("💥", "Something went terribly wrong", "Just kidding. The owner is messing with you."); setTimeout(unblock, 3500); },
|
| 90 |
+
typewriter: () => toast("psst", "Somebody is typing…", 4000),
|
| 91 |
+
};
|
| 92 |
+
function note(n) {
|
| 93 |
+
if (n.kind === "troll") { try { (EFFECTS[n.effect] || EFFECTS.confetti)(); } catch (e) { /* ignore */ } if (n.body) toast("From the owner", n.body); }
|
| 94 |
+
else toast("From the owner", n.body, 14000);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
/* the account button: sign in or make an account with email and password (the same account in every app),
|
| 98 |
+
ask the owner for a new password, or link with a Cramly pairing code */
|
| 99 |
+
let chip = null, box = null, linked = false, who = "";
|
| 100 |
+
const api = async (path, opts = {}) => {
|
| 101 |
+
const key = keyOf(), r = await realFetch(HUB + path, { ...opts, headers: { "Content-Type": "application/json", ...(key ? { Authorization: "Bearer " + key } : {}), ...(opts.headers || {}) } });
|
| 102 |
+
let j = {}; try { j = await r.json(); } catch (e) { /* empty */ }
|
| 103 |
+
if (!r.ok) throw new Error(j.error || "That did not work.");
|
| 104 |
+
return j;
|
| 105 |
+
};
|
| 106 |
+
const signedIn = (j, how) => { store.set("key", j.key); store.set("email", j.email || ""); store.set("ticket", ""); linked = true; who = j.email || j.name || ""; if (box) { box.remove(); box = null; } drawChip(); poll(); toast(how, `Signed in as ${who}`, 4000); window.dispatchEvent(new CustomEvent("hub:account", { detail: { email: who } })); };
|
| 107 |
+
function drawChip() {
|
| 108 |
+
if (!chip) { chip = el("button", "hub-chip", "☁"); chip.title = "Your account"; chip.setAttribute("aria-label", "Your account"); chip.onclick = () => togglePanel(); document.body.append(chip); }
|
| 109 |
+
chip.textContent = linked ? (who[0] || "✓").toUpperCase() : "👤";
|
| 110 |
+
chip.style.opacity = linked ? ".9" : "";
|
| 111 |
+
}
|
| 112 |
+
function form(view) {
|
| 113 |
+
const f = (id, type, ph, extra = "") => `<input class="hub-in" id="${id}" type="${type}" placeholder="${ph}" ${extra}>`;
|
| 114 |
+
const notice = `<p class="hub-small">The site owner can see the email you sign up with, never your password.</p>`;
|
| 115 |
+
if (view === "up") return `<h3>Create your account</h3><p>One account for every app by Legion.</p>${f("hubE", "email", "Email", 'autocomplete="email"')}${f("hubP", "password", "Password (8+ characters)", 'autocomplete="new-password"')}<button id="hubGo">Create account</button><div class="err" id="hubErr" role="alert"></div><p class="hub-links"><a href="#" data-v="in">I already have an account</a></p>${notice}`;
|
| 116 |
+
if (view === "forgot") return `<h3>Forgot your password?</h3><p>Your request goes to the owner. Once they approve it you can set a new password right here.</p>${f("hubE", "email", "Your account's email", 'autocomplete="email"')}<button id="hubGo">Ask for a new password</button><div class="err" id="hubErr" role="alert"></div><p class="hub-links"><a href="#" data-v="in">Back to sign in</a></p>`;
|
| 117 |
+
if (view === "wait") return `<h3>Waiting for the owner</h3><p>Your request <b id="hubCode2"></b> is with the owner. Keep this page open or come back later: when it's approved you can set a new password here.</p><div class="err" id="hubErr" role="alert"></div><button class="alt" id="hubCheck">Check again</button><p class="hub-links"><a href="#" data-v="cancel">Cancel the request</a></p>`;
|
| 118 |
+
if (view === "newpw") return `<h3>Set a new password</h3><p>The owner approved your request.</p>${f("hubP", "password", "New password (8+ characters)", 'autocomplete="new-password"')}<button id="hubGo">Save and sign in</button><div class="err" id="hubErr" role="alert"></div>`;
|
| 119 |
+
if (view === "code") return `<h3>Link with a code</h3><p>In Cramly open <b>Settings</b>, tap <b>Use Cramly on another device</b>, and type the 6 digits here.</p><input id="hubCode" inputmode="numeric" maxlength="6" placeholder="000000" aria-label="6 digit code"><button id="hubGo">Link</button><div class="err" id="hubErr" role="alert"></div><p class="hub-links"><a href="#" data-v="in">Sign in with email instead</a></p>`;
|
| 120 |
+
return `<h3>Sign in</h3><p>One account for every app by Legion.</p>${f("hubE", "email", "Email", 'autocomplete="email"')}${f("hubP", "password", "Password", 'autocomplete="current-password"')}<button id="hubGo">Sign in</button><div class="err" id="hubErr" role="alert"></div><p class="hub-links"><a href="#" data-v="up">Create an account</a> · <a href="#" data-v="forgot">Forgot password?</a> · <a href="#" data-v="code">Cramly code</a></p>`;
|
| 121 |
+
}
|
| 122 |
+
async function togglePanel(view) {
|
| 123 |
+
if (box && !view) { box.remove(); box = null; return; }
|
| 124 |
+
if (!box) { box = el("div", "hub-box"); box.setAttribute("role", "dialog"); document.body.append(box); }
|
| 125 |
+
if (linked) {
|
| 126 |
+
box.innerHTML = `<h3>Signed in</h3><p><b>${esc(store.get("email") || who || "your account")}</b><br>The same account works in every app by Legion.</p><button class="alt" id="hubOut">Sign out here</button><p style="margin:10px 0 0;font-size:11px">Device id: ${esc(dev)}</p>`;
|
| 127 |
+
box.querySelector("#hubOut").onclick = () => { store.set("key", ""); store.set("email", ""); linked = false; who = ""; box.remove(); box = null; drawChip(); poll(); window.dispatchEvent(new CustomEvent("hub:account", { detail: { email: "" } })); };
|
| 128 |
+
if (!store.get("email")) api("/api/uni/acct/me").then(j => { if (j.email) { store.set("email", j.email); const b = box && box.querySelector("p b"); if (b) b.textContent = j.email; } }).catch(() => { });
|
| 129 |
+
return;
|
| 130 |
+
}
|
| 131 |
+
if (!view) { const t = store.get("ticket"); view = t ? "wait" : "in"; }
|
| 132 |
+
box.innerHTML = form(view);
|
| 133 |
+
const $ = id => box.querySelector("#" + id), err = $("hubErr");
|
| 134 |
+
box.querySelectorAll("[data-v]").forEach(a => a.onclick = e => { e.preventDefault(); if (a.dataset.v === "cancel") store.set("ticket", ""); togglePanel(a.dataset.v === "cancel" ? "in" : a.dataset.v); });
|
| 135 |
+
const busyBtn = async (fn) => { const b = $("hubGo"); if (b) b.disabled = true; err.textContent = "…"; try { await fn(); } catch (e) { err.textContent = e.message === "Failed to fetch" ? "The account server didn't answer. It may be waking up: try again in a minute." : e.message; } finally { if (b) b.disabled = false; } };
|
| 136 |
+
const enter = (id, fn) => { const i = $(id); if (i) i.addEventListener("keydown", e => { if (e.key === "Enter") fn(); }); };
|
| 137 |
+
if (view === "in" || view === "up") {
|
| 138 |
+
const go = () => busyBtn(async () => { const j = await api(`/api/uni/acct/${view === "up" ? "signup" : "login"}`, { method: "POST", body: JSON.stringify({ email: $("hubE").value, password: $("hubP").value, app: APP }) }); signedIn(j, view === "up" ? "Account made" : "Welcome back"); });
|
| 139 |
+
$("hubGo").onclick = go; enter("hubP", go); $("hubE").focus();
|
| 140 |
+
} else if (view === "forgot") {
|
| 141 |
+
const go = () => busyBtn(async () => { const j = await api("/api/uni/acct/forgot", { method: "POST", body: JSON.stringify({ email: $("hubE").value, app: APP }) }); store.set("ticket", j.ticket); store.set("tcode", j.code); togglePanel("wait"); });
|
| 142 |
+
$("hubGo").onclick = go; enter("hubE", go); $("hubE").focus();
|
| 143 |
+
} else if (view === "wait") {
|
| 144 |
+
$("hubCode2").textContent = "#" + (store.get("tcode") || "");
|
| 145 |
+
const check = () => busyBtn(async () => { const j = await api("/api/uni/acct/forgot?ticket=" + encodeURIComponent(store.get("ticket"))); if (j.status === "approved") togglePanel("newpw"); else if (j.status === "pending") err.textContent = "Still waiting for the owner."; else { store.set("ticket", ""); err.textContent = j.status === "denied" ? "The owner said no to this request." : "This request expired. Ask again."; } });
|
| 146 |
+
$("hubCheck").onclick = check; check();
|
| 147 |
+
} else if (view === "newpw") {
|
| 148 |
+
const go = () => busyBtn(async () => { const j = await api("/api/uni/acct/reset", { method: "POST", body: JSON.stringify({ ticket: store.get("ticket"), password: $("hubP").value, app: APP }) }); signedIn(j, "Password changed"); });
|
| 149 |
+
$("hubGo").onclick = go; enter("hubP", go); $("hubP").focus();
|
| 150 |
+
} else if (view === "code") {
|
| 151 |
+
const go = () => busyBtn(async () => { const code = $("hubCode").value.trim(); if (code.length < 6) throw new Error("Type all 6 digits."); const j = await api("/api/uni/link", { method: "POST", body: JSON.stringify({ code }) }); signedIn(j, "Linked"); });
|
| 152 |
+
$("hubGo").onclick = go; enter("hubCode", go); $("hubCode").focus();
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
window.HUB.open = v => togglePanel(v || "in");
|
| 156 |
+
window.HUB.email = () => linked ? (store.get("email") || who) : "";
|
| 157 |
+
// a pending password request: tell the person when the owner has answered
|
| 158 |
+
async function checkTicket() {
|
| 159 |
+
const t = store.get("ticket"); if (!t || linked) return;
|
| 160 |
+
try { const j = await api("/api/uni/acct/forgot?ticket=" + encodeURIComponent(t)); if (j.status === "approved" && !box) toast("Good news", "Your password reset was approved. Tap the 👤 button to set a new password.", 15000); } catch (e) { /* later */ }
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
/* ask Cramly what this visitor should see */
|
| 164 |
+
let busy = false;
|
| 165 |
+
async function poll() {
|
| 166 |
+
if (busy) return; busy = true;
|
| 167 |
+
try {
|
| 168 |
+
const since = store.get("since") || "0", key = keyOf();
|
| 169 |
+
const ctl = new AbortController(), to = setTimeout(() => ctl.abort(), 25000);
|
| 170 |
+
const r = await realFetch(`${HUB}/api/uni/guard?app=${encodeURIComponent(APP)}&dev=${encodeURIComponent(dev)}&since=${since}`, { headers: key ? { Authorization: "Bearer " + key } : {}, signal: ctl.signal });
|
| 171 |
+
clearTimeout(to);
|
| 172 |
+
if (!r.ok) return;
|
| 173 |
+
const g = await r.json();
|
| 174 |
+
linked = !!g.linked; who = g.name || ""; if (key && !g.linked) store.set("key", ""); // the account was deleted
|
| 175 |
+
drawChip();
|
| 176 |
+
const contact = g.contact ? `<small>Questions? ${esc(g.contact)}</small>` : "";
|
| 177 |
+
if (g.banned) block("🚫", "This account has been blocked", g.reason || "The owner blocked access.", contact);
|
| 178 |
+
else if (g.timeout_until > g.now) {
|
| 179 |
+
const draw = () => { const s = Math.max(0, Math.round(g.timeout_until - g.now - (Date.now() - t0) / 1000)); const m = Math.floor(s / 60);
|
| 180 |
+
block("⏳", "You are on a break", (g.reason ? g.reason + " " : "") + `Back in ${m >= 1 ? m + " min " : ""}${s % 60}s.`, contact); if (s <= 0) { unblock(); poll(); } };
|
| 181 |
+
const t0 = Date.now(); if (tick) clearInterval(tick); draw(); tick = setInterval(draw, 1000);
|
| 182 |
+
} else if (g.off) block("🛠️", APP.charAt(0).toUpperCase() + APP.slice(1) + " is switched off", g.message || "Back soon.", contact);
|
| 183 |
+
else unblock();
|
| 184 |
+
if (!g.banned && !g.off) {
|
| 185 |
+
if (g.notes && g.notes.length) g.notes.forEach(note);
|
| 186 |
+
if (g.inbox && g.inbox.length) { g.inbox.forEach(m => toast("Message for you", m.body, 20000)); realFetch(HUB + "/api/uni/ack", { method: "POST", headers: { Authorization: "Bearer " + key } }).catch(() => {}); }
|
| 187 |
+
}
|
| 188 |
+
if (g.last != null) store.set("since", String(g.last));
|
| 189 |
+
checkTicket();
|
| 190 |
+
} catch (e) { /* fail open: Cramly may be asleep */ }
|
| 191 |
+
finally { busy = false; }
|
| 192 |
+
}
|
| 193 |
+
/* the credit on every app: "Made by Legion". A page can move it with data-credit="bl|br|tl|tr" on this script tag, or hide it with "none". */
|
| 194 |
+
const LOGO = "data:image/png;base64,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";
|
| 195 |
+
const credit = () => {
|
| 196 |
+
if (!document.querySelector('meta[name="author"]')) { const m = document.createElement("meta"); m.name = "author"; m.content = "Legion"; document.head.append(m); }
|
| 197 |
+
const pos = (me && me.dataset.credit) || "br";
|
| 198 |
+
if (pos === "none" || document.querySelector(".hub-credit")) return;
|
| 199 |
+
const c = el("a", "hub-credit" + (pos[0] === "t" ? " t" : "") + (pos[1] === "l" ? " l" : ""), `<img src="${LOGO}" alt="">Made by Legion · <u>Contact me</u>`);
|
| 200 |
+
c.href = "mailto:Awsaa-Legion@proton.me"; c.title = "Awsaa-Legion@proton.me";
|
| 201 |
+
c.setAttribute("aria-label", "Made by Legion. Contact me: Awsaa-Legion@proton.me");
|
| 202 |
+
document.body.append(c);
|
| 203 |
+
};
|
| 204 |
+
const start = () => { credit(); drawChip(); poll(); setInterval(poll, 30000); document.addEventListener("visibilitychange", () => { if (!document.hidden) poll(); }); };
|
| 205 |
+
if (document.body) start(); else addEventListener("DOMContentLoaded", start);
|
| 206 |
+
})();
|