Greasy Fork is available in English.

leetcode2notion

Save LeetCode problems to Notion after clicking a button.

As of 05.10.2024. See ბოლო ვერსია.

  1. // ==UserScript==
  2. // @name leetcode2notion
  3. // @namespace wuyifff
  4. // @version 1.2
  5. // @description Save LeetCode problems to Notion after clicking a button.
  6. // @author wuyifff
  7. // @match https://leetcode.cn/problems/*
  8. // @match https://leetcode.com/problems/*
  9. // @connect api.notion.com
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=leetcode.com
  11. // @grant GM_xmlhttpRequest
  12. // @license MIT
  13. // @homepage https://github.com/wuyifff/leetcode2notion
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18. // replace to your own token and ID
  19. const notionToken = '';
  20. const databaseId = '';
  21.  
  22. // 1. add save button
  23. // select language button (optional)
  24. let currentMinutes = 0;
  25. let currentSeconds = 0;
  26. function addUIElements() {
  27. // 1.1 save button
  28. const button = document.createElement("button");
  29. button.innerHTML = "Save to Notion";
  30. button.style.position = "fixed";
  31. button.style.bottom = "10px";
  32. button.style.right = "10px";
  33. button.style.zIndex = 1000;
  34. button.style.padding = "10px 20px";
  35. button.style.backgroundColor = "#4CAF50";
  36. button.style.color = "white";
  37. button.style.border = "none";
  38. button.style.borderRadius = "5px";
  39. button.style.cursor = "pointer";
  40. button.onclick = saveProblemToNotion;
  41.  
  42. // 1.2 save language button (disabled)
  43. const select = document.createElement("select");
  44. select.id = "languageSelect";
  45. select.style.position = "fixed";
  46. select.style.bottom = "50px";
  47. select.style.right = "10px";
  48. select.style.zIndex = 1000;
  49. select.style.padding = "10px";
  50. select.style.backgroundColor = "#4CAF50";
  51. select.style.color = "white";
  52. select.style.border = "none";
  53. select.style.borderRadius = "5px";
  54. select.style.cursor = "pointer";
  55. const optionPython = document.createElement("option");
  56. optionPython.value = "python";
  57. optionPython.innerText = "Python";
  58. const optionCpp = document.createElement("option");
  59. optionCpp.value = "cpp";
  60. optionCpp.innerText = "C++";
  61. select.appendChild(optionPython);
  62. select.appendChild(optionCpp);
  63.  
  64. // 1.3 timer element
  65. const timerSpan = document.createElement("span");
  66. timerSpan.className = 'ml-2 group/nav-back cursor-pointer gap-2 hover:text-lc-icon-primary dark:hover:text-dark-lc-icon-primary flex items-center h-[32px] transition-none hover:bg-fill-quaternary dark:hover:bg-fill-quaternary text-gray-60 dark:text-gray-60 px-2';
  67. let totalSeconds = 0;
  68. function updateTimer() {
  69. totalSeconds++;
  70. currentMinutes = Math.floor(totalSeconds / 60);
  71. currentSeconds = totalSeconds % 60;
  72. const formattedMinutes = currentMinutes < 10 ? `0${currentMinutes}` : currentMinutes;
  73. const formattedSeconds = currentSeconds < 10 ? `0${currentSeconds}` : currentSeconds;
  74. timerSpan.textContent = `Time: ${formattedMinutes}:${formattedSeconds}`;
  75. }
  76. var timerInterval = setInterval(updateTimer, 1000); // update every second
  77.  
  78. // set up container
  79. const container = document.createElement("div");
  80. container.style.display = "flex";
  81. container.style.flexDirection = "column";
  82. container.style.alignItems = "center";
  83. container.style.marginLeft = "10px";
  84. //container.appendChild(select);
  85. container.appendChild(button);
  86. container.style.position = "fixed";
  87. container.style.bottom = "10px";
  88. container.style.right = "10px";
  89. document.body.appendChild(container);
  90.  
  91. // timer is append at different location
  92. function tryAppendButton() {
  93. var targetDiv = document.getElementById('ide-top-btns');
  94. if (targetDiv) {
  95. targetDiv.appendChild(timerSpan);
  96. clearInterval(appendButtonInterval);
  97. console.log("append timer success");
  98. }
  99. }
  100. var appendButtonInterval = setInterval(tryAppendButton, 500);
  101. }
  102.  
  103. // 2. get leetcode problem info
  104. function getProblemData() {
  105. const title = document.querySelector('.text-title-large a')?.innerText || 'No title found';
  106. const difficultyElement = document.querySelector("div[class*='text-difficulty-']");
  107. const difficulty = difficultyElement ? difficultyElement.innerText : 'No difficulty found';
  108. const url = window.location.href;
  109. const tagElements = document.querySelectorAll("a[href*='/tag/']");
  110. const tagTexts = Array.from(tagElements).map(element => element.innerText);
  111.  
  112. const codeDiv = document.querySelector('.view-lines.monaco-mouse-cursor-text[role="presentation"]');
  113. let codeText = '';
  114. if (codeDiv) {
  115. const codeLines = codeDiv.querySelectorAll('div');
  116. codeText = Array.from(codeLines).map(line => line.innerText).join('\n');
  117. } else {
  118. codeText = 'No code found';
  119. }
  120. //console.log(codeText);
  121. //const selectedLanguage = document.getElementById("languageSelect").value;
  122. const selectedLanguage = 'python';
  123. return {
  124. title: title,
  125. difficulty: difficulty,
  126. url: url,
  127. tag: tagTexts,
  128. code: codeText,
  129. language: selectedLanguage,
  130. time: currentMinutes
  131. };
  132. }
  133.  
  134. // 3. save to notion and check if duplicate
  135. async function saveProblemToNotion() {
  136. const problemData = getProblemData();
  137. console.log(problemData);
  138.  
  139. const searchUrl = `https://api.notion.com/v1/search`;
  140. const searchBody = {
  141. "query": problemData.title,
  142. "filter": {
  143. "value": "page",
  144. "property": "object"
  145. },
  146. "sort": {
  147. "direction": "ascending",
  148. "timestamp": "last_edited_time"
  149. }
  150. };
  151.  
  152. GM_xmlhttpRequest({
  153. method: 'POST',
  154. url: searchUrl,
  155. headers: {
  156. 'Authorization': `Bearer ${notionToken}`,
  157. 'Content-Type': 'application/json',
  158. 'Notion-Version': '2022-06-28'
  159. },
  160. data: JSON.stringify(searchBody),
  161. onload: function(searchResponse) {
  162. if (searchResponse.status === 200) {
  163. const searchResult = JSON.parse(searchResponse.responseText);
  164. const existingPage = searchResult.results.find(result => result.properties?.Title?.title[0]?.text?.content === problemData.title);
  165.  
  166. if (existingPage) {
  167. const existingPageUrl = existingPage.url;
  168. alert('Problem already exists in Notion! Opening existing page...');
  169. window.open(existingPageUrl, '_blank');
  170. } else {
  171. createNewNotionPage(problemData);
  172. }
  173. } else {
  174. console.error('Error searching Notion database', searchResponse.responseText);
  175. alert('Failed to search Notion database. Check the console for details.');
  176. }
  177. },
  178. onerror: function(error) {
  179. console.error('Error in searching Notion database', error);
  180. alert('An error occurred while searching Notion database.');
  181. }
  182. });
  183. }
  184.  
  185. // 4. create new page
  186. function createNewNotionPage(problemData) {
  187. const tags = problemData.tag.map(tag => ({
  188. name: tag
  189. }));
  190.  
  191. const url = `https://api.notion.com/v1/pages`;
  192. const body = {
  193. parent: { database_id: databaseId },
  194. properties: {
  195. 'Title': {
  196. title: [
  197. {
  198. text: {
  199. content: problemData.title
  200. }
  201. }
  202. ]
  203. },
  204. 'Difficulty': {
  205. select: {
  206. name: problemData.difficulty
  207. }
  208. },
  209. 'Link': {
  210. url: problemData.url
  211. },
  212. 'Date': {
  213. date: {
  214. start: new Date().toISOString().split('T')[0] // format YYYY-MM-DD
  215. }
  216. },
  217. 'Tags': {
  218. multi_select: tags
  219. },
  220. 'Time': {
  221. number: problemData.time
  222. },
  223. },
  224. children: [
  225. {
  226. object: 'block',
  227. type: 'code',
  228. code: {
  229. rich_text: [
  230. {
  231. type: 'text',
  232. text: {
  233. content: problemData.code
  234. }
  235. }
  236. ],
  237. language: problemData.language
  238. }
  239. }
  240. ]
  241. };
  242.  
  243. GM_xmlhttpRequest({
  244. method: 'POST',
  245. url: url,
  246. headers: {
  247. 'Authorization': `Bearer ${notionToken}`,
  248. 'Content-Type': 'application/json',
  249. 'Notion-Version': '2022-06-28'
  250. },
  251. data: JSON.stringify(body),
  252. onload: function(response) {
  253. if (response.status === 200) {
  254. const responseData = JSON.parse(response.responseText);
  255. const notionPageUrl = responseData.url;
  256. alert('Problem saved to Notion!');
  257. window.open(notionPageUrl, '_blank');
  258. } else {
  259. console.error('Failed to save to Notion', response.responseText);
  260. alert('Failed to save to Notion. Check the console for more details.');
  261. }
  262. },
  263. onerror: function(error) {
  264. console.error('Error in saving to Notion', error);
  265. alert('An error occurred while saving to Notion.');
  266. }
  267. });
  268. }
  269.  
  270. addUIElements();
  271.  
  272. })();