Compare commits
6 Commits
5e8114f206
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d1390c480 | ||
|
|
44a0b5c464 | ||
|
|
7157587c1a | ||
|
|
452a57970f | ||
|
|
376dd1c3a9 | ||
|
|
bfe0f7f476 |
21
README.md
21
README.md
@@ -27,6 +27,23 @@ TODO: need to make this extension gecko-compatible.
|
|||||||
|
|
||||||
## Usage
|
## 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!
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Puzzle leaderboard tracker",
|
"name": "Puzzle leaderboard tracker",
|
||||||
"description": "Base Level Extension",
|
"description": "Base Level Extension",
|
||||||
"version": "1.0",
|
"version": "1.1.1",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "popup.html",
|
"default_popup": "popup.html",
|
||||||
|
|||||||
21
popup.html
21
popup.html
@@ -44,6 +44,20 @@
|
|||||||
#reset-button {
|
#reset-button {
|
||||||
background-color: lightcoral;
|
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] {
|
[messages] {
|
||||||
display: none;
|
display: none;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
@@ -71,7 +85,12 @@
|
|||||||
<button id="start-button">Start a new contest</button>
|
<button id="start-button">Start a new contest</button>
|
||||||
<button id="end-button" style="display: none;">End the 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="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 messages id="messages">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
84
popup.js
84
popup.js
@@ -1,7 +1,9 @@
|
|||||||
let startTime = 0;
|
let startTime = 0;
|
||||||
let endTime = 0;
|
let endTime = 0;
|
||||||
let messagesCount = 0;
|
let messagesCount = 0;
|
||||||
|
let puzzleScoreboard = null;
|
||||||
let showMessages = false;
|
let showMessages = false;
|
||||||
|
let showConnect = false;
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const data = await chrome.storage.local.get(['startTime', 'endTime']);
|
const data = await chrome.storage.local.get(['startTime', 'endTime']);
|
||||||
@@ -22,6 +24,11 @@ async function init() {
|
|||||||
resetButton.onclick = resetData;
|
resetButton.onclick = resetData;
|
||||||
const messagesButton = document.getElementById('messages-button');
|
const messagesButton = document.getElementById('messages-button');
|
||||||
messagesButton.onclick = switchMessages;
|
messagesButton.onclick = switchMessages;
|
||||||
|
const connectButton = document.getElementById('connect-button');
|
||||||
|
connectButton.onclick = switchConnectForm;
|
||||||
|
const connectSubmitButton = document.getElementById('connect-submit-button');
|
||||||
|
connectSubmitButton.onclick = connectToPuzzleScoreboard;
|
||||||
|
initConnect();
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +61,7 @@ function startContest() {
|
|||||||
document.getElementById('reset-button').style.display = 'none';
|
document.getElementById('reset-button').style.display = 'none';
|
||||||
document.getElementById('messages').innerHTML = '';
|
document.getElementById('messages').innerHTML = '';
|
||||||
if (showMessages) switchMessages();
|
if (showMessages) switchMessages();
|
||||||
|
sendStartTime(startTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
function endContest() {
|
function endContest() {
|
||||||
@@ -79,14 +87,86 @@ function switchMessages() {
|
|||||||
const messagesButtonEl = document.getElementById('messages-button');
|
const messagesButtonEl = document.getElementById('messages-button');
|
||||||
if (showMessages) {
|
if (showMessages) {
|
||||||
messagesEl.style.display = 'none';
|
messagesEl.style.display = 'none';
|
||||||
messagesButtonEl.innerHTML = 'show messages';
|
messagesButtonEl.innerHTML = 'Show messages';
|
||||||
} else {
|
} else {
|
||||||
messagesEl.style.display = 'block';
|
messagesEl.style.display = 'block';
|
||||||
messagesButtonEl.innerHTML = 'hide messages';
|
messagesButtonEl.innerHTML = 'Hide messages';
|
||||||
}
|
}
|
||||||
showMessages = !showMessages;
|
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) {
|
function dis(n) {
|
||||||
if (n < 10) return "0" + n;
|
if (n < 10) return "0" + n;
|
||||||
return n.toString();
|
return n.toString();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const seenMessages = new Set();
|
const seenMessages = new Set();
|
||||||
|
const messageContents = new Map();
|
||||||
const seenDataAnnounceMessages = new Set();
|
const seenDataAnnounceMessages = new Set();
|
||||||
let awaitingTimes = [];
|
let awaitingTimes = [];
|
||||||
let messages = [];
|
let messages = [];
|
||||||
@@ -26,6 +27,14 @@ function displayTime(rawTime) {
|
|||||||
return `${Math.floor(rawTime / 3600)}:${dis(Math.floor((rawTime % 3600) / 60))}:${dis(rawTime % 60)}`;
|
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() {
|
function checkNewMessages() {
|
||||||
// Check announce messages and log their appearing times.
|
// Check announce messages and log their appearing times.
|
||||||
const announceMessageElements = document.querySelectorAll("div[data-announce-message]");
|
const announceMessageElements = document.querySelectorAll("div[data-announce-message]");
|
||||||
@@ -37,31 +46,40 @@ function checkNewMessages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check in-call messages.
|
// Check in-call messages.
|
||||||
const messageElements = document.querySelectorAll("div[data-message-id] div[jscontroller]");
|
const messageElements = document.querySelectorAll("div[data-message-id]");
|
||||||
let newElements = false;
|
let newElements = false;
|
||||||
for (let e of messageElements) {
|
for (let messageNode of messageElements) {
|
||||||
if (seenMessages.has(e)) continue;
|
const el = messageNode.firstChild.firstChild.firstChild.firstChild;
|
||||||
|
if (seenMessages.has(el)) continue;
|
||||||
newElements = true;
|
newElements = true;
|
||||||
seenMessages.add(e);
|
seenMessages.add(el);
|
||||||
let completionTime = Math.floor((Date.now() - startTime) / 1000);
|
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 awaiting times are available, use them first (i.e. the messages section just got opened).
|
||||||
if (awaitingTimes.length) completionTime = awaitingTimes.shift();
|
if (awaitingTimes.length) completionTime = awaitingTimes.shift();
|
||||||
|
|
||||||
const messageNode = e.parentNode.parentNode.parentNode.parentNode;
|
|
||||||
const rootNode = messageNode.parentNode.parentNode;
|
const rootNode = messageNode.parentNode.parentNode;
|
||||||
const name = rootNode.firstChild.firstChild.innerHTML;
|
const name = rootNode.firstChild.firstChild.innerHTML;
|
||||||
const dTime = displayTime(completionTime);
|
const dTime = displayTime(completionTime);
|
||||||
if (startTime > 0) {
|
if (startTime > 0) {
|
||||||
messages.push({completionTime: completionTime, name: name, text: e.innerHTML, displayTime: dTime});
|
const text = el.innerHTML;
|
||||||
|
messages.push({completionTime: completionTime, name: name, text: text, displayTime: dTime});
|
||||||
// Set timeout for rendering the completion time, required to avoid weird stuff.
|
const messageContent = `(<span style="color: red;">${dTime}</span>) ${el.innerHTML}`;
|
||||||
setTimeout(() => {
|
messageContents.set(el, messageContent);
|
||||||
e.innerHTML = `(<span style="color: red;">${dTime}</span>) ${e.innerHTML}`;
|
|
||||||
}, 20);
|
// 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) {
|
if (newElements) {
|
||||||
chrome.storage.local.set({ messages: messages });
|
chrome.storage.local.set({ messages: messages });
|
||||||
}
|
}
|
||||||
@@ -73,6 +91,24 @@ function checkNewMessages() {
|
|||||||
setTimeout(checkNewMessages, 100);
|
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() {
|
async function update() {
|
||||||
const data = await chrome.storage.local.get(['startTime', 'endTime']);
|
const data = await chrome.storage.local.get(['startTime', 'endTime']);
|
||||||
if (data?.startTime >= 0 && data.startTime != startTime) {
|
if (data?.startTime >= 0 && data.startTime != startTime) {
|
||||||
|
|||||||
Reference in New Issue
Block a user