您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AtCoderの解説に投票します。
当前为
您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
- // ==UserScript==
- // @name AtCoder Editorial Voting
- // @namespace https://atcoder.jp/
- // @version 2024-04-20
- // @description AtCoderの解説に投票します。
- // @license MIT
- // @author magurofly
- // @match https://atcoder.jp/contests/*/editorial
- // @match https://atcoder.jp/contests/*/editorial?*
- // @match https://atcoder.jp/contests/*/tasks/*/editorial
- // @match https://atcoder.jp/contests/*/tasks/*/editorial?*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=atcoder.jp
- // @grant unsafeWindow
- // @grant GM_getValue
- // @grant GM_setValue
- // ==/UserScript==
- // AtCoder で定義されている以下の変数を使用します
- // - contestScreenName
- // - userScreenName
- (function() {
- "use strict";
- // このスクリプトの機能
- // - 解説リンクに投票スコアと投票ボタンを表示する
- // - バックエンドにログインする(ため、一時的に所属欄を書き換える)
- // - 投票する
- let token = GM_getValue("token", null);
- function canonicalizeEditorialLink(url) {
- const prefix = "https://atcoder.jp/jump?url=";
- if (url.startsWith(prefix)) {
- return decodeURIComponent(url.slice(prefix.length));
- }
- return url;
- }
- function encodeFormData(data) {
- return Object.keys(data).map(key => encodeURIComponent(key) + "=" + encodeURIComponent(data[key]) ).join("&");
- }
- async function callApi(name, body) {
- const result = await fetch("https://magurofly.zapto.org/" + name, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(body),
- }).then(res => res.json());
- if (result.status == "error") {
- if (result.reason == "invalid token") {
- token = null;
- }
- throw "Error: " + result.reason;
- }
- return result;
- }
- async function login() {
- // 所属トークンを得る
- const affiliationTokenData = await callApi("create-affiliation-token", { atcoder_id: unsafeWindow.userScreenName });
- const affiliation_token = affiliationTokenData.affiliation_token;
- // 設定を得る
- const profileSettings = new DOMParser().parseFromString(await fetch("https://atcoder.jp/settings").then(res => res.text()), "text/html");
- const data = {};
- for (const input of profileSettings.querySelector("#main-container form").elements) {
- data[input.name] = input.value;
- }
- const oldAffiliation = data["ui.Affiliation"];
- // 所属に所属トークンを設定する
- data["ui.Affiliation"] = affiliation_token;
- await fetch("https://atcoder.jp/settings", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- body: encodeFormData(data),
- });
- // 認証する
- const tokenData = await callApi("create-token", { atcoder_id: unsafeWindow.userScreenName, affiliation_token });
- // 所属を元に戻す
- data["ui.Affiliation"] = oldAffiliation;
- await fetch("https://atcoder.jp/settings", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- body: encodeFormData(data),
- });
- // トークンを保存する
- token = tokenData.token;
- GM_setValue("token", token);
- }
- // 投票する
- async function sendVote(editorial, vote) {
- if (token == null) {
- await login();
- }
- callApi("vote", {
- token,
- contest: unsafeWindow.contestScreenName,
- editorial,
- vote,
- });
- }
- // レート分布を表示するやつ
- class Histogram {
- constructor() {
- this.canvas = document.createElement("canvas");
- this.canvas.width = 320;
- this.canvas.height = 160;
- this.ctx = this.canvas.getContext("2d");
- this.dist = [0, 0, 0, 0, 0, 0, 0, 0];
- this.draw();
- }
- setRatingDistribution(dist) {
- this.dist = dist;
- this.draw();
- }
- draw() {
- const colors = ["#808080", "#804000", "#008000", "#00C0C0", "#0000FF", "#C0C000", "#FF8000", "#FF0000"];
- const vHalf = this.canvas.height / 2;
- const vUnit = (vHalf - 16) / Math.max(4, ...this.dist.map(y => Math.abs(y)));
- const hUnit = this.canvas.width / 8;
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
- this.ctx.fillStyle = "#333";
- this.ctx.fillRect(0, this.canvas.height / 2 - 1, hUnit * 8, 2);
- this.ctx.font = "12px serif";
- this.ctx.textAlign = "center";
- for (let i = 0; i < 8; i++) {
- const x = hUnit * i;
- const value = this.dist[i];
- this.ctx.fillStyle = colors[i];
- if (value > 0) {
- this.ctx.fillRect(x, vHalf - 1 - vUnit * value, hUnit, vUnit * value - 1);
- this.ctx.fillStyle = "#333";
- this.ctx.fillText(value.toString(), x + hUnit / 2, vHalf - 4 - vUnit * value);
- } else if (value < 0) {
- this.ctx.fillRect(x, vHalf + 1 + vUnit * -value, hUnit, vUnit * value - 1);
- this.ctx.fillStyle = "#333";
- this.ctx.fillText(value.toString(), x + hUnit / 2, vHalf + 16 + vUnit * -value);
- }
- }
- }
- }
- // 解説リンクにスコアと投票ボタンを表示する
- class Voting {
- constructor(editorial, elements) {
- this.editorial = editorial;
- this.elements = elements;
- this.score = 0;
- this.vote = 0;
- elements.btnUpVote.onclick = this.setVote.bind(this, 1);
- elements.btnDownVote.onclick = this.setVote.bind(this, -1);
- }
- setCurrentVote(score, vote, dist) {
- this.vote = vote;
- this.score = score;
- this.elements.scoreView.textContent = score;
- this.elements.histogram.setRatingDistribution(dist);
- if (vote == 1) {
- this.elements.btnUpVote.classList.add("active");
- this.elements.btnUpVote.onclick = this.setVote.bind(this, 0);
- this.elements.btnDownVote.classList.remove("active");
- this.elements.btnDownVote.onclick = this.setVote.bind(this, -1);
- } else if (vote == -1) {
- this.elements.btnUpVote.classList.remove("active");
- this.elements.btnUpVote.onclick = this.setVote.bind(this, 1);
- this.elements.btnDownVote.classList.add("active");
- this.elements.btnDownVote.onclick = this.setVote.bind(this, 0);
- } else {
- this.elements.btnUpVote.classList.remove("active");
- this.elements.btnUpVote.onclick = this.setVote.bind(this, 1);
- this.elements.btnDownVote.classList.remove("active");
- this.elements.btnDownVote.onclick = this.setVote.bind(this, -1);
- }
- }
- async setVote(vote) {
- this.score += vote - this.vote;
- this.setCurrentVote(this.score, this.vote);
- if (vote == 1) {
- await sendVote(this.editorial, "up");
- } else if (vote == -1) {
- await sendVote(this.editorial, "down");
- } else {
- await sendVote(this.editorial, "none");
- }
- }
- }
- const votes = [];
- for (const link of unsafeWindow.document.querySelectorAll("#main-container a[rel=noopener]")) {
- // リンク先を正規化する
- const editorial = canonicalizeEditorialLink(link.href);
- // ここのデザインは burioden 様に助けていただきました
- const scoreView = document.createElement("span");
- Object.assign(scoreView.style, {
- verticalAlign: "middle",
- display: "inline-block",
- boxSizing: "border-box",
- height: "100%",
- padding: "1px 5px",
- lineHeight: "1.5",
- borderTop: "1px solid #aaa",
- borderBottom: "1px solid #aaa",
- background: "transparent",
- color: "#333",
- fontSize: "12px",
- });
- scoreView.textContent = "0";
- const btnUpVote = document.createElement("button");
- btnUpVote.className = "btn btn-xs btn-warning";
- Object.assign(btnUpVote.style, {
- border: "1px solid #aaa",
- borderRadius: "0 5px 5px 0",
- height: "100%",
- });
- btnUpVote.type = "button";
- btnUpVote.textContent = "+";
- const btnDownVote = document.createElement("button");
- btnDownVote.className = "btn btn-xs btn-info";
- Object.assign(btnDownVote.style, {
- border: "1px solid #aaa",
- borderRadius: "5px 0 0 5px",
- height: "100%",
- });
- btnDownVote.type = "button";
- btnDownVote.textContent = "-";
- const buttonGroup = document.createElement("span");
- Object.assign(buttonGroup.style, {
- display: "inline-block",
- height: "1.5em",
- margin: "0 8px",
- });
- buttonGroup.appendChild(btnDownVote);
- buttonGroup.appendChild(scoreView);
- buttonGroup.appendChild(btnUpVote);
- link.parentElement.insertBefore(buttonGroup, link);
- // キャンバスをつくる
- const anchor = buttonGroup.getBoundingClientRect();
- const histogram = new Histogram();
- Object.assign(histogram.canvas.style, {
- position: "absolute",
- left: anchor.x + anchor.width * 0.5 + "px",
- top: anchor.y + anchor.height + "px",
- display: "none",
- border: "1px solid #aaa",
- background: "#fff",
- boxShadow: "10px 5px 5px #333",
- });
- document.body.appendChild(histogram.canvas);
- scoreView.addEventListener("mouseover", () => {
- histogram.canvas.style.display = "block";
- });
- scoreView.addEventListener("mouseout", () => {
- histogram.canvas.style.display = "none";
- });
- votes.push(new Voting(editorial, { scoreView, btnUpVote, btnDownVote, buttonGroup, histogram }));
- }
- callApi("statuses", { token, editorials: votes.map(v => v.editorial) }).then(res => {
- for (let i = 0; i < res.results.length; i++) {
- const { score, scores_by_rating, current_vote } = res.results[i];
- const vote = current_vote == "up" ? 1 : current_vote == "down" ? -1 : 0;
- const dist = [0, 0, 0, 0, 0, 0, 0, 0];
- for (const [key, value] of Object.entries(scores_by_rating)) {
- const rating = parseInt(key.split("-")[0]);
- if (rating < 2800) {
- dist[Math.trunc(rating / 400)] += value;
- } else {
- dist[7] += value;
- }
- }
- votes[i].setCurrentVote(score, vote, dist);
- }
- });
- })();