diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..7afa880
Binary files /dev/null and b/icon.png differ
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..51fa703
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,19 @@
+{
+ "name": "Puzzle leaderboard tracker",
+ "description": "Base Level Extension",
+ "version": "1.0",
+ "manifest_version": 3,
+ "action": {
+ "default_popup": "popup.html",
+ "default_icon": "icon.png"
+ },
+ "permissions": [
+ "storage"
+ ],
+ "content_scripts": [
+ {
+ "js": ["scripts/content.js"],
+ "matches": ["https://meet.google.com/*"]
+ }
+ ]
+}
diff --git a/popup.html b/popup.html
new file mode 100644
index 0000000..3f1875a
--- /dev/null
+++ b/popup.html
@@ -0,0 +1,80 @@
+
+
+
+
+ Puzzle contest
+
+
0:00:00
+
+
+
+
+
+
+
+
+
+
diff --git a/popup.js b/popup.js
new file mode 100644
index 0000000..d5a43b2
--- /dev/null
+++ b/popup.js
@@ -0,0 +1,110 @@
+let startTime = 0;
+let endTime = 0;
+let messagesCount = 0;
+let showMessages = false;
+
+async function init() {
+ const data = await chrome.storage.local.get(['startTime', 'endTime']);
+ if (data?.endTime > 0) {
+ startTime = data.startTime;
+ endTime = data.endTime;
+ document.getElementById('reset-button').style.display = 'block';
+ } else if (data?.startTime > 0) {
+ startTime = data.startTime;
+ document.getElementById('start-button').style.display = 'none';
+ document.getElementById('end-button').style.display = 'block';
+ }
+ const startButton = document.getElementById('start-button');
+ startButton.onclick = startContest;
+ const endButton = document.getElementById('end-button');
+ endButton.onclick = endContest;
+ const resetButton = document.getElementById('reset-button');
+ resetButton.onclick = resetData;
+ const messagesButton = document.getElementById('messages-button');
+ messagesButton.onclick = switchMessages;
+ update();
+}
+
+async function fetchMessages() {
+ const data = await chrome.storage.local.get('messages');
+ if (data.messages && data.messages.length > messagesCount) {
+ const messagesEl = document.getElementById('messages');
+ let html = messagesEl.innerHTML;
+ for (let i = messagesCount; i < data.messages.length; i++) {
+ const {displayTime, completionTime, name, text} = data.messages[i];
+ html += `${displayTime}${name}${text}
`
+ }
+ messagesEl.innerHTML = html;
+ messagesCount = data.messages.length;
+ }
+}
+
+function sendMessage(message, callback) {
+ chrome.tabs.query({active: true, currentWindow: true}, tabs => {
+ tabs.forEach(tab => chrome.tabs.sendMessage(tab.id, message, callback));
+ });
+}
+
+function startContest() {
+ startTime = Date.now();
+ endTime = 0;
+ chrome.storage.local.set({ startTime: startTime, endTime: 0, messages: [] });
+ document.getElementById('start-button').style.display = 'none';
+ document.getElementById('end-button').style.display = 'block';
+ document.getElementById('reset-button').style.display = 'none';
+ document.getElementById('messages').innerHTML = '';
+ if (showMessages) switchMessages();
+}
+
+function endContest() {
+ endTime = Date.now();
+ chrome.storage.local.set({ endTime: endTime });
+ document.getElementById('start-button').style.display = 'block';
+ document.getElementById('end-button').style.display = 'none';
+ document.getElementById('reset-button').style.display = 'block';
+}
+
+function resetData() {
+ chrome.storage.local.set({ startTime: 0, endTime: 0, messages: [] });
+ startTime = 0;
+ endTime = 0;
+ document.getElementById('time').innerHTML = '0:00:00';
+ document.getElementById('reset-button').style.display = 'none';
+ document.getElementById('messages').innerHTML = '';
+ if (showMessages) switchMessages();
+}
+
+function switchMessages() {
+ const messagesEl = document.getElementById('messages');
+ const messagesButtonEl = document.getElementById('messages-button');
+ if (showMessages) {
+ messagesEl.style.display = 'none';
+ messagesButtonEl.innerHTML = 'show messages';
+ } else {
+ messagesEl.style.display = 'block';
+ messagesButtonEl.innerHTML = 'hide messages';
+ }
+ showMessages = !showMessages;
+}
+
+function dis(n) {
+ if (n < 10) return "0" + n;
+ return n.toString();
+}
+
+function update() {
+ // Update the display time in the popup.
+ let displayTime = '0:00:00';
+ if (startTime != 0) {
+ let rawTime = Math.floor((Date.now() - startTime) / 1000);
+ if (endTime > 0) rawTime = Math.floor((endTime - startTime) / 1000);
+ displayTime = `${Math.floor(rawTime / 3600)}:${dis(Math.floor((rawTime % 3600) / 60))}:${dis(rawTime % 60)}`;
+ }
+ document.getElementById('time').innerHTML = displayTime;
+ setTimeout(update, 1000);
+
+ // Fetch and save new messages.
+ fetchMessages();
+}
+
+setTimeout(init, 20);
diff --git a/scripts/content.js b/scripts/content.js
new file mode 100644
index 0000000..5c0937f
--- /dev/null
+++ b/scripts/content.js
@@ -0,0 +1,73 @@
+const seenMessages = new Set();
+const seenDataAnnounceMessages = new Set();
+let awaitingTimes = [];
+let messages = [];
+let startTime = 0;
+
+async function init() {
+ const data = await chrome.storage.local.get(['startTime', 'messages']);
+ if (data?.startTime > 0) {
+ startTime = data.startTime;
+ }
+ if (data.messages) {
+ messages = data.messages;
+ }
+ checkNewMessages();
+ update();
+}
+
+function dis(n) {
+ if (n < 10) return "0" + n;
+ return n.toString();
+}
+
+function displayTime(rawTime) {
+ if (rawTime < 3600) return `${dis(Math.floor((rawTime % 3600) / 60))}:${dis(rawTime % 60)}`;
+ return `${Math.floor(rawTime / 3600)}:${dis(Math.floor((rawTime % 3600) / 60))}:${dis(rawTime % 60)}`;
+}
+
+function checkNewMessages() {
+ // Check announce messages and log their appearing times.
+ const announceMessageElements = document.querySelectorAll("div[data-announce-message]");
+ for (let e of announceMessageElements) {
+ if (seenDataAnnounceMessages.has(e)) continue;
+ seenDataAnnounceMessages.add(e);
+ const completionTime = Math.floor((Date.now() - startTime) / 1000);
+ awaitingTimes.push(completionTime);
+ }
+
+ // 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);
+ 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 = `${dTime}${e.innerHTML}`
+ }
+ 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).
+ if (messageElements.length && awaitingTimes.length) awaitingTimes = [];
+
+ setTimeout(checkNewMessages, 100);
+}
+
+async function update() {
+ const data = await chrome.storage.local.get(['startTime', 'endTime']);
+ if (data?.startTime >= 0 && data.startTime != startTime) {
+ startTime = data.startTime;
+ messages = [];
+ }
+ setTimeout(update, 2000);
+}
+
+setTimeout(init, 20);