// ==UserScript==
// @name Leetcode solution's saver
// @namespace http://tampermonkey.net/
// @version 1.0.4
// @description Script saves the file to a folder with corresponding difficulty, name and file extension
// @author https://github.com/ruvn-1fgas
// @match https://leetcode.com/*
// @require https://code.jquery.com/jquery-latest.js
// @require https://unpkg.com/turndown/dist/turndown.js
// @icon https://www.google.com/s2/favicons?sz=64&domain=leetcode.com
// @grant GM_download
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
/* globals jQuery, $, waitForKeyElements */
const API_URL = 'https://leetcode.com/graphql/';
const LANG_LIST = [{ 'name': 'C++', 'slug': 'cpp', 'id': 0, 'extension': '.cpp' }, { 'name': 'Java', 'slug': 'java', 'id': 1, 'extension': '.java' }, { 'name': 'Python', 'slug': 'python', 'id': 2, 'extension': '.py' }, { 'name': 'MySQL', 'slug': 'mysql', 'id': 3, 'extension': '.sql' }, { 'name': 'C', 'slug': 'c', 'id': 4, 'extension': '.c' }, { 'name': 'C#', 'slug': 'csharp', 'id': 5, 'extension': '.cs' }, { 'name': 'JavaScript', 'slug': 'javascript', 'id': 6, 'extension': '.js' }, { 'name': 'Ruby', 'slug': 'ruby', 'id': 7, 'extension': '.rb' }, { 'name': 'Bash', 'slug': 'bash', 'id': 8, 'extension': '.sh' }, { 'name': 'Swift', 'slug': 'swift', 'id': 9, 'extension': '.swift' }, { 'name': 'Go', 'slug': 'golang', 'id': 10, 'extension': '.go' }, { 'name': 'Python3', 'slug': 'python3', 'id': 11, 'extension': '.py' }, { 'name': 'Scala', 'slug': 'scala', 'id': 12, 'extension': '.scala' }, { 'name': 'Kotlin', 'slug': 'kotlin', 'id': 13, 'extension': '.kt' }, { 'name': 'MS SQL Server', 'slug': 'mssql', 'id': 14, 'extension': '.sql' }, { 'name': 'Oracle', 'slug': 'oraclesql', 'id': 15, 'extension': '.sql' }, { 'name': 'HTML', 'slug': 'html', 'id': 16, 'extension': '.html' }, { 'name': 'Python ML (beta)', 'slug': 'pythonml', 'id': 17, 'extension': '.py' }, { 'name': 'Rust', 'slug': 'rust', 'id': 18, 'extension': '.rs' }, { 'name': 'PHP', 'slug': 'php', 'id': 19, 'extension': '.php' }, { 'name': 'TypeScript', 'slug': 'typescript', 'id': 20, 'extension': '.ts' }, { 'name': 'Racket', 'slug': 'racket', 'id': 21, 'extension': '.rkt' }, { 'name': 'Erlang', 'slug': 'erlang', 'id': 22, 'extension': '.erl' }, { 'name': 'Elixir', 'slug': 'elixir', 'id': 23, 'extension': '.ex' }, { 'name': 'Dart', 'slug': 'dart', 'id': 24, 'extension': '.dart' }, { 'name': 'Python Data Science (beta)', 'slug': 'pythondata', 'id': 25, 'extension': '.py' }, { 'name': 'React', 'slug': 'react', 'id': 26, 'extension': '.js' }, { 'name': 'Vanilla JS', 'slug': 'vanillajs', 'id': 27, 'extension': '.js' }];
let turndownService = new TurndownService();
turndownService.addRule('pre', {
filter: ['pre'],
replacement: function (content) {
return '\n```\n' + content + '```'
}
});
(function (open) {
XMLHttpRequest.prototype.open = function () {
this.addEventListener("readystatechange", function () {
if (this.readyState == 4 && window.location.href.search('/problems/') != -1) {
main();
}
}, false);
open.apply(this, arguments);
};
})(XMLHttpRequest.prototype.open);
function main() {
'use strict'
let buttons = $('.relative.ml-auto.flex.items-center.gap-3');
if (buttons === undefined) {
return;
}
if (buttons.children().length > 3) {
return;
}
let downloadButton = createButton('Download', buttons.children().first().attr('class'), { 'margin-right': '4px' }, download);
buttons.prepend(downloadButton);
}
function createButton(buttonName, buttonClass, buttonStyle, buttonClick) {
let button = $('<button>' + buttonName + '</button>');
button.addClass(buttonClass);
button.removeClass('cursor-not-allowed');
button.css(buttonStyle);
button.click(buttonClick);
return button;
}
async function download() {
const taskInfo = await getTaskInfo();
for (let key in taskInfo) {
if (taskInfo[key] === undefined) {
return;
}
}
const taskDesc = await getTaskDesc(taskInfo);
const taskCode = await getTaskCode(taskInfo);
if (taskCode === undefined) {
return;
}
save(taskInfo, taskCode, taskDesc);
}
async function fetchData(body, operationName) {
const csrftoken = getCsrfToken();
try {
const response = await fetch(API_URL, {
body,
method: "POST",
headers: {
"x-csrftoken": csrftoken,
"content-type": "application/json",
}
});
const data = await response.json();
return data.data[operationName];
} catch (error) {
console.log(error);
}
}
async function getTaskInfo() {
const slug = window.location.href.split('/')[4];
let body = `{\"query\":\"\\n query questionTitle($titleSlug: String!) {\\n question(titleSlug: $titleSlug) {\\n questionId\\n questionFrontendId\\n title\\n titleSlug\\n isPaidOnly\\n difficulty\\n likes\\n dislikes\\n }\\n}\\n \",\"variables\":{\"titleSlug\":\"${slug}\"},\"operationName\":\"questionTitle\"}`
const question = await fetchData(body, "question");
const currentLanguageHolder = $('.flex.items-center').filter(function () {
return $(this).attr('class') === 'flex items-center';
});
const childs = Array.from(currentLanguageHolder.children());
const currentLanguage = LANG_LIST.find(({ name }) => childs.some(child => $(child).text().trim() === name));
const { questionId, questionFrontendId, title, titleSlug, difficulty } = question;
return {
taskId: questionId,
taskName: `${questionFrontendId}. ${title}`,
titleSlug: titleSlug,
level: difficulty,
language: currentLanguage?.name,
langId: currentLanguage?.id,
fileExt: currentLanguage?.extension
};
}
async function getTaskDesc(taskInfo) {
const { titleSlug } = taskInfo;
const body = `{\"query\":\"\\n query questionContent($titleSlug: String!) {\\n question(titleSlug: $titleSlug) {\\n content\\n mysqlSchemas\\n dataSchemas\\n }\\n}\\n \",\"variables\":{\"titleSlug\":\"${titleSlug}\"},\"operationName\":\"questionContent\"}`
const { content } = await fetchData(body, "question");
const markdown = turndownService.turndown(content);
return markdown;
}
async function getTaskCode(taskInfo) {
const { langId, taskId } = taskInfo;
const body = `{\"query\":\"\\n query syncedCode($questionId: Int!, $lang: Int!) {\\n syncedCode(questionId: $questionId, lang: $lang) {\\n timestamp\\n code\\n }\\n}\\n \",\"variables\":{\"lang\":${taskInfo.langId},\"questionId\":${taskInfo.taskId}},\"operationName\":\"syncedCode\"}`;
const { code } = await fetchData(body, "syncedCode");
return code;
}
function getCsrfToken() {
let csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken')).split('=')[1];
return csrfToken;
}
async function saveCode(taskInfo, taskCode) {
const { taskName, level, fileExt } = taskInfo;
const filename = `Leetcode/${level}/${taskName}${fileExt}`;
const bl = new Blob([taskCode], { type: `text/${fileExt}` });
const download = {
url: URL.createObjectURL(bl),
name: filename
};
GM_download(download);
}
async function save(taskInfo, taskCode, taskDesc) {
const { taskName, level, fileExt } = taskInfo;
const codeFilename = `Leetcode/${level}/${taskName}${fileExt}`;
// check if file exists
let solvedTasks = localStorage.getItem('solved_tasks');
solvedTasks = solvedTasks === null ? [] : solvedTasks.split(',');
let isExists = solvedTasks === null ? false : solvedTasks.includes(`${taskName}${fileExt}`);
if (!isExists) {
const codeBl = new Blob([taskCode], { type: `text/${fileExt}` });
const codeDownload = {
url: URL.createObjectURL(codeBl),
name: codeFilename
};
GM_download(codeDownload);
if (solvedTasks === null) {
solvedTasks = [];
}
solvedTasks.push(`${taskName}${fileExt}`);
localStorage.setItem('solved_tasks', solvedTasks);
}
let solvedTasksDescs = localStorage.getItem('solved_tasks_descs');
solvedTasksDescs = solvedTasksDescs === null ? [] : solvedTasksDescs.split(',');
isExists = solvedTasksDescs === null ? false : solvedTasksDescs.includes(taskName);
if (!isExists) {
const descFilename = `Leetcode/${level}/${taskName}.md`;
const descBl = new Blob([taskDesc], { type: 'text/md' });
const descDownload = {
url: URL.createObjectURL(descBl),
name: descFilename
};
GM_download(descDownload);
if (solvedTasksDescs === null) {
solvedTasksDescs = [];
}
solvedTasksDescs.push(taskName);
localStorage.setItem('solved_tasks_descs', solvedTasksDescs);
}
}