您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AtCoder Problemsの難易度を表示します。
// ==UserScript== // @name atcoder-difficulty-display // @namespace https://github.com/hotarunw // @version 2.0.1 // @description AtCoder Problemsの難易度を表示します。 // @author hotaru-n // @license MIT // @supportURL https://github.com/hotarunw/atcoder-difficulty-display/issues // @match https://atcoder.jp/contests/* // @exclude https://atcoder.jp/contests/ // @match https://atcoder.jp/settings // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @require https://greasyforks.org/scripts/437862-atcoder-problems-api/code/atcoder-problems-api.js?version=1004589 // ==/UserScript== const nonPenaltyJudge = ["AC", "CE", "IE", "WJ", "WR"]; /** 設定 ネタバレ防止のID, Key */ const hideDifficultyID = "hide-difficulty-atcoder-difficulty-display"; /** * 後方互換処理 */ const backwardCompatibleProcessing = () => { const oldLocalStorageKeys = [ "atcoderDifficultyDisplayUserSubmissions", "atcoderDifficultyDisplayUserSubmissionslastFetchedAt", "atcoderDifficultyDisplayEstimatedDifficulties", "atcoderDifficultyDisplayEstimatedDifficultieslastFetchedAt", ]; /** 過去バージョンのlocalStorageデータを削除する */ oldLocalStorageKeys.forEach((key) => { localStorage.removeItem(key); }); }; const getTypical90Difficulty = (title) => { if (title.includes("★1")) return 149; if (title.includes("★2")) return 399; if (title.includes("★3")) return 799; if (title.includes("★4")) return 1199; if (title.includes("★5")) return 1599; if (title.includes("★6")) return 1999; if (title.includes("★7")) return 2399; return NaN; }; const getTypical90Description = (title) => { if (title.includes("★1")) return "200 点問題レベル"; if (title.includes("★2")) return "300 点問題レベル"; if (title.includes("★3")) return ""; if (title.includes("★4")) return "400 点問題レベル"; if (title.includes("★5")) return "500 点問題レベル"; if (title.includes("★6")) return "これが安定して解ければ上級者です"; if (title.includes("★7")) return "チャレンジ問題枠です"; return "エラー: 競プロ典型 90 問の難易度読み取りに失敗しました"; }; const addTypical90Difficulty = (problemModels, problems) => { const models = problemModels; const problemsT90 = problems.filter((element) => element.contest_id === "typical90"); problemsT90.forEach((element) => { const difficulty = getTypical90Difficulty(element.title); const model = { slope: NaN, intercept: NaN, variance: NaN, difficulty, discrimination: NaN, irt_loglikelihood: NaN, irt_users: NaN, is_experimental: false, extra_difficulty: `${getTypical90Description(element.title)}`, }; models[element.id] = model; }); return models; }; // 次のコードを引用 // [AtCoderProblems/theme\.ts at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/style/theme.ts) // 8b1b86c740e627e59abf056a11c00582e12b30ff const ThemeLight = { difficultyBlackColor: "#404040", difficultyGreyColor: "#808080", difficultyBrownColor: "#804000", difficultyGreenColor: "#008000", difficultyCyanColor: "#00C0C0", difficultyBlueColor: "#0000FF", difficultyYellowColor: "#C0C000", difficultyOrangeColor: "#FF8000", difficultyRedColor: "#FF0000", }; ({ ...ThemeLight, difficultyBlackColor: "#FFFFFF", difficultyGreyColor: "#C0C0C0", difficultyBrownColor: "#B08C56", difficultyGreenColor: "#3FAF3F", difficultyCyanColor: "#42E0E0", difficultyBlueColor: "#8888FF", difficultyYellowColor: "#FFFF56", difficultyOrangeColor: "#FFB836", difficultyRedColor: "#FF6767", }); // 次のコードを引用・編集 // [AtCoderProblems/index\.ts at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/utils/index.ts) // 5835f5dcacfa0cbdcc8ab1116939833d5ab71ed4 const clipDifficulty = (difficulty) => Math.round(difficulty >= 400 ? difficulty : 400 / Math.exp(1.0 - difficulty / 400)); const RatingColors = [ "Black", "Grey", "Brown", "Green", "Cyan", "Blue", "Yellow", "Orange", "Red", ]; const getRatingColor = (rating) => { const index = Math.min(Math.floor(rating / 400), RatingColors.length - 2); return RatingColors[index + 1] ?? "Black"; }; const getRatingColorClass = (rating) => { const ratingColor = getRatingColor(rating); switch (ratingColor) { case "Black": return "difficulty-black"; case "Grey": return "difficulty-grey"; case "Brown": return "difficulty-brown"; case "Green": return "difficulty-green"; case "Cyan": return "difficulty-cyan"; case "Blue": return "difficulty-blue"; case "Yellow": return "difficulty-yellow"; case "Orange": return "difficulty-orange"; case "Red": return "difficulty-red"; default: return "difficulty-black"; } }; const getRatingColorCode = (ratingColor, theme = ThemeLight) => { switch (ratingColor) { case "Black": return theme.difficultyBlackColor; case "Grey": return theme.difficultyGreyColor; case "Brown": return theme.difficultyBrownColor; case "Green": return theme.difficultyGreenColor; case "Cyan": return theme.difficultyCyanColor; case "Blue": return theme.difficultyBlueColor; case "Yellow": return theme.difficultyYellowColor; case "Orange": return theme.difficultyOrangeColor; case "Red": return theme.difficultyRedColor; default: return theme.difficultyBlackColor; } }; // 次のコードを引用・編集 // [AtCoderProblems/TopcoderLikeCircle\.tsx at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/components/TopcoderLikeCircle.tsx) // 02d7ed77d8d8a9fa8d32cb9981f18dfe53f2c5f0 // FIXME: ダークテーマ対応 const useTheme = () => ThemeLight; const getRatingMetalColorCode = (metalColor) => { switch (metalColor) { case "Bronze": return { base: "#965C2C", highlight: "#FFDABD" }; case "Silver": return { base: "#808080", highlight: "white" }; case "Gold": return { base: "#FFD700", highlight: "white" }; default: return { base: "#FFD700", highlight: "white" }; } }; const getStyleOptions = (color, fillRatio, theme) => { if (color === "Bronze" || color === "Silver" || color === "Gold") { const metalColor = getRatingMetalColorCode(color); return { borderColor: metalColor.base, background: `linear-gradient(to right, \ ${metalColor.base}, ${metalColor.highlight}, ${metalColor.base})`, }; } const colorCode = getRatingColorCode(color, theme); return { borderColor: colorCode, background: `border-box linear-gradient(to top, \ ${colorCode} ${fillRatio * 100}%, \ rgba(0,0,0,0) ${fillRatio * 100}%)`, }; }; const topcoderLikeCircle = (color, rating, big = true, extraDescription = "") => { const fillRatio = rating >= 3200 ? 1.0 : (rating % 400) / 400; const className = `topcoder-like-circle ${big ? "topcoder-like-circle-big" : ""} rating-circle`; const theme = useTheme(); const styleOptions = getStyleOptions(color, fillRatio, theme); const styleOptionsString = `border-color: ${styleOptions.borderColor}; background: ${styleOptions.background};`; const content = extraDescription ? `Difficulty: ${extraDescription}` : `Difficulty: ${rating}`; // FIXME: TooltipにSolve Prob, Solve Timeを追加 return `<span class="${className}" style="${styleOptionsString}" data-toggle="tooltip" title="${content}" data-placement="bottom" />`; }; // 次のコードを引用・編集 // [AtCoderProblems/DifficultyCircle\.tsx at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/components/DifficultyCircle.tsx) // 0469e07274fda2282c9351c2308ed73880728e95 const getColor = (difficulty) => { if (difficulty < 3200) return getRatingColor(difficulty); if (difficulty < 3600) return "Bronze"; if (difficulty < 4000) return "Silver"; return "Gold"; }; const difficultyCircle = (difficulty, big = true, extraDescription = "") => { if (Number.isNaN(difficulty)) { // Unavailableの難易度円はProblemsとは異なりGlyphiconの「?」を使用 const className = `glyphicon glyphicon-question-sign aria-hidden='true' difficulty-unavailable ${big ? "difficulty-unavailable-icon-big" : "difficulty-unavailable-icon"}`; const content = "Difficulty is unavailable."; return `<span class="${className}" data-toggle="tooltip" title="${content}" data-placement="bottom" />`; } const color = getColor(difficulty); return topcoderLikeCircle(color, difficulty, big, extraDescription); }; var html = "<h2>atcoder-difficulty-display</h2>\n<hr>\n<a href=\"https://github.com/hotaru-n/atcoder-difficulty-display\">GitHub</a>\n<div class=\"form-horizontal\">\n <div class=\"form-group\">\n <label class=\"control-label col-sm-3\">ネタバレ防止</label>\n <div class=\"col-sm-5\">\n <div class=\"checkbox\">\n <label>\n <input type=\"checkbox\" id=\"hide-difficulty-atcoder-difficulty-display\">\n 画面上のボタンを押した後に難易度が表示されるようにする\n </label>\n </div>\n </div>\n </div>\n</div>\n"; var css = ".difficulty-red {\n color: #ff0000;\n}\n\n.difficulty-orange {\n color: #ff8000;\n}\n\n.difficulty-yellow {\n color: #c0c000;\n}\n\n.difficulty-blue {\n color: #0000ff;\n}\n\n.difficulty-cyan {\n color: #00c0c0;\n}\n\n.difficulty-green {\n color: #008000;\n}\n\n.difficulty-brown {\n color: #804000;\n}\n\n.difficulty-grey {\n color: #808080;\n}\n\n.topcoder-like-circle {\n display: block;\n border-radius: 50%;\n border-style: solid;\n border-width: 1px;\n width: 12px;\n height: 12px;\n}\n\n.topcoder-like-circle-big {\n border-width: 3px;\n width: 36px;\n height: 36px;\n}\n\n.rating-circle {\n margin-right: 5px;\n display: inline-block;\n}\n\n.difficulty-unavailable {\n color: #17a2b8;\n}\n\n.difficulty-unavailable-icon {\n margin-right: 0.3px;\n}\n\n.difficulty-unavailable-icon-big {\n font-size: 36px;\n margin-right: 5px;\n}\n\n.label-status-a {\n color: white;\n}\n\n.label-success-after-contest {\n background-color: #9ad59e;\n}\n\n.label-warning-after-contest {\n background-color: #ffdd99;\n}"; // AtCoderの問題ページをパースする /** * URLをパースする パラメータを消す \ * 例: in: https://atcoder.jp/contests/abc210?lang=en \ * 例: out: (5)['https:', '', 'atcoder.jp', 'contests', 'abc210'] */ const parseURL = (url) => { // 区切り文字`/`で分割する // ?以降の文字列を削除してパラメータを削除する return url.split("/").map((x) => x.replace(/\?.*/i, "")); }; const URL = parseURL(window.location.href); /** * 表セル要素から、前の要素のテキストが引数と一致する要素を探す * 個別の提出ページで使うことを想定 * 例: searchSubmissionInfo(["問題", "Task"]) */ const searchSubmissionInfo = (key) => { const tdTags = document.getElementsByTagName("td"); const tdTagsArray = Array.prototype.slice.call(tdTags); return tdTagsArray.filter((elem) => { const prevElem = elem.previousElementSibling; const text = prevElem?.textContent; if (typeof text === "string") return key.includes(text); return false; })[0]; }; /** コンテストタイトル 例: AtCoder Beginner Contest 210 */ document.getElementsByClassName("contest-title")[0]?.textContent ?? ""; /** コンテストID 例: abc210 */ const contestID = URL[4] ?? ""; /** * ページ種類 \ * 基本的にコンテストIDの次のパス * ### 例外 * 個別の問題: task * 個別の提出: submission * 個別の問題ページで解説ボタンを押すと遷移する個別の問題の解説一覧ページ: task_editorial */ const pageType = (() => { if (URL.length < 6) return ""; if (URL.length >= 7 && URL[5] === "submissions" && URL[6] !== "me") return "submission"; if (URL.length >= 8 && URL[5] === "tasks" && URL[7] === "editorial") return "task_editorial"; if (URL.length >= 7 && URL[5] === "tasks") return "task"; return URL[5] ?? ""; })(); /** 問題ID 例: abc210_a */ const taskID = (() => { if (pageType === "task") { // 問題ページでは、URLから問題IDを取り出す return URL[6] ?? ""; } if (pageType === "submission") { // 個別の提出ページでは、問題リンクのURLから問題IDを取り出す // 提出情報の問題のURLを取得する const taskCell = searchSubmissionInfo(["問題", "Task"]); if (!taskCell) return ""; const taskLink = taskCell.getElementsByTagName("a")[0]; if (!taskLink) return ""; const taskUrl = parseURL(taskLink.href); const taskIDParsed = taskUrl[6] ?? ""; return taskIDParsed; } return ""; })(); /** 問題名 例: A - Cabbages */ (() => { if (pageType === "task") { // 問題ページでは、h2から問題名を取り出す return (document .getElementsByClassName("h2")[0] ?.textContent?.trim() .replace(/\n.*/i, "") ?? ""); } if (pageType === "submission") { // 個別の提出ページでは、問題リンクのテキストから問題名を取り出す // 提出情報の問題のテキストを取得する const taskCell = searchSubmissionInfo(["問題", "Task"]); if (!taskCell) return ""; const taskLink = taskCell.getElementsByTagName("a")[0]; if (!taskLink) return ""; return taskLink.textContent ?? ""; } return ""; })(); /** 提出ユーザー 例: machikane */ (() => { if (pageType !== "submission") return ""; // 個別の提出ページのとき const userCell = searchSubmissionInfo(["ユーザ", "User"]); if (!userCell) return ""; return userCell?.textContent?.trim() ?? ""; })(); /** 提出結果 例: AC */ (() => { if (pageType !== "submission") return ""; // 個別の提出ページのとき const statusCell = searchSubmissionInfo(["結果", "Status"]); if (!statusCell) return ""; return statusCell?.textContent?.trim() ?? ""; })(); /** 得点 例: 100 */ (() => { if (pageType !== "submission") return 0; // 個別の提出ページのとき const scoreCell = searchSubmissionInfo(["得点", "Score"]); if (!scoreCell) return 0; return parseInt(scoreCell?.textContent?.trim() ?? "0", 10); })(); /** * 得点が最大の提出を返す */ const parseMaxScore = (submissionsArg) => { if (submissionsArg.length === 0) { return undefined; } const maxScore = submissionsArg.reduce((left, right) => left.point > right.point ? left : right); return maxScore; }; /** * ペナルティ数を数える */ const parsePenalties = (submissionsArg) => { let penalties = 0; let hasAccepted = false; submissionsArg.forEach((element) => { hasAccepted = element.result === "AC" || hasAccepted; if (!hasAccepted && !nonPenaltyJudge.includes(element.result)) { penalties += 1; } }); return penalties; }; /** * 最初にACした提出を返す */ const parseFirstAcceptedTime = (submissionsArg) => { const ac = submissionsArg.filter((element) => element.result === "AC"); return ac[0]; }; /** * 代表的な提出を返す * 1. 最後にACした提出 * 2. 最後の提出 * 3. undefined */ const parseRepresentativeSubmission = (submissionsArg) => { const ac = submissionsArg.filter((element) => element.result === "AC"); const nonAC = submissionsArg.filter((element) => element.result !== "AC"); if (ac.length > 0) return ac.slice(-1)[0]; if (nonAC.length > 0) return nonAC.slice(-1)[0]; return undefined; }; /** * 提出をパースして情報を返す * 対象: コンテスト前,中,後の提出 別コンテストの同じ問題への提出 * 返す情報: 得点が最大の提出 最初のACの提出 代表的な提出 ペナルティ数 */ const analyzeSubmissions = (submissionsArg) => { const submissions = submissionsArg.filter((element) => element.problem_id === taskID); const beforeContest = submissions.filter((element) => element.contest_id === contestID && element.epoch_second < startTime.unix()); const duringContest = submissions.filter((element) => element.contest_id === contestID && element.epoch_second >= startTime.unix() && element.epoch_second < endTime.unix()); const afterContest = submissions.filter((element) => element.contest_id === contestID && element.epoch_second >= endTime.unix()); const anotherContest = submissions.filter((element) => element.contest_id !== contestID); return { before: { maxScore: parseMaxScore(beforeContest), firstAc: parseFirstAcceptedTime(beforeContest), representative: parseRepresentativeSubmission(beforeContest), }, during: { maxScore: parseMaxScore(duringContest), firstAc: parseFirstAcceptedTime(duringContest), representative: parseRepresentativeSubmission(duringContest), penalties: parsePenalties(duringContest), }, after: { maxScore: parseMaxScore(afterContest), firstAc: parseFirstAcceptedTime(afterContest), representative: parseRepresentativeSubmission(afterContest), }, another: { maxScore: parseMaxScore(anotherContest), firstAc: parseFirstAcceptedTime(anotherContest), representative: parseRepresentativeSubmission(anotherContest), }, }; }; /** * 提出状況を表すラベルを生成 */ const generateStatusLabel = (submission, type) => { if (submission === undefined) { return ""; } const isAC = submission.result === "AC"; let className = ""; switch (type) { case "before": className = "label-primary"; break; case "during": className = isAC ? "label-success" : "label-warning"; break; case "after": className = isAC ? "label-success-after-contest" : "label-warning-after-contest"; break; case "another": className = "label-default"; break; } let content = ""; switch (type) { case "before": content = "コンテスト前の提出"; break; case "during": content = "コンテスト中の提出"; break; case "after": content = "コンテスト後の提出"; break; case "another": content = "別コンテストの同じ問題への提出"; break; } const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`; return `<span class="label ${className}" data-toggle="tooltip" data-placement="bottom" title="${content}"> <a class="label-status-a" href=${href}>${submission.result}</a> </span> `; }; /** * ペナルティ数を表示 */ const generatePenaltiesCount = (penalties) => { if (penalties <= 0) { return ""; } const content = "コンテスト中のペナルティ数"; return `<span data-toggle="tooltip" data-placement="bottom" title="${content}"class="difficulty-red" style='font-weight: bold; font-size: x-small;'> (${penalties.toString()}) </span>`; }; /** * 最初のACの時間を表示 */ const generateFirstAcTime = (submission) => { if (submission === undefined) { return ""; } const content = "提出時間"; const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`; const elapsed = submission.epoch_second - startTime.unix(); const elapsedSeconds = elapsed % 60; const elapsedMinutes = Math.trunc(elapsed / 60); return `<span data-toggle="tooltip" data-placement="bottom" title="${content}"> <a class="difficulty-orange" style='font-weight: bold; font-size: x-small;' href=${href}> ${elapsedMinutes}:${elapsedSeconds} </a> </span>`; }; /** * マラソン用に得点を表示するスパンを生成 */ const generateScoreSpan = (submission, type) => { if (submission === undefined) { return ""; } // マラソン用を考えているのでとりあえず1万点未満は表示しない if (submission.point < 10000) { return ""; } let className = ""; switch (type) { case "before": className = "difficulty-blue"; break; case "during": className = "difficulty-green"; break; case "after": className = "difficulty-yellow"; break; case "another": className = "difficulty-grey"; break; } let content = ""; switch (type) { case "before": content = "コンテスト前の提出"; break; case "during": content = "コンテスト中の提出"; break; case "after": content = "コンテスト後の提出"; break; case "another": content = "別コンテストの同じ問題への提出"; break; } const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`; return `<span data-toggle="tooltip" data-placement="bottom" title="${content}"> <a class="${className}" style='font-weight: bold;' href=${href}> ${submission.point} </a> </span> `; }; /** * 色付け対象の要素の配列を取得する * * 個別の問題ページのタイトル * * 問題へのリンク * * 解説ページのH3の問題名 */ const getElementsColorizable = () => { const elementsColorizable = []; // 問題ページのタイトル if (pageType === "task") { const element = document.getElementsByClassName("h2")[0]; if (element) { elementsColorizable.push({ element, taskID, big: true }); } } // aタグ要素 問題ページ、提出ページ等のリンクを想定 const aTagsRaw = document.getElementsByTagName("a"); let aTagsArray = Array.prototype.slice.call(aTagsRaw); // 問題ページの一番左の要素は除く 見た目の問題です aTagsArray = aTagsArray.filter((element) => !((pageType === "tasks" || pageType === "score") && !element.parentElement?.previousElementSibling)); // 左上の日本語/英語切り替えリンクは除く aTagsArray = aTagsArray.filter((element) => !element.href.includes("?lang=")); // 解説ページの問題名の右のリンクは除く aTagsArray = aTagsArray.filter((element) => !(pageType === "editorial" && element.children[0]?.classList.contains("glyphicon-new-window"))); const aTagsConverted = aTagsArray.map((element) => { const url = parseURL(element.href); const taskIDFromURL = (url[url.length - 2] ?? "") === "tasks" ? url[url.length - 1] ?? "" : ""; // 個別の解説ページではbig const big = element.parentElement?.tagName.includes("H2") ?? false; // Comfortable AtCoderのドロップダウンではafterbegin const afterbegin = element.parentElement?.parentElement?.classList.contains("dropdown-menu") ?? false; return { element, taskID: taskIDFromURL, big, afterbegin }; }); elementsColorizable.push(...aTagsConverted); // h3タグ要素 解説ページの問題名を想定 const h3TagsRaw = document.getElementsByTagName("h3"); const h3TagsArray = Array.prototype.slice.call(h3TagsRaw); const h3TagsConverted = h3TagsArray.map((element) => { const url = parseURL(element.getElementsByTagName("a")[0]?.href ?? ""); const taskIDFromURL = (url[url.length - 2] ?? "") === "tasks" ? url[url.length - 1] ?? "" : ""; return { element, taskID: taskIDFromURL, big: true, afterbegin: true }; }); // FIXME: 別ユーザースクリプトが指定した要素を色付けする機能 // 指定したクラスがあれば対象とすることを考えている // ユーザースクリプトの実行順はユーザースクリプトマネージャーの設定で変更可能 elementsColorizable.push(...h3TagsConverted); return elementsColorizable; }; /** * 問題ステータス(実行時間制限とメモリ制限が書かれた部分)のHTMLオブジェクトを取得 */ const getElementOfProblemStatus = () => { if (pageType !== "task") return undefined; const psRaw = document ?.getElementById("main-container") ?.getElementsByTagName("p"); const ps = Array.prototype.slice.call(psRaw); if (!psRaw) return undefined; const problemStatuses = ps.filter((p) => { return (p.textContent?.includes("メモリ制限") || p.textContent?.includes("Memory Limit")); }); return problemStatuses[0]; }; /** 常設コンテストID一覧 */ const permanentContestIDs = [ "practice", "APG4b", "abs", "practice2", "typical90", "math-and-algorithm", "tessoku-book", ]; // FIXME: FIXME: Problemsでデータ取れなかったらコンテストが終了していない判定で良さそう /** * 開いているページのコンテストが終了していればtrue \ * 例外処理として以下の場合もtrueを返す * * コンテストが常設コンテスト * * コンテストのページ以外にいる <https://atcoder.jp/contests/*> */ var isContestOver = () => { if (!(URL[3] === "contests" && URL.length >= 5)) return true; if (permanentContestIDs.includes(contestID)) return true; return Date.now() > endTime.valueOf(); }; /** * コンテストページ <https://atcoder.jp/contests/*> の処理 \ * メインの処理 */ const contestPageProcess = async () => { // コンテスト終了前は不要なので無効化する if (!isContestOver()) return; // FIXME: ダークテーマ対応 GM_addStyle(css); /** 問題一覧取得 */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const problems = await getProblems(); /** 難易度取得 */ const problemModels = addTypical90Difficulty( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await getEstimatedDifficulties(), problems); // FIXME: PAST対応 // FIXME: JOI非公式難易度表対応 /** 提出状況取得 */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const submissions = await getSubmissions(userScreenName); // 色付け対象の要素の配列を取得する // 難易度が無いものを除く const elementsColorizable = getElementsColorizable().filter((element) => element.taskID in problemModels); // 問題ステータス(個別の問題ページの実行時間制限とメモリ制限が書かれた部分)を取得する const elementProblemStatus = getElementOfProblemStatus(); /** * 色付け処理を実行する */ const colorizeElement = () => { // 問題見出し、問題リンクを色付け elementsColorizable.forEach((element) => { const model = problemModels[element.taskID]; // 難易度がUnavailableならばdifficultyプロパティが無い // difficultyの値をNaNとする const difficulty = clipDifficulty(model?.difficulty ?? NaN); // 色付け if (!Number.isNaN(difficulty)) { const color = getRatingColorClass(difficulty); // eslint-disable-next-line no-param-reassign element.element.classList.add(color); } else { element.element.classList.add("difficulty-unavailable"); } // 🧪追加 if (model?.is_experimental) { element.element.insertAdjacentText("afterbegin", "🧪"); } // ◒難易度円追加 element.element.insertAdjacentHTML(element.afterbegin ? "afterbegin" : "beforebegin", difficultyCircle(difficulty, element.big, model?.extra_difficulty)); }); // 個別の問題ページのところに難易度等情報を追加 if (elementProblemStatus) { // 難易度の値を表示する // 難易度推定の対象外なら、この値はundefined const model = problemModels[taskID]; // 難易度がUnavailableのときはdifficultyの値をNaNとする // 難易度がUnavailableならばdifficultyプロパティが無い const difficulty = clipDifficulty(model?.difficulty ?? NaN); // 色付け let className = ""; if (difficulty) { className = getRatingColorClass(difficulty); } else if (model) { className = "difficulty-unavailable"; } else { className = ""; } // Difficultyの値設定 let value = ""; if (difficulty) { value = difficulty.toString(); } else if (model) { value = "Unavailable"; } else { value = "None"; } // 🧪追加 const experimentalText = model?.is_experimental ? "🧪" : ""; const content = `${experimentalText}${value}`; elementProblemStatus.insertAdjacentHTML("beforeend", ` / Difficulty: <span style='font-weight: bold;' class="${className}">${content}</span>`); /** この問題への提出 提出時間ソート済みと想定 */ const thisTaskSubmissions = submissions.filter((element) => element.problem_id === taskID); const analyze = analyzeSubmissions(thisTaskSubmissions); // コンテスト前中後外の提出状況 コンテスト中の解答時間とペナルティ数を表示する let statuesHTML = ""; statuesHTML += generateStatusLabel(analyze.before.representative, "before"); statuesHTML += generateStatusLabel(analyze.during.representative, "during"); statuesHTML += generateStatusLabel(analyze.after.representative, "after"); statuesHTML += generateStatusLabel(analyze.another.representative, "another"); statuesHTML += generatePenaltiesCount(analyze.during.penalties); statuesHTML += generateFirstAcTime(analyze.during.firstAc); if (statuesHTML.length > 0) { elementProblemStatus.insertAdjacentHTML("beforeend", ` / Status: ${statuesHTML}`); } // コンテスト前中後外の1万点以上の最大得点を表示する // NOTE: マラソン用のため、1万点以上とした let scoresHTML = ""; scoresHTML += generateScoreSpan(analyze.before.maxScore, "before"); scoresHTML += generateScoreSpan(analyze.during.maxScore, "during"); scoresHTML += generateScoreSpan(analyze.after.maxScore, "after"); scoresHTML += generateScoreSpan(analyze.another.maxScore, "another"); if (scoresHTML.length > 0) { elementProblemStatus.insertAdjacentHTML("beforeend", ` / Scores: ${scoresHTML}`); } } // bootstrap3のtooltipを有効化 難易度円の値を表示するtooltip // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, no-undef $('[data-toggle="tooltip"]').tooltip(); }; // 色付け処理実行 if (!GM_getValue(hideDifficultyID, false)) { // 設定 ネタバレ防止がOFFなら何もせず実行 colorizeElement(); } else { // 設定 ネタバレ防止がONなら // ページ上部にボタンを追加 押すと色付け処理が実行される const place = document.getElementsByTagName("h2")[0] ?? document.getElementsByClassName("h2")[0] ?? undefined; if (place) { place.insertAdjacentHTML("beforebegin", `<input type="button" id="${hideDifficultyID}" class="btn btn-info" value="Show Difficulty" />`); const button = document.getElementById(hideDifficultyID); if (button) { button.addEventListener("click", () => { button.style.display = "none"; colorizeElement(); }); } } } }; /** * 設定ページ <https://atcoder.jp/settings> の処理 \ * 設定ボタンを追加する */ const settingPageProcess = () => { const insertion = document.getElementsByClassName("form-horizontal")[0]; if (insertion === undefined) return; insertion.insertAdjacentHTML("afterend", html); // 設定 ネタバレ防止のチェックボックスの読み込み 切り替え 保存処理を追加 const hideDifficultyChechbox = document.getElementById(hideDifficultyID); if (hideDifficultyChechbox && hideDifficultyChechbox instanceof HTMLInputElement) { hideDifficultyChechbox.checked = GM_getValue(hideDifficultyID, false); hideDifficultyChechbox.addEventListener("change", () => { GM_setValue(hideDifficultyID, hideDifficultyChechbox.checked); }); } }; /** * 最初に実行される部分 \ * 共通の処理を実行した後ページごとの処理を実行する */ (async () => { // 共通の処理 backwardCompatibleProcessing(); // ページ別の処理 if (URL[3] === "contests" && URL.length >= 5) { await contestPageProcess(); } if (URL[3] === "settings" && URL.length === 4) { settingPageProcess(); } })().catch((error) => { // eslint-disable-next-line no-console console.error("[atcoder-difficulty-display]", error); });