洛谷超级任务计划(第三方),不限题目数量
// ==UserScript==
// @name 洛谷超级任务计划(第三方)
// @namespace http://tampermonkey.net/
// @version 1.12
// @description 洛谷超级任务计划(第三方),不限题目数量
// @author Anguei, Legendword
// @match https://www.luogu.org/problemnew/show/*
// @match https://www.luogu.org/
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
// 可能将要实现的功能:
// 1. 添加异常处理,如 get 请求没有 200
// 2. 显示当前任务计划总数
// 3. 在只知道题号的情况下进行导入
// 感谢 @memset0 提供创意
// 感谢 @Legendword 协助完成 jQuery 相关代码
// 感谢 @memset0, @Legendword 帮助找 bug
var originalLimit = 28;
var nowUrl = window.location.href;
var LuoguSuperTodolist = {
settings: {
keepOriginalList: false,
debugMode: false // 发布前将此设为 false
}
};
// var myUid = document.cookie.match(/_uid=[0-9]+/)[0].substr(5);
// console.log(myUid)
// console.log('如果上面获取到的 uid 不正确,请反馈作者,谢谢')
var myUid = GM_getValue("myUid");
if (myUid == undefined || myUid == 'undefined')
myUid = prompt("任务计划脚本更新,请正确输入您的 uid(数字)以保障插件正常运行");
GM_setValue("myUid", myUid);
function getOriginalList() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.luogu.org/', false);
xhr.send(null);
if (xhr.status == 200) {
console.log('get original todo list: 200');
return extractData(xhr.responseText);
} else {
return {};
}
function extractData(content) {
var psid = content.split('" target="_blank"><b>');
if (psid[0].indexOf("还没有计划完成的题目<br>") != -1) {
return {};
}
return clearData(psid);
function clearData(psid) { // psid: problems' id
var res = {}
for (var i = 1; i < psid.length; i++) { // 从 1 开始循环,因为 split 导致 psid[0] 是垃圾文本串
var pId = '', pName = '', j = 0;
for (; j < psid[i].length; j++) { // 获取题号
if (psid[i][j] != '<') {
pId = pId.concat(psid[i][j]);
} else break;
}
for (j += 5; j < psid[i].length; j++) { // 获取题目名称
if (psid[i][j] != '<') {
pName = pName.concat(psid[i][j]);
} else break;
}
res[pId] = pName;
if (psid[i].match(/智能推荐/) != null) break;
}
return res;
}
}
}
function syncList() {
console.log('syncing');
LuoguSuperTodolist.problems = getOriginalList();
initList(); // 把洛谷原计划保存到脚本
function initList() {
var old = GM_getValue('problems');
if (old != undefined || old != 'undefined') {
for (var i in old) {
LuoguSuperTodolist.problems[i] = old[i]
}
}
GM_setValue('problems', LuoguSuperTodolist.problems);
}
}
function getAc(uid) { // 从原来代码复制过来的
// 向指定的个人空间发送 get 请求,获取 AC 列表
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://' + window.location.host + '/space/show?uid=' + uid, false);
xhr.send(null);
console.log('got ' + uid + "'s AC list: " + xhr.status);
if (xhr.status == 200) {
return extractData(xhr.responseText); // 返回 AC 列表
} else {
return []; // 空列表
}
function extractData(content) {
// 如果你有一个问题打算用正则表达式来解决,那么就是两个问题了。
// 所以窝还是用 split() 解决这一个问题吧!
var acs = content.replace(/<span style=\"display:none\">\n.*?\n<\/span>/g, ''); // 把随机的干扰题号去除
acs = acs.split('[<a data-pjax href="/problem/show?pid='); // 使用 split() 方法把通过的题目分割出来
acs = clearData(acs); // 把分割好的数据清洁一下
return acs;
function clearData(acs) {
var res = new Array();
res.push(new Array());
res.push(new Array());
var g = 0;
for (var i = 1; i < acs.length; i++) { // 把每一行非题号字符删掉(从 1 开始循环为了避开 split 之后产生的垃圾)
var tmpStr = "";
for (var j = 0; j < acs[i].length; j++) {
if (acs[i][j] != '"') { // 引号后面的不是题号部分字符
tmpStr = tmpStr.concat(acs[i][j]); // 拼接字符串
}
else break;
}
res[g].push(tmpStr);
if (acs[i].length > 50) { // 这是最后一个题目 / 下一个是「尝试过的题目」
g++;
}
}
return res;
}
}
}
function getDictLength(dict) {
var res = 0;
for (var i in dict) res++;
return res;
}
function updateMainPageList() {
var tmp = getAc(myUid);
var myAc = tmp[0], myAttempt = tmp[1];
myAc.sort();
myAttempt.sort();
// 清除官方的任务计划
if (!LuoguSuperTodolist.settings.keepOriginalList) {
$("h2:contains('智能推荐')").prevAll().remove();
// 当官方计划为空时,删除那句话
if ($("h2:contains('智能推荐')").parent().html().indexOf("<h2") > 0) {
$("h2:contains('智能推荐')").parent().html($("h2:contains('智能推荐')").parent().html().slice($("h2:contains('智能推荐')").parent().html().indexOf("<h2")));
}
}
// 在 Luogu 官方任务计划后面添加第三方计划
var problems = GM_getValue('problems')
$("h2:contains('智能推荐')").before('<h2>任务计划 (' + getDictLength(problems) + ' 题)' + '<button class="am-btn am-btn-sm am-btn-primary lg-right" id="LuoguSuperTodolist-export">编辑</button></h2>');
for (var i in problems) {
var state = getState(i);
var color = { 'Y': 'green', 'N': 'black', '?': 'orange' };
var content = { 'Y': '<i class="am-icon-check"></i>', 'N': '<i class="am-icon-minus"></i>', '?': '?' };
$("h2:contains('智能推荐')").before(
'<div class="tasklist-item LuoguSuperTodolist-tasklist-item" data-pid="'
+ i
+ '"><div><a href="/recordnew/lists?uid='
+ myUid
+ '&pid='
+ i
+ '" target="_blank"><strong class="lg-fg-'
+ color[state]
+ '">' + content[state]
+ '</strong></a>'
+ '<a class="colored" style="padding-left: 3px" href="/problemnew/show/'
+ i
+ '" target="_blank"><b>'
+ i
+ '</b> '
+ problems[i]
+ '</a></div></div>'
);
}
$("#LuoguSuperTodolist-export").click(function () {
if (LuoguSuperTodolist.exportOpen) { // 响应「完成」按钮
/*while (!importProblem()) {
$("#edit-problem")[0].value = generateExportedList(); // 恢复原文本串
} // 直到成功导入*/
importProblem();
$("#LuoguSuperTodolist-export").html("编辑");
$("#LuoguSuperTodolist-exportRes").remove();
} else { // 响应「导入 / 导出」按钮
var listString = generateExportedList();
$("h2:contains('任务计划')").after(
"<div id='LuoguSuperTodolist-exportRes'>编辑下方文本进行编辑操作。</br>格式:[题号] + '#'(井号) + [题目标题],一行一题。</br>拖动右下角可以改变编辑框大小。"
+ "<div class='am-form-group am-form'><textarea id='edit-problem'>"
+ listString
+ "</textarea></div>"
);
console.log(listString);
$("#LuoguSuperTodolist-export").html("完成");
}
LuoguSuperTodolist.exportOpen ^= 1; // 使用异或运算切换状态,简便快捷
function importProblem() { // 尚不支持检测题号是否合法
var input = $("#edit-problem").val()
input = input.split('\n'); // jQuery 自带 split 有问题,必须用 string 的是 split
console.log(input);
// var problems = LuoguSuperTodolist.problems;
var problems = {}; // for oier sp
for (var i = 0; i < input.length; i++) {
if (!checkString(input[i])) {
alert('输入数据不合法!')
return false;
}
input[i] = input[i].split(' # '); // 注意这里不能是单纯的井号
if (problems[input[i][0]] == undefined) {
problems[input[i][0]] = input[i][1];
console.log(problems[input[i][0]]);
}
}
LuoguSuperTodolist.problems = problems;
console.log(LuoguSuperTodolist.problems);
GM_setValue('problems', LuoguSuperTodolist.problems);
updateMainPageList(); // 导入成功后需要更新显示
function checkString(s) { // 计算字符串中 # 号数量,检查格式
var pos = -1;
for (var i = 0; i < s.length; i++) {
if (s[i] == '#') {
if (pos == -1) {
pos = i;
} else {
pos = -2;
}
}
}
return pos > 0 && s[pos - 1] == ' ' && s[pos + 1] == ' ';
}
}
function generateExportedList() {
var problems = LuoguSuperTodolist.problems;
var res = "";
for (var c in problems) {
res += "\n" + c + " # " + problems[c];
}
return res;
}
});
function getState(pid) {
if (binarySearch(pid, myAc)) return 'Y';
else if (binarySearch(pid, myAttempt)) return '?';
else return 'N';
}
function binarySearch(target, array) { // 使用二分查找算法进行比较
var l = 0, r = array.length;
while (l < r) {
var mid = parseInt((l + r) / 2); // JavaScript 除法默认不是整数。。
if (target == array[mid]) return true;
else if (target > array[mid]) l = mid + 1;
else r = mid;
}
return false;
}
}
function addButton() {
$('#remove-tasklist').remove();
$('#add-tasklist').remove(); // 移除旧的按钮
var problemId = nowUrl.match(/[A-Z]+[0-9]+/)[0];
if (nowUrl.match(/[A-Z]+[0-9]+[A-Z]/) != null) {
problemId = nowUrl.match(/[A-Z]+[0-9]+[A-Z]/)[0]; // CF 题库特判
}
console.log(problemId);
var problemTitle = getTitle();
if (!isInList()) {
$(".lg-summary-content")
.append('<p>'
+ '<a href="javascript: ;" '
+ 'id="update-todolist" '
+ 'class="am-btn am-btn-sm am-btn-primary">'
+ '添加至超级任务计划'
+ '</a>'
+ '</p>');
$("#update-todolist").click(swapState);
} else {
$(".lg-summary-content")
.append('<p>'
+ '<a href="javascript: ;" '
+ 'id="update-todolist" '
+ 'class="am-btn am-btn-sm am-btn-danger">'
+ '从任务计划移除'
+ '</a>'
+ '</p>');
$("#update-todolist").click(swapState);
}
function getTitle() {
return document.title.substr((problemId.length + 1), document.title.length - (problemId.length + 1) - 5)
}
function isInList() {
var nowList = GM_getValue('problems');
if (nowList == undefined) return false;
return nowList[problemId] != undefined;
}
function swapState(ev) { // 是不是应该改成检测现在的属性,然后交换属性
if (isInList()) { // 已经在任务计划列表,删掉它。变成蓝色按钮
removeFromList(problemId, problemTitle);
$("#update-todolist").attr("class", "am-btn am-btn-sm am-btn-primary");
$("#update-todolist").html("添加至超级任务计划");
} else { // 不在任务计划当中,加进去,变成红色按钮
addToList(problemId, problemTitle)
$("#update-todolist").attr("class", "am-btn am-btn-sm am-btn-danger");
$("#update-todolist").html("从任务计划移除");
}
function addToList(id, title) {
var nowList = GM_getValue('problems');
nowList[id] = title;
GM_setValue('problems', nowList);
LuoguSuperTodolist.problems = GM_getValue('problems')
if (getDictLength(getOriginalList()) < originalLimit) { // 原计划长度足够装下新题目,装进去
$.post("/api/user/tasklistAdd", { pid: id });
}
}
function removeFromList(id, title) {
var nowList = GM_getValue('problems');
delete nowList[id];
GM_setValue('problems', nowList);
LuoguSuperTodolist.problems = GM_getValue('problems')
var originalList = getOriginalList();
if (findInDict(id, originalList)) { // 删除的题目在原计划当中,在原计划也删除
$.post("/api/user/tasklistRemove", { pid: id })
}
checkOther(); // 看剩余空间够不够再同步几道题进去
function findInDict(target, dict) {
for (var i in dict) if (i == target) return true;
return false;
}
function checkOther() {
var diff = originalLimit - (getDictLength(originalList) - 1); // 长度 - 1 是因为刚才删掉了一个
console.log(diff);
if (diff > 0) {
var cnt = 0;
var arr = new Array();
for (var i in LuoguSuperTodolist.problems) {
if (!findInDict(i, originalList)) { // superList 当中不在 originalList 的题目
arr.push(i);
cnt += 1;
if (cnt == diff) break;
}
}
for (var i = 0; i < arr.length; i++) {
$.post("/api/user/tasklistAdd", { pid: arr[i] });
}
}
}
}
}
}
function start() {
var lastUse = GM_getValue('lastUse');
var date = Date();
date = date.split(' ');
date = date[0] + date[1];
GM_setValue('lastUse', date);
if (lastUse == undefined || lastUse != date || LuoguSuperTodolist.settings.debugMode) { // 首次运行脚本,将原任务计划保存
console.log('更新后首次运行脚本,请耐心等待初始化');
syncList();
}
LuoguSuperTodolist.problems = GM_getValue('problems')
if (nowUrl == 'https://www.luogu.org/') {
updateMainPageList(); // 更新主页的 todolist
} else if (nowUrl.match(/problem/) != null) { // 题目页面运行脚本,添加按钮
addButton();
}
}
start();