Greasy Fork is available in English.

leetcode2notion

Save LeetCode problems to Notion after clicking a button.

נכון ליום 08-09-2024. ראה הגרסה האחרונה.

  1. // ==UserScript==
  2. // @name leetcode2notion
  3. // @namespace wuyifff
  4. // @version 1.1
  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. function addUIElements() {
  25. const button = document.createElement("button");
  26. button.innerHTML = "Save to Notion";
  27. button.style.position = "fixed";
  28. button.style.bottom = "10px";
  29. button.style.right = "10px";
  30. button.style.zIndex = 1000;
  31. button.style.padding = "10px 20px";
  32. button.style.backgroundColor = "#4CAF50";
  33. button.style.color = "white";
  34. button.style.border = "none";
  35. button.style.borderRadius = "5px";
  36. button.style.cursor = "pointer";
  37. button.onclick = saveProblemToNotion;
  38.  
  39. const select = document.createElement("select");
  40. select.id = "languageSelect";
  41. select.style.position = "fixed";
  42. select.style.bottom = "50px";
  43. select.style.right = "10px";
  44. select.style.zIndex = 1000;
  45. select.style.padding = "10px";
  46. select.style.backgroundColor = "#4CAF50";
  47. select.style.color = "white";
  48. select.style.border = "none";
  49. select.style.borderRadius = "5px";
  50. select.style.cursor = "pointer";
  51.  
  52. const optionPython = document.createElement("option");
  53. optionPython.value = "python";
  54. optionPython.innerText = "Python";
  55.  
  56. const optionCpp = document.createElement("option");
  57. optionCpp.value = "cpp";
  58. optionCpp.innerText = "C++";
  59.  
  60. select.appendChild(optionPython);
  61. select.appendChild(optionCpp);
  62.  
  63. const container = document.createElement("div");
  64. container.style.display = "flex";
  65. container.style.flexDirection = "column";
  66. container.style.alignItems = "center";
  67. container.style.marginLeft = "10px";
  68. //container.appendChild(select);
  69. container.appendChild(button);
  70.  
  71. container.style.position = "fixed";
  72. container.style.bottom = "10px";
  73. container.style.right = "10px";
  74. document.body.appendChild(container);
  75. }
  76.  
  77. // 2. get leetcode problem info
  78. function getProblemData() {
  79. const title = document.querySelector('.text-title-large a')?.innerText || 'No title found';
  80. const difficultyElement = document.querySelector("div[class*='text-difficulty-']");
  81. const difficulty = difficultyElement ? difficultyElement.innerText : 'No difficulty found';
  82. const url = window.location.href;
  83. const tagElements = document.querySelectorAll("a[href*='/tag/']");
  84. const tagTexts = Array.from(tagElements).map(element => element.innerText);
  85.  
  86. const codeDiv = document.querySelector('.view-lines.monaco-mouse-cursor-text[role="presentation"]');
  87. let codeText = '';
  88. if (codeDiv) {
  89. const codeLines = codeDiv.querySelectorAll('div');
  90. codeText = Array.from(codeLines).map(line => line.innerText).join('\n');
  91. } else {
  92. codeText = 'No code found';
  93. }
  94. //console.log(codeText);
  95. //const selectedLanguage = document.getElementById("languageSelect").value;
  96. const selectedLanguage = 'python';
  97. return {
  98. title: title,
  99. difficulty: difficulty,
  100. url: url,
  101. tag: tagTexts,
  102. code: codeText,
  103. language: selectedLanguage
  104. };
  105. }
  106.  
  107. // 3. save to notion and check if duplicate
  108. async function saveProblemToNotion() {
  109. const problemData = getProblemData();
  110. console.log(problemData);
  111.  
  112. const searchUrl = `https://api.notion.com/v1/search`;
  113. const searchBody = {
  114. "query": problemData.title,
  115. "filter": {
  116. "value": "page",
  117. "property": "object"
  118. },
  119. "sort": {
  120. "direction": "ascending",
  121. "timestamp": "last_edited_time"
  122. }
  123. };
  124.  
  125. GM_xmlhttpRequest({
  126. method: 'POST',
  127. url: searchUrl,
  128. headers: {
  129. 'Authorization': `Bearer ${notionToken}`,
  130. 'Content-Type': 'application/json',
  131. 'Notion-Version': '2022-06-28'
  132. },
  133. data: JSON.stringify(searchBody),
  134. onload: function(searchResponse) {
  135. if (searchResponse.status === 200) {
  136. const searchResult = JSON.parse(searchResponse.responseText);
  137. const existingPage = searchResult.results.find(result => result.properties?.Title?.title[0]?.text?.content === problemData.title);
  138.  
  139. if (existingPage) {
  140. const existingPageUrl = existingPage.url;
  141. alert('Problem already exists in Notion! Opening existing page...');
  142. window.open(existingPageUrl, '_blank');
  143. } else {
  144. createNewNotionPage(problemData);
  145. }
  146. } else {
  147. console.error('Error searching Notion database', searchResponse.responseText);
  148. alert('Failed to search Notion database. Check the console for details.');
  149. }
  150. },
  151. onerror: function(error) {
  152. console.error('Error in searching Notion database', error);
  153. alert('An error occurred while searching Notion database.');
  154. }
  155. });
  156. }
  157.  
  158. // 4. create new page
  159. function createNewNotionPage(problemData) {
  160. const tags = problemData.tag.map(tag => ({
  161. name: tag
  162. }));
  163.  
  164. const url = `https://api.notion.com/v1/pages`;
  165. const body = {
  166. parent: { database_id: databaseId },
  167. properties: {
  168. 'Title': {
  169. title: [
  170. {
  171. text: {
  172. content: problemData.title
  173. }
  174. }
  175. ]
  176. },
  177. 'Difficulty': {
  178. select: {
  179. name: problemData.difficulty
  180. }
  181. },
  182. 'Link': {
  183. url: problemData.url
  184. },
  185. 'Date': {
  186. date: {
  187. start: new Date().toISOString().split('T')[0] // format YYYY-MM-DD
  188. }
  189. },
  190. 'Tags': {
  191. multi_select: tags
  192. }
  193. },
  194. children: [
  195. {
  196. object: 'block',
  197. type: 'code',
  198. code: {
  199. rich_text: [
  200. {
  201. type: 'text',
  202. text: {
  203. content: problemData.code
  204. }
  205. }
  206. ],
  207. language: problemData.language
  208. }
  209. }
  210. ]
  211. };
  212.  
  213. GM_xmlhttpRequest({
  214. method: 'POST',
  215. url: url,
  216. headers: {
  217. 'Authorization': `Bearer ${notionToken}`,
  218. 'Content-Type': 'application/json',
  219. 'Notion-Version': '2022-06-28'
  220. },
  221. data: JSON.stringify(body),
  222. onload: function(response) {
  223. if (response.status === 200) {
  224. const responseData = JSON.parse(response.responseText);
  225. const notionPageUrl = responseData.url;
  226. alert('Problem saved to Notion!');
  227. window.open(notionPageUrl, '_blank');
  228. } else {
  229. console.error('Failed to save to Notion', response.responseText);
  230. alert('Failed to save to Notion. Check the console for more details.');
  231. }
  232. },
  233. onerror: function(error) {
  234. console.error('Error in saving to Notion', error);
  235. alert('An error occurred while saving to Notion.');
  236. }
  237. });
  238. }
  239.  
  240. addUIElements();
  241.  
  242. })();