Greasy Fork 还支持 简体中文。

comfortable-yukicoder

yukicoder にいくつかの機能を追加します.主に動線を増やします.

目前為 2021-12-06 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name comfortable-yukicoder
  3. // @namespace iilj
  4. // @version 1.1.0
  5. // @description yukicoder にいくつかの機能を追加します.主に動線を増やします.
  6. // @author iilj
  7. // @license MIT
  8. // @supportURL https://github.com/iilj/comfortable-yukicoder/issues
  9. // @match https://yukicoder.me/contests/*
  10. // @match https://yukicoder.me/contests/*/*
  11. // @match https://yukicoder.me/problems/no/*
  12. // @match https://yukicoder.me/problems/*
  13. // @match https://yukicoder.me/submissions/*
  14. // @grant GM_addStyle
  15. // ==/UserScript==
  16. var css$1 = "div#js-cy-timer {\n position: fixed;\n right: 10px;\n bottom: 10px;\n width: 140px;\n height: 70px;\n margin: 0;\n text-align: center;\n line-height: 20px;\n font-size: 15px;\n z-index: 50;\n border: 7px solid #36353a;\n border-radius: 7px;\n background-color: #bdc4bd;\n padding: 8px 0;\n}";
  17.  
  18. const pad = (num, length = 2) => `00${num}`.slice(-length);
  19. const days = ['日', '月', '火', '水', '木', '金', '土'];
  20. const formatDate = (date, format = '%Y-%m-%d (%a) %H:%M:%S.%f %z') => {
  21. const offset = date.getTimezoneOffset();
  22. const offsetSign = offset < 0 ? '+' : '-';
  23. const offsetHours = Math.floor(Math.abs(offset) / 60);
  24. const offsetMinutes = Math.abs(offset) % 60;
  25. let ret = format.replace(/%Y/g, String(date.getFullYear()));
  26. ret = ret.replace(/%m/g, pad(date.getMonth() + 1));
  27. ret = ret.replace(/%d/g, pad(date.getDate()));
  28. ret = ret.replace(/%a/g, days[date.getDay()]);
  29. ret = ret.replace(/%H/g, pad(date.getHours()));
  30. ret = ret.replace(/%M/g, pad(date.getMinutes()));
  31. ret = ret.replace(/%S/g, pad(date.getSeconds()));
  32. ret = ret.replace(/%f/g, pad(date.getMilliseconds(), 3));
  33. ret = ret.replace(/%z/g, `${offsetSign}${pad(offsetHours)}:${pad(offsetMinutes)}`);
  34. return ret;
  35. };
  36. const formatTime = (hours, minutes, seconds) => {
  37. return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
  38. };
  39.  
  40. const diffMsToString = (diffMs) => {
  41. const diffWholeSecs = Math.ceil(diffMs / 1000);
  42. const diffSecs = diffWholeSecs % 60;
  43. const diffMinutes = Math.floor(diffWholeSecs / 60) % 60;
  44. const diffHours = Math.floor(diffWholeSecs / 3600) % 24;
  45. const diffDate = Math.floor(diffWholeSecs / (3600 * 24));
  46. const diffDateText = diffDate > 0 ? `${diffDate}日と` : '';
  47. return diffDateText + formatTime(diffHours, diffMinutes, diffSecs);
  48. };
  49. class Timer {
  50. constructor() {
  51. GM_addStyle(css$1);
  52. this.element = document.createElement('div');
  53. this.element.id = Timer.ELEMENT_ID;
  54. document.body.appendChild(this.element);
  55. this.top = document.createElement('div');
  56. this.element.appendChild(this.top);
  57. this.bottom = document.createElement('div');
  58. this.element.appendChild(this.bottom);
  59. this.prevSeconds = -1;
  60. this.startDate = undefined;
  61. this.endDate = undefined;
  62. this.intervalID = window.setInterval(() => {
  63. this.updateTime();
  64. }, 100);
  65. }
  66. updateTime() {
  67. const d = new Date();
  68. const seconds = d.getSeconds();
  69. if (seconds === this.prevSeconds)
  70. return;
  71. this.prevSeconds = seconds;
  72. if (this.startDate !== undefined && this.endDate !== undefined) {
  73. if (d < this.startDate) {
  74. this.top.textContent = '開始まであと';
  75. const diffMs = this.startDate.getTime() - d.getTime();
  76. this.bottom.textContent = diffMsToString(diffMs);
  77. }
  78. else if (d < this.endDate) {
  79. this.top.textContent = '残り時間';
  80. const diffMs = this.endDate.getTime() - d.getTime();
  81. this.bottom.textContent = diffMsToString(diffMs);
  82. }
  83. else {
  84. this.top.textContent = formatDate(d, '%Y-%m-%d (%a)');
  85. this.bottom.textContent = formatDate(d, '%H:%M:%S %z');
  86. }
  87. }
  88. else {
  89. this.top.textContent = formatDate(d, '%Y-%m-%d (%a)');
  90. this.bottom.textContent = formatDate(d, '%H:%M:%S %z');
  91. }
  92. }
  93. registerContest(contest) {
  94. this.startDate = new Date(contest.Date);
  95. this.endDate = new Date(contest.EndDate);
  96. }
  97. }
  98. Timer.ELEMENT_ID = 'js-cy-timer';
  99.  
  100. var css = "#toplinks > div#cy-tabs-container > a {\n position: relative;\n background: linear-gradient(to bottom, white 0%, #fff2f3 100%);\n}\n#toplinks > div#cy-tabs-container > a ul.js-cy-contest-problems-ul {\n margin: 0;\n padding: 0;\n list-style-type: none;\n overflow: hidden;\n position: absolute;\n left: 0;\n top: 33px;\n width: max-content;\n min-height: 0;\n height: 0;\n z-index: 3;\n transition: min-height 0.4s;\n}\n#toplinks > div#cy-tabs-container > a ul.js-cy-contest-problems-ul > li > a {\n width: 100%;\n height: 100%;\n display: block;\n margin: 0;\n padding: 0.3rem;\n padding-left: 0.6rem;\n padding-right: 0.6rem;\n font-size: 16px;\n color: #fff;\n line-height: 1.75;\n background-color: #428bca;\n}\n#toplinks > div#cy-tabs-container > a ul.js-cy-contest-problems-ul > li > a:hover {\n background-color: #3071a9;\n}\n#toplinks > div#cy-tabs-container > a:hover {\n opacity: 1;\n}\n#toplinks > div#cy-tabs-container > a:hover ul.js-cy-contest-problems-ul {\n height: auto;\n}";
  101.  
  102. const header = [
  103. 'A',
  104. 'B',
  105. 'C',
  106. 'D',
  107. 'E',
  108. 'F',
  109. 'G',
  110. 'H',
  111. 'I',
  112. 'J',
  113. 'K',
  114. 'L',
  115. 'M',
  116. 'N',
  117. 'O',
  118. 'P',
  119. 'Q',
  120. 'R',
  121. 'S',
  122. 'T',
  123. 'U',
  124. 'V',
  125. 'W',
  126. 'X',
  127. 'Y',
  128. 'Z',
  129. ];
  130. const getHeaderFromNum = (num) => {
  131. const idx = num - 1;
  132. if (idx < header.length) {
  133. return header[idx];
  134. }
  135. else {
  136. const r = idx % header.length;
  137. return getHeaderFromNum(Math.floor(idx / header.length)) + header[r];
  138. }
  139. };
  140. const getHeader = (idx) => getHeaderFromNum(idx + 1);
  141.  
  142. class TopLinksManager {
  143. constructor() {
  144. GM_addStyle(css);
  145. const toplinks = document.querySelector('div#toplinks');
  146. if (toplinks === null) {
  147. throw Error('div#toplinks が見つかりません');
  148. }
  149. this.tabContainer = document.createElement('div');
  150. this.tabContainer.classList.add('left');
  151. this.tabContainer.id = TopLinksManager.TAB_CONTAINER_ID;
  152. toplinks.insertAdjacentElement('beforeend', this.tabContainer);
  153. this.id2element = new Map();
  154. }
  155. initLink(txt, id, href = '#') {
  156. const newtab = document.createElement('a');
  157. newtab.innerText = txt;
  158. newtab.id = id;
  159. newtab.setAttribute('href', href);
  160. this.tabContainer.appendChild(newtab);
  161. this.id2element.set(id, newtab);
  162. return newtab;
  163. }
  164. confirmLink(id, href) {
  165. const tab = this.id2element.get(id);
  166. if (tab === undefined) {
  167. throw new Error(`不明な id: ${id}`);
  168. }
  169. tab.href = href;
  170. }
  171. initContestSubmissions() {
  172. this.initLink('自分の提出', TopLinksManager.ID_CONTEST_SUBMISSION);
  173. }
  174. confirmContestSubmissions(contestId) {
  175. this.confirmLink(TopLinksManager.ID_CONTEST_SUBMISSION, `/contests/${contestId}/submissions?my_submission=enabled`);
  176. }
  177. initContestProblems() {
  178. this.initLink('コンテスト問題一覧', TopLinksManager.ID_CONTEST);
  179. }
  180. confirmContestProblems(contestId, contestProblems) {
  181. this.confirmLink(TopLinksManager.ID_CONTEST, `/contests/${contestId}`);
  182. this.addContestProblems(contestProblems);
  183. }
  184. initContestLinks() {
  185. this.initContestProblems();
  186. this.initLink('コンテスト順位表', TopLinksManager.ID_CONTEST_TABLE);
  187. this.initContestSubmissions();
  188. }
  189. confirmContestLinks(contestId, contestProblems) {
  190. this.confirmLink(TopLinksManager.ID_CONTEST_TABLE, `/contests/${contestId}/table`);
  191. this.confirmContestSubmissions(contestId);
  192. this.confirmContestProblems(contestId, contestProblems);
  193. }
  194. addContestProblems(contestProblems) {
  195. const tab = this.id2element.get(TopLinksManager.ID_CONTEST);
  196. if (tab === undefined) {
  197. throw new Error(`id=${TopLinksManager.ID_CONTEST} の要素が追加される前に更新が要求されました`);
  198. }
  199. const ul = document.createElement('ul');
  200. ul.classList.add('js-cy-contest-problems-ul');
  201. console.log(contestProblems);
  202. contestProblems.forEach((problem, index) => {
  203. console.log(problem);
  204. const li = document.createElement('li');
  205. const link = document.createElement('a');
  206. const header = getHeader(index);
  207. link.textContent = `${header} - ${problem.Title}`;
  208. if (problem.No !== null) {
  209. link.href = `/problems/no/${problem.No}`;
  210. }
  211. else {
  212. link.href = `/problems/${problem.ProblemId}`;
  213. }
  214. li.appendChild(link);
  215. ul.appendChild(li);
  216. });
  217. // add caret
  218. const caret = document.createElement('span');
  219. caret.classList.add('caret');
  220. tab.appendChild(caret);
  221. tab.insertAdjacentElement('beforeend', ul);
  222. }
  223. confirmWithoutContest(problem) {
  224. [TopLinksManager.ID_CONTEST, TopLinksManager.ID_CONTEST_TABLE].forEach((id) => {
  225. const tab = this.id2element.get(id);
  226. if (tab !== undefined)
  227. tab.remove();
  228. });
  229. // https://yukicoder.me/problems/no/5000/submissions?my_submission=enabled
  230. if (problem.No !== null) {
  231. this.confirmLink(TopLinksManager.ID_CONTEST_SUBMISSION, `/problems/no/${problem.No}/submissions?my_submission=enabled`);
  232. }
  233. else {
  234. this.confirmLink(TopLinksManager.ID_CONTEST_SUBMISSION, `/problems/${problem.ProblemId}/submissions?my_submission=enabled`);
  235. }
  236. }
  237. }
  238. TopLinksManager.TAB_CONTAINER_ID = 'cy-tabs-container';
  239. TopLinksManager.ID_CONTEST = 'js-cy-contest';
  240. TopLinksManager.ID_CONTEST_TABLE = 'js-cy-contest-table';
  241. TopLinksManager.ID_CONTEST_SUBMISSION = 'js-cy-contest-submissions';
  242.  
  243. const onContestPage = async (contestId, APIClient) => {
  244. const toplinksManager = new TopLinksManager();
  245. toplinksManager.initContestSubmissions();
  246. toplinksManager.confirmContestSubmissions(contestId);
  247. const timer = new Timer();
  248. const contest = await APIClient.fetchContestById(contestId);
  249. timer.registerContest(contest);
  250. };
  251.  
  252. const getContestProblems = (contest, problems) => {
  253. const pid2problem = new Map();
  254. problems.forEach((problem) => {
  255. pid2problem.set(problem.ProblemId, problem);
  256. });
  257. const contestProblems = contest.ProblemIdList.map((problemId) => {
  258. const problem = pid2problem.get(problemId);
  259. if (problem !== undefined)
  260. return problem;
  261. return {
  262. No: null,
  263. ProblemId: problemId,
  264. Title: '',
  265. AuthorId: -1,
  266. TesterId: -1,
  267. TesterIds: '',
  268. Level: 0,
  269. ProblemType: 0,
  270. Tags: '',
  271. Date: null,
  272. Statistics: {
  273. //
  274. },
  275. };
  276. });
  277. return contestProblems;
  278. };
  279. const anchorToUserID = (anchor) => {
  280. const userLnkMatchArray = /^https:\/\/yukicoder\.me\/users\/(\d+)/.exec(anchor.href);
  281. if (userLnkMatchArray === null)
  282. return -1;
  283. const userId = Number(userLnkMatchArray[1]);
  284. return userId;
  285. };
  286. const getYourUserId = () => {
  287. const yourIdLnk = document.querySelector('#header #usermenu-btn');
  288. if (yourIdLnk === null)
  289. return -1; // ログインしていない場合
  290. return anchorToUserID(yourIdLnk);
  291. };
  292.  
  293. const onLeaderboardPage = async (contestId, APIClient) => {
  294. const myRankTableRow = document.querySelector('table.table tbody tr.my_rank');
  295. if (myRankTableRow !== null) {
  296. const myRankTableRowCloned = myRankTableRow.cloneNode(true);
  297. const tbody = document.querySelector('table.table tbody');
  298. if (tbody === null) {
  299. throw new Error('順位表テーブルが見つかりません');
  300. }
  301. tbody.insertAdjacentElement('afterbegin', myRankTableRowCloned);
  302. // const myRankTableFirstRow: HTMLTableRowElement | null =
  303. // document.querySelector<HTMLTableRowElement>('table.table tbody tr.my_rank');
  304. // myRankTableFirstRow.style.borderBottom = '2px solid #ddd';
  305. myRankTableRowCloned.style.borderBottom = '2px solid #ddd';
  306. }
  307. const toplinksManager = new TopLinksManager();
  308. toplinksManager.initContestProblems();
  309. toplinksManager.initContestSubmissions();
  310. toplinksManager.confirmContestSubmissions(contestId);
  311. const timer = new Timer();
  312. const [problems, contest] = await Promise.all([APIClient.fetchProblems(), APIClient.fetchContestById(contestId)]);
  313. timer.registerContest(contest);
  314. const contestProblems = getContestProblems(contest, problems);
  315. toplinksManager.confirmContestProblems(contest.Id, contestProblems);
  316. };
  317.  
  318. const createCard = () => {
  319. const newdiv = document.createElement('div');
  320. // styling newdiv
  321. newdiv.style.display = 'inline-block';
  322. newdiv.style.borderRadius = '2px';
  323. newdiv.style.padding = '10px';
  324. newdiv.style.margin = '10px 0px';
  325. newdiv.style.border = '1px solid rgb(59, 173, 214)';
  326. newdiv.style.backgroundColor = 'rgba(120, 197, 231, 0.1)';
  327. const newdivWrapper = document.createElement('div');
  328. newdivWrapper.appendChild(newdiv);
  329. return [newdiv, newdivWrapper];
  330. };
  331.  
  332. class ContestInfoCard {
  333. constructor(isProblemPage = true) {
  334. this.isProblemPage = isProblemPage;
  335. const [card, cardWrapper] = createCard();
  336. this.card = card;
  337. {
  338. // create newdiv
  339. this.contestDiv = document.createElement('div');
  340. // add contest info
  341. this.contestLnk = document.createElement('a');
  342. this.contestLnk.innerText = '(fetching contest info...)';
  343. this.contestLnk.href = '#';
  344. this.contestDiv.appendChild(this.contestLnk);
  345. this.contestSuffix = document.createTextNode(` (id=---)`);
  346. this.contestDiv.appendChild(this.contestSuffix);
  347. // add problem info
  348. if (isProblemPage) {
  349. const space = document.createTextNode(` `);
  350. this.contestDiv.appendChild(space);
  351. this.problemLnk = document.createElement('a');
  352. this.problemLnk.innerText = '#?';
  353. this.problemLnk.href = '#';
  354. this.contestDiv.appendChild(this.problemLnk);
  355. this.problemSuffix = document.createTextNode(' (No.---)');
  356. this.contestDiv.appendChild(this.problemSuffix);
  357. }
  358. this.dateDiv = document.createElement('div');
  359. this.dateDiv.textContent = 'xxxx-xx-xx xx:xx:xx 〜 xxxx-xx-xx xx:xx:xx';
  360. // newdiv.innerText = `${contest.Name} (id=${contest.Id}) #${label} (No.${problem.No})`;
  361. card.appendChild(this.contestDiv);
  362. card.appendChild(this.dateDiv);
  363. if (isProblemPage) {
  364. this.prevNextProblemLinks = document.createElement('div');
  365. this.prevNextProblemLinks.textContent = '(情報取得中)';
  366. card.appendChild(this.prevNextProblemLinks);
  367. }
  368. }
  369. const content = document.querySelector('div#content');
  370. if (content === null) {
  371. throw new Error('div#content が見つかりませんでした');
  372. }
  373. content.insertAdjacentElement('afterbegin', cardWrapper);
  374. }
  375. confirmContest(contest) {
  376. this.contestLnk.innerText = `${contest.Name}`;
  377. this.contestLnk.href = `/contests/${contest.Id}`;
  378. this.contestSuffix.textContent = ` (id=${contest.Id})`;
  379. const format = '%Y-%m-%d (%a) %H:%M:%S';
  380. const start = formatDate(new Date(contest.Date), format);
  381. const end = formatDate(new Date(contest.EndDate), format);
  382. this.dateDiv.textContent = `${start} ${end}`;
  383. }
  384. confirmContestAndProblem(contest, problem, suffix = '') {
  385. this.confirmContest(contest);
  386. if (this.isProblemPage) {
  387. if (this.prevNextProblemLinks === undefined) {
  388. throw new ErrorEvent('prevNextProblemLinks が undefined です');
  389. }
  390. if (this.problemLnk === undefined) {
  391. throw new ErrorEvent('problemLnk が undefined です');
  392. }
  393. if (this.problemSuffix === undefined) {
  394. throw new ErrorEvent('problemSuffix が undefined です');
  395. }
  396. const idx = contest.ProblemIdList.findIndex((problemId) => problemId === problem.ProblemId);
  397. const label = getHeader(idx);
  398. this.problemLnk.innerText = `#${label}`;
  399. if (problem.No !== null) {
  400. this.problemLnk.href = `/problems/no/${problem.No}`;
  401. this.problemSuffix.textContent = ` (No.${problem.No})`;
  402. }
  403. else {
  404. this.problemLnk.href = `/problems/${problem.ProblemId}`;
  405. }
  406. this.prevNextProblemLinks.textContent = ' / ';
  407. if (idx > 0) {
  408. // prev
  409. const lnk = document.createElement('a');
  410. lnk.innerText = `←前の問題 (#${getHeader(idx - 1)})`;
  411. lnk.href = `/problems/${contest.ProblemIdList[idx - 1]}${suffix}`;
  412. this.prevNextProblemLinks.insertAdjacentElement('afterbegin', lnk);
  413. }
  414. if (idx + 1 < contest.ProblemIdList.length) {
  415. // next
  416. const lnk = document.createElement('a');
  417. lnk.innerText = `次の問題 (#${getHeader(idx + 1)})→`;
  418. lnk.href = `/problems/${contest.ProblemIdList[idx + 1]}${suffix}`;
  419. this.prevNextProblemLinks.insertAdjacentElement('beforeend', lnk);
  420. }
  421. }
  422. }
  423. confirmContestIsNotFound() {
  424. var _a, _b;
  425. this.contestLnk.remove();
  426. this.contestSuffix.remove();
  427. (_a = this.problemLnk) === null || _a === void 0 ? void 0 : _a.remove();
  428. (_b = this.problemSuffix) === null || _b === void 0 ? void 0 : _b.remove();
  429. this.dateDiv.remove();
  430. if (this.prevNextProblemLinks !== undefined) {
  431. this.prevNextProblemLinks.textContent = '(どのコンテストにも属さない問題です)';
  432. }
  433. }
  434. onProblemFetchFailed() {
  435. this.contestLnk.innerText = '???';
  436. if (this.prevNextProblemLinks !== undefined) {
  437. this.prevNextProblemLinks.textContent = '(情報が取得できませんでした)';
  438. }
  439. }
  440. }
  441.  
  442. const onProblemPage = async (fetchProblem, suffix, APIClient) => {
  443. const toplinksManager = new TopLinksManager();
  444. toplinksManager.initContestLinks();
  445. const contestInfoCard = new ContestInfoCard();
  446. const timer = new Timer();
  447. try {
  448. const [problem, problems, currentContest, pastContest, futureContests] = await Promise.all([
  449. fetchProblem(),
  450. APIClient.fetchProblems(),
  451. APIClient.fetchCurrentContests(),
  452. APIClient.fetchPastContests(),
  453. APIClient.fetchFutureContests(),
  454. ]);
  455. const contests = currentContest.concat(pastContest);
  456. let contest = contests.find((contest) => contest.ProblemIdList.includes(problem.ProblemId));
  457. if (contest === undefined) {
  458. // 未来のコンテストから探してみる
  459. if (problem.ProblemId !== undefined) {
  460. const futureContest = futureContests.find((contest) => contest.ProblemIdList.includes(problem.ProblemId));
  461. if (futureContest !== undefined) {
  462. contest = futureContest;
  463. // print contest info
  464. // contestInfoCard.confirmContestAndProblem(futureContest, problem, suffix);
  465. // return null;
  466. }
  467. else {
  468. contestInfoCard.confirmContestIsNotFound();
  469. toplinksManager.confirmWithoutContest(problem);
  470. return null;
  471. }
  472. }
  473. else {
  474. contestInfoCard.confirmContestIsNotFound();
  475. toplinksManager.confirmWithoutContest(problem);
  476. return null;
  477. }
  478. }
  479. const contestProblems = getContestProblems(contest, problems);
  480. // print contest info
  481. contestInfoCard.confirmContestAndProblem(contest, problem, suffix);
  482. // add tabs
  483. toplinksManager.confirmContestLinks(contest.Id, contestProblems);
  484. timer.registerContest(contest);
  485. return problem;
  486. }
  487. catch (error) {
  488. contestInfoCard.onProblemFetchFailed();
  489. return null;
  490. }
  491. };
  492. const onProblemPageByNo = async (problemNo, suffix, APIClient) => {
  493. return onProblemPage(() => APIClient.fetchProblemByNo(problemNo), suffix, APIClient);
  494. };
  495. const onProblemPageById = async (problemId, suffix, APIClient) => {
  496. return onProblemPage(() => APIClient.fetchProblemById(problemId), suffix, APIClient);
  497. };
  498. const colorScoreRow = (row, authorId, testerIds, yourId) => {
  499. const userLnk = row.querySelector('td.table_username a');
  500. if (userLnk === null) {
  501. throw new Error('テーブル行内にユーザへのリンクが見つかりませんでした');
  502. }
  503. const userId = anchorToUserID(userLnk);
  504. if (userId === -1)
  505. return;
  506. if (userId === authorId) {
  507. row.style.backgroundColor = 'honeydew';
  508. const label = document.createElement('div');
  509. label.textContent = '[作問者]';
  510. userLnk.insertAdjacentElement('afterend', label);
  511. }
  512. else if (testerIds.includes(userId)) {
  513. row.style.backgroundColor = 'honeydew';
  514. const label = document.createElement('div');
  515. label.textContent = '[テスター]';
  516. userLnk.insertAdjacentElement('afterend', label);
  517. }
  518. if (userId === yourId) {
  519. row.style.backgroundColor = 'aliceblue';
  520. const label = document.createElement('div');
  521. label.textContent = '[あなた]';
  522. userLnk.insertAdjacentElement('afterend', label);
  523. }
  524. };
  525. const onProblemScorePage = (problem) => {
  526. const yourId = getYourUserId();
  527. const testerIds = problem.TesterIds.split(',').map((testerIdString) => Number(testerIdString));
  528. const rows = document.querySelectorAll('table.table tbody tr');
  529. rows.forEach((row) => {
  530. colorScoreRow(row, problem.AuthorId, testerIds, yourId);
  531. });
  532. };
  533.  
  534. const colorSubmissionRow = (row, authorId, testerIds, yourId) => {
  535. const userLnk = row.querySelector('td.table_username a');
  536. if (userLnk === null) {
  537. throw new Error('テーブル行内にユーザへのリンクが見つかりませんでした');
  538. }
  539. const userId = anchorToUserID(userLnk);
  540. if (userId === -1)
  541. return;
  542. if (userId === authorId) {
  543. row.style.backgroundColor = 'honeydew';
  544. const label = document.createElement('div');
  545. label.textContent = '[作問者]';
  546. userLnk.insertAdjacentElement('afterend', label);
  547. }
  548. else if (testerIds.includes(userId)) {
  549. row.style.backgroundColor = 'honeydew';
  550. const label = document.createElement('div');
  551. label.textContent = '[テスター]';
  552. userLnk.insertAdjacentElement('afterend', label);
  553. }
  554. if (userId === yourId) {
  555. row.style.backgroundColor = 'aliceblue';
  556. const label = document.createElement('div');
  557. label.textContent = '[あなた]';
  558. userLnk.insertAdjacentElement('afterend', label);
  559. }
  560. };
  561. const onProblemSubmissionsPage = (problem) => {
  562. const yourId = getYourUserId();
  563. const testerIds = problem.TesterIds.split(',').map((testerIdString) => Number(testerIdString));
  564. const rows = document.querySelectorAll('table.table tbody tr');
  565. rows.forEach((row) => {
  566. colorSubmissionRow(row, problem.AuthorId, testerIds, yourId);
  567. });
  568. };
  569. const onContestSubmissionsPage = async (contestId, APIClient) => {
  570. const toplinksManager = new TopLinksManager();
  571. toplinksManager.initContestProblems();
  572. toplinksManager.initContestSubmissions();
  573. const contestInfoCard = new ContestInfoCard(false);
  574. const yourId = getYourUserId();
  575. const [contest, problems] = await Promise.all([APIClient.fetchContestById(contestId), APIClient.fetchProblems()]);
  576. // print contest info
  577. contestInfoCard.confirmContest(contest);
  578. // add tabs
  579. const contestProblems = getContestProblems(contest, problems);
  580. toplinksManager.confirmContestProblems(contest.Id, contestProblems);
  581. toplinksManager.confirmContestSubmissions(contest.Id);
  582. const problemId2Label = contest.ProblemIdList.reduce((curMap, problemId, idx) => curMap.set(problemId, getHeader(idx)), new Map());
  583. const problemNo2ProblemMap = problems.reduce((curMap, problem) => {
  584. if (problem.No !== null)
  585. curMap.set(problem.No, problem);
  586. return curMap;
  587. }, new Map());
  588. // collect problemNos
  589. const rows = document.querySelectorAll('table.table tbody tr');
  590. for (let i = 0; i < rows.length; i++) {
  591. const row = rows[i];
  592. // add label to each problem link
  593. const lnk = row.querySelector('td a[href^="/problems/no/"]');
  594. if (lnk === null) {
  595. throw new Error('テーブル行内に問題へのリンクが見つかりませんでした');
  596. }
  597. const contestSubmissionsPageProblemLnkMatchArray = /^https:\/\/yukicoder\.me\/problems\/no\/(\d+)/.exec(lnk.href);
  598. if (contestSubmissionsPageProblemLnkMatchArray === null) {
  599. throw new Error('テーブル行内に含まれる問題リンク先が不正です');
  600. }
  601. const problemNo = Number(contestSubmissionsPageProblemLnkMatchArray[1]);
  602. if (!problemNo2ProblemMap.has(problemNo)) {
  603. try {
  604. const problem = await APIClient.fetchProblemByNo(problemNo);
  605. problemNo2ProblemMap.set(problemNo, problem);
  606. }
  607. catch (error) {
  608. problemNo2ProblemMap.set(problemNo, null);
  609. }
  610. }
  611. const problem = problemNo2ProblemMap.get(problemNo);
  612. if (problem === null || problem === undefined)
  613. return;
  614. const label = problemId2Label.get(problem.ProblemId);
  615. if (label !== undefined)
  616. lnk.insertAdjacentText('afterbegin', `#${label} `);
  617. // color authors and testers
  618. const testerIds = problem.TesterIds.split(',').map((testerIdString) => Number(testerIdString));
  619. colorSubmissionRow(row, problem.AuthorId, testerIds, yourId);
  620. }
  621. };
  622.  
  623. const SUBMISSION_STATUSES = ['AC', 'WA', 'TLE', '--', 'MLE', 'OLE', 'QLE', 'RE', 'CE', 'IE'];
  624. const stringToStatus = (resultText) => {
  625. for (let i = 0; i < SUBMISSION_STATUSES.length; ++i) {
  626. if (SUBMISSION_STATUSES[i] == resultText)
  627. return SUBMISSION_STATUSES[i];
  628. }
  629. throw new Error(`未知のジャッジステータスです: ${resultText}`);
  630. };
  631. const onSubmissionResultPage = async (APIClient) => {
  632. const toplinksManager = new TopLinksManager();
  633. const contestInfoCard = new ContestInfoCard();
  634. const [resultCard, resultCardWrapper] = createCard();
  635. {
  636. // count
  637. const resultCountMap = SUBMISSION_STATUSES.reduce((prevMap, label) => prevMap.set(label, 0), new Map());
  638. // ジャッジ中(提出直後)は,このテーブルは存在しない
  639. const testTable = document.getElementById('test_table');
  640. if (testTable !== null) {
  641. const results = testTable.querySelectorAll('tbody tr td span.label');
  642. results.forEach((span) => {
  643. var _a;
  644. const resultText = span.textContent;
  645. if (resultText === null) {
  646. throw new Error('ジャッジ結果テキストが空です');
  647. }
  648. const resultLabel = stringToStatus(resultText.trim());
  649. const cnt = (_a = resultCountMap.get(resultLabel)) !== null && _a !== void 0 ? _a : 0;
  650. resultCountMap.set(resultLabel, cnt + 1);
  651. });
  652. }
  653. const content = document.querySelector('div#testcase_table h4');
  654. // 提出直後,ジャッジ中は null
  655. if (content !== null) {
  656. content.insertAdjacentElement('afterend', resultCardWrapper);
  657. // print result
  658. const addResultRow = (cnt, label) => {
  659. const resultEntry = document.createElement('div');
  660. const labelSpan = document.createElement('span');
  661. labelSpan.textContent = label;
  662. labelSpan.classList.add('label');
  663. labelSpan.classList.add(label === 'AC' ? 'label-success' : label === 'IE' ? 'label-danger' : 'label-warning');
  664. resultEntry.appendChild(labelSpan);
  665. const countSpan = document.createTextNode(` × ${cnt}`);
  666. resultEntry.appendChild(countSpan);
  667. resultCard.appendChild(resultEntry);
  668. };
  669. resultCountMap.forEach((cnt, label) => {
  670. if (cnt > 0)
  671. addResultRow(cnt, label);
  672. });
  673. }
  674. }
  675. const lnk = document.querySelector('div#content a[href^="/problems/no/"]');
  676. if (lnk === null) {
  677. throw new Error('結果ページ中に問題ページへのリンクが見つかりませんでした');
  678. }
  679. toplinksManager.initLink('問題', 'js-cy-problem', lnk.href);
  680. toplinksManager.initContestLinks();
  681. const submissionPageProblemLnkMatchArray = /^https:\/\/yukicoder\.me\/problems\/no\/(\d+)/.exec(lnk.href);
  682. if (submissionPageProblemLnkMatchArray === null) {
  683. throw new Error('結果ページに含まれる問題ページへのリンク先が不正です');
  684. }
  685. // get problems/contests info
  686. const problemNo = Number(submissionPageProblemLnkMatchArray[1]);
  687. const [problem, problems, currentContest, pastContest] = await Promise.all([
  688. APIClient.fetchProblemByNo(problemNo),
  689. APIClient.fetchProblems(),
  690. APIClient.fetchCurrentContests(),
  691. APIClient.fetchPastContests(),
  692. ]);
  693. const contests = currentContest.concat(pastContest);
  694. const contest = contests.find((contest) => contest.ProblemIdList.includes(problem.ProblemId));
  695. // add tabs
  696. if (contest !== undefined) {
  697. const contestProblems = getContestProblems(contest, problems);
  698. toplinksManager.confirmContestLinks(contest.Id, contestProblems);
  699. // print contest info
  700. contestInfoCard.confirmContestAndProblem(contest, problem);
  701. }
  702. };
  703.  
  704. const BASE_URL = 'https://yukicoder.me';
  705. const STATIC_API_BASE_URL = `${BASE_URL}/api/v1`;
  706. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  707. const assertResultIsValid = (obj) => {
  708. if ('Message' in obj)
  709. throw new Error(obj.Message);
  710. };
  711. const fetchJson = async (url) => {
  712. const res = await fetch(url);
  713. if (!res.ok) {
  714. throw new Error(res.statusText);
  715. }
  716. const obj = (await res.json());
  717. assertResultIsValid(obj);
  718. return obj;
  719. };
  720. // TODO pid/no->contest, の変換も受け持つほうが良い?(html 解析絡みをこのクラスに隠蔽できる)
  721. // 「現在のコンテスト」
  722. class CachedAPIClient {
  723. constructor() {
  724. this.pastContestsMap = new Map();
  725. this.currentContestsMap = new Map();
  726. this.futureContestsMap = new Map();
  727. this.problemsMapById = new Map();
  728. this.problemsMapByNo = new Map();
  729. }
  730. async fetchPastContests() {
  731. if (this.pastContests === undefined) {
  732. this.pastContests = await fetchJson(`${STATIC_API_BASE_URL}/contest/past`);
  733. this.pastContests.forEach((contest) => {
  734. if (!this.pastContestsMap.has(contest.Id))
  735. this.pastContestsMap.set(contest.Id, contest);
  736. });
  737. }
  738. return this.pastContests;
  739. }
  740. async fetchCurrentContests() {
  741. if (this.currentContests === undefined) {
  742. this.currentContests = await fetchJson(`${STATIC_API_BASE_URL}/contest/current`);
  743. this.currentContests.forEach((contest) => {
  744. if (!this.currentContestsMap.has(contest.Id))
  745. this.currentContestsMap.set(contest.Id, contest);
  746. });
  747. }
  748. return this.currentContests;
  749. }
  750. async fetchFutureContests() {
  751. if (this.futureContests === undefined) {
  752. this.futureContests = await fetchJson(`${STATIC_API_BASE_URL}/contest/future`);
  753. this.futureContests.forEach((contest) => {
  754. if (!this.futureContestsMap.has(contest.Id))
  755. this.futureContestsMap.set(contest.Id, contest);
  756. });
  757. }
  758. return this.futureContests;
  759. }
  760. async fetchContestById(contestId) {
  761. if (this.pastContestsMap.has(contestId)) {
  762. return this.pastContestsMap.get(contestId);
  763. }
  764. if (this.currentContestsMap.has(contestId)) {
  765. return this.currentContestsMap.get(contestId);
  766. }
  767. if (this.futureContestsMap.has(contestId)) {
  768. return this.futureContestsMap.get(contestId);
  769. }
  770. const contest = await fetchJson(`${STATIC_API_BASE_URL}/contest/id/${contestId}`);
  771. const currentDate = new Date();
  772. const startDate = new Date(contest.Date);
  773. const endDate = new Date(contest.EndDate);
  774. if (currentDate > endDate) {
  775. this.pastContestsMap.set(contestId, contest);
  776. }
  777. else if (currentDate > startDate) {
  778. this.currentContestsMap.set(contestId, contest);
  779. }
  780. return contest;
  781. }
  782. async fetchProblems() {
  783. if (this.problems === undefined) {
  784. this.problems = await fetchJson(`${STATIC_API_BASE_URL}/problems`);
  785. this.problems.forEach((problem) => {
  786. if (!this.problemsMapById.has(problem.ProblemId))
  787. this.problemsMapById.set(problem.ProblemId, problem);
  788. if (problem.No !== null && !this.problemsMapByNo.has(problem.No))
  789. this.problemsMapByNo.set(problem.No, problem);
  790. });
  791. }
  792. return this.problems;
  793. }
  794. async fetchProblemById(problemId) {
  795. if (this.problemsMapById.has(problemId)) {
  796. return this.problemsMapById.get(problemId);
  797. }
  798. try {
  799. const problem = await fetchJson(`${STATIC_API_BASE_URL}/problems/${problemId}`);
  800. this.problemsMapById.set(problem.ProblemId, problem);
  801. if (problem.No !== null)
  802. this.problemsMapByNo.set(problem.No, problem);
  803. return problem;
  804. }
  805. catch (_a) {
  806. await this.fetchProblems();
  807. if (this.problemsMapById.has(problemId)) {
  808. return this.problemsMapById.get(problemId);
  809. }
  810. // 問題一覧には載っていない -> 未来のコンテストの問題
  811. // ProblemId なので,未来のコンテスト一覧に載っている pid リストから,
  812. // コンテストは特定可能.
  813. return { ProblemId: problemId, No: null };
  814. }
  815. }
  816. async fetchProblemByNo(problemNo) {
  817. if (this.problemsMapByNo.has(problemNo)) {
  818. return this.problemsMapByNo.get(problemNo);
  819. }
  820. try {
  821. const problem = await fetchJson(`${STATIC_API_BASE_URL}/problems/no/${problemNo}`);
  822. this.problemsMapById.set(problem.ProblemId, problem);
  823. if (problem.No !== null)
  824. this.problemsMapByNo.set(problem.No, problem);
  825. return problem;
  826. }
  827. catch (_a) {
  828. await this.fetchProblems();
  829. if (this.problemsMapByNo.has(problemNo)) {
  830. return this.problemsMapByNo.get(problemNo);
  831. }
  832. // 問題一覧には載っていない -> 未来のコンテストの問題
  833. return { No: problemNo };
  834. }
  835. }
  836. }
  837.  
  838. void (async () => {
  839. const href = location.href;
  840. const hrefMatchArray = /^https:\/\/yukicoder\.me(.+)/.exec(href);
  841. if (hrefMatchArray === null)
  842. return;
  843. const path = hrefMatchArray[1];
  844. const APIClient = new CachedAPIClient();
  845. // on problem page (ProblemNo)
  846. // e.g. https://yukicoder.me/problems/no/1313
  847. const problemPageMatchArray = /^\/problems\/no\/(\d+)(.*)/.exec(path);
  848. if (problemPageMatchArray !== null) {
  849. // get contest info
  850. const problemNo = Number(problemPageMatchArray[1]);
  851. const suffix = problemPageMatchArray[2];
  852. const problem = await onProblemPageByNo(problemNo, suffix, APIClient);
  853. if (problem === null)
  854. return;
  855. const problemSubmissionsPageMatchArray = /^\/problems\/no\/(\d+)\/submissions/.exec(path);
  856. if (problemSubmissionsPageMatchArray !== null) {
  857. onProblemSubmissionsPage(problem);
  858. }
  859. // on problem score page (ProblemNo)
  860. // e.g. https://yukicoder.me/problems/no/5004/score
  861. const problemScorePageMatchArray = /^\/problems\/no\/(\d+)\/score(.*)/.exec(path);
  862. if (problemScorePageMatchArray !== null) {
  863. onProblemScorePage(problem);
  864. }
  865. return;
  866. }
  867. // on problem page (ProblemId)
  868. // e.g. https://yukicoder.me/problems/5191
  869. const problemPageByIdMatchArray = /^\/problems\/(\d+)(.*)/.exec(path);
  870. if (problemPageByIdMatchArray !== null) {
  871. // get contest info
  872. const problemId = Number(problemPageByIdMatchArray[1]);
  873. const suffix = problemPageByIdMatchArray[2];
  874. const problem = await onProblemPageById(problemId, suffix, APIClient);
  875. if (problem === null)
  876. return;
  877. const problemSubmissionsPageMatchArray = /^\/problems\/(\d+)\/submissions/.exec(path);
  878. if (problemSubmissionsPageMatchArray !== null) {
  879. onProblemSubmissionsPage(problem);
  880. }
  881. return;
  882. }
  883. // on contest submissions page / statistics page
  884. // e.g. https://yukicoder.me/contests/300/submissions, https://yukicoder.me/contests/300/statistics
  885. const contestSubmissionsPageMatchArray = /^\/contests\/(\d+)\/(submissions|statistics)/.exec(path);
  886. if (contestSubmissionsPageMatchArray !== null) {
  887. const contestId = Number(contestSubmissionsPageMatchArray[1]);
  888. await onContestSubmissionsPage(contestId, APIClient);
  889. return;
  890. }
  891. // on submission result page
  892. // e.g. https://yukicoder.me/submissions/591424
  893. const submissionPageMatchArray = /^\/submissions\/\d+/.exec(path);
  894. if (submissionPageMatchArray !== null) {
  895. await onSubmissionResultPage(APIClient);
  896. return;
  897. }
  898. // on contest leaderboard page
  899. // e.g. https://yukicoder.me/contests/300/table
  900. const leaderboardPageMatchArray = /^\/contests\/(\d+)\/(table|all)/.exec(path);
  901. if (leaderboardPageMatchArray !== null) {
  902. const contestId = Number(leaderboardPageMatchArray[1]);
  903. await onLeaderboardPage(contestId, APIClient);
  904. return;
  905. }
  906. // on contest problem list page
  907. // e.g. https://yukicoder.me/contests/300
  908. const contestPageMatchArray = /^\/contests\/(\d+)$/.exec(path);
  909. if (contestPageMatchArray !== null) {
  910. const contestId = Number(contestPageMatchArray[1]);
  911. await onContestPage(contestId, APIClient);
  912. return;
  913. }
  914. })();