├── img
├── config1.png
├── config2.png
├── overview1.png
└── overview2.png
├── types
├── html.d.ts
├── scss.d.ts
└── atcoder-embedded.d.ts
├── .vscode
├── settings.json
└── tasks.json
├── tsconfig.json
├── src
├── components
│ ├── setting.html
│ ├── difficultyCircle.ts
│ └── topcoderLikeCircle.ts
├── utils
│ ├── isContestOver.ts
│ ├── problemsIndex.ts
│ ├── index.ts
│ ├── getElementsColorizable.ts
│ ├── parser.ts
│ └── analyzeSubmissions.ts
├── style
│ ├── theme.ts
│ └── _custom.scss
└── main.ts
├── LICENSE
├── NOTICE.md
├── .eslintrc.json
├── rollup.config.js
├── package.json
├── .gitignore
└── README.md
/img/config1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hotarunw/atcoder-difficulty-display/HEAD/img/config1.png
--------------------------------------------------------------------------------
/img/config2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hotarunw/atcoder-difficulty-display/HEAD/img/config2.png
--------------------------------------------------------------------------------
/img/overview1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hotarunw/atcoder-difficulty-display/HEAD/img/overview1.png
--------------------------------------------------------------------------------
/img/overview2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hotarunw/atcoder-difficulty-display/HEAD/img/overview2.png
--------------------------------------------------------------------------------
/types/html.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.html" {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/types/scss.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.scss" {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "task.allowAutomaticTasks": "on"
4 | }
5 |
--------------------------------------------------------------------------------
/types/atcoder-embedded.d.ts:
--------------------------------------------------------------------------------
1 | declare let userScreenName: string;
2 | declare let contestScreenName: string;
3 | declare let startTime: import("moment").Moment;
4 | declare let endTime: import("moment").Moment;
5 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "npm",
6 | "script": "bundle:watch",
7 | "group": "build",
8 | "problemMatcher": [],
9 | "label": "npm: bundle:watch",
10 | "detail": "rollup -c -w",
11 | "runOptions": { "runOn": "folderOpen" }
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "strict": true,
7 | "noUncheckedIndexedAccess": true,
8 | "target": "es2021",
9 | "typeRoots": ["types", "node_modules/@types"],
10 | "resolveJsonModule": true
11 | },
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/setting.html:
--------------------------------------------------------------------------------
1 |
atcoder-difficulty-display
2 |
3 | GitHub
4 |
20 |
--------------------------------------------------------------------------------
/src/utils/isContestOver.ts:
--------------------------------------------------------------------------------
1 | import { contestID, URL } from "./parser";
2 |
3 | /** 常設コンテストID一覧 */
4 | const permanentContestIDs = [
5 | "practice",
6 | "APG4b",
7 | "abs",
8 | "practice2",
9 | "typical90",
10 | "math-and-algorithm",
11 | "tessoku-book",
12 | ];
13 |
14 | // FIXME: FIXME: Problemsでデータ取れなかったらコンテストが終了していない判定で良さそう
15 | /**
16 | * 開いているページのコンテストが終了していればtrue \
17 | * 例外処理として以下の場合もtrueを返す
18 | * * コンテストが常設コンテスト
19 | * * コンテストのページ以外にいる
20 | */
21 | export default () => {
22 | if (!(URL[3] === "contests" && URL.length >= 5)) return true;
23 | if (permanentContestIDs.includes(contestID)) return true;
24 | return Date.now() > endTime.valueOf();
25 | };
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 hotarunw
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/style/theme.ts:
--------------------------------------------------------------------------------
1 | // 次のコードを引用
2 | // [AtCoderProblems/theme\.ts at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/style/theme.ts)
3 | // 8b1b86c740e627e59abf056a11c00582e12b30ff
4 |
5 | export const ThemeLight = {
6 | difficultyBlackColor: "#404040",
7 | difficultyGreyColor: "#808080",
8 | difficultyBrownColor: "#804000",
9 | difficultyGreenColor: "#008000",
10 | difficultyCyanColor: "#00C0C0",
11 | difficultyBlueColor: "#0000FF",
12 | difficultyYellowColor: "#C0C000",
13 | difficultyOrangeColor: "#FF8000",
14 | difficultyRedColor: "#FF0000",
15 | };
16 |
17 | export const ThemeDark: typeof ThemeLight = {
18 | ...ThemeLight,
19 | difficultyBlackColor: "#FFFFFF",
20 | difficultyGreyColor: "#C0C0C0",
21 | difficultyBrownColor: "#B08C56",
22 | difficultyGreenColor: "#3FAF3F",
23 | difficultyCyanColor: "#42E0E0",
24 | difficultyBlueColor: "#8888FF",
25 | difficultyYellowColor: "#FFFF56",
26 | difficultyOrangeColor: "#FFB836",
27 | difficultyRedColor: "#FF6767",
28 | };
29 |
30 | export const ThemePurple = ThemeLight;
31 |
32 | export type Theme = typeof ThemeLight;
33 |
--------------------------------------------------------------------------------
/NOTICE.md:
--------------------------------------------------------------------------------
1 | # 使用したソフトウェアのライセンス表示
2 |
3 | ## [kenkoooo/AtCoderProblems: Extend your AtCoder](https://github.com/kenkoooo/AtCoderProblems)
4 |
5 | MIT License
6 |
7 | Copyright (c) 2019 kenkoooo
8 |
9 | Permission is hereby granted, free of charge, to any person obtaining a copy
10 | of this software and associated documentation files (the "Software"), to deal
11 | in the Software without restriction, including without limitation the rights
12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | copies of the Software, and to permit persons to whom the Software is
14 | furnished to do so, subject to the following conditions:
15 |
16 | The above copyright notice and this permission notice shall be included in all
17 | copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | SOFTWARE.
26 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "greasemonkey": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/eslint-recommended",
10 | "plugin:@typescript-eslint/recommended",
11 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
12 | "airbnb",
13 | "plugin:import/errors",
14 | "plugin:import/warnings",
15 | "plugin:import/typescript",
16 | "plugin:prettier/recommended",
17 | "prettier"
18 | ],
19 | "globals": {
20 | "contestScreenName": "readonly",
21 | "endTime": "readonly",
22 | "startTime": "readonly",
23 | "userScreenName": "readonly"
24 | },
25 | "parser": "@typescript-eslint/parser",
26 | "parserOptions": {
27 | "ecmaVersion": 2021,
28 | "extraFileExtensions": [".html"],
29 | "project": "./tsconfig.json",
30 | "sourceType": "module"
31 | },
32 | "plugins": ["@typescript-eslint", "import", "html"],
33 | "root": true,
34 | "rules": {
35 | "func-style": ["error", "expression", { "allowArrowFunctions": true }],
36 | "import/extensions": [
37 | "error",
38 | "ignorePackages",
39 | {
40 | "js": "never",
41 | "jsx": "never",
42 | "ts": "never",
43 | "tsx": "never"
44 | }
45 | ]
46 | },
47 | "settings": {
48 | "import/resolver": {
49 | "typescript": { "project": "src/" }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/difficultyCircle.ts:
--------------------------------------------------------------------------------
1 | // 次のコードを引用・編集
2 | // [AtCoderProblems/DifficultyCircle\.tsx at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/components/DifficultyCircle.tsx)
3 | // 0469e07274fda2282c9351c2308ed73880728e95
4 |
5 | import { getRatingColor } from "../utils/problemsIndex";
6 | import { topcoderLikeCircle } from "./topcoderLikeCircle";
7 |
8 | const getColor = (difficulty: number) => {
9 | if (difficulty < 3200) return getRatingColor(difficulty);
10 | if (difficulty < 3600) return "Bronze";
11 | if (difficulty < 4000) return "Silver";
12 | return "Gold";
13 | };
14 |
15 | const difficultyCircle = (
16 | difficulty: number,
17 | big = true,
18 | extraDescription = ""
19 | ): string => {
20 | if (Number.isNaN(difficulty)) {
21 | // Unavailableの難易度円はProblemsとは異なりGlyphiconの「?」を使用
22 | const className = `glyphicon glyphicon-question-sign aria-hidden='true'
23 | difficulty-unavailable
24 | ${big ? "difficulty-unavailable-icon-big" : "difficulty-unavailable-icon"}`;
25 | const content = "Difficulty is unavailable.";
26 |
27 | return ``;
31 | }
32 | const color = getColor(difficulty);
33 |
34 | return topcoderLikeCircle(color, difficulty, big, extraDescription);
35 | };
36 |
37 | export default difficultyCircle;
38 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-call */
2 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */
3 | /* eslint-disable @typescript-eslint/restrict-template-expressions */
4 | import typescript from "@rollup/plugin-typescript";
5 | import html from "rollup-plugin-html";
6 | import scss from "rollup-plugin-scss";
7 | import packageJson from "./package.json" assert { type: "json" };
8 |
9 | const userScriptBanner = `
10 | // ==UserScript==
11 | // @name ${packageJson.name}
12 | // @namespace https://github.com/hotarunw
13 | // @version ${packageJson.version}
14 | // @description ${packageJson.description}
15 | // @author ${packageJson.author}
16 | // @license ${packageJson.license}
17 | // @supportURL ${packageJson.bugs.url}
18 | // @match https://atcoder.jp/contests/*
19 | // @exclude https://atcoder.jp/contests/
20 | // @match https://atcoder.jp/settings
21 | // @grant GM_addStyle
22 | // @grant GM_setValue
23 | // @grant GM_getValue
24 | // @require https://greasyfork.org/scripts/437862-atcoder-problems-api/code/atcoder-problems-api.js?version=1004589
25 | // ==/UserScript==`.trim();
26 |
27 | export default [
28 | {
29 | input: "src/main.ts",
30 | output: {
31 | banner: userScriptBanner,
32 | file: "dist/dist.js",
33 | },
34 | plugins: [
35 | html({
36 | include: "**/*.html",
37 | }),
38 | scss({
39 | output: false,
40 | }),
41 | typescript(),
42 | ],
43 | },
44 | ];
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "atcoder-difficulty-display",
3 | "version": "2.0.1",
4 | "description": "AtCoder Problemsの難易度を表示します。",
5 | "typeRoots": [
6 | "types",
7 | "node_modules/@types"
8 | ],
9 | "type": "module",
10 | "scripts": {
11 | "lint": "eslint --ignore-path .gitignore \"./src/**/*.{js,jsx,ts,tsx}\"",
12 | "lint:fix": "eslint --ignore-path .gitignore \"./src/**/*.{js,jsx,ts,tsx}\" --fix",
13 | "prettier": "prettier --write --ignore-path .gitignore \"**/*.{css,scss,html,js,json,jsx,md,ts,tsx}\"",
14 | "bundle": "rollup -c",
15 | "bundle:watch": "rollup -c -w",
16 | "test": "jest"
17 | },
18 | "author": "hotarunw",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/hotarunw/atcoder-difficulty-display/issues"
22 | },
23 | "devDependencies": {
24 | "@rollup/plugin-typescript": "^11.1.5",
25 | "@types/jest": "^29.5.6",
26 | "@types/tampermonkey": "^4.20.4",
27 | "@typescript-eslint/eslint-plugin": "^6.8.0",
28 | "@typescript-eslint/parser": "^6.8.0",
29 | "eslint": "^8.52.0",
30 | "eslint-config-airbnb": "^19.0.4",
31 | "eslint-config-prettier": "^9.0.0",
32 | "eslint-import-resolver-typescript": "^3.6.1",
33 | "eslint-plugin-html": "^7.1.0",
34 | "eslint-plugin-prettier": "^5.0.1",
35 | "jest": "^29.7.0",
36 | "prettier": "^3.0.3",
37 | "rollup": "^4.22.4",
38 | "rollup-plugin-html": "^0.2.1",
39 | "rollup-plugin-node-resolve": "^5.2.0",
40 | "rollup-plugin-scss": "^4.0.0",
41 | "sass": "^1.69.4",
42 | "ts-jest": "^29.1.1",
43 | "typescript": "^5.2.2"
44 | },
45 | "dependencies": {
46 | "atcoder-problems-api": "github:key-moon/atcoder-problems-api",
47 | "moment-es6": "^1.0.0"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | lerna-debug.log*
10 | .pnpm-debug.log*
11 |
12 | # Diagnostic reports (https://nodejs.org/api/report.html)
13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
14 |
15 | # Runtime data
16 | pids
17 | *.pid
18 | *.seed
19 | *.pid.lock
20 |
21 | # Directory for instrumented libs generated by jscoverage/JSCover
22 | lib-cov
23 |
24 | # Coverage directory used by tools like istanbul
25 | coverage
26 | *.lcov
27 |
28 | # nyc test coverage
29 | .nyc_output
30 |
31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
32 | .grunt
33 |
34 | # Bower dependency directory (https://bower.io/)
35 | bower_components
36 |
37 | # node-waf configuration
38 | .lock-wscript
39 |
40 | # Compiled binary addons (https://nodejs.org/api/addons.html)
41 | build/Release
42 |
43 | # Dependency directories
44 | node_modules/
45 | jspm_packages/
46 |
47 | # Snowpack dependency directory (https://snowpack.dev/)
48 | web_modules/
49 |
50 | # TypeScript cache
51 | *.tsbuildinfo
52 |
53 | # Optional npm cache directory
54 | .npm
55 |
56 | # Optional eslint cache
57 | .eslintcache
58 |
59 | # Microbundle cache
60 | .rpt2_cache/
61 | .rts2_cache_cjs/
62 | .rts2_cache_es/
63 | .rts2_cache_umd/
64 |
65 | # Optional REPL history
66 | .node_repl_history
67 |
68 | # Output of 'npm pack'
69 | *.tgz
70 |
71 | # Yarn Integrity file
72 | .yarn-integrity
73 |
74 | # dotenv environment variables file
75 | .env
76 | .env.test
77 | .env.production
78 |
79 | # parcel-bundler cache (https://parceljs.org/)
80 | .cache
81 | .parcel-cache
82 |
83 | # Next.js build output
84 | .next
85 | out
86 |
87 | # Nuxt.js build / generate output
88 | .nuxt
89 | dist
90 |
91 | # Gatsby files
92 | .cache/
93 | # Comment in the public line in if your project uses Gatsby and not Next.js
94 | # https://nextjs.org/blog/next-9-1#public-directory-support
95 | # public
96 |
97 | # vuepress build output
98 | .vuepress/dist
99 |
100 | # Serverless directories
101 | .serverless/
102 |
103 | # FuseBox cache
104 | .fusebox/
105 |
106 | # DynamoDB Local files
107 | .dynamodb/
108 |
109 | # TernJS port file
110 | .tern-port
111 |
112 | # Stores VSCode versions used for testing VSCode extensions
113 | .vscode-test
114 |
115 | # yarn v2
116 | .yarn/cache
117 | .yarn/unplugged
118 | .yarn/build-state.yml
119 | .yarn/install-state.gz
120 | .pnp.*
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | ## atcoder-difficulty-display
4 |
5 | [](https://greasyfork.org/ja/scripts/397185-atcoder-difficulty-display)
6 | [](https://github.com/hotarunw/atcoder-difficulty-display)
7 |
8 | 
9 | 
10 |
11 | [**AtCoder**](https://atcoder.jp/) のページに [**AtCoder Problems**](https://kenkoooo.com/atcoder/) の難易度を表示するユーザースクリプトです。
12 |
13 | ## Description
14 |
15 | AtCoder のページに AtCoder Problems が推定した**難易度**を表示します。
16 |
17 | 難易度が表示されて色付けされます。
18 | 推定不可能な問題の場合は**Unavailable**と表示します。
19 | 実験的手法で推定された難易度には、AtCoder Problems と同様に「🧪」をつけます。
20 |
21 | ## 現在あるバグ
22 |
23 | まれに Difficulty が何も表示されなくなるバグがあります。
24 | 開発者ツールのアプリケーションタブから **ATCODER-PROBLEMS-API-** という名前の IndexedDB データベースを削除すると解消します。
25 |
26 | バグの原因が分からなくて困っています。心当たりあれば Issueに投稿してほしいです。バグ発生時のデータベースのデータもほしいです。
27 |
28 | ## Install
29 |
30 | 1. [**Tampermonkey**](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=ja) などのユーザースクリプトマネージャをインストールします。
31 | 2. [](https://greasyfork.org/ja/scripts/397185-atcoder-difficulty-display) からユーザースクリプトをインストールします。
32 |
33 | ### ネタバレ防止機能
34 |
35 | 
36 | 
37 |
38 | difficulty のネタバレ防止目的で最初は difficulty を表示せず、ボタンを押すことで difficulty を表示できます。
39 |
40 | [基本設定 \- AtCoder](https://atcoder.jp/settings) の下部にあるネタバレ防止のチェックボックスを ON にすることで有効になります。
41 |
42 | ## Note
43 |
44 | このユーザースクリプトは**AtCoder Problems**の API を使っているのみで、**AtCoder Problems**とは関わりはありません。
45 |
46 | 意見があれば [GitHub リポジトリ](https://github.com/hotarunw/atcoder-difficulty-display) に Issue を立ててください。
47 |
48 | 難易度については [AtCoder Problems の難易度推定について](http://pepsin-amylase.hatenablog.com/entry/atcoder-problems-difficulty) を見てください。
49 |
50 | ## License
51 |
52 | [](https://opensource.org/licenses/MIT)
53 |
54 | Copyright (c) 2020 hotarunw
55 |
56 | This software is released under the MIT License, see LICENSE.
57 |
58 | ### 使用した OSS ソフトウェアのライセンス表示
59 |
60 | [NOTICE](./NOTICE.md)
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/components/topcoderLikeCircle.ts:
--------------------------------------------------------------------------------
1 | // 次のコードを引用・編集
2 | // [AtCoderProblems/TopcoderLikeCircle\.tsx at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/components/TopcoderLikeCircle.tsx)
3 | // 02d7ed77d8d8a9fa8d32cb9981f18dfe53f2c5f0
4 |
5 | import { Theme, ThemeLight } from "../style/theme";
6 | import { getRatingColorCode, RatingColor } from "../utils/problemsIndex";
7 |
8 | // FIXME: ダークテーマ対応
9 | const useTheme = () => ThemeLight;
10 |
11 | type RatingMetalColor = "Bronze" | "Silver" | "Gold";
12 | const getRatingMetalColorCode = (metalColor: RatingMetalColor) => {
13 | switch (metalColor) {
14 | case "Bronze":
15 | return { base: "#965C2C", highlight: "#FFDABD" };
16 | case "Silver":
17 | return { base: "#808080", highlight: "white" };
18 | case "Gold":
19 | return { base: "#FFD700", highlight: "white" };
20 | default:
21 | return { base: "#FFD700", highlight: "white" };
22 | }
23 | };
24 |
25 | type RatingColorWithMetal = RatingColor | RatingMetalColor;
26 | const getStyleOptions = (
27 | color: RatingColorWithMetal,
28 | fillRatio: number,
29 | theme: Theme
30 | ) => {
31 | if (color === "Bronze" || color === "Silver" || color === "Gold") {
32 | const metalColor = getRatingMetalColorCode(color);
33 | return {
34 | borderColor: metalColor.base,
35 | background: `linear-gradient(to right, \
36 | ${metalColor.base}, ${metalColor.highlight}, ${metalColor.base})`,
37 | };
38 | }
39 | const colorCode = getRatingColorCode(color, theme);
40 | return {
41 | borderColor: colorCode,
42 | background: `border-box linear-gradient(to top, \
43 | ${colorCode} ${fillRatio * 100}%, \
44 | rgba(0,0,0,0) ${fillRatio * 100}%)`,
45 | };
46 | };
47 |
48 | export const topcoderLikeCircle = (
49 | color: RatingColorWithMetal,
50 | rating: number,
51 | big = true,
52 | extraDescription = ""
53 | ): string => {
54 | const fillRatio = rating >= 3200 ? 1.0 : (rating % 400) / 400;
55 | const className = `topcoder-like-circle
56 | ${big ? "topcoder-like-circle-big" : ""} rating-circle`;
57 | const theme = useTheme();
58 | const styleOptions = getStyleOptions(color, fillRatio, theme);
59 | const styleOptionsString = `border-color: ${styleOptions.borderColor}; background: ${styleOptions.background};`;
60 | const content = extraDescription
61 | ? `Difficulty: ${extraDescription}`
62 | : `Difficulty: ${rating}`;
63 | // FIXME: TooltipにSolve Prob, Solve Timeを追加
64 | return ``;
68 | };
69 |
70 | export default topcoderLikeCircle;
71 |
--------------------------------------------------------------------------------
/src/utils/problemsIndex.ts:
--------------------------------------------------------------------------------
1 | // 次のコードを引用・編集
2 | // [AtCoderProblems/index\.ts at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/utils/index.ts)
3 | // 5835f5dcacfa0cbdcc8ab1116939833d5ab71ed4
4 | import { Theme, ThemeLight } from "../style/theme";
5 |
6 | export const clipDifficulty = (difficulty: number): number =>
7 | Math.round(
8 | difficulty >= 400 ? difficulty : 400 / Math.exp(1.0 - difficulty / 400)
9 | );
10 |
11 | export const RatingColors = [
12 | "Black",
13 | "Grey",
14 | "Brown",
15 | "Green",
16 | "Cyan",
17 | "Blue",
18 | "Yellow",
19 | "Orange",
20 | "Red",
21 | ] as const;
22 |
23 | export type RatingColor = (typeof RatingColors)[number];
24 | export const getRatingColor = (rating: number): RatingColor => {
25 | const index = Math.min(Math.floor(rating / 400), RatingColors.length - 2);
26 | return RatingColors[index + 1] ?? "Black";
27 | };
28 | export type RatingColorClassName =
29 | | "difficulty-black"
30 | | "difficulty-grey"
31 | | "difficulty-brown"
32 | | "difficulty-green"
33 | | "difficulty-cyan"
34 | | "difficulty-blue"
35 | | "difficulty-yellow"
36 | | "difficulty-orange"
37 | | "difficulty-red";
38 | export const getRatingColorClass = (rating: number): RatingColorClassName => {
39 | const ratingColor = getRatingColor(rating);
40 | switch (ratingColor) {
41 | case "Black":
42 | return "difficulty-black";
43 | case "Grey":
44 | return "difficulty-grey";
45 | case "Brown":
46 | return "difficulty-brown";
47 | case "Green":
48 | return "difficulty-green";
49 | case "Cyan":
50 | return "difficulty-cyan";
51 | case "Blue":
52 | return "difficulty-blue";
53 | case "Yellow":
54 | return "difficulty-yellow";
55 | case "Orange":
56 | return "difficulty-orange";
57 | case "Red":
58 | return "difficulty-red";
59 | default:
60 | return "difficulty-black";
61 | }
62 | };
63 | export const getRatingColorCode = (
64 | ratingColor: RatingColor,
65 | theme: Theme = ThemeLight
66 | ): string => {
67 | switch (ratingColor) {
68 | case "Black":
69 | return theme.difficultyBlackColor;
70 | case "Grey":
71 | return theme.difficultyGreyColor;
72 | case "Brown":
73 | return theme.difficultyBrownColor;
74 | case "Green":
75 | return theme.difficultyGreenColor;
76 | case "Cyan":
77 | return theme.difficultyCyanColor;
78 | case "Blue":
79 | return theme.difficultyBlueColor;
80 | case "Yellow":
81 | return theme.difficultyYellowColor;
82 | case "Orange":
83 | return theme.difficultyOrangeColor;
84 | case "Red":
85 | return theme.difficultyRedColor;
86 | default:
87 | return theme.difficultyBlackColor;
88 | }
89 | };
90 |
--------------------------------------------------------------------------------
/src/style/_custom.scss:
--------------------------------------------------------------------------------
1 | // 次のコードを引用・編集
2 | // [AtCoderProblems/\_custom\.scss at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/style/_custom.scss)
3 | // fe2824f4e9954b66408ac9c2c677d075bcbb2c03
4 |
5 | // Color Variables
6 |
7 | $difficulty-grey-color: #808080 !default;
8 | $difficulty-brown-color: #804000 !default;
9 | $difficulty-green-color: #008000 !default;
10 | $difficulty-cyan-color: #00c0c0 !default;
11 | $difficulty-blue-color: #0000ff !default;
12 | $difficulty-yellow-color: #c0c000 !default;
13 | $difficulty-orange-color: #ff8000 !default;
14 | $difficulty-red-color: #ff0000 !default;
15 |
16 | $success-bg-color: #c3e6cb !default;
17 | $success-hover-bg-color: #b1dfbb !default;
18 |
19 | $success-intime-bg-color: #9ad59e !default;
20 | $success-intime-hover-bg-color: #88cc88 !default;
21 |
22 | $success-before-contest-bg-color: #99ccff !default;
23 | $success-before-contest-hover-bg-color: #88bbff !default;
24 |
25 | $warning-bg-color: #ffeeba !default;
26 | $warning-hover-bg-color: #ffe8a1 !default;
27 |
28 | $warning-intime-bg-color: #ffdd99 !default;
29 | $warning-intime-hover-bg-color: #f0cc99 !default;
30 |
31 | $danger-bg-color: #f5c6cb !default;
32 | $danger-hover-bg-color: #f1b0b7 !default;
33 |
34 | $penalties-color: #ff0000 !default;
35 |
36 | .difficulty-red {
37 | color: $difficulty-red-color;
38 | }
39 | .difficulty-orange {
40 | color: $difficulty-orange-color;
41 | }
42 | .difficulty-yellow {
43 | color: $difficulty-yellow-color;
44 | }
45 | .difficulty-blue {
46 | color: $difficulty-blue-color;
47 | }
48 | .difficulty-cyan {
49 | color: $difficulty-cyan-color;
50 | }
51 | .difficulty-green {
52 | color: $difficulty-green-color;
53 | }
54 | .difficulty-brown {
55 | color: $difficulty-brown-color;
56 | }
57 | .difficulty-grey {
58 | color: $difficulty-grey-color;
59 | }
60 |
61 | .topcoder-like-circle {
62 | display: block;
63 | border-radius: 50%;
64 | border-style: solid;
65 | border-width: 1px;
66 | width: 12px;
67 | height: 12px;
68 | }
69 |
70 | .topcoder-like-circle-big {
71 | border-width: 3px;
72 | width: 36px;
73 | height: 36px;
74 | }
75 |
76 | .rating-circle {
77 | margin-right: 5px;
78 | display: inline-block;
79 | }
80 |
81 | // AtCoderDifficultyDisplayで追加
82 | $difficulty-unavailable-color: #17a2b8 !default;
83 |
84 | .difficulty-unavailable {
85 | color: $difficulty-unavailable-color;
86 | }
87 |
88 | .difficulty-unavailable-icon {
89 | margin-right: 0.3px;
90 | }
91 |
92 | .difficulty-unavailable-icon-big {
93 | font-size: 36px;
94 | margin-right: 5px;
95 | }
96 |
97 | .label-status-a {
98 | color: white;
99 | }
100 |
101 | .label-success-after-contest {
102 | background-color: $success-intime-bg-color;
103 | }
104 |
105 | .label-warning-after-contest {
106 | background-color: $warning-intime-bg-color;
107 | }
108 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import type { getEstimatedDifficulties } from "atcoder-problems-api/information";
2 | import type { getSubmissions } from "atcoder-problems-api/submission";
3 | import { Problem } from "atcoder-problems-api/types";
4 |
5 | export type SubmissionEntry = (ReturnType<
6 | typeof getSubmissions
7 | > extends Promise
8 | ? P
9 | : never)[number];
10 |
11 | export type ProblemModel = (ReturnType<
12 | typeof getEstimatedDifficulties
13 | > extends Promise
14 | ? P
15 | : never)[number];
16 |
17 | export type ProblemModelEx = ProblemModel & {
18 | /** Difficultyの代わりに表示する説明 */
19 | extra_difficulty?: string;
20 | };
21 |
22 | export const nonPenaltyJudge = ["AC", "CE", "IE", "WJ", "WR"];
23 |
24 | /** 設定 ネタバレ防止のID, Key */
25 | export const hideDifficultyID = "hide-difficulty-atcoder-difficulty-display";
26 |
27 | /**
28 | * 後方互換処理
29 | */
30 | export const backwardCompatibleProcessing = () => {
31 | const oldLocalStorageKeys = [
32 | "atcoderDifficultyDisplayUserSubmissions",
33 | "atcoderDifficultyDisplayUserSubmissionslastFetchedAt",
34 | "atcoderDifficultyDisplayEstimatedDifficulties",
35 | "atcoderDifficultyDisplayEstimatedDifficultieslastFetchedAt",
36 | ];
37 |
38 | /** 過去バージョンのlocalStorageデータを削除する */
39 | oldLocalStorageKeys.forEach((key) => {
40 | localStorage.removeItem(key);
41 | });
42 | };
43 |
44 | const getTypical90Difficulty = (title: string): number => {
45 | if (title.includes("★1")) return 149;
46 | if (title.includes("★2")) return 399;
47 | if (title.includes("★3")) return 799;
48 | if (title.includes("★4")) return 1199;
49 | if (title.includes("★5")) return 1599;
50 | if (title.includes("★6")) return 1999;
51 | if (title.includes("★7")) return 2399;
52 | return NaN;
53 | };
54 |
55 | const getTypical90Description = (title: string): string => {
56 | if (title.includes("★1")) return "200 点問題レベル";
57 | if (title.includes("★2")) return "300 点問題レベル";
58 | if (title.includes("★3")) return "";
59 | if (title.includes("★4")) return "400 点問題レベル";
60 | if (title.includes("★5")) return "500 点問題レベル";
61 | if (title.includes("★6")) return "これが安定して解ければ上級者です";
62 | if (title.includes("★7")) return "チャレンジ問題枠です";
63 | return "エラー: 競プロ典型 90 問の難易度読み取りに失敗しました";
64 | };
65 |
66 | export const addTypical90Difficulty = (
67 | problemModels: { [key: string]: ProblemModel },
68 | problems: Problem[]
69 | ): { [key: string]: ProblemModelEx } => {
70 | const models: { [key: string]: ProblemModelEx } = problemModels;
71 | const problemsT90 = problems.filter(
72 | (element) => element.contest_id === "typical90"
73 | );
74 | problemsT90.forEach((element) => {
75 | const difficulty = getTypical90Difficulty(element.title);
76 |
77 | const model: ProblemModelEx = {
78 | slope: NaN,
79 | intercept: NaN,
80 | variance: NaN,
81 | difficulty,
82 | discrimination: NaN,
83 | irt_loglikelihood: NaN,
84 | irt_users: NaN,
85 | is_experimental: false,
86 | extra_difficulty: `${getTypical90Description(element.title)}`,
87 | };
88 | models[element.id] = model;
89 | });
90 | return models;
91 | };
92 |
--------------------------------------------------------------------------------
/src/utils/getElementsColorizable.ts:
--------------------------------------------------------------------------------
1 | import { pageType, parseURL, taskID } from "./parser";
2 |
3 | export type ElementAndId = {
4 | element: HTMLElement;
5 | taskID: string;
6 | big?: boolean;
7 | afterbegin?: boolean;
8 | };
9 |
10 | /**
11 | * 色付け対象の要素の配列を取得する
12 | * * 個別の問題ページのタイトル
13 | * * 問題へのリンク
14 | * * 解説ページのH3の問題名
15 | */
16 | export const getElementsColorizable = (): ElementAndId[] => {
17 | const elementsColorizable: ElementAndId[] = [];
18 |
19 | // 問題ページのタイトル
20 | if (pageType === "task") {
21 | const element = document.getElementsByClassName("h2")[0] as HTMLElement;
22 | if (element) {
23 | elementsColorizable.push({ element, taskID, big: true });
24 | }
25 | }
26 |
27 | // aタグ要素 問題ページ、提出ページ等のリンクを想定
28 | const aTagsRaw = document.getElementsByTagName("a");
29 | let aTagsArray = Array.prototype.slice.call(aTagsRaw) as HTMLAnchorElement[];
30 | // 問題ページの一番左の要素は除く 見た目の問題です
31 | aTagsArray = aTagsArray.filter(
32 | (element) =>
33 | !(
34 | (pageType === "tasks" || pageType === "score") &&
35 | !element.parentElement?.previousElementSibling
36 | )
37 | );
38 |
39 | // 左上の日本語/英語切り替えリンクは除く
40 | aTagsArray = aTagsArray.filter((element) => !element.href.includes("?lang="));
41 |
42 | // 解説ページの問題名の右のリンクは除く
43 | aTagsArray = aTagsArray.filter(
44 | (element) =>
45 | !(
46 | pageType === "editorial" &&
47 | element.children[0]?.classList.contains("glyphicon-new-window")
48 | )
49 | );
50 |
51 | const aTagsConverted: ElementAndId[] = aTagsArray.map((element) => {
52 | const url = parseURL(element.href);
53 |
54 | const taskIDFromURL =
55 | (url[url.length - 2] ?? "") === "tasks" ? url[url.length - 1] ?? "" : "";
56 | // 個別の解説ページではbig
57 | const big = element.parentElement?.tagName.includes("H2") ?? false;
58 |
59 | // Comfortable AtCoderのドロップダウンではafterbegin
60 | const afterbegin =
61 | element.parentElement?.parentElement?.classList.contains(
62 | "dropdown-menu"
63 | ) ?? false;
64 | return { element, taskID: taskIDFromURL, big, afterbegin };
65 | });
66 |
67 | elementsColorizable.push(...aTagsConverted);
68 |
69 | // h3タグ要素 解説ページの問題名を想定
70 | const h3TagsRaw = document.getElementsByTagName("h3");
71 | const h3TagsArray = Array.prototype.slice.call(
72 | h3TagsRaw
73 | ) as HTMLAnchorElement[];
74 | const h3TagsConverted: ElementAndId[] = h3TagsArray.map((element) => {
75 | const url = parseURL(element.getElementsByTagName("a")[0]?.href ?? "");
76 |
77 | const taskIDFromURL =
78 | (url[url.length - 2] ?? "") === "tasks" ? url[url.length - 1] ?? "" : "";
79 | return { element, taskID: taskIDFromURL, big: true, afterbegin: true };
80 | });
81 |
82 | // FIXME: 別ユーザースクリプトが指定した要素を色付けする機能
83 | // 指定したクラスがあれば対象とすることを考えている
84 | // ユーザースクリプトの実行順はユーザースクリプトマネージャーの設定で変更可能
85 |
86 | elementsColorizable.push(...h3TagsConverted);
87 |
88 | return elementsColorizable;
89 | };
90 |
91 | /**
92 | * 問題ステータス(実行時間制限とメモリ制限が書かれた部分)のHTMLオブジェクトを取得
93 | */
94 | export const getElementOfProblemStatus = () => {
95 | if (pageType !== "task") return undefined;
96 | const psRaw = document
97 | ?.getElementById("main-container")
98 | ?.getElementsByTagName("p");
99 | const ps = Array.prototype.slice.call(psRaw) as HTMLParagraphElement[];
100 | if (!psRaw) return undefined;
101 |
102 | const problemStatuses = ps.filter((p) => {
103 | return (
104 | p.textContent?.includes("メモリ制限") ||
105 | p.textContent?.includes("Memory Limit")
106 | );
107 | });
108 | return problemStatuses[0];
109 | };
110 |
--------------------------------------------------------------------------------
/src/utils/parser.ts:
--------------------------------------------------------------------------------
1 | // AtCoderの問題ページをパースする
2 |
3 | /**
4 | * URLをパースする パラメータを消す \
5 | * 例: in: https://atcoder.jp/contests/abc210?lang=en \
6 | * 例: out: (5)['https:', '', 'atcoder.jp', 'contests', 'abc210']
7 | */
8 | export const parseURL = (url: string) => {
9 | // 区切り文字`/`で分割する
10 | // ?以降の文字列を削除してパラメータを削除する
11 | return url.split("/").map((x) => x.replace(/\?.*/i, ""));
12 | };
13 |
14 | export const URL = parseURL(window.location.href);
15 |
16 | /**
17 | * 表セル要素から、前の要素のテキストが引数と一致する要素を探す
18 | * 個別の提出ページで使うことを想定
19 | * 例: searchSubmissionInfo(["問題", "Task"])
20 | */
21 | const searchSubmissionInfo = (key: string[]) => {
22 | const tdTags = document.getElementsByTagName("td");
23 | const tdTagsArray = Array.prototype.slice.call(
24 | tdTags
25 | ) as HTMLTableCellElement[];
26 |
27 | return tdTagsArray.filter((elem) => {
28 | const prevElem = elem.previousElementSibling;
29 | const text = prevElem?.textContent;
30 | if (typeof text === "string") return key.includes(text);
31 | return false;
32 | })[0];
33 | };
34 |
35 | /** コンテストタイトル 例: AtCoder Beginner Contest 210 */
36 | export const contestTitle =
37 | document.getElementsByClassName("contest-title")[0]?.textContent ?? "";
38 |
39 | /** コンテストID 例: abc210 */
40 | export const contestID = URL[4] ?? "";
41 |
42 | /**
43 | * ページ種類 \
44 | * 基本的にコンテストIDの次のパス
45 | * ### 例外
46 | * 個別の問題: task
47 | * 個別の提出: submission
48 | * 個別の問題ページで解説ボタンを押すと遷移する個別の問題の解説一覧ページ: task_editorial
49 | */
50 | export const pageType = ((): string => {
51 | if (URL.length < 6) return "";
52 | if (URL.length >= 7 && URL[5] === "submissions" && URL[6] !== "me")
53 | return "submission";
54 | if (URL.length >= 8 && URL[5] === "tasks" && URL[7] === "editorial")
55 | return "task_editorial";
56 | if (URL.length >= 7 && URL[5] === "tasks") return "task";
57 | return URL[5] ?? "";
58 | })();
59 |
60 | /** 問題ID 例: abc210_a */
61 | export const taskID = ((): string => {
62 | if (pageType === "task") {
63 | // 問題ページでは、URLから問題IDを取り出す
64 | return URL[6] ?? "";
65 | }
66 | if (pageType === "submission") {
67 | // 個別の提出ページでは、問題リンクのURLから問題IDを取り出す
68 | // 提出情報の問題のURLを取得する
69 | const taskCell = searchSubmissionInfo(["問題", "Task"]);
70 | if (!taskCell) return "";
71 | const taskLink = taskCell.getElementsByTagName("a")[0];
72 | if (!taskLink) return "";
73 | const taskUrl = parseURL(taskLink.href);
74 |
75 | const taskIDParsed = taskUrl[6] ?? "";
76 | return taskIDParsed;
77 | }
78 | return "";
79 | })();
80 |
81 | /** 問題名 例: A - Cabbages */
82 | export const taskTitle = ((): string => {
83 | if (pageType === "task") {
84 | // 問題ページでは、h2から問題名を取り出す
85 | return (
86 | document
87 | .getElementsByClassName("h2")[0]
88 | ?.textContent?.trim()
89 | .replace(/\n.*/i, "") ?? ""
90 | );
91 | }
92 | if (pageType === "submission") {
93 | // 個別の提出ページでは、問題リンクのテキストから問題名を取り出す
94 | // 提出情報の問題のテキストを取得する
95 | const taskCell = searchSubmissionInfo(["問題", "Task"]);
96 | if (!taskCell) return "";
97 | const taskLink = taskCell.getElementsByTagName("a")[0];
98 | if (!taskLink) return "";
99 | return taskLink.textContent ?? "";
100 | }
101 | return "";
102 | })();
103 |
104 | /** 提出ユーザー 例: machikane */
105 | export const submissionsUser = ((): string => {
106 | if (pageType !== "submission") return "";
107 | // 個別の提出ページのとき
108 |
109 | const userCell = searchSubmissionInfo(["ユーザ", "User"]);
110 | if (!userCell) return "";
111 |
112 | return userCell?.textContent?.trim() ?? "";
113 | })();
114 |
115 | /** 提出結果 例: AC */
116 | export const judgeStatus = ((): string => {
117 | if (pageType !== "submission") return "";
118 | // 個別の提出ページのとき
119 |
120 | const statusCell = searchSubmissionInfo(["結果", "Status"]);
121 | if (!statusCell) return "";
122 |
123 | return statusCell?.textContent?.trim() ?? "";
124 | })();
125 |
126 | /** 得点 例: 100 */
127 | export const judgeScore = ((): number => {
128 | if (pageType !== "submission") return 0;
129 | // 個別の提出ページのとき
130 |
131 | const scoreCell = searchSubmissionInfo(["得点", "Score"]);
132 | if (!scoreCell) return 0;
133 |
134 | return parseInt(scoreCell?.textContent?.trim() ?? "0", 10);
135 | })();
136 |
--------------------------------------------------------------------------------
/src/utils/analyzeSubmissions.ts:
--------------------------------------------------------------------------------
1 | import { nonPenaltyJudge, SubmissionEntry } from "./index";
2 | import { contestID, taskID } from "./parser";
3 |
4 | export type analyzeReturn = {
5 | before: {
6 | maxScore: SubmissionEntry | undefined;
7 | firstAc: SubmissionEntry | undefined;
8 | representative: SubmissionEntry | undefined;
9 | };
10 | during: {
11 | maxScore: SubmissionEntry | undefined;
12 | firstAc: SubmissionEntry | undefined;
13 | representative: SubmissionEntry | undefined;
14 | penalties: number;
15 | };
16 | after: {
17 | maxScore: SubmissionEntry | undefined;
18 | firstAc: SubmissionEntry | undefined;
19 | representative: SubmissionEntry | undefined;
20 | };
21 | another: {
22 | maxScore: SubmissionEntry | undefined;
23 | firstAc: SubmissionEntry | undefined;
24 | representative: SubmissionEntry | undefined;
25 | };
26 | };
27 |
28 | /**
29 | * 得点が最大の提出を返す
30 | */
31 | const parseMaxScore = (submissionsArg: SubmissionEntry[]) => {
32 | if (submissionsArg.length === 0) {
33 | return undefined;
34 | }
35 | const maxScore = submissionsArg.reduce((left, right) =>
36 | left.point > right.point ? left : right
37 | );
38 | return maxScore;
39 | };
40 |
41 | /**
42 | * ペナルティ数を数える
43 | */
44 | const parsePenalties = (submissionsArg: SubmissionEntry[]) => {
45 | let penalties = 0;
46 | let hasAccepted = false;
47 | submissionsArg.forEach((element) => {
48 | hasAccepted = element.result === "AC" || hasAccepted;
49 | if (!hasAccepted && !nonPenaltyJudge.includes(element.result)) {
50 | penalties += 1;
51 | }
52 | });
53 |
54 | return penalties;
55 | };
56 |
57 | /**
58 | * 最初にACした提出を返す
59 | */
60 | const parseFirstAcceptedTime = (submissionsArg: SubmissionEntry[]) => {
61 | const ac = submissionsArg.filter((element) => element.result === "AC");
62 | return ac[0];
63 | };
64 |
65 | /**
66 | * 代表的な提出を返す
67 | * 1. 最後にACした提出
68 | * 2. 最後の提出
69 | * 3. undefined
70 | */
71 | const parseRepresentativeSubmission = (submissionsArg: SubmissionEntry[]) => {
72 | const ac = submissionsArg.filter((element) => element.result === "AC");
73 | const nonAC = submissionsArg.filter((element) => element.result !== "AC");
74 |
75 | if (ac.length > 0) return ac.slice(-1)[0];
76 | if (nonAC.length > 0) return nonAC.slice(-1)[0];
77 | return undefined;
78 | };
79 |
80 | /**
81 | * 提出をパースして情報を返す
82 | * 対象: コンテスト前,中,後の提出 別コンテストの同じ問題への提出
83 | * 返す情報: 得点が最大の提出 最初のACの提出 代表的な提出 ペナルティ数
84 | */
85 | export const analyzeSubmissions = (
86 | submissionsArg: SubmissionEntry[]
87 | ): analyzeReturn => {
88 | const submissions = submissionsArg.filter(
89 | (element) => element.problem_id === taskID
90 | );
91 |
92 | const beforeContest = submissions.filter(
93 | (element) =>
94 | element.contest_id === contestID &&
95 | element.epoch_second < startTime.unix()
96 | );
97 |
98 | const duringContest = submissions.filter(
99 | (element) =>
100 | element.contest_id === contestID &&
101 | element.epoch_second >= startTime.unix() &&
102 | element.epoch_second < endTime.unix()
103 | );
104 |
105 | const afterContest = submissions.filter(
106 | (element) =>
107 | element.contest_id === contestID && element.epoch_second >= endTime.unix()
108 | );
109 |
110 | const anotherContest = submissions.filter(
111 | (element) => element.contest_id !== contestID
112 | );
113 |
114 | return {
115 | before: {
116 | maxScore: parseMaxScore(beforeContest),
117 | firstAc: parseFirstAcceptedTime(beforeContest),
118 | representative: parseRepresentativeSubmission(beforeContest),
119 | },
120 | during: {
121 | maxScore: parseMaxScore(duringContest),
122 | firstAc: parseFirstAcceptedTime(duringContest),
123 | representative: parseRepresentativeSubmission(duringContest),
124 | penalties: parsePenalties(duringContest),
125 | },
126 | after: {
127 | maxScore: parseMaxScore(afterContest),
128 | firstAc: parseFirstAcceptedTime(afterContest),
129 | representative: parseRepresentativeSubmission(afterContest),
130 | },
131 | another: {
132 | maxScore: parseMaxScore(anotherContest),
133 | firstAc: parseFirstAcceptedTime(anotherContest),
134 | representative: parseRepresentativeSubmission(anotherContest),
135 | },
136 | };
137 | };
138 |
139 | export type Period = "before" | "during" | "after" | "another";
140 |
141 | /**
142 | * 提出状況を表すラベルを生成
143 | */
144 | export const generateStatusLabel = (
145 | submission: SubmissionEntry | undefined,
146 | type: Period
147 | ) => {
148 | if (submission === undefined) {
149 | return "";
150 | }
151 |
152 | const isAC = submission.result === "AC";
153 |
154 | let className = "";
155 | switch (type) {
156 | case "before":
157 | className = "label-primary";
158 | break;
159 | case "during":
160 | className = isAC ? "label-success" : "label-warning";
161 | break;
162 | case "after":
163 | className = isAC
164 | ? "label-success-after-contest"
165 | : "label-warning-after-contest";
166 | break;
167 | case "another":
168 | className = "label-default";
169 | break;
170 | default:
171 | break;
172 | }
173 |
174 | let content = "";
175 | switch (type) {
176 | case "before":
177 | content = "コンテスト前の提出";
178 | break;
179 | case "during":
180 | content = "コンテスト中の提出";
181 | break;
182 | case "after":
183 | content = "コンテスト後の提出";
184 | break;
185 | case "another":
186 | content = "別コンテストの同じ問題への提出";
187 | break;
188 | default:
189 | break;
190 | }
191 |
192 | const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`;
193 |
194 | return `
196 | ${submission.result}
197 | `;
198 | };
199 |
200 | /**
201 | * ペナルティ数を表示
202 | */
203 | export const generatePenaltiesCount = (penalties: number) => {
204 | if (penalties <= 0) {
205 | return "";
206 | }
207 |
208 | const content = "コンテスト中のペナルティ数";
209 |
210 | return `
211 | (${penalties.toString()})
212 | `;
213 | };
214 |
215 | /**
216 | * 最初のACの時間を表示
217 | */
218 | export const generateFirstAcTime = (
219 | submission: SubmissionEntry | undefined
220 | ) => {
221 | if (submission === undefined) {
222 | return "";
223 | }
224 |
225 | const content = "提出時間";
226 |
227 | const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`;
228 |
229 | const elapsed = submission.epoch_second - startTime.unix();
230 | const elapsedSeconds = elapsed % 60;
231 | const elapsedMinutes = Math.trunc(elapsed / 60);
232 |
233 | return `
234 |
235 | ${elapsedMinutes}:${elapsedSeconds}
236 |
237 | `;
238 | };
239 |
240 | /**
241 | * マラソン用に得点を表示するスパンを生成
242 | */
243 | export const generateScoreSpan = (
244 | submission: SubmissionEntry | undefined,
245 | type: Period
246 | ) => {
247 | if (submission === undefined) {
248 | return "";
249 | }
250 |
251 | // マラソン用を考えているのでとりあえず1万点未満は表示しない
252 | if (submission.point < 10000) {
253 | return "";
254 | }
255 |
256 | let className = "";
257 | switch (type) {
258 | case "before":
259 | className = "difficulty-blue";
260 | break;
261 | case "during":
262 | className = "difficulty-green";
263 | break;
264 | case "after":
265 | className = "difficulty-yellow";
266 | break;
267 | case "another":
268 | className = "difficulty-grey";
269 | break;
270 | default:
271 | break;
272 | }
273 |
274 | let content = "";
275 | switch (type) {
276 | case "before":
277 | content = "コンテスト前の提出";
278 | break;
279 | case "during":
280 | content = "コンテスト中の提出";
281 | break;
282 | case "after":
283 | content = "コンテスト後の提出";
284 | break;
285 | case "another":
286 | content = "別コンテストの同じ問題への提出";
287 | break;
288 | default:
289 | break;
290 | }
291 |
292 | const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`;
293 |
294 | return `
296 |
297 | ${submission.point}
298 |
299 | `;
300 | };
301 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | getEstimatedDifficulties,
3 | getProblems,
4 | } from "atcoder-problems-api/information";
5 | import type { getSubmissions } from "atcoder-problems-api/submission";
6 | import {
7 | addTypical90Difficulty,
8 | backwardCompatibleProcessing,
9 | hideDifficultyID,
10 | } from "./utils";
11 | // HACK: もっとスマートに呼ぶ方法はある?
12 | // atcoder-problems-apiをバンドルせずに型だけ呼び出す
13 | // ユーザースクリプトの@requireで呼ぶためバンドルは不要
14 | import difficultyCircle from "./components/difficultyCircle";
15 | import html from "./components/setting.html";
16 | import css from "./style/_custom.scss";
17 | import {
18 | analyzeSubmissions,
19 | generateFirstAcTime,
20 | generatePenaltiesCount,
21 | generateScoreSpan,
22 | generateStatusLabel,
23 | } from "./utils/analyzeSubmissions";
24 | import {
25 | getElementOfProblemStatus,
26 | getElementsColorizable,
27 | } from "./utils/getElementsColorizable";
28 | import isContestOver from "./utils/isContestOver";
29 | import { URL, taskID } from "./utils/parser";
30 | import { clipDifficulty, getRatingColorClass } from "./utils/problemsIndex";
31 |
32 | /**
33 | * コンテストページ の処理 \
34 | * メインの処理
35 | */
36 | const contestPageProcess = async () => {
37 | // コンテスト終了前は不要なので無効化する
38 | if (!isContestOver()) return;
39 |
40 | // FIXME: ダークテーマ対応
41 | GM_addStyle(css);
42 |
43 | /** 問題一覧取得 */
44 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
45 | // @ts-ignore
46 | const problems = await getProblems();
47 |
48 | /** 難易度取得 */
49 | const problemModels = addTypical90Difficulty(
50 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
51 | // @ts-ignore
52 | await getEstimatedDifficulties(),
53 | problems
54 | );
55 | // FIXME: PAST対応
56 | // FIXME: JOI非公式難易度表対応
57 |
58 | /** 提出状況取得 */
59 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
60 | // @ts-ignore
61 | const submissions = await getSubmissions(userScreenName);
62 |
63 | // 色付け対象の要素の配列を取得する
64 | // 難易度が無いものを除く
65 | const elementsColorizable = getElementsColorizable().filter(
66 | (element) => element.taskID in problemModels
67 | );
68 |
69 | // 問題ステータス(個別の問題ページの実行時間制限とメモリ制限が書かれた部分)を取得する
70 | const elementProblemStatus = getElementOfProblemStatus();
71 |
72 | /**
73 | * 色付け処理を実行する
74 | */
75 | const colorizeElement = () => {
76 | // 問題見出し、問題リンクを色付け
77 | elementsColorizable.forEach((element) => {
78 | const model = problemModels[element.taskID];
79 | // 難易度がUnavailableならばdifficultyプロパティが無い
80 | // difficultyの値をNaNとする
81 | const difficulty = clipDifficulty(model?.difficulty ?? NaN);
82 | // 色付け
83 | if (!Number.isNaN(difficulty)) {
84 | const color = getRatingColorClass(difficulty);
85 | // eslint-disable-next-line no-param-reassign
86 | element.element.classList.add(color);
87 | } else {
88 | element.element.classList.add("difficulty-unavailable");
89 | }
90 |
91 | // 🧪追加
92 | if (model?.is_experimental) {
93 | element.element.insertAdjacentText("afterbegin", "🧪");
94 | }
95 |
96 | // ◒難易度円追加
97 | element.element.insertAdjacentHTML(
98 | element.afterbegin ? "afterbegin" : "beforebegin",
99 | difficultyCircle(difficulty, element.big, model?.extra_difficulty)
100 | );
101 | });
102 |
103 | // 個別の問題ページのところに難易度等情報を追加
104 | if (elementProblemStatus) {
105 | // 難易度の値を表示する
106 |
107 | // 難易度推定の対象外なら、この値はundefined
108 | const model = problemModels[taskID];
109 |
110 | // 難易度がUnavailableのときはdifficultyの値をNaNとする
111 | // 難易度がUnavailableならばdifficultyプロパティが無い
112 | const difficulty = clipDifficulty(model?.difficulty ?? NaN);
113 |
114 | // 色付け
115 | let className = "";
116 | if (difficulty) {
117 | className = getRatingColorClass(difficulty);
118 | } else if (model) {
119 | className = "difficulty-unavailable";
120 | } else {
121 | className = "";
122 | }
123 |
124 | // Difficultyの値設定
125 | let value = "";
126 | if (difficulty) {
127 | value = difficulty.toString();
128 | } else if (model) {
129 | value = "Unavailable";
130 | } else {
131 | value = "None";
132 | }
133 | // 🧪追加
134 | const experimentalText = model?.is_experimental ? "🧪" : "";
135 |
136 | const content = `${experimentalText}${value}`;
137 |
138 | elementProblemStatus.insertAdjacentHTML(
139 | "beforeend",
140 | ` / Difficulty:
141 | ${content}`
142 | );
143 |
144 | /** この問題への提出 提出時間ソート済みと想定 */
145 | const thisTaskSubmissions = submissions.filter(
146 | (element) => element.problem_id === taskID
147 | );
148 | const analyze = analyzeSubmissions(thisTaskSubmissions);
149 |
150 | // コンテスト前中後外の提出状況 コンテスト中の解答時間とペナルティ数を表示する
151 | let statuesHTML = "";
152 | statuesHTML += generateStatusLabel(
153 | analyze.before.representative,
154 | "before"
155 | );
156 | statuesHTML += generateStatusLabel(
157 | analyze.during.representative,
158 | "during"
159 | );
160 | statuesHTML += generateStatusLabel(analyze.after.representative, "after");
161 | statuesHTML += generateStatusLabel(
162 | analyze.another.representative,
163 | "another"
164 | );
165 | statuesHTML += generatePenaltiesCount(analyze.during.penalties);
166 | statuesHTML += generateFirstAcTime(analyze.during.firstAc);
167 |
168 | if (statuesHTML.length > 0) {
169 | elementProblemStatus.insertAdjacentHTML(
170 | "beforeend",
171 | ` / Status: ${statuesHTML}`
172 | );
173 | }
174 |
175 | // コンテスト前中後外の1万点以上の最大得点を表示する
176 | // NOTE: マラソン用のため、1万点以上とした
177 | let scoresHTML = "";
178 | scoresHTML += generateScoreSpan(analyze.before.maxScore, "before");
179 | scoresHTML += generateScoreSpan(analyze.during.maxScore, "during");
180 | scoresHTML += generateScoreSpan(analyze.after.maxScore, "after");
181 | scoresHTML += generateScoreSpan(analyze.another.maxScore, "another");
182 |
183 | if (scoresHTML.length > 0) {
184 | elementProblemStatus.insertAdjacentHTML(
185 | "beforeend",
186 | ` / Scores: ${scoresHTML}`
187 | );
188 | }
189 | }
190 |
191 | // bootstrap3のtooltipを有効化 難易度円の値を表示するtooltip
192 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
193 | // @ts-ignore
194 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, no-undef
195 | $('[data-toggle="tooltip"]').tooltip();
196 | };
197 |
198 | // 色付け処理実行
199 | if (!GM_getValue(hideDifficultyID, false)) {
200 | // 設定 ネタバレ防止がOFFなら何もせず実行
201 | colorizeElement();
202 | } else {
203 | // 設定 ネタバレ防止がONなら
204 | // ページ上部にボタンを追加 押すと色付け処理が実行される
205 | const place =
206 | document.getElementsByTagName("h2")[0] ??
207 | document.getElementsByClassName("h2")[0] ??
208 | undefined;
209 | if (place) {
210 | place.insertAdjacentHTML(
211 | "beforebegin",
212 | ``
214 | );
215 |
216 | const button = document.getElementById(hideDifficultyID);
217 |
218 | if (button) {
219 | button.addEventListener("click", () => {
220 | button.style.display = "none";
221 | colorizeElement();
222 | });
223 | }
224 | }
225 | }
226 | };
227 |
228 | /**
229 | * 設定ページ の処理 \
230 | * 設定ボタンを追加する
231 | */
232 | const settingPageProcess = () => {
233 | const insertion = document.getElementsByClassName("form-horizontal")[0];
234 | if (insertion === undefined) return;
235 |
236 | insertion.insertAdjacentHTML("afterend", html);
237 |
238 | // 設定 ネタバレ防止のチェックボックスの読み込み 切り替え 保存処理を追加
239 | const hideDifficultyChechbox = document.getElementById(hideDifficultyID);
240 | if (
241 | hideDifficultyChechbox &&
242 | hideDifficultyChechbox instanceof HTMLInputElement
243 | ) {
244 | hideDifficultyChechbox.checked = GM_getValue(hideDifficultyID, false);
245 | hideDifficultyChechbox.addEventListener("change", () => {
246 | GM_setValue(hideDifficultyID, hideDifficultyChechbox.checked);
247 | });
248 | }
249 | };
250 |
251 | /**
252 | * 最初に実行される部分 \
253 | * 共通の処理を実行した後ページごとの処理を実行する
254 | */
255 | (async () => {
256 | // 共通の処理
257 | backwardCompatibleProcessing();
258 |
259 | // ページ別の処理
260 | if (URL[3] === "contests" && URL.length >= 5) {
261 | await contestPageProcess();
262 | }
263 | if (URL[3] === "settings" && URL.length === 4) {
264 | settingPageProcess();
265 | }
266 | })().catch((error) => {
267 | // eslint-disable-next-line no-console
268 | console.error("[atcoder-difficulty-display]", error);
269 | });
270 |
--------------------------------------------------------------------------------