Files
the-button/js/common_v=2.js
2025-09-12 11:20:36 +02:00

152 lines
4.2 KiB
JavaScript

/*
NOTE: this just contains user interface logic;
there is probably not much interesting stuff here
*/
let name, n, x, n_old, n_last;
async function init() {
const params = (new URL(document.URL)).searchParams;
if(params.has('name')) {
name = params.get('name');
} else {
/* missing name, redirect to start screen */
location.replace('.');
return;
}
/* people can do more than 2^53 button clicks, so we need big integers */
if(params.has('n') && params.has('x')) {
n = BigInt(params.get('n'));
x = BigInt(params.get('x'));
} else {
n = 0n;
x = 0n;
}
n_old = 0n;
n_last = -1n;
if(typeof prepare == 'function') {
await prepare();
}
const button = document.querySelector('button');
button.textContent = n;
button.addEventListener('click', onClick);
setInterval(() => {
if(n > n_last) {
/* to spare the browser, we do this at most 10 times per second */
const params = new URLSearchParams({name, n, x});
history.replaceState(null, '', '?' + params);
button.textContent = n;
n_last = n;
}
}, 100);
communicate();
}
document.addEventListener('DOMContentLoaded', init);
async function communicate() {
const error = document.querySelector('#error');
try {
/* only submit increased click counts, otherwise just fetch scores */
const path = 'server.php' + ((n_last > n_old) ? location.search : '');
const result = await fetch(path);
if(!result.ok) {
throw new Error(`${result.status} ${result.statusText}`);
}
if(n_last > n_old) {
n_old = n_last;
}
const json = await result.json();
if(json.flag && !location.pathname.endsWith('/' + json.flag)) {
/* advance to next level */
location.replace(json.flag + location.search);
return;
}
if(json.scores) {
displayScores(json.scores, json.time);
}
if(json.error) {
throw new Error(json.error);
}
const timeStr = new Date(json.time * 1000).toLocaleTimeString('de-DE');
error.textContent = `Zuletzt aktualisiert: ${timeStr}`;
} catch(e) {
console.warn(e);
error.textContent = e;
}
setTimeout(communicate, 1000);
}
function displayScores(scores, time) {
const table = document.querySelector('table');
while(table.rows.length > 1) {
table.deleteRow(-1);
}
scores.sort((a, b) => {
const n_a = BigInt(a.n);
const n_b = BigInt(b.n);
if(n_a > n_b) {
return -1;
}
if(n_a < n_b) {
return 1;
}
if(a.time < b.time) {
return -1;
}
if(a.time > b.time) {
return 1;
}
return 0;
});
scores.forEach((entry) => {
const row = table.insertRow();
const nameCell = row.insertCell();
/* protect against unicode bidi control characters */
const bdi = document.createElement('bdi');
bdi.textContent = entry.name;
nameCell.appendChild(bdi);
nameCell.title = entry.name;
const timeCell = row.insertCell();
const date = new Date(entry.time * 1000);
if(time - entry.time < 86400) {
timeCell.textContent = date.toLocaleTimeString('de-DE');
} else {
timeCell.textContent = date.toLocaleDateString('de-DE');
}
timeCell.title = date.toLocaleString('de-DE');
const nCell = row.insertCell();
nCell.textContent = entry.n;
nCell.title = entry.n;
});
}
/* compute SHA-256 digest represented as integer (big endian) */
async function sha256(str) {
const msg = new TextEncoder().encode(str);
const buffer = await crypto.subtle.digest('SHA-256', msg);
const bytes = new Uint8Array(buffer);
let val = 0n;
for(const b of bytes) {
val = (val << 8n) | BigInt(b);
}
return val;
}
/* compute a^k modulo m */
function pow(a, k, m) {
let b = 1n;
for(; k; k >>= 1n) {
if(k & 1n) {
b *= a;
b %= m;
}
a *= a;
a %= m;
}
return b;
}