Skip to content

Commit bfbb395

Browse files
committed
fix: instead of click listener listen to url changes to handle all the cases
1 parent 1d9ca71 commit bfbb395

File tree

2 files changed

+217
-85
lines changed

2 files changed

+217
-85
lines changed

chrome-extension/background.js

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,122 @@
11
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
22
if (
33
changeInfo.status === "complete" &&
4-
/^https:\/\/leetcode.com\/contest\/.*\/ranking\//.test(tab.url)
4+
/^https:\/\/leetcode.com\/contest\/.+/.test(tab.url)
55
) {
66
chrome.scripting
77
.executeScript({
8-
target: { tabId: tabId },
8+
target: {
9+
tabId: tabId,
10+
},
911
files: ["./foreground.js"],
1012
})
1113
.then(() => {
1214
console.log(
1315
`Injected the foreground script into tab: ${tabId}`
1416
);
17+
chrome.tabs.sendMessage(tabId, {
18+
message: "url_updated",
19+
url: tab.url,
20+
});
1521
})
1622
.catch((err) => console.error(err));
1723
}
1824
});
1925

20-
const BASE_URL = new URL("http://127.0.0.1:8080/api/v1/predictions"); // TODO: replace it
26+
const API_URLS = [
27+
"https://leetcode-rating-predictor.herokuapp.com/api/v1/predictions",
28+
"https://leetcode-predictor.herokuapp.com/api/v1/predictions",
29+
];
2130

2231
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2332
if (request.message === "get_predictions") {
24-
const url = BASE_URL;
25-
url.searchParams.set("contestId", request.data.contestId);
26-
let handles = "";
27-
request.data.handles.forEach((handle, index) => {
28-
handles +=
29-
handle + (index !== request.data.handles.length - 1 ? ";" : "");
30-
});
31-
url.searchParams.set("handles", handles);
32-
fetch(url)
33-
.then((res) => res.json())
33+
getPredictions(request.data)
3434
.then((res) => {
35+
// console.log(res);
3536
sendResponse(res);
3637
})
37-
.catch((err) => console.error(err));
38+
.catch((err) => {
39+
console.error(err);
40+
});
3841
return true;
3942
}
4043
});
44+
45+
async function getPredictions(data) {
46+
try {
47+
let urlIndex = await getURL_INDEX();
48+
if (urlIndex === undefined || urlIndex < 0 || urlIndex >= API_URLS) {
49+
urlIndex = 0;
50+
}
51+
for (let i = 0; i < API_URLS.length; i++) {
52+
try {
53+
const ind = (i + urlIndex) % API_URLS.length;
54+
const url = new URL(API_URLS[ind]);
55+
56+
url.searchParams.set("contestId", data.contestId);
57+
let handles = "";
58+
data.handles.forEach((handle, index) => {
59+
handles +=
60+
handle + (index !== data.handles.length - 1 ? ";" : "");
61+
});
62+
url.searchParams.set("handles", handles);
63+
64+
const resp = await fetchFromAPI(url);
65+
setURL_INDEX(ind).catch((err) => {
66+
console.error(err);
67+
});
68+
return resp;
69+
} catch (err) {
70+
console.error(err);
71+
}
72+
}
73+
} catch (err) {
74+
console.error(err);
75+
}
76+
}
77+
78+
async function fetchFromAPI(url, retries = 5) {
79+
let resp = await fetch(url);
80+
if (resp.status !== 200) {
81+
if (retries > 0) {
82+
resp = await fetchFromAPI(url, retries - 1);
83+
return resp;
84+
}
85+
throw new Error(resp.statusText);
86+
}
87+
resp = await resp.json();
88+
return resp;
89+
}
90+
91+
async function setURL_INDEX(index) {
92+
try {
93+
const promise = new Promise((resolve, reject) => {
94+
chrome.storage.sync.set(
95+
{
96+
url_index: index,
97+
},
98+
function () {
99+
resolve();
100+
}
101+
);
102+
});
103+
await promise;
104+
} catch (err) {
105+
return err;
106+
}
107+
}
108+
109+
async function getURL_INDEX() {
110+
try {
111+
const promise = new Promise((resolve, reject) => {
112+
chrome.storage.sync.get(["url_index"], function (result) {
113+
resolve(result.url_index);
114+
});
115+
});
116+
const index = await promise;
117+
return index;
118+
} catch (err) {
119+
console.error(err);
120+
return -1;
121+
}
122+
}

chrome-extension/foreground.js

Lines changed: 121 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ if (!window.CFPredictorInjected) {
77
const tbody = document.querySelector("tbody");
88
if (!tbody) {
99
setTimeout(setEventListener, 100);
10+
return;
1011
}
1112
const trs = tbody.querySelectorAll("tr");
13+
if (!trs) {
14+
setTimeout(setEventListener, 100);
15+
return;
16+
}
1217
if (trs.length <= 1) {
1318
// listen only if there is more than one row
1419
isListenerActive = false;
@@ -19,33 +24,54 @@ if (!window.CFPredictorInjected) {
1924
isListenerActive = false;
2025
return;
2126
}
27+
if (isListenerActive) return;
2228
isListenerActive = true;
23-
tds[1].addEventListener("DOMCharacterDataModified", () => {
29+
tds[1].addEventListener("DOMCharacterDataModified", async () => {
2430
window.clearTimeout(predictionsTimer);
25-
predictionsTimer = setTimeout(fetchPredictions, 1000);
31+
predictionsTimer = setTimeout(fetchPredictions, 500);
2632
});
2733
} catch (err) {
2834
console.error(err);
2935
}
3036
};
3137

38+
const isContestRankingPage = (url) => {
39+
return /^https:\/\/leetcode.com\/contest\/.*\/ranking/.test(url);
40+
};
41+
3242
let rowsChanged = new Map();
33-
let colInserted = false;
43+
let deltaTHInserted = false;
3444

3545
const fetchPredictions = async () => {
3646
const thead = document.querySelector("thead");
3747
if (!thead) {
38-
predictionsTimer = setTimeout(fetchPredictions, 100);
48+
predictionsTimer = setTimeout(fetchPredictions, 500);
3949
return;
4050
}
51+
4152
const tbody = document.querySelector("tbody");
53+
if (!tbody) {
54+
predictionsTimer = setTimeout(fetchPredictions, 500);
55+
return;
56+
}
57+
4258
const rows = tbody.querySelectorAll("tr");
43-
const contestId = document
44-
.querySelector(".ranking-title-wrapper")
45-
.querySelector("span")
46-
.querySelector("a")
47-
.innerHTML.toLowerCase()
48-
.replace(/\s/g, "-");
59+
if (!rows) {
60+
predictionsTimer = setTimeout(fetchPredictions, 500);
61+
return;
62+
}
63+
let contestId;
64+
try {
65+
contestId = document
66+
.querySelector(".ranking-title-wrapper")
67+
.querySelector("span")
68+
.querySelector("a")
69+
.innerHTML.toLowerCase()
70+
.replace(/\s/g, "-");
71+
} catch {
72+
predictionsTimer = setTimeout(fetchPredictions, 500);
73+
return;
74+
}
4975
const handlesMap = new Map();
5076

5177
const handles = [...rows].map((row, index) => {
@@ -70,90 +96,114 @@ if (!window.CFPredictorInjected) {
7096
return handle;
7197
}
7298
} catch (err) {
73-
console.error(err);
99+
console.debug(err);
74100
}
75101
});
76102

77103
chrome.runtime.sendMessage(
78104
{
79105
message: "get_predictions",
80-
data: { contestId, handles },
106+
data: {
107+
contestId,
108+
handles,
109+
},
81110
},
82111
(response) => {
83-
if (response.status === "OK") {
84-
if (!colInserted) {
85-
const th = document.createElement("th");
86-
th.innerText = "Δ";
87-
thead.querySelector("tr").appendChild(th);
88-
colInserted = true;
89-
}
90-
const rowsUpdated = new Map();
91-
for (item of response.items) {
92-
try {
93-
const id = (
94-
item.data_region +
95-
"/" +
96-
item._id
97-
).toLowerCase();
98-
if (handlesMap.has(id)) {
99-
const rowIndex = handlesMap.get(id);
100-
const row = rows[rowIndex];
101-
let td;
102-
if (rowsChanged.has(rowIndex)) {
103-
td = row.lastChild;
104-
} else {
105-
td = document.createElement("td");
106-
}
107-
if (item.delta == null) {
108-
td.innerText = "?";
109-
td.style.color = "gray";
110-
} else {
111-
const delta =
112-
Math.round(item.delta * 100) / 100;
113-
td.innerText =
114-
delta > 0 ? "+" + delta : delta;
115-
if (delta > 0) {
116-
td.style.color = "green";
112+
if (!response) {
113+
return;
114+
}
115+
try {
116+
if (response.status === "OK") {
117+
if (!deltaTHInserted && response.meta.total_count) {
118+
const th = document.createElement("th");
119+
th.innerText = "Δ";
120+
thead.querySelector("tr").appendChild(th);
121+
deltaTHInserted = true;
122+
}
123+
const rowsUpdated = new Map();
124+
for (item of response.items) {
125+
try {
126+
const id = (
127+
item.data_region +
128+
"/" +
129+
item._id
130+
).toLowerCase();
131+
if (handlesMap.has(id)) {
132+
const rowIndex = handlesMap.get(id);
133+
const row = rows[rowIndex];
134+
let td;
135+
if (rowsChanged.has(rowIndex)) {
136+
td = row.lastChild;
117137
} else {
138+
td = document.createElement("td");
139+
}
140+
if (item.delta == null) {
141+
td.innerText = "?";
118142
td.style.color = "gray";
143+
} else {
144+
const delta =
145+
Math.round(item.delta * 100) / 100;
146+
td.innerText =
147+
delta > 0 ? "+" + delta : delta;
148+
if (delta > 0) {
149+
td.style.color = "green";
150+
} else {
151+
td.style.color = "gray";
152+
}
153+
// td.style.fontWeight = "bold";
119154
}
120-
// td.style.fontWeight = "bold";
121-
}
122-
if (!rowsChanged.has(rowIndex)) {
123-
row.appendChild(td);
155+
if (!rowsChanged.has(rowIndex)) {
156+
row.appendChild(td);
157+
}
158+
rowsUpdated.set(rowIndex, true);
124159
}
125-
rowsUpdated.set(rowIndex, true);
160+
} catch (err) {
161+
console.warn(err);
126162
}
127-
} catch (err) {
128-
console.error(err);
129163
}
130-
}
131-
for (rowIndex of rowsChanged.keys()) {
132-
if (!rowsUpdated.has(rowIndex)) {
133-
try {
134-
const row = rows[rowIndex];
135-
row.lastChild.innerText = "";
136-
} catch (err) {
137-
console.error(err);
164+
for (rowIndex of rowsChanged.keys()) {
165+
if (
166+
!rowsUpdated.has(rowIndex) &&
167+
rowIndex < rows.length
168+
) {
169+
try {
170+
const row = rows[rowIndex];
171+
row.lastChild.innerText = "";
172+
} catch {}
173+
}
174+
if (rowIndex >= rows.length) {
175+
rowsChanged.delete(rowIndex);
138176
}
139177
}
178+
for (rowIndex of rowsUpdated.keys()) {
179+
rowsChanged.set(rowIndex, true);
180+
}
140181
}
141-
for (rowIndex of rowsUpdated.keys()) {
142-
rowsChanged.set(rowIndex, true);
143-
}
182+
} catch (err) {
183+
console.warn(err);
144184
}
145185
}
146186
);
147187
};
148-
fetchPredictions();
149-
setEventListener();
150188

151-
// event listener for "click" event on page-btn class items
152-
[...document.querySelectorAll(".page-btn")].forEach((item) => {
153-
item.addEventListener("click", (e) => {
154-
if (!isListenerActive) {
189+
// listen to url changes
190+
chrome.runtime.onMessage.addListener(function (
191+
request,
192+
sender,
193+
sendResponse
194+
) {
195+
if (request.message === "url_updated") {
196+
if (!isListenerActive && isContestRankingPage(request.url)) {
197+
window.clearTimeout(predictionsTimer);
198+
predictionsTimer = setTimeout(fetchPredictions, 500);
199+
}
200+
if (!isContestRankingPage(request.url)) {
201+
isListenerActive = false;
202+
rowsChanged.clear();
203+
deltaTHInserted = false;
204+
} else {
155205
setEventListener();
156206
}
157-
});
207+
}
158208
});
159209
}

0 commit comments

Comments
 (0)