Compare commits

...

8 Commits

Author SHA1 Message Date
sto
5d1390c480 send start time 2025-06-23 19:22:19 +02:00
sto
44a0b5c464 Add connection checks 2025-06-21 18:35:33 +02:00
sto
7157587c1a Bump to v1.1 2025-06-19 16:16:52 +02:00
sto
452a57970f Add connection to public scoreboard 2025-06-13 19:37:02 +02:00
sto
376dd1c3a9 Fix message finder after gmeet update 2025-05-31 17:21:34 +02:00
sto
bfe0f7f476 Add more doc 2025-04-14 20:00:09 +02:00
sto
5e8114f206 Don't display and save completion times when data is reset 2025-04-14 19:27:45 +02:00
sto
eb1c6d943a Update the message content after a short timeout, to fix coloring, and add parenthesis 2025-04-14 19:20:50 +02:00
5 changed files with 178 additions and 14 deletions

View File

@@ -27,6 +27,23 @@ TODO: need to make this extension gecko-compatible.
## Usage
Once you have installed the extension in your favorite browser, click again on the puzzle icon in your toolbar to open it (you can also pin it so that it appear in the toolbar).
Once you have installed the extension in your favorite browser, click again on the puzzle icon in your toolbar to open it (you can also pin it so that it appears in the toolbar).
That's it!
From the extension popup, you should be able to start a contest, end it, and reset all saved data.
When in a Google Meet tab, once a contest is started, all new messages with be automatically prepended by the time since start of the contest.
Those messages will also be saved in the `messages` section of the extension popup (so that they are kept in case you close the Google Meet tab by mistake).
## How it works
The extension will look for new messages in the Google Meet messages section, if opened, and assign times to them as soon as they appear there.
If you close the message section, the extension will still be able to log the times of incoming messages because they are appearing in notifications in the bottom right corner of the screen, but it won't actually assign them to messages and save them until you re-open the messages section.
Best is to keep that section open all along though if possible.
## Word of caution
Please note this work is technically dependent on the structure of Google Meet pages, if Google decides to completely rework them, the extension may break in unexpected ways.
However, Google Meet is a quite stable product, and such changes are very rare. So if you check things work nicely the day before your contest, or ideally a few hours right before, then it should be alright!

View File

@@ -1,7 +1,7 @@
{
"name": "Puzzle leaderboard tracker",
"description": "Base Level Extension",
"version": "1.0",
"version": "1.1.1",
"manifest_version": 3,
"action": {
"default_popup": "popup.html",

View File

@@ -44,6 +44,20 @@
#reset-button {
background-color: lightcoral;
}
[connect-form] {
align-items: center;
display: none;
flex-direction: row;
margin-top: 3px;
}
[connect-form] > input {
height: 24px;
}
#connect-submit-button {
height: 22px;
margin-left: 5px;
margin-top: 0;
}
[messages] {
display: none;
margin-top: 10px;
@@ -71,7 +85,12 @@
<button id="start-button">Start a new contest</button>
<button id="end-button" style="display: none;">End the contest</button>
<button id="reset-button" style="display: none;">Reset all saved data</button>
<button id="messages-button">show messages</button>
<button id="connect-button">Connect to a puzzle scoreboard</button>
<div connect-form id="connect-form">
<input type="text" id="connect-text">
<button id="connect-submit-button">connect</button>
</div>
<button id="messages-button">Show messages</button>
<div messages id="messages">
</div>
</div>

View File

@@ -1,7 +1,9 @@
let startTime = 0;
let endTime = 0;
let messagesCount = 0;
let puzzleScoreboard = null;
let showMessages = false;
let showConnect = false;
async function init() {
const data = await chrome.storage.local.get(['startTime', 'endTime']);
@@ -22,6 +24,11 @@ async function init() {
resetButton.onclick = resetData;
const messagesButton = document.getElementById('messages-button');
messagesButton.onclick = switchMessages;
const connectButton = document.getElementById('connect-button');
connectButton.onclick = switchConnectForm;
const connectSubmitButton = document.getElementById('connect-submit-button');
connectSubmitButton.onclick = connectToPuzzleScoreboard;
initConnect();
update();
}
@@ -54,6 +61,7 @@ function startContest() {
document.getElementById('reset-button').style.display = 'none';
document.getElementById('messages').innerHTML = '';
if (showMessages) switchMessages();
sendStartTime(startTime);
}
function endContest() {
@@ -79,14 +87,86 @@ function switchMessages() {
const messagesButtonEl = document.getElementById('messages-button');
if (showMessages) {
messagesEl.style.display = 'none';
messagesButtonEl.innerHTML = 'show messages';
messagesButtonEl.innerHTML = 'Show messages';
} else {
messagesEl.style.display = 'block';
messagesButtonEl.innerHTML = 'hide messages';
messagesButtonEl.innerHTML = 'Hide messages';
}
showMessages = !showMessages;
}
function switchConnectForm() {
const connectFormEl = document.getElementById('connect-form')
if (showConnect) {
connectFormEl.style.display = 'none';
} else {
connectFormEl.style.display = 'flex';
}
showConnect = !showConnect;
}
async function sendStartTime(startTime) {
const data = await chrome.storage.local.get(['puzzleScoreboard']);
if (!data?.puzzleScoreboard || !data.puzzleScoreboard.includes('/message')) return;
fetch(data.puzzleScoreboard.replace('/message', '/start'), {
method: "post",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({startTime}),
});
}
async function scoreboardConnect(puzzleScoreboard) {
if (puzzleScoreboard.includes('/message')) {
const resp = await fetch(puzzleScoreboard.replace('/message', '/connect'), {
method: "post",
headers: {'Content-Type': 'application/json'},
});
if (resp.status != 200) return {connected: false, error: 'Invalid contest token'};
const json = await resp.json();
return {connected: true, name: json?.name};
}
return {connected: false, error: 'Invalid connection link'};
}
async function initConnect() {
const data = await chrome.storage.local.get(['puzzleScoreboard']);
if (data?.puzzleScoreboard) {
puzzleScoreboard = data.puzzleScoreboard;
const connection = await scoreboardConnect(puzzleScoreboard);
const connectButtonEl = document.getElementById('connect-button')
if (connection.connected) connectButtonEl.innerHTML = `Connected: <span style="color: green">${connection.name}</span>`;
else connectButtonEl.innerHTML = `<span style="color: red">${connection.error}</span>`;
const connectTextEl = document.getElementById('connect-text')
connectTextEl.style.display = 'none';
const connectSubmitButtonEl = document.getElementById('connect-submit-button')
connectSubmitButtonEl.innerHTML = 'disconnect';
}
}
async function connectToPuzzleScoreboard() {
const connectButtonEl = document.getElementById('connect-button')
const connectTextEl = document.getElementById('connect-text')
const connectSubmitButtonEl = document.getElementById('connect-submit-button')
if (puzzleScoreboard) {
puzzleScoreboard = null;
chrome.storage.local.remove('puzzleScoreboard');
connectButtonEl.innerHTML = `Connect to a puzzle scoreboard`;
connectTextEl.style.display = 'block';
connectSubmitButtonEl.innerHTML = 'connect';
} else {
puzzleScoreboard = connectTextEl.value;
chrome.storage.local.set({ puzzleScoreboard: puzzleScoreboard });
const connection = await scoreboardConnect(puzzleScoreboard);
const connectFormEl = document.getElementById('connect-form')
connectFormEl.style.display = 'none';
showConnect = false;
if (connection.connected) connectButtonEl.innerHTML = `Connected: <span style="color: green">${connection.name}</span>`;
else connectButtonEl.innerHTML = `<span style="color: red">${connection.error}</span>`;
connectTextEl.style.display = 'none';
connectSubmitButtonEl.innerHTML = 'disconnect';
}
}
function dis(n) {
if (n < 10) return "0" + n;
return n.toString();

View File

@@ -1,4 +1,5 @@
const seenMessages = new Set();
const messageContents = new Map();
const seenDataAnnounceMessages = new Set();
let awaitingTimes = [];
let messages = [];
@@ -26,6 +27,14 @@ function displayTime(rawTime) {
return `${Math.floor(rawTime / 3600)}:${dis(Math.floor((rawTime % 3600) / 60))}:${dis(rawTime % 60)}`;
}
function updateMessageContent(content, el, map) {
if (el.innerHTML != content) el.innerHTML = content;
}
function updateMessageContents() {
messageContents.forEach(updateMessageContent);
}
function checkNewMessages() {
// Check announce messages and log their appearing times.
const announceMessageElements = document.querySelectorAll("div[data-announce-message]");
@@ -37,22 +46,43 @@ function checkNewMessages() {
}
// Check in-call messages.
const messageElements = document.querySelectorAll("div[data-message-id] div[jscontroller]");
for (let e of messageElements) {
if (seenMessages.has(e)) continue;
seenMessages.add(e);
const messageElements = document.querySelectorAll("div[data-message-id]");
let newElements = false;
for (let messageNode of messageElements) {
const el = messageNode.firstChild.firstChild.firstChild.firstChild;
if (seenMessages.has(el)) continue;
newElements = true;
seenMessages.add(el);
let completionTime = Math.floor((Date.now() - startTime) / 1000);
// If awaiting times are available, use them first (i.e. the messages section just got opened).
if (awaitingTimes.length) completionTime = awaitingTimes.shift();
const messageNode = e.parentNode.parentNode.parentNode.parentNode;
const rootNode = messageNode.parentNode.parentNode;
const name = rootNode.firstChild.firstChild.innerHTML;
const dTime = displayTime(completionTime);
messages.push({completionTime: completionTime, name: name, text: e.innerHTML, displayTime: dTime});
e.innerHTML = `<span style="color: red; margin-right: 5px;">${dTime}</span>${e.innerHTML}`
if (startTime > 0) {
const text = el.innerHTML;
messages.push({completionTime: completionTime, name: name, text: text, displayTime: dTime});
const messageContent = `(<span style="color: red;">${dTime}</span>) ${el.innerHTML}`;
messageContents.set(el, messageContent);
// Re-show all finishing times, as GMeet reset those each time a new message is written.
setTimeout(updateMessageContents, 30);
setTimeout(updateMessageContents, 60);
setTimeout(updateMessageContents, 90);
setTimeout(updateMessageContents, 120);
setTimeout(updateMessageContents, 150);
setTimeout(updateMessageContents, 180);
// Upload the message to the puzzle scoreboard, if connected.
uploadMessage(name, text, completionTime);
}
}
if (newElements) {
chrome.storage.local.set({ messages: messages });
}
// If awaiting messages are still present, but the messages section is opened with no new messages,
// delete all awaiting messages (that should never happen though, there was an error somewhere).
@@ -61,6 +91,24 @@ function checkNewMessages() {
setTimeout(checkNewMessages, 100);
}
async function uploadMessage(author, text, time_seconds) {
data = await chrome.storage.local.get(['puzzleScoreboard']);
if (data?.puzzleScoreboard) {
fetch(data.puzzleScoreboard, {
method: "post",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
author,
text,
time_seconds,
}),
});
}
}
async function update() {
const data = await chrome.storage.local.get(['startTime', 'endTime']);
if (data?.startTime >= 0 && data.startTime != startTime) {