├── LICENSE ├── README.md └── comparator.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Luogu-Ac-Comparator 2 | 3 | 比较洛谷上你和其他用户之间通过的题目异同。 4 | 5 | 可以方便你和同学之间相互竞争、了解,促进进步。 6 | 7 | 顺便把所有千题神犇的通过转化为精确数字、解决洛谷 AC 数不实时更新的问题。 8 | 9 | **这是一个用户脚本,在使用之前,请确保你的浏览器安装了 Tampermonkey 插件。** 10 | 11 | [访问这里安装脚本](https://greasyfork.org/zh-CN/scripts/371669-%E6%B4%9B%E8%B0%B7%E9%80%9A%E8%BF%87%E9%A2%98%E7%9B%AE%E6%AF%94%E8%BE%83%E5%99%A8-yyfcpp) 12 | 13 | ## 使用方法 14 | 进入要比较对象的洛谷个人主页或题目评测记录页面。稍等片刻即显示比较结果。 15 | 16 | 红色显示你未 AC 的题目,绿色显示也通过的题目。橙色显示你尝试过的题目。 17 | 18 | 19 | 示例:(emm 这是发布初期的示例图,懒得换了 hhhh) 20 | 21 | ![example1](https://s1.ax1x.com/2018/09/30/i1P59K.png) 22 | 23 | ![example2](https://s1.ax1x.com/2018/09/30/i1PI1O.png) 24 | 25 | **注意:如果对方开启了完全隐私保护,此脚本不可用。** 26 | 27 | ## TODO 28 | - [x] 适配新版本洛谷 29 | - [ ] 增加个性化开关 30 | 31 | ## 感谢 32 | 感谢 @abc1763613206 协助我发布脚本。 33 | 感谢 @Legendword 帮助我染色。 34 | 感谢 @qq1010903229 帮助完善功能。 35 | 感谢 @引领天下 催更。 -------------------------------------------------------------------------------- /comparator.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 洛谷通过题目比较器 - yyfcpp 3 | // @namespace http://tampermonkey.net/ 4 | // @version 4.1 5 | // @description 比较你和其他用户在洛谷通过的题目 6 | // @author Anguei 7 | // @match https://www.luogu.com.cn/user/* 8 | // @match https://www.luogu.com.cn/record/list* 9 | // @grant GM_setValue 10 | // @grant GM_getValue 11 | // ==/UserScript== 12 | 13 | var version = GM_getValue('version'); 14 | if (version == undefined || version == 'undefined') { 15 | alert('感谢您长久以来的支持。洛谷 AC 比较器咕咕咕许久后,今日终于更新了。') 16 | } 17 | GM_setValue('version', '4.0'); 18 | 19 | 20 | var myUid = GM_getValue('myUid'); 21 | if (myUid == undefined || myUid == 'undefined') { 22 | while (1) { 23 | myUid = prompt( 24 | 'AC 比较器插件更新,请正确输入您的 uid(数字)以保障插件正常运行'); 25 | if (prompt('请再次输入 uid') != myUid) { 26 | alert('两次输入数据不同,请重试'); 27 | } else 28 | break; 29 | } 30 | } 31 | GM_setValue('myUid', myUid); 32 | 33 | window.onload = function() { 34 | console.log('ACC: starting...'); 35 | 36 | function getJson(uid) { 37 | console.log('gettting json of ' + uid); 38 | var oReq = new XMLHttpRequest(); 39 | oReq.open('GET', 'https://www.luogu.com.cn/user/' + uid, false); 40 | oReq.send(); 41 | if (oReq.status != 200) throw ('AC 比较器:网络不稳定,终止运行。'); 42 | var text = oReq.responseText; 43 | // console.log(text); 44 | var e = text.match(/JSON\.parse\(.*\)/)[0]; 45 | var json = eval(e); 46 | return json; 47 | } 48 | 49 | function getProblems(pJsons) { 50 | var arr = []; 51 | for (var i = 0; i < pJsons.length; ++i) arr.push(pJsons[i]['pid']); 52 | return arr; 53 | } 54 | 55 | 56 | if (window.location.href.match(/list/) == null) { // 个人主页 57 | var hisUid = window.location.href.match(/[0-9]+/)[0]; 58 | console.log(myUid); 59 | console.log(hisUid); 60 | if (hisUid == myUid) { 61 | var myJson = getJson(myUid); 62 | var myAcCnt = myJson['currentData']['passedProblems'].length; 63 | setInterval(setAcCnt, 1000, myAcCnt); 64 | } else { 65 | var myJson = getJson(myUid); 66 | var hisJson = getJson(hisUid); 67 | console.log('got all jsons'); 68 | var hisAcCnt = hisJson['currentData']['passedProblems'].length; 69 | setInterval(setAcCnt, 1000, hisAcCnt); 70 | 71 | var myAced = getProblems(myJson['currentData']['passedProblems']); 72 | var myTried = getProblems(myJson['currentData']['submittedProblems']); 73 | var hisAced = getProblems(hisJson['currentData']['passedProblems']); 74 | var hisTried = getProblems(hisJson['currentData']['submittedProblems']); 75 | 76 | setInterval(function() { 77 | if (window.location.href.match(/#practice/) == null) return; // 练习页面 78 | for (var i = 0; i < hisAced.length; ++i) { 79 | if (myAced.indexOf(hisAced[i]) != -1) 80 | deco(2, i, '#008000'); 81 | else if (myTried.indexOf(hisAced[i]) != -1) 82 | deco(2, i, '#ff8c00'); 83 | else 84 | deco(2, i, 'red') 85 | } 86 | for (var i = 0; i < hisTried.length; ++i) { 87 | if (myAced.indexOf(hisTried[i]) != -1) 88 | deco(1, i, '#008000'); 89 | else if (myTried.indexOf(hisTried[i]) != -1) 90 | deco(1, i, '#ff8c00'); 91 | else 92 | deco(1, i, 'red') 93 | } 94 | }, hisAcCnt); 95 | } 96 | 97 | 98 | function setAcCnt(x) { 99 | document 100 | .querySelector( 101 | '#app > div.main-container > main > div > div.card.user-header-container.padding-0 > div.user-header-bottom > div.user-stat-data.lfe-caption > div > div:nth-child(4) > a > span.value') 102 | .innerText = x; 103 | } 104 | 105 | function deco(tryOrAc, idx, color) { 106 | // console.log(tryOrAc, idx, color); 107 | document 108 | .querySelector( 109 | '#app > div.main-container > main > div > div.full-container > section.main > div:nth-child(' + 110 | tryOrAc + ') > div.problems > span:nth-child(' + (idx + 1) + 111 | ') > a') 112 | .style = 'color: ' + color; 113 | } 114 | } else { // 评测记录 115 | var myJson = getJson(myUid); 116 | var myAcCnt = myJson['currentData']['passedProblems'].length; 117 | var myAced = getProblems(myJson['currentData']['passedProblems']); 118 | var myTried = getProblems(myJson['currentData']['submittedProblems']); 119 | 120 | setInterval(function() { 121 | for (var i = 1; i <= 20; ++i) { 122 | var selector = 123 | '#app > div.main-container > main > div > div > div > div.border.table > div > div:nth-child(' + 124 | i + ') > div.problem > div > a'; 125 | var problem = document.querySelector(selector).innerText; 126 | var problemId = problem.split(' ')[0]; 127 | 128 | if (myAced.indexOf(problemId) != -1) 129 | document.querySelector(selector).style = 'color: ' + 130 | '#008000'; 131 | else if (myTried.indexOf(problemId) != -1) 132 | document.querySelector(selector).style = 'color: ' + 133 | '#ff8c00'; 134 | else 135 | document.querySelector(selector).style = 'color: ' + 136 | 'red'; 137 | } 138 | }, 300); 139 | } 140 | }; 141 | 142 | 143 | 144 | /*// 几个开关,可根据用户喜好自行定义 145 | var settings = {}; 146 | var colored = 0; 147 | 148 | // alert(document.body.innerHTML.match('uid=([0-9]+)')[0]); 149 | // console.log(document.body.innerHTML); 150 | var myUid = GM_getValue('myUid'); 151 | if(myUid == undefined || myUid == 'undefined') 152 | myUid = 153 | prompt('比较器脚本更新,请正确输入您的 uid(数字)以保障插件正常运行'); 154 | GM_setValue('myUid', myUid); 155 | 156 | 157 | function getAc(uid) { 158 | // 向指定的个人空间发送 get 请求,获取 AC 列表 159 | var xhr = new XMLHttpRequest(); 160 | xhr.open( 161 | 'GET', 'https://' + window.location.host + '/space/show?uid=' + uid, 162 | false); 163 | xhr.send(null); 164 | console.log('got ' + uid + '\'s AC list: ' + xhr.status); 165 | if (xhr.status == 200) { 166 | return extractData(xhr.responseText); // 返回 AC 列表 167 | } else { 168 | return []; // 空列表 169 | } 170 | 171 | function extractData(content) { 172 | // 如果你有一个问题打算用正则表达式来解决,那么就是两个问题了。 173 | // 所以窝还是用 split() 解决这一个问题吧! 174 | var acs = content.replace( 175 | /\n.*?\n<\/span>/g, 176 | ''); // 把随机的干扰题号去除 177 | acs = acs.split( 178 | '[ 200 | 50) { // 这是最后一个题目 / 下一个是「尝试过的题目」 201 | g++; 202 | } 203 | } 204 | return res; 205 | } 206 | } 207 | } 208 | 209 | 210 | function compare_new(hisAc, myAc, myAttempt) { 211 | myAc.sort(); // 排序,用于二分查找 212 | myAttempt.sort(); 213 | var tot = hisAc.length; 214 | for (var i = 0; i < hisAc.length; i++) { 215 | var meToo = false; 216 | if (binarySearch(hisAc[i], myAc)) { 217 | meToo = true; 218 | tot--; 219 | } 220 | if (++colored <= settings['limOfColoring']) { // 如果尚未超过阈值 221 | changeStyle(hisAc[i], meToo); // 改变题号颜色 222 | } 223 | if (!meToo) { // 没 AC 过,有没有尝试过? 224 | if (binarySearch(hisAc[i], myAttempt)) { 225 | meToo = true; 226 | } 227 | if (colored <= settings['limOfColoring']) { 228 | changeStyle2(hisAc[i], meToo); 229 | } 230 | } 231 | } 232 | if (settings['totDisplaying']) { // 如果打开显示未 AC 总数的开关 233 | displayTot(tot); // 显示 AC 总数 234 | } 235 | 236 | function changeStyle(pid, meToo) // AC 过的题目 237 | { 238 | var cssSelector = 'a[href=\'/problemnew/show/' + pid + '\']'; 239 | // 由于洛谷使用随机页面结构,导致了一点小问题,所以要 240 | // querySelectorAll,防止染色失败 241 | var elements = document.querySelectorAll(cssSelector); 242 | for (var i = 0; i < elements.length; i++) { 243 | elements[i].style.color = meToo ? '#008000' : 'red'; 244 | } 245 | } 246 | 247 | function changeStyle2(pid, meToo) // 尝试过的题目 248 | { 249 | var cssSelector = 'a[href=\'/problemnew/show/' + pid + '\']'; 250 | var elements = document.querySelectorAll(cssSelector); 251 | for (var i = 0; i < elements.length; i++) { 252 | elements[i].style.color = meToo ? '#ff8c00' : 'red'; 253 | } 254 | } 255 | 256 | function displayTot(tot) { 257 | var cssSelector = 258 | '#app-body-new > div.am-g.lg-main-content > div.am-u-md-4.lg-right > 259 | div:nth-child(2) > h2'; document.querySelector(cssSelector).style.fontSize = 260 | '18px'; // 避免在一些低分辨率显示器上一行显示不开 261 | document.querySelector(cssSelector).textContent = 262 | '通过题目(其中有 ' + tot + ' 道题你尚未 AC)'; 263 | } 264 | } 265 | 266 | 267 | function binarySearch(target, array) { // 使用二分查找算法进行比较 268 | var l = 0, r = array.length; 269 | while (l < r) { 270 | var mid = parseInt((l + r) / 2); // JavaScript 除法默认不是整数。。 271 | if (target == array[mid]) 272 | return true; 273 | else if (target > array[mid]) 274 | l = mid + 1; 275 | else 276 | r = mid; 277 | } 278 | return false; 279 | } 280 | 281 | 282 | function displayAcCnt(AcCnt) { 283 | for (var i = 2; i <= 3; 284 | i++) { // 解决页面结构不稳定导致的 AC 数无法正常显示问题 285 | var cssSelector = 286 | '#app-body-new > div.am-g.lg-main-content > div.am-u-md-4.lg-right > 287 | section > div > ul > li:nth-child(' + i + 288 | ') > ul > li:nth-child(2) > span.lg-bignum-num'; // 适配新的洛谷 UI 289 | if (document.querySelector(cssSelector) != 290 | null) { // 确定了 AC 数的选择器 291 | document.querySelector(cssSelector).innerHTML = 292 | AcCnt + ''; // 更新 AC 数 293 | if (settings['colorChanging']) { // 如果打开颜色变化的开关 294 | changeAcColor(cssSelector, AcCnt); 295 | } 296 | break; 297 | } 298 | } 299 | 300 | function changeAcColor(cssSelector, AcCnt) { 301 | if (AcCnt >= 1275) 302 | document.querySelector(cssSelector).style = 'color:#FF0000;'; 303 | else if (AcCnt >= 867) 304 | document.querySelector(cssSelector).style = 305 | 'color:rgb(255,' + ((1275 - AcCnt) / 2) + ',0);'; 306 | else if (AcCnt >= 765) 307 | document.querySelector(cssSelector).style = 'color:rgb(' + 308 | ((AcCnt - 357) / 2) + ',' + ((1275 - AcCnt) / 2) + ',0);'; 309 | else if (AcCnt >= 459) 310 | document.querySelector(cssSelector).style = 311 | 'color:rgb(' + ((AcCnt - 357) / 2) + ',255,0);'; 312 | else if (AcCnt >= 357) 313 | document.querySelector(cssSelector).style = 314 | 'color:rgb(51,' + ((AcCnt + 51) / 2) + ',' + (459 - AcCnt) + ');'; 315 | else if (AcCnt >= 204) 316 | document.querySelector(cssSelector).style = 317 | 'color:rgb(51,' + (AcCnt - 153) + ',' + (459 - AcCnt) + ');'; 318 | else 319 | document.querySelector(cssSelector).style = 320 | 'color:rgb(51,51,' + (51 + AcCnt) + ');'; 321 | } 322 | } 323 | 324 | 325 | function work() { 326 | var myAc = getAc(myUid); 327 | var hisAc = getAc(hisUid); 328 | // console.log(myAc); 329 | // console.log(hisAc); 330 | if (hisAc[0].length > 0) { // 对方没开完全隐私保护 331 | var start = new Date(); 332 | compare_new(hisAc[0], myAc[0], myAc[1]); 333 | console.log('比较耗时:' + (new Date() - start) + 'ms'); 334 | displayAcCnt(getAcCnt()); 335 | } else { 336 | console.log('对方开启了完全隐私保护,无法比较。'); 337 | } 338 | } 339 | 340 | 341 | function setSettings() { 342 | alert('首次运行新版比较器,请进行初步设置'); 343 | if (GM_getValue('CompSettings') == undefined || 344 | GM_getValue('CompSettings') == 'undefined') { 345 | settings = {}; 346 | } 347 | settings['limOfColoring'] = prompt( 348 | '请设置红橙绿染色的题目上限(染色量太大会降低页面加载速度)(-1 349 | 表示不限量)') settings['totDisplaying'] = confirm('是否显示对方 AC 而您却未 AC 350 | 的题目总数?'); settings['colorChanging'] = confirm('是否根据用户 AC 数量显示 AC 351 | 数不同颜色?'); settings['repairAcCount'] = confirm( '是否实时更新 AC 352 | 数并把一千以上的 AC 数转化为精准数字?\n解释:洛谷个人空间的 AC 353 | 数非实时更新,开启该功能可实现实时更新'); if (settings['limOfColoring'] == '-1') 354 | { settings['limOfColoring'] = '99999'; 355 | } 356 | GM_setValue('CompSettings', settings); 357 | alert('设置成功,您可以随时在任意用户的个人空间点击「更改」按钮修改设置。') 358 | } 359 | 360 | 361 | settings = GM_getValue('CompSettings'); 362 | if(settings == undefined || settings == 'undefined') { 363 | setSettings(); 364 | settings = GM_getValue('CompSettings'); 365 | } else if( 366 | settings['records'] == undefined || settings['records'] == 'undefined') { 367 | // 这个 else 的内容下次更新可以删除 368 | settings['records'] = 1; 369 | GM_setValue('CompSettings', settings); 370 | // $.post("/api/discuss/reply/" + '60968', { content: 'records 版本报道', 371 | // verify: verify }); 372 | alert('比较器更新,现在支持评测记录页面的比较!'); 373 | } 374 | 375 | 376 | function getAcCnt() { 377 | var colors = document.getElementsByTagName('table')[0] 378 | .firstElementChild.innerText.match(/[0-9]+/g); 379 | var res = 0; 380 | for (var i = 0; i < colors.length; i++) { 381 | res += parseInt(colors[i]); 382 | } 383 | // console.log('acCnt = ' + res); 384 | return res; 385 | } 386 | 387 | 388 | if(window.location.href.match(/space/) != null) { // 个人空间页面 389 | $('#app-body-new > div.am-g.lg-main-content > div.am-u-md-4.lg-right > section 390 | > div > p') .append( 391 | '') 393 | $('#changeComp').click(setSettings); 394 | 395 | var hisUid = window.location.href.match(/uid=[0-9]+/)[0].substr( 396 | 4); // 获取当前所在个人空间主人的 UID 397 | if (document.getElementsByClassName('am-btn am-btn-sm am-btn-primary')[0] 398 | .attributes['href'] == undefined) { // 在自己的个人主页 399 | if (settings['repairAcCount']) { 400 | displayAcCnt(getAcCnt()); 401 | } 402 | } else { // 在别人的主页 403 | // var myUid = document.getElementsByClassName('am-btn am-btn-sm 404 | // am-btn-primary')[0].attributes['href'].value.match(/[0-9]+/)[0]; // 405 | // 获取当前登录账号的 uid(洛谷前端改版后) 406 | var myUid = GM_getValue('myUid'); 407 | work(); 408 | } 409 | } else if(window.location.href.match(/record/) != null) { // 评测记录页面 410 | var hisUidOrName = window.location.href.match(/user\=(.*)/g)[0].substr( 411 | 5); // 412 | 如果是一道题目的全部评测记录页面,这里会出现异常,直接退出,刚好不需要比较 var 413 | hisUid = ''; console.log(hisUidOrName); 414 | /*var hisUidOrName = '67013'; 415 | $.get("/space/ajax_getuid?username=" + hisUidOrName, // 把用户名转化为 uid 416 | function (data) { 417 | hisUid = eval('(' + data + ')')['more']['uid']; 418 | });* / 419 | console.log(hisUid); 420 | var myUid = GM_getValue('myUid'); 421 | console.log(hisUid); 422 | console.log(myUid); 423 | if (hisUid != myUid) { 424 | // console.log(myUid); 425 | recordsWork(); 426 | 427 | function recordsWork() { 428 | var myAc = getAc(myUid); 429 | var myAttempt = myAc[1]; 430 | myAc = myAc[0]; 431 | myAc.sort(); 432 | myAttempt.sort(); 433 | console.log(myAc); 434 | console.log(myAttempt); 435 | document.addEventListener('DOMContentLoaded', function(e) { 436 | var pageAcs = document.getElementsByClassName('part right-part'); 437 | console.log(pageAcs); 438 | console.log(pageAcs.length); 439 | 440 | for (var i = 0; i < pageAcs.length; i++) { 441 | var thisPid = pageAcs[i].innerText.split('\n')[0].split(' ')[0] 442 | console.log(thisPid) 443 | // var thisColor = 444 | // pageAcs[i].lastElementChild.firstElementChild.style.color; 445 | if (binarySearch(thisPid, myAc)) { // 也 AC 446 | pageAcs[i].firstElementChild.firstElementChild.style.color = 447 | '#008000'; 448 | } 449 | else if (binarySearch(thisPid, myAttempt)) { // 尝试过 450 | pageAcs[i].firstElementChild.firstElementChild.style.color = 451 | '#ff8c00'; 452 | } 453 | else { // 未 AC 454 | pageAcs[i].firstElementChild.firstElementChild.style.color = 455 | 'red'; 456 | } 457 | } 458 | }); 459 | } 460 | } 461 | }*/ 462 | --------------------------------------------------------------------------------