diff --git a/ext/html/offscreen.html b/ext/html/offscreen.html
new file mode 100644
index 0000000..c707b01
--- /dev/null
+++ b/ext/html/offscreen.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ext/js/background.js b/ext/js/background.js
index f647453..fd4f9a0 100644
--- a/ext/js/background.js
+++ b/ext/js/background.js
@@ -1,3 +1,11 @@
+if (typeof importScripts !== 'undefined') {
+ importScripts(
+ "../vendor/browser-polyfill.js",
+ "config.js",
+ "common.js",
+ );
+}
+
(function () {
'use strict';
@@ -16,6 +24,19 @@
const contestProblemList = {};
+ /**
+ * The offscreen document being currently created, if any.
+ */
+ let offscreenBeingCreated;
+
+ /**
+ * Return the web browser name this extension is running on.
+ * @returns {string} 'chrome' or 'firefox' depending on the browser
+ */
+ function currentBrowser() {
+ return browser.runtime.getURL('').startsWith('chrome-extension://') ? 'chrome' : 'firefox';
+ }
+
function displayStatusNotification(submitID, problemCode, status) {
browser.notifications.create({
type: 'basic',
@@ -48,14 +69,14 @@
}
/**
- * Display extension's page action.
- * @param tab tab object
+ * Display extension's action.
*/
- function enablePageAction(tab) {
- browser.pageAction.show(tab.id);
- browser.pageAction.onClicked.addListener(() =>
- browser.runtime.openOptionsPage(),
- );
+ function enableAction() {
+ const listener = () => {
+ browser.runtime.openOptionsPage();
+ };
+
+ browser.action.onClicked.addListener(listener);
}
/**
@@ -180,17 +201,18 @@
/**
* Add highlight.js CSS to the page using the selected style.
*/
- function injectHighlightJsCss(tab) {
- storage
+ async function injectHighlightJsCss(tab) {
+ await storage
.get({
[HIGHLIGHT_JS_STYLE_KEY]:
DEFAULT_SETTINGS[HIGHLIGHT_JS_STYLE_KEY],
})
- .then((response) => {
+ .then(async (response) => {
let style = response.highlightJsStyle;
if (style !== 'none') {
- browser.tabs.insertCSS(tab.id, {
- file: `vendor/bower/hjsstyles/${style}.css`,
+ await browser.scripting.insertCSS({
+ files: [`vendor/bower/hjsstyles/${style}.css`],
+ target: { tabId: tab.id },
});
}
});
@@ -285,9 +307,21 @@
if (!response.ok) {
throw new Error(`HTTP Status ${response.status}`);
}
- contestProblemList[contestID] = parseProblemList(
- $.parseHTML(await response.text()),
- );
+ const responseText = await response.text();
+
+ if (currentBrowser() === 'chrome') {
+ await setupOffscreenDocument("/html/offscreen.html");
+ const parseResponse = await chrome.runtime.sendMessage({
+ type: 'parseProblemList',
+ target: 'offscreen',
+ data: responseText
+ });
+ await chrome.offscreen.closeDocument();
+
+ contestProblemList[contestID] = parseResponse;
+ } else {
+ contestProblemList[contestID] = parseProblemList(responseText);
+ }
} catch (error) {
console.error(error);
}
@@ -295,6 +329,35 @@
return contestProblemList[contestID] ?? {};
}
+ /**
+ * Setup offscreen document for parsing HTML
+ * @param path path to the offscreen document
+ * @returns {Promise} promise that resolves when the document is ready
+ */
+ async function setupOffscreenDocument(path) {
+ const offscreenUrl = chrome.runtime.getURL(path);
+ const existingContexts = await chrome.runtime.getContexts({
+ contextTypes: ['OFFSCREEN_DOCUMENT'],
+ documentUrls: [offscreenUrl]
+ });
+
+ if (existingContexts.length > 0) {
+ return;
+ }
+
+ if (offscreenBeingCreated) {
+ await offscreenBeingCreated;
+ } else {
+ offscreenBeingCreated = chrome.offscreen.createDocument({
+ url: path,
+ reasons: [chrome.offscreen.Reason.DOM_PARSER],
+ justification: 'Parse DOM',
+ });
+ await offscreenBeingCreated;
+ offscreenBeingCreated = null;
+ }
+ }
+
/**
* Save joined contest list in Storage
* @param {array} contestList contest list
@@ -316,17 +379,20 @@
});
setUpSessionCookies();
- browser.runtime.onMessage.addListener((request, sender) => {
- if (request.action === 'enablePageAction') {
- enablePageAction(sender.tab);
+ browser.runtime.onMessage.addListener(async (request, sender) => {
+ if (request.action === 'enableAction') {
+ enableAction(sender.tab);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'saveLastContestID') {
saveLastContestID(getContestID(sender.url));
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'displayStatusNotification') {
displayStatusNotification(
request.submitID,
request.problemCode,
request.problemStatus,
);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'modifyContestItemList') {
modifyContestItemList(
request.listName,
@@ -334,6 +400,7 @@
request.value,
request.add,
);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'getContestItemList') {
return new Promise((resolve) => {
getContestItemList(
@@ -343,16 +410,21 @@
);
});
} else if (request.action === 'injectHighlightJsCss') {
- injectHighlightJsCss(sender.tab);
+ await injectHighlightJsCss(sender.tab);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'saveContestProblemList') {
saveContestProblemList(request.contestID, request.problems);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'getContestProblemList') {
return getProblemList(request.contestID);
} else if (request.action === 'setJoinedContestList') {
saveJoinedContestList(request.contestList);
+ return new Promise((resolve) => resolve(null));
} else if (request.action === 'getJoinedContestList') {
return getJoinedContestList();
}
- return new Promise((resolve) => resolve(null));
+
+ console.warn(`Unexpected message type received: '${request.action}'.`);
+ return false;
});
})();
diff --git a/ext/js/common.js b/ext/js/common.js
index eb8c55f..5e76a97 100644
--- a/ext/js/common.js
+++ b/ext/js/common.js
@@ -27,7 +27,9 @@ if (typeof module !== 'undefined') {
};
}
-function parseProblemList(jqueryHandles) {
+function parseProblemList(htmlText) {
+ const jqueryHandles = $.parseHTML(htmlText);
+
return Object.fromEntries(
jqueryHandles.flatMap((el) =>
[
diff --git a/ext/js/general.js b/ext/js/general.js
index 75efb05..a80285d 100644
--- a/ext/js/general.js
+++ b/ext/js/general.js
@@ -1,7 +1,7 @@
(function () {
'use strict';
- browser.runtime.sendMessage({ action: 'enablePageAction' });
+ browser.runtime.sendMessage({ action: 'enableAction' });
const BANNER_URL = browser.runtime.getURL('images/satori_banner.png');
const TCS_LOGO_URL = browser.runtime.getURL('images/tcslogo.svg');
diff --git a/ext/js/offscreen.html b/ext/js/offscreen.html
deleted file mode 100644
index 177e040..0000000
--- a/ext/js/offscreen.html
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/ext/js/offscreen.js b/ext/js/offscreen.js
index e26ae88..20c0aa1 100644
--- a/ext/js/offscreen.js
+++ b/ext/js/offscreen.js
@@ -1,48 +1,12 @@
-importScripts(
- "../vendor/browser-polyfill.js",
- "config.js",
- "../vendor/bower/jquery.min.js"
-);
-
-browser.runtime.onMessage.addListener(handleMessages);
-
-async function handleMessages(message) {
- if (message.target !== 'offscreen') {
+browser.runtime.onMessage.addListener(async (request) => {
+ if (request.target !== 'offscreen') {
return false;
}
- if (message.type === 'parseProblemList') {
- parseProblemList(message.data);
- } else {
- console.warn(`Unexpected message type received: '${message.type}'.`);
- return false;
+ if (request.type === 'parseProblemList') {
+ return parseProblemList(request.data);
}
-}
-
-function parseProblemList(htmlText) {
- const jqueryHandles = $.parseHTML(htmlText);
-
- return Object.fromEntries(
- jqueryHandles.flatMap((el) =>
- [
- ...$(el).find('#content table.results tr:not(:first-of-type)'),
- ].map((tr) => [
- $(tr).find('td:nth-child(1)').text(),
- {
- title: $(tr).find('td:nth-child(2)').text(),
- href: $(tr).find('td:nth-child(2) a').attr('href'),
- pdfHref: $(tr).find('td:nth-child(3) a').attr('href'),
- submitHref: $(tr).find('td:nth-child(5) a').attr('href'),
- },
- ]),
- ),
- );
-}
-function sendToBackground(type, data) {
- browser.runtime.sendMessage({
- type,
- target: 'background',
- data
- });
-}
\ No newline at end of file
+ console.warn(`Unexpected message type received: '${request.type}'.`);
+ return false;
+});
diff --git a/ext/js/results.js b/ext/js/results.js
index 2800c79..021bc16 100644
--- a/ext/js/results.js
+++ b/ext/js/results.js
@@ -17,9 +17,7 @@
let problemStatus = tr.find('td:last').text();
let url = document.location.href;
- console.log("XD");
const contestID = getContestID(url);
- console.log("XD2");
/**
diff --git a/ext/js/service-worker.js b/ext/js/service-worker.js
deleted file mode 100644
index 32c3739..0000000
--- a/ext/js/service-worker.js
+++ /dev/null
@@ -1,368 +0,0 @@
-importScripts(
- "../vendor/browser-polyfill.js",
- "config.js",
-);
-
-(function () {
- 'use strict';
-
- const SATORI_URL_CONTEST = SATORI_URL_HTTPS + 'contest/';
-
- let storage = browser.storage.sync || browser.storage.local;
-
- let lastContestID;
- /**
- * A set that stores information about which tabs have Satori open. It is
- * used to redirect the user to the last contest (if they are already
- * browsing the website, then we shouldn't redirect).
- * @type {Set}
- */
- let satoriTabs = new Set();
-
- const contestProblemList = {};
-
- function displayStatusNotification(submitID, problemCode, status) {
- browser.notifications.create({
- type: 'basic',
- title: 'Submit ' + submitID + ' (' + problemCode + ') status',
- message: 'New problem status: ' + status,
- iconUrl: 'icon48.png',
- });
- }
-
- /**
- * Retrieve the ID of the last visited contest and store it in lastContestID
- * variable.
- * @see setUpLastContestRedirect
- */
- function retrieveLastContestID() {
- storage.get('lastContestID').then((response) => {
- lastContestID = response.lastContestID;
- });
- }
-
- /**
- * Save last contest ID both in Storage and our local variable.
- * @param {string} contestID last contest ID
- * @see setUpLastContestRedirect
- */
- function saveLastContestID(contestID) {
- storage.set({ lastContestID: contestID });
- lastContestID = contestID;
- }
-
- /**
- * Display extension's page action.
- * @param tab tab object
- */
- function enablePageAction(tab) {
- const listener = () => {
- browser.runtime.openOptionsPage();
- };
-
- if (chrome.action !== undefined) {
- chrome.action.onClicked.addListener(listener);
- } else {
- browser.pageAction.show(tab.id);
- browser.pageAction.onClicked.addListener(listener);
- }
- }
-
- /**
- * Add or remove a value to given list bound of a specified contest.
- * @param {string} listName name of the list to look for in the storage
- * @param {string} contestID ID of the contest to modify its list
- * @param {string} value value to add or remove
- * @param {boolean} add whether to add or remove given element
- * @see getContestItemList
- */
- function modifyContestItemList(listName, contestID, value, add) {
- storage
- .get({
- [listName]: {},
- })
- .then((items) => {
- let list = items[listName];
- if (!(contestID in list)) {
- list[contestID] = [];
- }
-
- let s = new Set(list[contestID]);
- if (add) {
- s.add(value);
- } else {
- s.delete(value);
- }
- list[contestID] = [...s];
-
- storage.set({ [listName]: list });
- });
- }
-
- /**
- * Retrieve a list bound to given contest.
- *
- * This function is used e.g. to store the list of problem hidden in for a
- * contest.
- *
- * @param {string} listName name of the list to get
- * @param {string} contestID ID of the contest to get the list for
- * @param {function} sendResponse function to call with the data retrieved
- * @see modifyContestItemList
- */
- function getContestItemList(listName, contestID, sendResponse) {
- storage
- .get({
- [listName]: {},
- })
- .then((items) => {
- if (contestID in items[listName]) {
- sendResponse(items[listName][contestID]);
- } else {
- sendResponse([]);
- }
- });
- }
-
- /**
- * Add an onBeforeRequest listener that redirects to the last contest
- * if the user just entered Satori webpage.
- *
- * Also, the function adds webNavigation.onCommitted and tabs.onRemoved
- * listeners to keep the list of Satori tabs.
- */
- function setUpLastContestRedirect() {
- // Store which tabs have Satori open
- browser.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
- if (
- tab.url &&
- (tab.url.startsWith(SATORI_URL_HTTP) ||
- tab.url.startsWith(SATORI_URL_HTTPS))
- ) {
- satoriTabs.add(tabId);
- } else {
- satoriTabs.delete(tabId);
- }
- });
-
- browser.tabs.onRemoved.addListener((tabId) => satoriTabs.delete(tabId));
-
- // browser.webRequest.onBeforeRequest.addListener(
- // function (details) {
- // if (
- // typeof lastContestID === 'undefined' ||
- // lastContestID === null ||
- // satoriTabs.has(details.tabId)
- // ) {
- // // If we haven't saved any contest yet, then do nothing
- // // Also, don't redirect if the user is already browsing
- // // Satori
- // return;
- // }
- //
- // return {
- // redirectUrl: SATORI_URL_CONTEST + lastContestID + '/',
- // };
- // },
- // {
- // urls: [
- // '*://satori.tcs.uj.edu.pl/',
- // '*://satori.tcs.uj.edu.pl/news',
- // ],
- // types: ['main_frame'],
- // },
- // ['blocking'],
- // );
- }
-
- /**
- * Add highlight.js CSS to the page using the selected style.
- */
- async function injectHighlightJsCss(tab) {
- await storage
- .get({
- [HIGHLIGHT_JS_STYLE_KEY]:
- DEFAULT_SETTINGS[HIGHLIGHT_JS_STYLE_KEY],
- })
- .then(async (response) => {
- let style = response.highlightJsStyle;
- if (style !== 'none') {
- await browser.scripting.insertCSS({
- files: [`vendor/bower/hjsstyles/${style}.css`],
- target: { tabId: tab.id },
- });
- }
- });
- }
-
- const satoriDomain = new URL(SATORI_URL_HTTPS).hostname;
-
- function getKeepSignedInDuration() {
- return storage
- .get({
- [KEEP_SIGNED_IN_DURATION_KEY]:
- DEFAULT_SETTINGS[KEEP_SIGNED_IN_DURATION_KEY],
- })
- .then((response) => {
- const duration = response[KEEP_SIGNED_IN_DURATION_KEY];
- if (duration === 'none') {
- return null;
- }
- return parseInt(duration, 10);
- });
- }
-
- function updateCookie(cookie) {
- return getKeepSignedInDuration().then((duration) => {
- if (duration === null) {
- return;
- }
- const newCookie = {
- expirationDate:
- Math.round(Date.now() / 1000) + duration * 24 * 60 * 60,
- httpOnly: cookie.httpOnly,
- name: cookie.name,
- path: cookie.path,
- sameSite: cookie.sameSite,
- secure: cookie.secure,
- storeId: cookie.storeId,
- value: cookie.value,
- url: SATORI_URL_HTTPS,
- };
- return browser.cookies.set(newCookie);
- });
- }
-
- function getTokenCookies() {
- return browser.cookies.getAll({
- domain: satoriDomain,
- name: 'satori_token',
- path: '/',
- });
- }
-
- function updateExistingCookie() {
- return getTokenCookies().then((cookies) => {
- if (cookies.length === 0) {
- return;
- }
- if (cookies.length > 1) {
- console.warn('Too many satori_token cookies');
- return;
- }
- const [cookie] = cookies;
- if (cookie.expirationDate === undefined && cookie.value !== '') {
- return updateCookie(cookie);
- }
- });
- }
-
- function setUpSessionCookies() {
- updateExistingCookie().catch(console.error);
- browser.cookies.onChanged.addListener(({ removed, cookie }) => {
- if (
- !removed &&
- cookie.domain === satoriDomain &&
- cookie.name === 'satori_token' &&
- cookie.path === '/'
- ) {
- updateExistingCookie().catch(console.error);
- }
- });
- }
-
- function saveContestProblemList(contestID, problems) {
- contestProblemList[contestID] = problems;
- }
-
- async function getProblemList(contestID) {
- if (!contestProblemList[contestID]) {
- try {
- const response = await fetch(
- `${SATORI_URL_HTTPS}contest/${contestID}/problems`,
- );
- if (!response.ok) {
- throw new Error(`HTTP Status ${response.status}`);
- }
-
- await chrome.offscreen.createDocument({
- url: "offscreen.html",
- reasons: [chrome.offscreen.Reason.DOM_PARSER],
- justification: 'Parse DOM'
- });
- const parseResponse = await chrome.runtime.sendMessage({
- type: 'parseProblemList',
- target: 'offscreen',
- data: await response.text()
- });
- await chrome.offscreen.closeDocument();
- console.log(parseResponse);
-
- contestProblemList[contestID] = parseResponse;
- } catch (error) {
- console.error(error);
- }
- }
- return contestProblemList[contestID] ?? {};
- }
-
- /**
- * Save joined contest list in Storage
- * @param {array} contestList contest list
- */
- function saveJoinedContestList(contestList) {
- storage.set({ contestList });
- }
-
- /**
- * Save joined contest list in Storage
- * @return {array} contest list
- */
- async function getJoinedContestList() {
- return (await storage.get('contestList')).contestList ?? [];
- }
-
- retrieveLastContestID();
- setUpLastContestRedirect();
- setUpSessionCookies();
-
- browser.runtime.onMessage.addListener(async (request, sender) => {
- if (request.action === 'enablePageAction') {
- enablePageAction(sender.tab);
- } else if (request.action === 'saveLastContestID') {
- saveLastContestID(getContestID(sender.url));
- } else if (request.action === 'displayStatusNotification') {
- displayStatusNotification(
- request.submitID,
- request.problemCode,
- request.problemStatus,
- );
- } else if (request.action === 'modifyContestItemList') {
- modifyContestItemList(
- request.listName,
- getContestID(sender.url),
- request.value,
- request.add,
- );
- } else if (request.action === 'getContestItemList') {
- return new Promise((resolve) => {
- getContestItemList(
- request.listName,
- getContestID(sender.url),
- resolve,
- );
- });
- } else if (request.action === 'injectHighlightJsCss') {
- await injectHighlightJsCss(sender.tab);
- } else if (request.action === 'saveContestProblemList') {
- saveContestProblemList(request.contestID, request.problems);
- } else if (request.action === 'getContestProblemList') {
- return getProblemList(request.contestID);
- } else if (request.action === 'setJoinedContestList') {
- saveJoinedContestList(request.contestList);
- } else if (request.action === 'getJoinedContestList') {
- return getJoinedContestList();
- }
- return new Promise((resolve) => resolve(null));
- });
-})();
diff --git a/ext/manifest.json b/ext/manifest.json
index f061764..30b1140 100644
--- a/ext/manifest.json
+++ b/ext/manifest.json
@@ -10,7 +10,7 @@
"browser_specific_settings": {
"gecko": {
"id": "{a7a2f4f4-8648-4221-8e88-af047e0fe5ca}",
- "strict_min_version": "42.0"
+ "strict_min_version": "132.0"
}
},
@@ -44,7 +44,14 @@
},
"background": {
- "service_worker": "js/service-worker.js"
+ "service_worker": "js/background.js",
+ "scripts": [
+ "vendor/browser-polyfill.js",
+ "js/config.js",
+ "vendor/bower/jquery.min.js",
+ "js/common.js",
+ "js/background.js"
+ ]
},
"web_accessible_resources": [
diff --git a/ext/scss/options.scss b/ext/scss/options.scss
index aee1589..e83b3a8 100644
--- a/ext/scss/options.scss
+++ b/ext/scss/options.scss
@@ -1,3 +1,18 @@
+html, body {
+ font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ color: #333;
+ background-color: #fff;
+}
+
+body {
+ margin: 1em;
+}
+
+h2 {
+ font-weight: 400;
+}
+
h2:not(:first-child) {
margin-top: 35px;
}
diff --git a/gulpfile.js b/gulpfile.js
index 394cae9..1d1ee62 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -37,6 +37,7 @@ let distFiles = [
'icon48.png',
'icon128.png',
'options.html',
+ 'html/**/*',
'css/**/*',
'js/**/*',
'images/**/*',