├── .babelrc
├── .github
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .gitignore
├── README.md
├── content-script.css
├── content-script.js
├── deployment-zip-maker.sh
├── docs
├── HowToGetAPIKey.md
├── LICENSE
└── termsNCondition.md
├── icons
├── close_eye.png
├── eye_open.png
├── github.png
├── linkedin.png
├── logo-128.png
├── logo-96.png
├── logo.png
└── mail.png
├── images
├── api-key-img.png
├── cohere-login-page.png
├── lock-hide.gif
├── logoFull.png
├── solve-highlight.gif
├── ui-1280x800-dark.png
├── ui-640x400.png
├── ui-dark-new.png
├── ui-dark.png
├── ui.png
├── ui1-m.png
├── ui2-1280x800.png
├── ui2-m.png
├── ui3-m.png
├── ui3.png
├── ui4-1280x800.png
└── ui4.png
├── manifest-chrome.json
├── manifest-firefox.json
├── popup.css
├── popup.css.map
├── popup.html
├── popup.js
├── popup.scss
├── scripts
├── constants.js
├── content-script-main.js
├── debugger.js
├── feature-strategies.js
├── message-publisher.js
├── mode.js
├── mutation-observer.js
├── page-checker.js
├── popup-main.js
├── service-worker-main.js
├── strategies
│ ├── base-strategy.js
│ ├── coding-area-strategy.js
│ ├── contest-strategy.js
│ ├── problem-set-strategy.js
│ └── strategy-factory.js
└── utils.js
└── service-worker.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "env"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | 1. Is there any specific feature that is not working or the entire extension failed?
11 |
12 | 2. Which version of extension are you using?
13 |
14 | 3. Which browser are you using? Have you tried changing it (As the extension is also available for firefox and edge)
15 |
16 | 4. On which url of leetcode issue is encountered?
17 |
18 | 5. Are you using the old or new UI of leetcode?
19 |
20 | 6. Please add any screenshots or additional information that might help
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # MacOS Files
2 | .DS_Store
3 |
4 | # TODO: Add More Relevant Ones
5 | popup.css.map
6 |
7 | # Deployment files
8 | deployment/
9 | *.zip
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
$1
');
350 | storeDataWithObjectWrapping("outputDivCode", outputDiv.innerHTML);
351 | })
352 | .catch(error => {
353 | alert("Operation failed. Please check your api key.");
354 | print("error while sending request to cohere" + error);
355 | }).finally( () => {
356 | actionButtonUsable();
357 | })
358 | }
359 |
360 | function actionButtonUsable() {
361 | const triggerButton = document.getElementById('trigger-action');
362 | triggerButton.innerText="TC & SC"
363 | triggerButton.style.backgroundColor = 'buttonface'; // Set background to green when usable
364 | triggerButton.disabled = false; // Enable the button
365 | }
366 |
367 | function actionButtonUnusable() {
368 | const triggerButton = document.getElementById('trigger-action');
369 | triggerButton.innerText="processing.."
370 | triggerButton.style.backgroundColor = 'yellow'; // Set background to yellow when unusable
371 | triggerButton.disabled = true; // Disable the button
372 | }
373 | },{"./constants.js":1,"./debugger.js":2,"./utils.js":4}],4:[function(require,module,exports){
374 | const print = require('./debugger.js');
375 | let browser = window.browser || window.chrome;
376 |
377 | /**
378 | * Checks if an object is iterable.
379 | * @param {Object} obj - The object to check.
380 | * @returns {boolean} - True if the object is iterable, false otherwise.
381 | */
382 | function isIterable(obj) {
383 | if(obj != null && typeof obj[Symbol.iterator] === 'function')
384 | return true;
385 | print(`${JSON.stringify(obj)} is not iterable`)
386 | }
387 |
388 | function storeDataWithObjectWrapping(key, value) {
389 | const data = {};
390 | data[key] = value;
391 | storeData(data);
392 | }
393 |
394 | function storeData(data) {
395 | browser.storage.local.set(data);
396 | }
397 |
398 |
399 | function getData(key, callback) {
400 | try {
401 | browser.storage.local.get([key], result => callback(result[key]));
402 | }
403 | catch (err) {
404 | print("Error while retrieving key");
405 | }
406 | }
407 |
408 | async function getDataAsUint8Array(key) {
409 | return new Uint8Array((await browser.storage.local.get(key))[key] || []);
410 | }
411 |
412 | // Export the isIterable function using CommonJS syntax
413 | module.exports = {isIterable, storeDataWithObjectWrapping, getData, getDataAsUint8Array, storeData};
414 | },{"./debugger.js":2}]},{},[3]);
415 |
--------------------------------------------------------------------------------
/popup.scss:
--------------------------------------------------------------------------------
1 | $f1: 20px;
2 | $f2: 12px;
3 | $c1: #F0F0F0;
4 | $bg-dark: rgba(40, 42, 46, 1);
5 | $bg2-dark: rgba(34, 35, 37, 1);
6 | $bg3-dark: rgb(60, 60, 61);
7 | $c1-dark: rgba(255, 255, 255, 0.9);
8 | $link-color: rgb(14, 115, 222);
9 |
10 | html,
11 | body {
12 | margin: 0;
13 | padding: 0;
14 | outline: 0;
15 | vertical-align: baseline;
16 | background: transparent;
17 | }
18 |
19 | .wrapper {
20 | max-width: 1000px;
21 | min-width: 280px;
22 | }
23 |
24 | .wrapper {
25 | .link {
26 | font-size: $f2;
27 | text-align: center;
28 | }
29 |
30 | .brand {
31 | background-color: $bg-dark;
32 | font-size: $f1;
33 | padding: 10px;
34 | color: $c1-dark;
35 |
36 | .title {
37 | text-decoration: none;
38 | color: $c1-dark;
39 |
40 | &:hover {
41 | color: grey;
42 | }
43 | }
44 |
45 | ;
46 | box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px,
47 | rgba(0, 0, 0, 0) 0px 0px 0px 0px,
48 | rgba(0, 0, 0, 0.25) 0px 4px 4px 0px,
49 | rgba(0, 0, 0, 0.3) 0px 1px 2px 0px;
50 | }
51 |
52 | .logo {
53 | height: 50px;
54 | width: 50px;
55 | border-radius: 10px;
56 | }
57 |
58 | .github {
59 | margin-top: 10px;
60 | filter: invert(1);
61 | }
62 |
63 | .brand span {
64 | position: absolute;
65 | padding-left: 10px;
66 | }
67 |
68 | .notice {
69 | padding: 10px;
70 | color: white;
71 | line-height: 15px;
72 | background-color: $bg3-dark;
73 | font-size: $f2;
74 | }
75 |
76 | .options {
77 | padding: 10px;
78 | background-color: $bg2-dark;
79 | color: $c1-dark;
80 |
81 | div {
82 | margin-top: 10px;
83 | margin-bottom: 10px;
84 |
85 | input {
86 | float: right;
87 |
88 | &:hover {
89 | cursor: pointer;
90 | }
91 | }
92 | }
93 |
94 | input[type="checkbox"] {
95 | display: none;
96 | }
97 | }
98 |
99 | .footer {
100 | background-color: $bg-dark;
101 | padding: 5px 10px;
102 |
103 | a {
104 | text-decoration: none;
105 | }
106 |
107 | span {
108 | position: relative;
109 | color: $c1-dark;
110 | }
111 |
112 | #fBtn span {
113 | text-decoration: underline;
114 | }
115 |
116 | // img {
117 | // height: 20px;
118 | // width: auto;
119 | // }
120 | #deBtn {
121 | float: right;
122 | cursor: pointer;
123 | }
124 | }
125 |
126 | .details {
127 | color: $c1-dark;
128 | background-color: $bg-dark;
129 | padding: 5px 10px;
130 | line-height: 15px;
131 | box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.25) 0px 4px 4px 0px, rgba(0, 0, 0, 0.3) 0px 1px 2px 0px;
132 |
133 | a {
134 | color: $c1-dark;
135 | }
136 |
137 | }
138 |
139 | .creator {
140 | color: $c1-dark;
141 | background-color: $bg-dark;
142 | padding: 5px 10px;
143 | text-align: center;
144 |
145 | .linkedin {
146 | height: 20px;
147 | width: 20px;
148 | margin-bottom: -5px;
149 | }
150 | }
151 |
152 | .contributors {
153 | a {
154 | color: $c1-dark;
155 | }
156 |
157 | padding: 5px 10px;
158 | text-align: center;
159 | }
160 |
161 | }
162 |
163 | /* Checkbox UI enhancement */
164 |
165 | .checkbox-label {
166 | display: inline-block;
167 | width: 1.2rem;
168 | height: 1.2rem;
169 | background-image: url('/icons/close_eye.png');
170 | background-size: contain;
171 | }
172 |
173 | input[type="checkbox"]:checked+.checkbox-label {
174 | background-image: url('/icons/eye_open.png');
175 | }
176 |
177 |
178 | .option-div {
179 | width: 100%;
180 | display: flex;
181 | justify-content: space-between;
182 | }
183 |
184 | .ai-box {
185 | background-color: $bg2-dark;
186 | color: $c1-dark;
187 | padding:10px;
188 |
189 | .ai-terms {
190 | margin-bottom: 5px;
191 |
192 | a {
193 | color: $c1;
194 | }
195 | }
196 |
197 | #api-key {
198 | width: 99%;
199 | margin-top: 5px;
200 | margin-bottom: 5px;
201 | }
202 |
203 | .ai-inputs {
204 | margin-top: 5px;
205 | margin-bottom: 5px;
206 | }
207 |
208 | .ai-getKey {
209 | font-size: $f2;
210 | text-decoration: underline;
211 | cursor: pointer;
212 |
213 | :hover {
214 | color: $link-color;
215 | text-decoration: $link-color;
216 | }
217 |
218 | a {
219 | color: $c1;
220 | }
221 | }
222 |
223 | .output {
224 | height: 300px;
225 | overflow: auto;
226 | border-radius: 5px; // Optional: rounded corners
227 | color: $bg-dark; // Text color (modify as needed)
228 | font-size: $f2; // Font size (modify as needed)
229 | font-family: monospace;
230 | white-space: pre-wrap; /* Preserve both spaces and line breaks */
231 | }
232 |
233 | .ai-button {
234 | margin-left: 5px;
235 |
236 | &:hover {
237 | cursor: pointer; // Change cursor on hover
238 | }
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/scripts/constants.js:
--------------------------------------------------------------------------------
1 | const KEY_NAME_OPTIONS = "options";
2 | const APP_NAME = "Leetcode Enhancer";
3 | const MESSAGE_ACTIVATE_ICON = 'activate_icon';
4 | const MESSAGE_GET_CODE = 'get_code'
5 | const FIREFOX_APP_PAGE_URL = "https://addons.mozilla.org/en-US/firefox/addon/leetcode-enhancer/";
6 | const CHROME_APP_PAGE_URL = "https://chrome.google.com/webstore/detail/leetcode-enhancer/gcmncppaaebldbkgkcbojghpmpjkdlmp";
7 |
8 | module.exports = {KEY_NAME_OPTIONS, APP_NAME, MESSAGE_ACTIVATE_ICON, FIREFOX_APP_PAGE_URL, CHROME_APP_PAGE_URL, MESSAGE_GET_CODE};
--------------------------------------------------------------------------------
/scripts/content-script-main.js:
--------------------------------------------------------------------------------
1 | const sendMessage = require('./message-publisher.js');
2 | const findMode = require('./page-checker.js');
3 | const initMutationObserver = require('./mutation-observer.js');
4 | const {isIterable} = require('./utils.js');
5 | const Mode = require('./mode.js');
6 | const print = require('./debugger.js');
7 | const { MESSAGE_ACTIVATE_ICON, MESSAGE_GET_CODE } = require('./constants.js');
8 | const { FeatureStrategyFactory } = require('./feature-strategies.js');
9 |
10 | var browser = browser || chrome;
11 | let mode = findMode();
12 | let currentStrategy = FeatureStrategyFactory.getStrategy(mode);
13 |
14 | // publish message to background script (i.e. service worker) for activating icon
15 | sendMessage({ message: MESSAGE_ACTIVATE_ICON });
16 |
17 | // init mutation observer
18 | initMutationObserver(browser, mode, modifyThenApplyChanges);
19 |
20 | function modifyThenApplyChanges(options) {
21 | if(!isIterable(options.options))
22 | return;
23 | applyChanges(options.options);
24 | }
25 |
26 | // ################### EVENT LISTENER #####################
27 | browser.runtime.onMessage.addListener(function(request, sender, sendResponse) {
28 | print(`Received Notification in content script: ${JSON.stringify(request)}`);
29 | if(request.options) {
30 | ops = []
31 | for (option of request.options) {
32 | ops.push(option);
33 | }
34 | applyChanges(ops);
35 | }
36 |
37 | if (request.action === MESSAGE_GET_CODE) {
38 | const code = getUserCode();
39 | print(`code obtained: ${code}`);
40 | sendResponse({ code: code });
41 | }
42 | });
43 |
44 | function applyChanges(options) {
45 | if (!currentStrategy) {
46 | print('No strategy found for current mode');
47 | return;
48 | }
49 |
50 | for (option of options) {
51 | let name = option.optionName;
52 | if (name === 'locked') {
53 | currentStrategy.hideLockedProblems(option.checked);
54 | } else if (name === 'highlight') {
55 | currentStrategy.highlightSolvedProblems(option.checked);
56 | } else if (name === 'solved') {
57 | currentStrategy.hideSolvedProb(option.checked);
58 | } else if(name === 'disUsers') {
59 | currentStrategy.setSolutionsUsers(option.checked);
60 | } else {
61 | currentStrategy.toggleByColName(name, option.checked);
62 | }
63 | }
64 | }
65 |
66 | function getUserCode() {
67 | if (!currentStrategy) {
68 | print('Unable to read the code. If you are seeing this error in code editor page, please report this issue.');
69 | return '';
70 | }
71 | return currentStrategy.getUserCode();
72 | }
--------------------------------------------------------------------------------
/scripts/debugger.js:
--------------------------------------------------------------------------------
1 | // debugger.js
2 | const {APP_NAME} = require('./constants');
3 | // Set the debug mode (true to enable debugging)
4 | const debug = false;
5 |
6 | /**
7 | * Prints a message to the console if debugging is enabled.
8 | * @param {string} message - The message to print.
9 | */
10 | function print(message) {
11 | if (debug) {
12 | console.log(`[${APP_NAME}]: ${message}`);
13 | }
14 | }
15 |
16 | // Export the print function
17 | module.exports = print;
18 |
--------------------------------------------------------------------------------
/scripts/feature-strategies.js:
--------------------------------------------------------------------------------
1 | const FeatureStrategy = require('./strategies/base-strategy');
2 | const FeatureStrategyFactory = require('./strategies/strategy-factory');
3 |
4 | module.exports = {
5 | FeatureStrategy,
6 | FeatureStrategyFactory
7 | };
--------------------------------------------------------------------------------
/scripts/message-publisher.js:
--------------------------------------------------------------------------------
1 | const print = require('./debugger.js');
2 |
3 | /**
4 | * Sends a message to the background script (service-worker) or other parts of the extension.
5 | * @param {Object} payload - The message payload to send.
6 | */
7 | function sendMessage(payload) {
8 | var browser = browser || chrome;
9 | if (!browser || !browser.runtime) {
10 | print('Browser API not available');
11 | return;
12 | }
13 |
14 | try {
15 | browser.runtime.sendMessage(payload);
16 | } catch (err) {
17 | print(`Failed to send message: ${JSON.stringify(payload)} because of the following error: ${err}`);
18 | }
19 | }
20 |
21 | // Export the sendMessage function
22 | module.exports = sendMessage;
23 |
--------------------------------------------------------------------------------
/scripts/mode.js:
--------------------------------------------------------------------------------
1 | // ################### MODES #####################
2 | /*
3 | 1 - problem set
4 | 5 - contest
5 | 6 - coding area
6 | */
7 |
8 | const Mode = {
9 | PROBLEM_SET: 'PROBLEM_SET',
10 | CODING_AREA: 'CODING_AREA',
11 | CONTEST: 'CONTEST'
12 | };
13 |
14 | module.exports = { Mode };
--------------------------------------------------------------------------------
/scripts/mutation-observer.js:
--------------------------------------------------------------------------------
1 | const print = require('./debugger.js');
2 | const { Mode } = require('./mode.js');
3 | const { KEY_NAME_OPTIONS } = require('./constants.js');
4 |
5 | const ERROR_IN_MUTATION_OBSERVER_CALLBACK = "Error in MutationObserver callback";
6 | const ERROR_PROBLEM_SET_UI_NOT_FOUND = "Problem Set UI element not found.";
7 | const ERROR_CODING_AREA_NOT_FOUND = "Coding Area element not found.";
8 | const ERROR_BASE_CONTENT_NOT_FOUND = "Base content element and qd-content element not found.";
9 | const ERROR_NO_VALID_MODE = "No valid mode found yet to observe.";
10 |
11 | // More specific selectors for better targeting
12 | const SELECTOR_PROBLEM_SET_UI = '#__next'; // Using ID selector for better specificity
13 | const SELECTOR_CODING_AREA = '#__next'; // Using ID selector for better specificity
14 | const SELECTOR_BASE_CONTENT = 'base_content';
15 | const SELECTOR_NEW_CONTEST_PAGE = 'qd-content';
16 |
17 | /**
18 | * Initialize the Mutation Observer.
19 | */
20 | function initMutationObserver(browser, mode, modifyThenApplyChanges) {
21 | const observer = new MutationObserver(function (mutations) {
22 | try {
23 | if (mutations.length) {
24 | browser.storage.local.get([KEY_NAME_OPTIONS], modifyThenApplyChanges);
25 | }
26 | } catch (error) {
27 | print(`${ERROR_IN_MUTATION_OBSERVER_CALLBACK}: ${error}`);
28 | }
29 | });
30 |
31 | // Start observing based on mode
32 | switch (mode) {
33 | case Mode.PROBLEM_SET:
34 | const ui = document.querySelector(SELECTOR_PROBLEM_SET_UI);
35 | if (ui) {
36 | observer.observe(ui, {
37 | childList: true,
38 | subtree: true
39 | });
40 | } else {
41 | print(ERROR_PROBLEM_SET_UI_NOT_FOUND);
42 | }
43 | break;
44 |
45 | case Mode.CODING_AREA:
46 | const code_ui = document.querySelector(SELECTOR_CODING_AREA);
47 | if (code_ui) {
48 | observer.observe(code_ui, {
49 | childList: true,
50 | subtree: true
51 | });
52 | } else {
53 | print(ERROR_CODING_AREA_NOT_FOUND);
54 | }
55 | break;
56 |
57 | case Mode.CONTEST:
58 | // Handle old contest page
59 | const old_contest_page = document.getElementById(SELECTOR_BASE_CONTENT);
60 | if (old_contest_page) {
61 | observer.observe(old_contest_page, {
62 | childList: true,
63 | subtree: true
64 | });
65 | break;
66 | }
67 |
68 | // Handle new contest page
69 | const new_contest_page = document.getElementById(SELECTOR_NEW_CONTEST_PAGE);
70 | if (new_contest_page) {
71 | observer.observe(new_contest_page, {
72 | childList: true,
73 | subtree: true
74 | });
75 | }
76 |
77 | // Handle new contest page sidebar (loads after 2-3 seconds)
78 | const next_root = document.querySelector('#__next');
79 | if (next_root) {
80 | observer.observe(next_root.parentElement, {
81 | childList: true,
82 | subtree: true
83 | });
84 | }
85 |
86 | if (!old_contest_page && !new_contest_page && !next_root) {
87 | print(ERROR_BASE_CONTENT_NOT_FOUND);
88 | }
89 | break;
90 |
91 | default:
92 | print(ERROR_NO_VALID_MODE);
93 | break;
94 | }
95 | return observer;
96 | }
97 |
98 | // Export the function to initialize the observer
99 | module.exports = initMutationObserver;
100 |
--------------------------------------------------------------------------------
/scripts/page-checker.js:
--------------------------------------------------------------------------------
1 | const { Mode } = require('./mode.js');
2 | const print = require('./debugger.js');
3 |
4 | // Constant URLs
5 | const PROBLEMSET_URL = '/problemset/';
6 | const PROBLEMS_URL = '/problems/';
7 | const CONTEST_URL = '/contest/';
8 |
9 | /**
10 | * Check if the current URL is for the problem set page.
11 | * @returns {boolean}
12 | */
13 | function isProblemSetPage() {
14 | return window.location.href.includes(PROBLEMSET_URL);
15 | }
16 |
17 | /**
18 | * Check if the current URL is for the coding area.
19 | * @returns {boolean}
20 | */
21 | function isCodingArea() {
22 | return window.location.href.includes(PROBLEMS_URL);
23 | }
24 |
25 | /**
26 | * Check if the current URL is for contests.
27 | * @returns {boolean}
28 | */
29 | function isContest() {
30 | return window.location.href.includes(CONTEST_URL);
31 | }
32 |
33 | /**
34 | * Determine the current mode based on the page type.
35 | * @returns {Mode} - The detected mode.
36 | */
37 | function findMode() {
38 | let mode;
39 | if (isContest()) {
40 | mode = Mode.CONTEST;
41 | } else if (isCodingArea()) {
42 | mode = Mode.CODING_AREA;
43 | } else if (isProblemSetPage()) {
44 | mode = Mode.PROBLEM_SET;
45 | }
46 | print(`Current mode value = ${mode}`);
47 | return mode;
48 | }
49 |
50 | module.exports = findMode;
51 |
--------------------------------------------------------------------------------
/scripts/popup-main.js:
--------------------------------------------------------------------------------
1 | const {KEY_NAME_OPTIONS, MESSAGE_GET_CODE} = require('./constants.js');
2 | const print = require('./debugger.js');
3 | const {isIterable, storeDataWithObjectWrapping, getData, getDataAsUint8Array, storeData} = require('./utils.js');
4 |
5 | /* ==================== Compatibility Between Chrome and Firefox ==================== */
6 | var browser = browser || chrome
7 |
8 | /* ==================== Initialization ==================== */
9 | document.addEventListener('DOMContentLoaded', initExtension);
10 |
11 | function initExtension() {
12 | attachCheckboxEventListeners();
13 | loadSavedSettings();
14 | initFeedbackButtons();
15 | initDetailsButton();
16 | initAISection();
17 | }
18 |
19 | /* ==================== AES Encryption and Decryption Functions ==================== */
20 |
21 | async function generateAESKey() {
22 | return window.crypto.subtle.generateKey(
23 | { name: "AES-GCM", length: 256 },
24 | true,
25 | ["encrypt", "decrypt"]
26 | );
27 | }
28 |
29 | async function encryptData(data, aesKey) {
30 | const iv = window.crypto.getRandomValues(new Uint8Array(12));
31 | const encodedData = new TextEncoder().encode(data);
32 |
33 | const encryptedData = await window.crypto.subtle.encrypt(
34 | { name: "AES-GCM", iv },
35 | aesKey,
36 | encodedData
37 | );
38 |
39 | return { encryptedData: new Uint8Array(encryptedData), iv };
40 | }
41 |
42 | async function decryptData(encryptedData, aesKey, iv) {
43 | const decryptedData = await window.crypto.subtle.decrypt(
44 | { name: "AES-GCM", iv },
45 | aesKey,
46 | encryptedData
47 | );
48 |
49 | return new TextDecoder().decode(decryptedData);
50 | }
51 |
52 | /* ==================== API Key Handling ==================== */
53 |
54 | async function storeApiKey(apiKey) {
55 | const aesKey = await generateAESKey();
56 | const { encryptedData, iv } = await encryptData(apiKey, aesKey);
57 |
58 | storeDataWithObjectWrapping('encryptedApiKey', Array.from(encryptedData));
59 | storeDataWithObjectWrapping('aesIV', Array.from(iv));
60 |
61 | const exportedKey = await window.crypto.subtle.exportKey("raw", aesKey);
62 | storeDataWithObjectWrapping('aesKey', Array.from(new Uint8Array(exportedKey)));
63 | }
64 |
65 | async function loadApiKey() {
66 | const encryptedData = await getDataAsUint8Array('encryptedApiKey');
67 | const iv = await getDataAsUint8Array('aesIV');
68 | const aesKeyBytes = await getDataAsUint8Array('aesKey');
69 |
70 | if (!encryptedData || !iv || !aesKeyBytes) {
71 | print("No API key or encryption details found.");
72 | return null;
73 | }
74 |
75 | const aesKey = await window.crypto.subtle.importKey(
76 | "raw",
77 | aesKeyBytes,
78 | { name: "AES-GCM" },
79 | false,
80 | ["decrypt"]
81 | );
82 |
83 | return await decryptData(encryptedData, aesKey, iv);
84 | }
85 |
86 | /* ==================== Checkbox Settings Handling ==================== */
87 |
88 | function attachCheckboxEventListeners() {
89 | const checkboxes = document.querySelectorAll("input[name=settings]");
90 | checkboxes.forEach(chb => chb.addEventListener('change', onCheckboxChange));
91 | }
92 |
93 | function loadSavedSettings() {
94 | getData(KEY_NAME_OPTIONS, updateCheckboxes);
95 | }
96 |
97 | function updateCheckboxes(options) {
98 | if (isIterable(options)) {
99 | options.forEach(option => {
100 | const chb = document.getElementById(option.optionName);
101 | chb.checked = option.checked;
102 | });
103 | }
104 | }
105 |
106 | function onCheckboxChange() {
107 | browser.tabs.query({}, function(tabs) {
108 | const options = [];
109 | const checkboxes = document.querySelectorAll('input[name=settings]');
110 | checkboxes.forEach(chb => options.push({ optionName: chb.value, checked: chb.checked }));
111 | storeDataWithObjectWrapping('options', options);
112 |
113 | const response = { options };
114 | try {
115 | tabs.forEach(tab => browser.tabs.sendMessage(tab.id, response));
116 | }
117 | catch {
118 | print("error while sending the message");
119 | }
120 | });
121 | }
122 |
123 | /* ==================== Feedback and Details Buttons ==================== */
124 |
125 | function initFeedbackButtons() {
126 | document.getElementById('fBtn').addEventListener("click", function() {
127 | const isFirefox = typeof InstallTrigger !== 'undefined';
128 | const isEdge = /Edge/.test(navigator.userAgent);
129 | if (isFirefox) {
130 | this.href = "https://addons.mozilla.org/en-US/firefox/addon/leetcode-enhancer/";
131 | } else if (isEdge) {
132 | this.href = "https://microsoftedge.microsoft.com/addons/detail/leetcode-enhancer/dgddijgkneackjhmijacbopefpladfia";
133 | }
134 | });
135 | }
136 |
137 | function initDetailsButton() {
138 | document.getElementById('deBtn').addEventListener("click", function() {
139 | const isClose = document.getElementById('deBtn').innerHTML == '►';
140 | document.getElementById('msg').style.display = isClose ? "block" : "none";
141 | document.getElementById('deBtn').innerHTML = isClose ? '▼' : '►';
142 | });
143 | }
144 |
145 | /* ==================== AI Section for API Key ==================== */
146 |
147 | function initAISection() {
148 | const apiKeyInput = document.getElementById('api-key');
149 | const deleteApiKeyButton = document.getElementById('delete-api-key');
150 | const triggerActionButton = document.getElementById('trigger-action');
151 |
152 | loadApiKey().then((decryptedApiKey) => {
153 | if (decryptedApiKey) {
154 | apiKeyInput.value = decryptedApiKey;
155 | } else {
156 | print("No API key found in storage.");
157 | }
158 | });
159 |
160 | apiKeyInput.addEventListener('input', () => {
161 | const apiKey = apiKeyInput.value.trim();
162 | if (apiKey.length > 0) {
163 | storeApiKey(apiKey);
164 | print("API Key saved.");
165 | }
166 | });
167 |
168 | deleteApiKeyButton.addEventListener('click', () => {
169 | apiKeyInput.value = '';
170 | browser.storage.local.remove(['encryptedApiKey', 'aesIV', 'aesKey']);
171 | print("API Key deleted.");
172 | });
173 |
174 | triggerActionButton.addEventListener('click', async () => {
175 | if(!isTermsCheckboxChecked()) {
176 | alert("Please accept the terms and condition");
177 | return;
178 | }
179 | const apiKey = apiKeyInput.value;
180 | if (apiKey) {
181 | sendMessageToContentScriptToGetCode(apiKey);
182 | } else {
183 | alert("Please provide API Key");
184 | }
185 | });
186 |
187 | const termsCheckbox = document.getElementById('terms-checkbox');
188 |
189 | loadTermsCheckboxState();
190 |
191 | termsCheckbox.addEventListener('change', function() {
192 | storeTermsCheckboxState(termsCheckbox.checked);
193 | });
194 |
195 | getData('outputDivCode', displayDataInOutputDiv);
196 |
197 | function displayDataInOutputDiv(outputData) {
198 | if(outputData) {
199 | document.getElementById("output").innerHTML = outputData;
200 | }
201 | }
202 | }
203 |
204 | function storeTermsCheckboxState(isChecked) {
205 | storeData({ termsAccepted: isChecked })
206 | }
207 |
208 | function loadTermsCheckboxState() {
209 | try {
210 | browser.storage.local.get('termsAccepted', (result) => {
211 | const isChecked = result.termsAccepted;
212 | const termsCheckbox = document.getElementById('terms-checkbox');
213 | if (termsCheckbox) {
214 | termsCheckbox.checked = !!isChecked;
215 | }
216 | });
217 | }
218 | catch (err) {
219 | print("Error while reading termsAccepted");
220 | }
221 | }
222 |
223 | function isTermsCheckboxChecked() {
224 | const termsCheckbox = document.getElementById('terms-checkbox');
225 | return termsCheckbox && termsCheckbox.checked;
226 | }
227 |
228 | function sendMessageToContentScriptToGetCode(apiKey) {
229 | browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
230 | if (!tabs || tabs.length === 0) {
231 | alert("No active tab found. Please make sure you're on a LeetCode page.");
232 | return;
233 | }
234 |
235 | const activeTab = tabs[0];
236 |
237 | if (!activeTab.url.includes("leetcode.com")) {
238 | alert("Please navigate to a LeetCode page first.");
239 | return;
240 | }
241 |
242 | try {
243 | browser.tabs.sendMessage(activeTab.id, { action: MESSAGE_GET_CODE })
244 | .then(response => {
245 | if (response && response.code) {
246 | const code = response.code;
247 | const question = "Provide time and space complexity of the code.\n";
248 | requestCoherePermissionIfNeeded(makeCohereRequest, apiKey, question + code);
249 | } else if (response && response.error) {
250 | alert(response.error);
251 | } else {
252 | alert("Failed to get code from the page. Please make sure you're on a LeetCode problem page.");
253 | }
254 | })
255 | .catch(error => {
256 | print(`Error: ${error.message}`);
257 | alert("Failed to communicate with the page. Please refresh the page and try again.");
258 | });
259 | } catch (err) {
260 | print(`Error while sending message: ${err.message}`);
261 | alert("An error occurred. Please refresh the page and try again.");
262 | }
263 | });
264 | }
265 |
266 | /* ==================== Cohere API Call ==================== */
267 |
268 | function requestCoherePermissionIfNeeded(callback, apiKey, payload) {
269 | browser.permissions.contains(
270 | { origins: ["https://api.cohere.ai/*"] },
271 | async (hasPermission) => {
272 | if (hasPermission) {
273 | // Permission already granted, proceed with the API call
274 | callback(apiKey, payload);
275 | } else {
276 | // Permission not granted, request it
277 | await browser.permissions.request(
278 | { origins: ["https://api.cohere.ai/*"] },
279 | (granted) => {
280 | if (granted) {
281 | // Permission granted, proceed with the API call
282 | callback(apiKey, payload);
283 | } else {
284 | // Permission denied, handle accordingly
285 | print("Permission denied for Cohere API access.");
286 | alert("Permission denied for Cohere API access. Please grant permission to use the Cohere API.");
287 | }
288 | }
289 | );
290 | }
291 | }
292 | );
293 | }
294 |
295 | function makeCohereRequest(apiKey, question) {
296 | actionButtonUnusable();
297 | const apiUrl = 'https://api.cohere.ai/v1/generate';
298 | const data = {
299 | model: 'command',
300 | prompt: question,
301 | temperature: 0.7,
302 | k: 0,
303 | p: 0.1,
304 | frequency_penalty: 0,
305 | presence_penalty: 0,
306 | stop_sequences: []
307 | };
308 |
309 | fetch(apiUrl, {
310 | method: 'POST',
311 | headers: {
312 | 'Authorization': `Bearer ${apiKey}`,
313 | 'Content-Type': 'application/json'
314 | },
315 | body: JSON.stringify(data)
316 | })
317 | .then(response => response.json())
318 | .then(data => {
319 | const outputDiv = document.getElementById('output');
320 | outputDiv.innerHTML = data.generations[0].text.replace(/\n/g, '$1
');
321 | storeDataWithObjectWrapping("outputDivCode", outputDiv.innerHTML);
322 | })
323 | .catch(error => {
324 | alert("Operation failed. Please check your api key.");
325 | print("error while sending request to cohere" + error);
326 | }).finally( () => {
327 | actionButtonUsable();
328 | })
329 | }
330 |
331 | function actionButtonUsable() {
332 | const triggerButton = document.getElementById('trigger-action');
333 | triggerButton.innerText="TC & SC"
334 | triggerButton.style.backgroundColor = 'buttonface'; // Set background to green when usable
335 | triggerButton.disabled = false; // Enable the button
336 | }
337 |
338 | function actionButtonUnusable() {
339 | const triggerButton = document.getElementById('trigger-action');
340 | triggerButton.innerText="processing.."
341 | triggerButton.style.backgroundColor = 'yellow'; // Set background to yellow when unusable
342 | triggerButton.disabled = true; // Disable the button
343 | }
--------------------------------------------------------------------------------
/scripts/service-worker-main.js:
--------------------------------------------------------------------------------
1 | const { CHROME_APP_PAGE_URL, FIREFOX_APP_PAGE_URL, MESSAGE_ACTIVATE_ICON } = require("./constants");
2 |
3 | // Detect browser type
4 | const isFirefox = typeof browser !== 'undefined';
5 |
6 | // Get the appropriate API
7 | var browser = browser || chrome;
8 |
9 | // enable popup button when content script sends notification
10 | browser.runtime.onMessage.addListener(
11 | function(request, sender, sendResponse) {
12 | if (request.message === MESSAGE_ACTIVATE_ICON) {
13 | if (isFirefox) {
14 | browser.action.show(sender.tab.id);
15 | } else {
16 | browser.action.enable(sender.tab.id);
17 | }
18 | }
19 | }
20 | );
21 |
22 | // uninstall feedback redirect
23 | if(isFirefox) {
24 | browser.runtime.setUninstallURL(FIREFOX_APP_PAGE_URL);
25 | } else {
26 | browser.runtime.setUninstallURL(CHROME_APP_PAGE_URL);
27 | }
28 |
--------------------------------------------------------------------------------
/scripts/strategies/base-strategy.js:
--------------------------------------------------------------------------------
1 | class FeatureStrategy {
2 | hideLockedProblems(checked) {}
3 | highlightSolvedProblems(checked) {}
4 | hideSolvedProb(checked) {}
5 | setSolutionsUsers(checked) {}
6 | toggleByColName(colName, checked) {}
7 | getUserCode() { return ''; }
8 | }
9 |
10 | module.exports = FeatureStrategy;
--------------------------------------------------------------------------------
/scripts/strategies/coding-area-strategy.js:
--------------------------------------------------------------------------------
1 | const FeatureStrategy = require('./base-strategy');
2 |
3 | class CodingAreaStrategy extends FeatureStrategy {
4 |
5 | hideSolvedDiff(checked) {
6 | const diffCodingArea = document.querySelector('[data-track-load="description_content"]')?.parentElement?.previousElementSibling?.firstChild;
7 | const diffNext = document.querySelectorAll("a[rel ='noopener noreferrer'] div");
8 |
9 | if (diffCodingArea) {
10 | diffCodingArea.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
11 | }
12 |
13 | if (diffNext) {
14 | diffNext.forEach(el => el.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer'));
15 | }
16 | }
17 |
18 | hideDiffOfSimilarProb(checked) {
19 | const allAnchors = document.querySelectorAll('a');
20 | if (!allAnchors || allAnchors.length === 0) return;
21 |
22 | const urlProb = "https://leetcode.com/problems/";
23 | const curUrl = urlProb + window.location.pathname.split("/")[2] + "/";
24 |
25 | allAnchors.forEach(anchor => {
26 | if (anchor.href.startsWith(urlProb) && !anchor.href.startsWith(curUrl)) {
27 | const diffElement = anchor.parentElement?.parentElement?.parentElement?.nextElementSibling;
28 | if (diffElement) {
29 | diffElement.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
30 | }
31 | }
32 | });
33 | }
34 |
35 | hideStatus(checked) {
36 | const solvedMarkParent = document.querySelector('[data-track-load="description_content"]')?.parentNode?.previousSibling?.previousSibling?.lastChild;
37 | if (solvedMarkParent && solvedMarkParent.classList.contains('text-body')) {
38 | solvedMarkParent.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
39 | }
40 | }
41 |
42 | hideAcceptance(checked) {
43 | const parentElement = document.querySelector('[data-track-load="description_content"]')?.parentElement?.nextSibling;
44 | if (!parentElement) return;
45 |
46 | const acceptanceRateElement = [...parentElement.children]
47 | .find(child => child.textContent.toLowerCase().includes('acceptance'))?.lastElementChild;
48 |
49 | if (acceptanceRateElement) {
50 | acceptanceRateElement.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
51 | }
52 | }
53 |
54 | hideSave(checked) {
55 | const saveButton = document.querySelector("svg[data-icon='star']");
56 | if (saveButton) {
57 | saveButton.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
58 | }
59 | }
60 |
61 | toggleByColName(colName, checked) {
62 | if (colName === 'difficulty') {
63 | this.hideSolvedDiff(checked);
64 | this.hideDiffOfSimilarProb(checked);
65 | } else if (colName === 'status') {
66 | this.hideStatus(checked);
67 | } else if (colName === 'acceptance') {
68 | this.hideAcceptance(checked);
69 | } else if(colName === 'save') {
70 | this.hideSave(checked);
71 | }
72 | }
73 |
74 | getUserCode() {
75 | const codeEditor = document.querySelector('div.editor-scrollable');
76 | return codeEditor ? codeEditor.textContent : '';
77 | }
78 | }
79 |
80 | module.exports = CodingAreaStrategy;
--------------------------------------------------------------------------------
/scripts/strategies/contest-strategy.js:
--------------------------------------------------------------------------------
1 | const FeatureStrategy = require('./base-strategy');
2 |
3 | class ContestStrategy extends FeatureStrategy {
4 | hideDiffFromContest(checked) {
5 |
6 | // old UI
7 | const diffLabel = document.querySelectorAll('.contest-question-info .list-group .list-group-item:nth-child(5) .label')[0];
8 | if (diffLabel) {
9 | diffLabel.style.visibility = checked ? 'visible_leetcode-enhancer' : 'hidden';
10 | return;
11 | }
12 |
13 | // new UI
14 | const easyDiffLabel = document.querySelector('.text-difficulty-easy');
15 | if (easyDiffLabel) {
16 | easyDiffLabel.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
17 | }
18 |
19 | const mediumDiffLabel = document.querySelector('.text-difficulty-medium');
20 | if (mediumDiffLabel) {
21 | mediumDiffLabel.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
22 | }
23 |
24 | const hardDiffLabel = document.querySelector('.text-difficulty-hard');
25 | if (hardDiffLabel) {
26 | hardDiffLabel.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
27 | }
28 |
29 | // Handle multiple elements in sidebar
30 | const easyDiffLabelsSideBar = document.querySelectorAll('.text-sd-easy');
31 | easyDiffLabelsSideBar.forEach(label => {
32 | label.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
33 | });
34 |
35 | const mediumDiffLabelsSideBar = document.querySelectorAll('.text-sd-medium');
36 | mediumDiffLabelsSideBar.forEach(label => {
37 | label.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
38 | });
39 |
40 | const hardDiffLabelsSideBar = document.querySelectorAll('.text-sd-hard');
41 | hardDiffLabelsSideBar.forEach(label => {
42 | label.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
43 | });
44 | }
45 |
46 | toggleByColName(colName, checked) {
47 | if (colName === 'difficulty') {
48 | this.hideDiffFromContest(checked);
49 | }
50 | }
51 | }
52 |
53 | module.exports = ContestStrategy;
--------------------------------------------------------------------------------
/scripts/strategies/problem-set-strategy.js:
--------------------------------------------------------------------------------
1 | const FeatureStrategy = require('./base-strategy');
2 |
3 | class NewProblemSetStrategy extends FeatureStrategy {
4 |
5 | hideLockedProblems(checked) {
6 | const temp = document.querySelectorAll('a[id]');
7 |
8 | temp.forEach(row => {
9 | const cell = row.querySelector(`div>div:nth-child(1)>svg`);
10 | if (cell && cell.getAttribute('data-icon') == 'lock') {
11 | row.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
12 | }
13 | });
14 | }
15 |
16 | highlightSolvedProblems(checked) {
17 | const temp = document.querySelectorAll('a[id]');
18 | const isDarkMode = document.querySelector('html').classList.contains('dark');
19 | temp.forEach(row => {
20 | const cell = row.querySelector(`div>div:nth-child(1)>svg`);
21 | if (cell && cell.getAttribute('data-icon') == 'check') {
22 | if(!checked) {
23 | row.classList.add(isDarkMode ? 'add-bg-dark_leetcode-enhancer' : 'add-bg-light_leetcode-enhancer');
24 | }
25 | else {
26 | row.classList.remove('add-bg-dark_leetcode-enhancer');
27 | row.classList.remove('add-bg-light_leetcode-enhancer');
28 | }
29 | }
30 | });
31 | }
32 |
33 | hideSolvedProb(checked) {
34 | const temp = document.querySelectorAll('a[id]');
35 |
36 | temp.forEach(row => {
37 | const cell = row.querySelector(`div>div:nth-child(1)>svg`);
38 | if (cell && cell.getAttribute('data-icon') == 'check') {
39 | row.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
40 | }
41 | });
42 | }
43 |
44 | toggleByColName(colName, checked) {
45 | const temp = document.querySelectorAll('a[id]');
46 |
47 | if(colName == 'status') {
48 | temp.forEach(row => {
49 | const cell = row.querySelector(`div>div:nth-child(1)`);
50 | if (cell) {
51 | cell.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
52 | }
53 | });
54 | }
55 | if(colName == 'acceptance') {
56 | temp.forEach(row => {
57 | const cell = row.querySelector(`div>div:nth-child(2)>div:nth-child(2)`);
58 | if (cell) {
59 | cell.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
60 | }
61 | });
62 | }
63 | else if(colName == 'difficulty') {
64 | temp.forEach(row => {
65 | const cell = row.querySelector(`div>div:nth-child(2)>p:nth-child(3)`);
66 | if (cell) {
67 | cell.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
68 | }
69 | });
70 | }
71 | else if(colName == 'frequency') {
72 | temp.forEach(row => {
73 | const cell = row.querySelector(`div>div:nth-child(3)`);
74 | if (cell) {
75 | cell.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
76 | }
77 | });
78 | }
79 | else if(colName == 'save') {
80 | temp.forEach(row => {
81 | const cell = row.querySelector(`div>div:nth-child(4)>div`);
82 | if (cell) {
83 | cell.classList[checked ? 'remove' : 'add']('hide_leetcode-enhancer');
84 | }
85 | });
86 | }
87 | }
88 | }
89 |
90 | module.exports = NewProblemSetStrategy;
--------------------------------------------------------------------------------
/scripts/strategies/strategy-factory.js:
--------------------------------------------------------------------------------
1 | const { Mode } = require('../mode.js');
2 | const ProblemSetStrategy = require('./problem-set-strategy.js');
3 | const CodingAreaStrategy = require('./coding-area-strategy.js');
4 | const ContestStrategy = require('./contest-strategy.js');
5 |
6 | class FeatureStrategyFactory {
7 | static getStrategy(mode) {
8 | switch (mode) {
9 | case Mode.PROBLEM_SET:
10 | return new ProblemSetStrategy();
11 | case Mode.CODING_AREA:
12 | return new CodingAreaStrategy();
13 | case Mode.CONTEST:
14 | return new ContestStrategy();
15 | default:
16 | console.log('No strategy found for mode:', mode);
17 | return null;
18 | }
19 | }
20 | }
21 |
22 | module.exports = FeatureStrategyFactory;
--------------------------------------------------------------------------------
/scripts/utils.js:
--------------------------------------------------------------------------------
1 | const print = require('./debugger.js');
2 | let browser = window.browser || window.chrome;
3 |
4 | /**
5 | * Checks if an object is iterable.
6 | * @param {Object} obj - The object to check.
7 | * @returns {boolean} - True if the object is iterable, false otherwise.
8 | */
9 | function isIterable(obj) {
10 | if(obj != null && typeof obj[Symbol.iterator] === 'function')
11 | return true;
12 | print(`${JSON.stringify(obj)} is not iterable`)
13 | }
14 |
15 | function storeDataWithObjectWrapping(key, value) {
16 | const data = {};
17 | data[key] = value;
18 | storeData(data);
19 | }
20 |
21 | function storeData(data) {
22 | browser.storage.local.set(data);
23 | }
24 |
25 |
26 | function getData(key, callback) {
27 | try {
28 | browser.storage.local.get([key], result => callback(result[key]));
29 | }
30 | catch (err) {
31 | print("Error while retrieving key");
32 | }
33 | }
34 |
35 | async function getDataAsUint8Array(key) {
36 | return new Uint8Array((await browser.storage.local.get(key))[key] || []);
37 | }
38 |
39 | // Export the isIterable function using CommonJS syntax
40 | module.exports = {isIterable, storeDataWithObjectWrapping, getData, getDataAsUint8Array, storeData};
--------------------------------------------------------------------------------
/service-worker.js:
--------------------------------------------------------------------------------
1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i