// ==UserScript== // @name ac-predictor-cn // @namespace https://github.com/GoodCoder666/ac-predictor-extension-CN // @icon https://atcoder.jp/favicon.ico // @version 1.2.16 // @description AtCoder 预测工具 (由GoodCoder666翻译为简体中文) // @author GoodCoder666 // @license MIT // @supportURL https://github.com/GoodCoder666/ac-predictor-extension-CN/issues // @match https://atcoder.jp/* // @exclude https://atcoder.jp/*/json // ==/UserScript== function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } var dom = "<div id=\"predictor-alert\" class=\"row\"><h5 class=\"sidemenu-txt\">加载中…</h5></div>\n<div id=\"predictor-data\" class=\"row\">\n <div class=\"input-group col-xs-12\">\n <span class=\"input-group-addon\">名次\n <style>\n .predictor-tooltip-icon:hover+.tooltip{\n opacity: .9;\n filter: alpha(opacity=90);\n }\n </style>\n <span class=\"predictor-tooltip-icon glyphicon glyphicon-question-sign\"></span>\n <div class=\"tooltip fade bottom\" style=\"pointer-events:none\">\n <div class=\"tooltip-arrow\" style=\"left: 18%;\"></div>\n <div class=\"tooltip-inner\">Rated 范围内的排名,多人同名次时加上人数。</div>\n </div>\n </span>\n <input class=\"form-control\" id=\"predictor-input-rank\">\n <span class=\"input-group-addon\">位</span>\n </div>\n \n <div class=\"input-group col-xs-12\">\n <span class=\"input-group-addon\">Performance</span>\n <input class=\"form-control\" id=\"predictor-input-perf\">\n </div>\n\n <div class=\"input-group col-xs-12\">\n <span class=\"input-group-addon\">预计 Rating</span>\n <input class=\"form-control\" id=\"predictor-input-rate\">\n </div>\n</div>\n<div class=\"row\">\n <div class=\"btn-group\">\n <button class=\"btn btn-default\" id=\"predictor-current\">现在的名次</button>\n <button type=\"button\" class=\"btn btn-primary\" id=\"predictor-reload\" data-loading-text=\"更新中…\">更新</button>\n <!--<button class=\"btn btn-default\" id=\"predictor-solved\" disabled>当前问题AC后</button>-->\n </div>\n</div>"; class Result { constructor(isRated, isSubmitted, userScreenName, place, ratedRank, oldRating, newRating, competitions, performance, innerPerformance) { this.IsRated = isRated; this.IsSubmitted = isSubmitted; this.UserScreenName = userScreenName; this.Place = place; this.RatedRank = ratedRank; this.OldRating = oldRating; this.NewRating = newRating; this.Competitions = competitions; this.Performance = performance; this.InnerPerformance = innerPerformance; } } function analyzeStandingsData(fixed, standingsData, aPerfs, defaultAPerf, ratedLimit, isHeuristic) { function analyze(isUserRated) { const contestantAPerf = []; const templateResults = {}; let currentRatedRank = 1; let lastRank = 0; const tiedUsers = []; let ratedInTiedUsers = 0; function applyTiedUsers() { tiedUsers.forEach((data) => { if (isUserRated(data)) { contestantAPerf.push(aPerfs[data.UserScreenName] || defaultAPerf); ratedInTiedUsers++; } }); const ratedRank = currentRatedRank + Math.max(0, ratedInTiedUsers - 1) / 2; tiedUsers.forEach((data) => { templateResults[data.UserScreenName] = new Result(!isHeuristic /* FIXME: Temporary disabled for the AHC rating system */ && isUserRated(data), !isHeuristic || data.TotalResult.Count !== 0, data.UserScreenName, data.Rank, ratedRank, fixed ? data.OldRating : data.Rating, null, data.Competitions, null, null); }); currentRatedRank += ratedInTiedUsers; tiedUsers.length = 0; ratedInTiedUsers = 0; } standingsData.forEach((data) => { if (lastRank !== data.Rank) applyTiedUsers(); lastRank = data.Rank; tiedUsers.push(data); }); applyTiedUsers(); return { contestantAPerf: contestantAPerf, templateResults: templateResults, }; } let analyzedData = analyze((data) => data.IsRated && (!isHeuristic || data.TotalResult.Count !== 0)); let isRated = true; if (analyzedData.contestantAPerf.length === 0) { analyzedData = analyze((data) => data.OldRating < ratedLimit && (!isHeuristic || data.TotalResult.Count !== 0)); isRated = false; } const res = analyzedData; res.isRated = isRated; return res; } class Contest { constructor(contestScreenName, contestInformation, standings, aPerfs) { this.ratedLimit = contestInformation.RatedRange[1] + 1; this.perfLimit = this.ratedLimit + 400; this.standings = standings; this.aPerfs = aPerfs; this.rankMemo = {}; const analyzedData = analyzeStandingsData(standings.Fixed, standings.StandingsData, aPerfs, contestInformation.isHeuristic ? 1000 : ({ 2000: 800, 2800: 1000, Infinity: 1200 }[this.ratedLimit] || 1200), this.ratedLimit, contestInformation.isHeuristic); this.contestantAPerf = analyzedData.contestantAPerf; this.templateResults = analyzedData.templateResults; this.IsRated = analyzedData.isRated; } getRatedRank(X) { if (this.rankMemo[X]) return this.rankMemo[X]; return (this.rankMemo[X] = this.contestantAPerf.reduce((val, APerf) => val + 1.0 / (1.0 + Math.pow(6.0, (X - APerf) / 400.0)), 0.5)); } getPerf(ratedRank) { return Math.min(this.getInnerPerf(ratedRank), this.perfLimit); } getInnerPerf(ratedRank) { let upper = 6144; let lower = -2048; while (upper - lower > 0.5) { const mid = (upper + lower) / 2; if (ratedRank > this.getRatedRank(mid)) upper = mid; else lower = mid; } return Math.round((upper + lower) / 2); } } class Results { } //Copyright © 2017 koba-e964. //from : https://github.com/koba-e964/atcoder-rating-estimator const finf = bigf(400); function bigf(n) { let pow1 = 1; let pow2 = 1; let numerator = 0; let denominator = 0; for (let i = 0; i < n; ++i) { pow1 *= 0.81; pow2 *= 0.9; numerator += pow1; denominator += pow2; } return Math.sqrt(numerator) / denominator; } function f(n) { return ((bigf(n) - finf) / (bigf(1) - finf)) * 1200.0; } /** * calculate unpositivized rating from performance history * @param {Number[]} [history] performance history with ascending order * @returns {Number} unpositivized rating */ function calcRatingFromHistory(history) { const n = history.length; let pow = 1; let numerator = 0.0; let denominator = 0.0; for (let i = n - 1; i >= 0; i--) { pow *= 0.9; numerator += Math.pow(2, history[i] / 800.0) * pow; denominator += pow; } return Math.log2(numerator / denominator) * 800.0 - f(n); } /** * calculate unpositivized rating from last state * @param {Number} [last] last unpositivized rating * @param {Number} [perf] performance * @param {Number} [ratedMatches] count of participated rated contest * @returns {number} estimated unpositivized rating */ function calcRatingFromLast(last, perf, ratedMatches) { if (ratedMatches === 0) return perf - 1200; last += f(ratedMatches); const weight = 9 - 9 * Math.pow(0.9, ratedMatches); const numerator = weight * Math.pow(2, (last / 800.0)) + Math.pow(2, (perf / 800.0)); const denominator = 1 + weight; return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1); } /** * (-inf, inf) -> (0, inf) * @param {Number} [rating] unpositivized rating * @returns {number} positivized rating */ function positivizeRating(rating) { if (rating >= 400.0) { return rating; } return 400.0 * Math.exp((rating - 400.0) / 400.0); } /** * (0, inf) -> (-inf, inf) * @param {Number} [rating] positivized rating * @returns {number} unpositivized rating */ function unpositivizeRating(rating) { if (rating >= 400.0) { return rating; } return 400.0 + 400.0 * Math.log(rating / 400.0); } /** * calculate the performance required to reach a target rate * @param {Number} [targetRating] targeted unpositivized rating * @param {Number[]} [history] performance history with ascending order * @returns {number} performance */ function calcRequiredPerformance(targetRating, history) { let valid = 10000.0; let invalid = -10000.0; for (let i = 0; i < 100; ++i) { const mid = (invalid + valid) / 2; const rating = Math.round(calcRatingFromHistory(history.concat([mid]))); if (targetRating <= rating) valid = mid; else invalid = mid; } return valid; } const colorNames = ["unrated", "gray", "brown", "green", "cyan", "blue", "yellow", "orange", "red"]; function getColor(rating) { const colorIndex = rating > 0 ? Math.min(Math.floor(rating / 400) + 1, 8) : 0; return colorNames[colorIndex]; } class OnDemandResults extends Results { constructor(contest, templateResults) { super(); this.Contest = contest; this.TemplateResults = templateResults; } getUserResult(userScreenName) { if (!Object.prototype.hasOwnProperty.call(this.TemplateResults, userScreenName)) return null; const baseResults = this.TemplateResults[userScreenName]; if (!baseResults) return null; if (!baseResults.Performance) { baseResults.InnerPerformance = this.Contest.getInnerPerf(baseResults.RatedRank); baseResults.Performance = Math.min(baseResults.InnerPerformance, this.Contest.perfLimit); baseResults.NewRating = Math.round(positivizeRating(calcRatingFromLast(unpositivizeRating(baseResults.OldRating), baseResults.Performance, baseResults.Competitions))); } return baseResults; } } class FixedResults extends Results { constructor(results) { super(); this.resultsDic = {}; results.forEach((result) => { this.resultsDic[result.UserScreenName] = result; }); } getUserResult(userScreenName) { return Object.prototype.hasOwnProperty.call(this.resultsDic, userScreenName) ? this.resultsDic[userScreenName] : null; } } class PredictorModel { constructor(model) { this.enabled = model.enabled; this.contest = model.contest; this.history = model.history; this.updateInformation(model.information); this.updateData(model.rankValue, model.perfValue, model.rateValue); } setEnable(state) { this.enabled = state; } updateInformation(information) { this.information = information; } updateData(rankValue, perfValue, rateValue) { this.rankValue = rankValue; this.perfValue = perfValue; this.rateValue = rateValue; } } class CalcFromRankModel extends PredictorModel { updateData(rankValue, perfValue, rateValue) { perfValue = this.contest.getPerf(rankValue); rateValue = positivizeRating(calcRatingFromHistory(this.history.concat([perfValue]))); super.updateData(rankValue, perfValue, rateValue); } } class CalcFromPerfModel extends PredictorModel { updateData(rankValue, perfValue, rateValue) { rankValue = this.contest.getRatedRank(perfValue); rateValue = positivizeRating(calcRatingFromHistory(this.history.concat([perfValue]))); super.updateData(rankValue, perfValue, rateValue); } } class CalcFromRateModel extends PredictorModel { updateData(rankValue, perfValue, rateValue) { perfValue = calcRequiredPerformance(unpositivizeRating(rateValue), this.history); rankValue = this.contest.getRatedRank(perfValue); super.updateData(rankValue, perfValue, rateValue); } } function roundValue(value, numDigits) { return Math.round(value * Math.pow(10, numDigits)) / Math.pow(10, numDigits); } class ContestInformation { constructor(canParticipateRange, ratedRange, penalty, isHeuristic) { this.CanParticipateRange = canParticipateRange; this.RatedRange = ratedRange; this.Penalty = penalty; this.isHeuristic = isHeuristic; } } function parseRangeString(s) { s = s.trim(); if (s === "-") return [0, -1]; if (s === "All") return [0, Infinity]; if (!/[-~]/.test(s)) return [0, -1]; const res = s.split(/[-~]/).map((x) => parseInt(x.trim())); if (isNaN(res[0])) res[0] = 0; if (isNaN(res[1])) res[1] = Infinity; return res; } function parseDurationString(s) { if (s === "None" || s === "なし") return 0; if (!/(\d+[^\d]+)/.test(s)) return NaN; const durationDic = { 日: 24 * 60 * 60 * 1000, day: 24 * 60 * 60 * 1000, days: 24 * 60 * 60 * 1000, 時間: 60 * 60 * 1000, hour: 60 * 60 * 1000, hours: 60 * 60 * 1000, 分: 60 * 1000, minute: 60 * 1000, minutes: 60 * 1000, 秒: 1000, second: 1000, seconds: 1000, }; let res = 0; s.match(/(\d+[^\d]+)/g).forEach((x) => { var _a; const trimmed = x.trim(); const num = parseInt(/\d+/.exec(trimmed)[0]); const unit = /[^\d]+/.exec(trimmed)[0]; const duration = (_a = durationDic[unit]) !== null && _a !== void 0 ? _a : 0; res += num * duration; }); return res; } function fetchJsonDataAsync(url) { return __awaiter(this, void 0, void 0, function* () { const response = yield fetch(url); if (response.ok) return (yield response.json()); throw new Error(`request to ${url} returns ${response.status}`); }); } function fetchTextDataAsync(url) { return __awaiter(this, void 0, void 0, function* () { const response = yield fetch(url); if (response.ok) return response.text(); throw new Error(`request to ${url} returns ${response.status}`); }); } function getStandingsDataAsync(contestScreenName) { return __awaiter(this, void 0, void 0, function* () { return yield fetchJsonDataAsync(`https://atcoder.jp/contests/${contestScreenName}/standings/json`); }); } function getAPerfsDataAsync(contestScreenName) { return __awaiter(this, void 0, void 0, function* () { let url = `https://data.ac-predictor.com/aperfs/${contestScreenName}.json`; // if (contestScreenName === "arc119") url = `https://raw.githubusercontent.com/key-moon/ac-predictor-data/master/aperfs/${contestScreenName}.json`; return yield fetchJsonDataAsync(url); }); } function getResultsDataAsync(contestScreenName) { return __awaiter(this, void 0, void 0, function* () { return yield fetchJsonDataAsync(`https://atcoder.jp/contests/${contestScreenName}/results/json`); }); } function getHistoryDataAsync(userScreenName) { return __awaiter(this, void 0, void 0, function* () { return yield fetchJsonDataAsync(`https://atcoder.jp/users/${userScreenName}/history/json`); }); } function getContestInformationAsync(contestScreenName) { return __awaiter(this, void 0, void 0, function* () { const html = yield fetchTextDataAsync(`https://atcoder.jp/contests/${contestScreenName}`); const topPageDom = new DOMParser().parseFromString(html, "text/html"); const dataParagraph = topPageDom.getElementsByClassName("small")[0]; const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(":")[1].trim()); const isAHC = /^ahc\d{3}$/.test(contestScreenName) || html.includes("This contest is rated for AHC rating"); return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2]), isAHC); }); } /** * ユーザーのPerformance履歴を時間昇順で取得 */ function getPerformanceHistories(history) { const onlyRated = history.filter((x) => x.IsRated); onlyRated.sort((a, b) => { return new Date(a.EndTime).getTime() - new Date(b.EndTime).getTime(); }); return onlyRated.map((x) => x.Performance); } /** * サイドメニューに追加される要素のクラス */ class SideMenuElement { shouldDisplayed(url) { return this.match.test(url); } /** * 要素のHTMLを取得 */ GetHTML() { return `<div class="menu-wrapper"> <div class="menu-header"> <h4 class="sidemenu-txt">${this.title}<span class="glyphicon glyphicon-menu-up" style="float: right"></span></h4> </div> <div class="menu-box"><div class="menu-content" id="${this.id}">${this.document}</div></div> </div>`; } } function getGlobalVals() { const script = [...document.querySelectorAll("head script:not([src])")].map((x) => x.innerHTML).join("\n"); const res = {}; script.match(/var [^ ]+ = .+$/gm).forEach((statement) => { const match = /var ([^ ]+) = (.+)$/m.exec(statement); function safeEval(val) { function trim(val) { while (val.endsWith(";") || val.endsWith(" ")) val = val.substr(0, val.length - 1); while (val.startsWith(" ")) val = val.substr(1, val.length - 1); return val; } function isStringToken(val) { return 1 < val.length && val.startsWith('"') && val.endsWith('"'); } function evalStringToken(val) { if (!isStringToken(val)) throw new Error(); return val.substr(1, val.length - 2); // TODO: parse escape } val = trim(val); if (isStringToken(val)) return evalStringToken(val); if (val.startsWith("moment(")) return new Date(evalStringToken(trim(val.substr(7, val.length - (7 + 1))))); return val; } res[match[1]] = safeEval(match[2]); }); return res; } const globalVals = getGlobalVals(); const userScreenName = globalVals["userScreenName"]; const contestScreenName = globalVals["contestScreenName"]; const startTime = globalVals["startTime"]; class AllRowUpdater { update(table) { Array.from(table.rows).forEach((row) => this.rowModifier.modifyRow(row)); } } class StandingsRowModifier { isHeader(row) { return row.parentElement.tagName.toLowerCase() == "thead"; } isFooter(row) { return row.firstElementChild.hasAttribute("colspan") && row.firstElementChild.getAttribute("colspan") == "3"; } modifyRow(row) { if (this.isHeader(row)) this.modifyHeader(row); else if (this.isFooter(row)) this.modifyFooter(row); else this.modifyContent(row); } } class PerfAndRateChangeAppender extends StandingsRowModifier { modifyContent(content) { var _a; this.removeOldElem(content); if (content.firstElementChild.textContent === "-") { const longCell = content.getElementsByClassName("standings-result")[0]; longCell.setAttribute("colspan", (parseInt(longCell.getAttribute("colspan")) + 2).toString()); return; } const userScreenName = content.querySelector(".standings-username .username span").textContent; const result = (_a = this.results) === null || _a === void 0 ? void 0 : _a.getUserResult(userScreenName); const perfElem = (result === null || result === void 0 ? void 0 : result.IsSubmitted) ? this.getRatingSpan(Math.round(positivizeRating(result.Performance))) : "-"; const ratingElem = result ? (result === null || result === void 0 ? void 0 : result.IsRated) && (this === null || this === void 0 ? void 0 : this.isRated) ? this.getChangedRatingElem(result.OldRating, result.NewRating) : this.getUnratedElem(result.OldRating) : "-"; content.insertAdjacentHTML("beforeend", `<td class="standings-result standings-perf">${perfElem}</td>`); content.insertAdjacentHTML("beforeend", `<td class="standings-result standings-rate">${ratingElem}</td>`); } getChangedRatingElem(oldRate, newRate) { const oldRateSpan = this.getRatingSpan(oldRate); const newRateSpan = this.getRatingSpan(newRate); const diff = this.toSignedString(newRate - oldRate); return `<span class="bold">${oldRateSpan}</span> → <span class="bold">${newRateSpan}</span> <span class="grey">(${diff})</span>`; } toSignedString(n) { return `${n >= 0 ? "+" : ""}${n}`; } getUnratedElem(rate) { return `<span class="bold">${this.getRatingSpan(rate)}</span> <span class="grey">(unrated)</span>`; } getRatingSpan(rate) { return `<span class="user-${getColor(rate)}">${rate}</span>`; } modifyFooter(footer) { this.removeOldElem(footer); footer.insertAdjacentHTML("beforeend", '<td class="standings-result standings-perf standings-rate" colspan="2">-</td>'); } modifyHeader(header) { this.removeOldElem(header); header.insertAdjacentHTML("beforeend", '<th class="standings-result-th standings-perf" style="width:84px;min-width:84px;">Performance</th><th class="standings-result-th standings-rate" style="width:168px;min-width:168px;">Rating 变化</th>'); } removeOldElem(row) { row.querySelectorAll(".standings-perf, .standings-rate").forEach((elem) => elem.remove()); } } class PredictorElement extends SideMenuElement { constructor() { super(...arguments); this.id = "predictor"; this.title = "Predictor"; this.match = /atcoder.jp\/contests\/.+/; this.document = dom; this.historyData = []; this.contestOnUpdated = []; this.resultsOnUpdated = []; } set contest(val) { this._contest = val; this.contestOnUpdated.forEach((func) => func(val)); } get contest() { return this._contest; } set results(val) { this._results = val; this.resultsOnUpdated.forEach((func) => func(val)); } get results() { return this._results; } isStandingsPage() { return /standings([^/]*)?$/.test(document.location.href); } afterAppend() { const loaded = () => !!document.getElementById("standings-tbody"); if (!this.isStandingsPage() || loaded()) { void this.initialize(); return; } const loadingElem = document.getElementById("vue-standings").getElementsByClassName("loading-show")[0]; new MutationObserver(() => { if (loaded()) void this.initialize(); }).observe(loadingElem, { attributes: true }); } initialize() { var _a; return __awaiter(this, void 0, void 0, function* () { const firstContestDate = new Date(2016, 6, 16, 21); const predictorElements = [ "predictor-input-rank", "predictor-input-perf", "predictor-input-rate", "predictor-current", "predictor-reload", ]; const isStandingsPage = this.isStandingsPage(); const contestInformation = yield getContestInformationAsync(contestScreenName); const rowUpdater = new PerfAndRateChangeAppender(); this.resultsOnUpdated.push((val) => { rowUpdater.results = val; }); this.contestOnUpdated.push((val) => { rowUpdater.isRated = val.IsRated; }); const tableUpdater = new AllRowUpdater(); tableUpdater.rowModifier = rowUpdater; const tableElement = (_a = document.getElementById("standings-tbody")) === null || _a === void 0 ? void 0 : _a.parentElement; let model = new PredictorModel({ rankValue: 0, perfValue: 0, rateValue: 0, enabled: false, history: this.historyData, }); const updateData = (aperfs, standings) => __awaiter(this, void 0, void 0, function* () { this.contest = new Contest(contestScreenName, contestInformation, standings, aperfs); model.contest = this.contest; if (this.contest.standings.Fixed && this.contest.IsRated) { const rawResult = yield getResultsDataAsync(contestScreenName); rawResult.sort((a, b) => (a.Place !== b.Place ? a.Place - b.Place : b.OldRating - a.OldRating)); const sortedStandingsData = Array.from(this.contest.standings.StandingsData); if (contestInformation.isHeuristic) sortedStandingsData.filter((x) => x.TotalResult.Count !== 0); sortedStandingsData.sort((a, b) => { if (a.TotalResult.Count === 0 && b.TotalResult.Count === 0) return 0; if (a.TotalResult.Count === 0) return 1; if (b.TotalResult.Count === 0) return -1; if (a.Rank !== b.Rank) return a.Rank - b.Rank; if (b.OldRating !== a.OldRating) return b.OldRating - a.OldRating; if (a.UserIsDeleted) return -1; if (b.UserIsDeleted) return 1; return 0; }); let lastPerformance = this.contest.perfLimit; let deletedCount = 0; this.results = new FixedResults(sortedStandingsData.map((data, index) => { let result = rawResult[index - deletedCount]; if (!result || data.OldRating !== result.OldRating) { deletedCount++; result = null; } return new Result(result ? result.IsRated : false, !contestInformation.isHeuristic || data.TotalResult.Count !== 0, data.UserScreenName, data.Rank, -1, data.OldRating, result ? result.NewRating : 0, 0, result && result.IsRated ? (lastPerformance = result.Performance) : lastPerformance, result ? result.InnerPerformance : 0); })); } else { this.results = new OnDemandResults(this.contest, this.contest.templateResults); } }); if (!shouldEnabledPredictor().verdict) { model.updateInformation(shouldEnabledPredictor().message); updateView(); return; } try { let aPerfs; let standings; try { standings = yield getStandingsDataAsync(contestScreenName); } catch (e) { throw new Error("Standings读取失败。"); } try { aPerfs = yield getAPerfsDataAsync(contestScreenName); } catch (e) { throw new Error("APerf获取失败。"); } yield updateData(aPerfs, standings); model.setEnable(true); model.updateInformation(`最后更新时间: ${new Date().toTimeString().split(" ")[0]}`); if (isStandingsPage) { new MutationObserver(() => { tableUpdater.update(tableElement); }).observe(tableElement.tBodies[0], { childList: true, }); const refreshElem = document.getElementById("refresh"); if (refreshElem) new MutationObserver((mutationRecord) => { const disabled = mutationRecord[0].target.classList.contains("disabled"); if (disabled) { void (() => __awaiter(this, void 0, void 0, function* () { yield updateStandingsFromAPI(); updateView(); }))(); } }).observe(refreshElem, { attributes: true, attributeFilter: ["class"], }); } } catch (e) { model.updateInformation(e.message); model.setEnable(false); } updateView(); { const reloadButton = document.getElementById("predictor-reload"); reloadButton.addEventListener("click", () => { void (() => __awaiter(this, void 0, void 0, function* () { model.updateInformation(""); reloadButton.disabled = true; updateView(); yield updateStandingsFromAPI(); reloadButton.disabled = false; updateView(); }))(); }); document.getElementById("predictor-current").addEventListener("click", () => { const myResult = this.contest.templateResults[userScreenName]; if (!myResult) return; model = new CalcFromRankModel(model); model.updateData(myResult.RatedRank, model.perfValue, model.rateValue); updateView(); }); document.getElementById("predictor-input-rank").addEventListener("keyup", () => { const inputString = document.getElementById("predictor-input-rank").value; const inputNumber = parseInt(inputString); if (!isFinite(inputNumber)) return; model = new CalcFromRankModel(model); model.updateData(inputNumber, 0, 0); updateView(); }); document.getElementById("predictor-input-perf").addEventListener("keyup", () => { const inputString = document.getElementById("predictor-input-perf").value; const inputNumber = parseInt(inputString); if (!isFinite(inputNumber)) return; model = new CalcFromPerfModel(model); model.updateData(0, inputNumber, 0); updateView(); }); document.getElementById("predictor-input-rate").addEventListener("keyup", () => { const inputString = document.getElementById("predictor-input-rate").value; const inputNumber = parseInt(inputString); if (!isFinite(inputNumber)) return; model = new CalcFromRateModel(model); model.updateData(0, 0, inputNumber); updateView(); }); } function updateStandingsFromAPI() { return __awaiter(this, void 0, void 0, function* () { try { const shouldEnabled = shouldEnabledPredictor(); if (!shouldEnabled.verdict) { model.updateInformation(shouldEnabled.message); model.setEnable(false); return; } const standings = yield getStandingsDataAsync(contestScreenName); const aperfs = yield getAPerfsDataAsync(contestScreenName); yield updateData(aperfs, standings); model.updateInformation(`最后更新时间: ${new Date().toTimeString().split(" ")[0]}`); model.setEnable(true); } catch (e) { model.updateInformation(e.message); model.setEnable(false); } }); } function shouldEnabledPredictor() { if (new Date() < startTime) return { verdict: false, message: "比赛暂未开始" }; if (startTime < firstContestDate) return { verdict: false, message: "这场比赛是在使用现行 Rating 制度之前举行的,无法准确计算 Rating 数据。", }; if (contestInformation.RatedRange[0] > contestInformation.RatedRange[1]) return { verdict: false, message: "This contest is unrated.", }; return { verdict: true, message: "" }; } function updateView() { const roundedRankValue = isFinite(model.rankValue) ? roundValue(model.rankValue, 2).toString() : ""; const roundedPerfValue = isFinite(model.perfValue) ? roundValue(model.perfValue, 2).toString() : ""; const roundedRateValue = isFinite(model.rateValue) ? roundValue(model.rateValue, 2).toString() : ""; document.getElementById("predictor-input-rank").value = roundedRankValue; document.getElementById("predictor-input-perf").value = roundedPerfValue; document.getElementById("predictor-input-rate").value = roundedRateValue; document.getElementById("predictor-alert").innerHTML = `<h5 class='sidemenu-txt'>${model.information}</h5>`; if (model.enabled) enabled(); else disabled(); if (isStandingsPage && shouldEnabledPredictor().verdict) { tableUpdater.update(tableElement); } function enabled() { predictorElements.forEach((element) => { document.getElementById(element).disabled = false; }); } function disabled() { predictorElements.forEach((element) => { document.getElementById(element).disabled = false; }); } } }); } afterOpen() { return __awaiter(this, void 0, void 0, function* () { getPerformanceHistories(yield getHistoryDataAsync(userScreenName)).forEach((elem) => this.historyData.push(elem)); }); } } const predictor = new PredictorElement(); var dom$1 = "<div id=\"estimator-alert\"></div>\n<div class=\"row\">\n\t<div class=\"input-group\">\n\t\t<span class=\"input-group-addon\" id=\"estimator-input-desc\"></span>\n\t\t<input type=\"number\" class=\"form-control\" id=\"estimator-input\">\n\t</div>\n</div>\n<div class=\"row\">\n\t<div class=\"input-group\">\n\t\t<span class=\"input-group-addon\" id=\"estimator-res-desc\"></span>\n\t\t<input class=\"form-control\" id=\"estimator-res\" disabled=\"disabled\">\n\t\t<span class=\"input-group-btn\">\n\t\t\t<button class=\"btn btn-default\" id=\"estimator-toggle\">交换</button>\n\t\t</span>\n\t</div>\n</div>\n<div class=\"row\" style=\"margin: 10px 0px;\">\n\t<a class=\"btn btn-default col-xs-offset-8 col-xs-4\" rel=\"nofollow\" onclick=\"window.open(encodeURI(decodeURI(this.href)),'twwindow','width=550, height=450, personalbar=0, toolbar=0, scrollbars=1'); return false;\" id=\"estimator-tweet\">Tweet</a>\n</div>"; class EstimatorModel { constructor(inputValue, perfHistory) { this.inputDesc = ""; this.resultDesc = ""; this.perfHistory = perfHistory; this.updateInput(inputValue); } updateInput(value) { this.inputValue = value; this.resultValue = this.calcResult(value); } toggle() { return null; } calcResult(input) { return input; } } class CalcRatingModel extends EstimatorModel { constructor(inputValue, perfHistory) { super(inputValue, perfHistory); this.inputDesc = "Performance"; this.resultDesc = "预计 Rating"; } toggle() { return new CalcPerfModel(this.resultValue, this.perfHistory); } calcResult(input) { return positivizeRating(calcRatingFromHistory(this.perfHistory.concat([input]))); } } class CalcPerfModel extends EstimatorModel { constructor(inputValue, perfHistory) { super(inputValue, perfHistory); this.inputDesc = "目标 Rating"; this.resultDesc = "所需 Performance"; } toggle() { return new CalcRatingModel(this.resultValue, this.perfHistory); } calcResult(input) { return calcRequiredPerformance(unpositivizeRating(input), this.perfHistory); } } function GetEmbedTweetLink(content, url) { return `https://twitter.com/share?text=${encodeURI(content)}&url=${encodeURI(url)}`; } function getLS(key) { const val = localStorage.getItem(key); return (val ? JSON.parse(val) : val); } function setLS(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); } catch (error) { console.log(error); } } const models = [CalcPerfModel, CalcRatingModel]; function GetModelFromStateCode(state, value, history) { let model = models.find((model) => model.name === state); if (!model) model = CalcPerfModel; return new model(value, history); } class EstimatorElement extends SideMenuElement { constructor() { super(...arguments); this.id = "estimator"; this.title = "Estimator"; this.document = dom$1; this.match = /atcoder.jp/; } afterAppend() { //nothing to do } // nothing to do afterOpen() { return __awaiter(this, void 0, void 0, function* () { const estimatorInputSelector = document.getElementById("estimator-input"); const estimatorResultSelector = document.getElementById("estimator-res"); let model = GetModelFromStateCode(getLS("sidemenu_estimator_state"), getLS("sidemenu_estimator_value"), getPerformanceHistories(yield getHistoryDataAsync(userScreenName))); updateView(); document.getElementById("estimator-toggle").addEventListener("click", () => { model = model.toggle(); updateLocalStorage(); updateView(); }); estimatorInputSelector.addEventListener("keyup", () => { updateModel(); updateLocalStorage(); updateView(); }); /** modelをinputの値に応じて更新 */ function updateModel() { const inputNumber = estimatorInputSelector.valueAsNumber; if (!isFinite(inputNumber)) return; model.updateInput(inputNumber); } /** modelの状態をLSに保存 */ function updateLocalStorage() { setLS("sidemenu_estimator_value", model.inputValue); setLS("sidemenu_estimator_state", model.constructor.name); } /** modelを元にviewを更新 */ function updateView() { const roundedInput = roundValue(model.inputValue, 2); const roundedResult = roundValue(model.resultValue, 2); document.getElementById("estimator-input-desc").innerText = model.inputDesc; document.getElementById("estimator-res-desc").innerText = model.resultDesc; estimatorInputSelector.value = String(roundedInput); estimatorResultSelector.value = String(roundedResult); const tweetStr = `AtCoderのハンドルネーム: ${userScreenName}\n${model.inputDesc}: ${roundedInput}\n${model.resultDesc}: ${roundedResult}\n`; document.getElementById("estimator-tweet").href = GetEmbedTweetLink(tweetStr, "https://greasyfork.org/ja/scripts/369954-ac-predictor"); } }); } } const estimator = new EstimatorElement(); var sidemenuHtml = "<style>\n #menu-wrap {\n display: block;\n position: fixed;\n top: 0;\n z-index: 20;\n width: 400px;\n right: -350px;\n transition: all 150ms 0ms ease;\n margin-top: 50px;\n }\n\n #sidemenu {\n background: #000;\n opacity: 0.85;\n }\n #sidemenu-key {\n border-radius: 5px 0px 0px 5px;\n background: #000;\n opacity: 0.85;\n color: #FFF;\n padding: 30px 0;\n cursor: pointer;\n margin-top: 100px;\n text-align: center;\n }\n\n #sidemenu {\n display: inline-block;\n width: 350px;\n float: right;\n }\n\n #sidemenu-key {\n display: inline-block;\n width: 50px;\n float: right;\n }\n\n .sidemenu-active {\n transform: translateX(-350px);\n }\n\n .sidemenu-txt {\n color: #DDD;\n }\n\n .menu-wrapper {\n border-bottom: 1px solid #FFF;\n }\n\n .menu-header {\n margin: 10px 20px 10px 20px;\n user-select: none;\n }\n\n .menu-box {\n overflow: hidden;\n transition: all 300ms 0s ease;\n }\n .menu-box-collapse {\n height: 0px !important;\n }\n .menu-box-collapse .menu-content {\n transform: translateY(-100%);\n }\n .menu-content {\n padding: 10px 20px 10px 20px;\n transition: all 300ms 0s ease;\n }\n .cnvtb-fixed {\n z-index: 19;\n }\n</style>\n<div id=\"menu-wrap\">\n <div id=\"sidemenu\" class=\"container\"></div>\n <div id=\"sidemenu-key\" class=\"glyphicon glyphicon-menu-left\"></div>\n</div>"; //import "./sidemenu.scss"; class SideMenu { constructor() { this.pendingElements = []; this.Generate(); } Generate() { document.getElementById("main-div").insertAdjacentHTML("afterbegin", sidemenuHtml); resizeSidemenuHeight(); const key = document.getElementById("sidemenu-key"); const wrap = document.getElementById("menu-wrap"); key.addEventListener("click", () => { this.pendingElements.forEach((elem) => { elem.afterOpen(); }); this.pendingElements.length = 0; key.classList.toggle("glyphicon-menu-left"); key.classList.toggle("glyphicon-menu-right"); wrap.classList.toggle("sidemenu-active"); }); window.addEventListener("onresize", resizeSidemenuHeight); document.getElementById("sidemenu").addEventListener("click", (event) => { const target = event.target; const header = target.closest(".menu-header"); if (!header) return; const box = target.closest(".menu-wrapper").querySelector(".menu-box"); box.classList.toggle("menu-box-collapse"); const arrow = target.querySelector(".glyphicon"); arrow.classList.toggle("glyphicon-menu-down"); arrow.classList.toggle("glyphicon-menu-up"); }); function resizeSidemenuHeight() { document.getElementById("sidemenu").style.height = `${window.innerHeight}px`; } } addElement(element) { if (!element.shouldDisplayed(document.location.href)) return; const sidemenu = document.getElementById("sidemenu"); sidemenu.insertAdjacentHTML("afterbegin", element.GetHTML()); const content = sidemenu.querySelector(".menu-content"); content.parentElement.style.height = `${content.offsetHeight}px`; element.afterAppend(); this.pendingElements.push(element); } } const sidemenu = new SideMenu(); const elements = [predictor, estimator]; for (let i = elements.length - 1; i >= 0; i--) { sidemenu.addElement(elements[i]); }