// ==UserScript==
// @name LeetCode 付费题目移除(兼容新版本力扣)
// @version 1.1.0.1
// @description LeetCode 付费题目移除, 匹配 1.题目列表 2. 官方题单 3. 标签 tag 界面 4. 题目界面的左下角"题目列表"
// @namespace https://greasyfork.org/zh-CN/users/412790
// @require https://code.jquery.com/jquery-3.4.1.min.js
// @match *://leetcode-cn.com/problemset/*
// @match *://leetcode-cn.com/tag/*
// @match *://leetcode-cn.com/problems/*
// @match *://leetcode-cn.com/problem-list/*
// @grant GM_addStyle
// @license GPL-3.0-only
// ==/UserScript==
/* globals $, MyToast */
GM_addStyle(`
*{
margin: 0;
}
.my-toast{
position: fixed;
background: rgba(0,0,0,.7);
border-radius: 4px;
top: 93%;
left: 50%;
transform: translate(-50%,-50%);
max-width: 60%;
text-align: center;
transition: all .3s;
}
.my-toast-text{
color: #fff;
padding: 4px 10px;
padding-bottom: 6px;
}
`);
let hideCount = 0, prevCount = 0, urlID = 0, hidden;
const scriptName = "LEETCODE-PAID-REMOVE";
const problemList = new Map();
const checkInterval = 604800e3; // 86400e3 * 7, 检查题目列表更新的时间间隔, 一天 86400e3 微秒, 即每隔一周检查一次;
const removeElement = (element) => {
element && element.parentNode && element.parentNode.removeChild(element);
};
const MyToast = function () {
// Empty Function
};
MyToast.prototype = {
create: function (str, duration) {
let Text = '<div class="my-toast-text" style="z-index:1024">' + str + '</div>',
Html = '<div class="my-toast" style="z-index:1024">' + Text + '</div>';
if (document.querySelector(".my-toast")) {
removeElement(document.querySelector(".my-toast"));
if (typeof (hidden) !== "undefined") {
clearTimeout(hidden);
}
}
document.body.insertAdjacentHTML('beforeend', Html);
if (duration == null) {
duration = 2e3;
}
this.show();
hidden = setTimeout(() => {
this.hide();
}, duration);
},
show: function () {
document.querySelector(".my-toast").style.display = "block";
document.querySelector(".my-toast").style.marginTop = "-" + Math.round(document.querySelector(".my-toast").offsetHeight / 2) + "px";
},
hide: function () {
if (document.querySelector(".my-toast")) {
prevCount = 0;
hideCount = 0;
removeElement(document.querySelector(".my-toast"));
}
},
toast: function (str, duration) {
console.log(str);
return this.create(str, duration);
}
};
const removeProblemSet = () => {
let rowGroup = $("[role=\"rowgroup\"]");
if (rowGroup.length <= 0) {
return;
}
for (let childElement of rowGroup.children()) {
const childJquery = $(childElement);
const problemName = childJquery.find("div").get(1).innerText;
const frontendQuestionId = parseProblemId(problemName);
if (problemList.get(frontendQuestionId)) {
childJquery.hide();
hideCount++;
} else {
childJquery.show();
}
}
if (hideCount !== prevCount) {
prevCount = hideCount;
simpleToast.toast(`已隐藏付费题目 ${hideCount} 道`);
}
};
const removeTag = () => {
let imgCollections = document.querySelectorAll(".ant-table-tbody img");
for (let img of imgCollections) {
if (img.getAttribute("myhidden") === "hidden") {
continue;
}
const node = getLevel0ParentNode(0, img);
if (node != null) {
img.setAttribute("myHidden", "hidden");
node.style.display = "none";
hideCount++;
}
}
if (hideCount !== prevCount) {
prevCount = hideCount;
simpleToast.toast(`已隐藏付费题目 ${hideCount} 道`);
}
};
const getLevel0ParentNode = (depth, node) => {
if (!node || depth === 10) {
return null;
}
if (node.className.match("ant-table-row ant-table-row-level-0")) {
return node;
} else if (node.parentNode) {
return getLevel0ParentNode(depth + 1, node.parentNode);
} else {
return null;
}
};
const addCss = () => {
GM_addStyle(`div[data-paid-only="true"]{display:none!important}`);
};
const parseProblemId = (problemName) => {
let problemId = "";
for (let char of problemName) {
if (char === ".") {
return problemId;
} else {
problemId += char;
}
}
return null;
}
const config = {
childList: true,
subtree: true,
attributes: true,
characterData: true,
attributeOldValue: false,
characterDataOldValue: false,
attributeFilter: []
};
let Observer = new MutationObserver(() => {
if (!document.querySelector("[role=\"rowgroup\"]") && !document.querySelector(".ant-table-content")) {
Observer.disconnect();
addObserverIfDesiredNodeAvailable();
}
work();
}),
simpleToast = new MyToast();
const addObserverIfDesiredNodeAvailable = () => {
if (document.querySelector("[role=\"rowgroup\"]")) {
Observer.observe(document.querySelector("[role=\"rowgroup\"]"), config);
work();
} else if (document.querySelector(".ant-table-content")) {
Observer.observe(document.querySelector(".ant-table-content"), config);
work();
} else {
setTimeout(() => addObserverIfDesiredNodeAvailable());
}
};
const prepare = () => {
const url = window.location.href;
if (/(leetcode-cn.com\/problemset\/)/.test(url)) {
urlID = 0;
coreInit();
} else if (/(leetcode-cn.com\/tag\/)/.test(url) || /(leetcode-cn.com\/problem-list\/)/.test(url)) {
urlID = 1;
coreInit();
} else {
urlID = 2;
addCss();
}
};
const checkNewWeek = (ts) => {
if (ts == null) {
return true;
}
ts = parseInt(ts);
const t = new Date(ts);
const tzOffset = new Date().getTimezoneOffset() + 480;
t.setMinutes(t.getMinutes() + tzOffset);
t.setHours(0, 0, 0, 0);
const d = new Date();
d.setMinutes(t.getMinutes() + tzOffset);
return (d - t > checkInterval);
}
const getProblemMsg = "开始请求全部题目信息", refreshProblemCacheMsg = "本地题目缓存过期, 开始请求全部题目信息";
const coreInit = () => {
if (!localStorage.getItem(scriptName + "-PROBLEM-LIST")) {
getProblems(getProblemMsg);
} else if (checkNewWeek(localStorage.getItem(scriptName + "-PROBLEM-UPDATE-TIME"))) {
getProblems(refreshProblemCacheMsg);
} else {
buildMap();
}
}
const getProblems = (msg) => {
simpleToast.toast(msg);
const startTime = new Date();
$.get({
url: 'https://leetcode-cn.com/api/problems/all/'
}).then((response) => {
writeLocalStorage(response, startTime);
buildMap();
});
};
const writeLocalStorage = (response, startTime) => {
localStorage.setItem(scriptName + "-PROBLEM-UPDATE-TIME", Date.now().toString());
localStorage.setItem(scriptName + "-PROBLEM-LIST", response);
const endTime = new Date();
simpleToast.toast(`获取并缓存题目数据成功, 耗时 ${new Date(endTime - startTime).getSeconds()} 秒, 将在一周之后再次更新缓存`);
}
const buildMap = () => {
for (const item of JSON.parse(localStorage.getItem(scriptName + "-PROBLEM-LIST"))["stat_status_pairs"]) {
problemList.set(item["stat"]["frontend_question_id"], item["paid_only"]);
}
addObserverIfDesiredNodeAvailable();
}
const work = () => {
if (urlID === 0) {
try {
removeProblemSet();
} catch (e) {
}
} else if (urlID === 1) {
try {
removeTag();
} catch (e) {
}
}
};
prepare();
window.onpopstate = () => {
Observer.disconnect();
addObserverIfDesiredNodeAvailable();
};