// ==UserScript== // @name ac-predictor // @namespace http://ac-predictor.azurewebsites.net/ // @version 1.0.4 // @description コンテスト中にAtCoderのパフォーマンスを予測します // @author keymoon // @license MIT // @supportURL https://github.com/key-moon/ac-predictor.user.js/issues // @match https://beta.atcoder.jp/* // @exclude https://beta.atcoder.jp/*/json // @downloadURL none // ==/UserScript== //NameSpace SideMenu = {}; SideMenu.Version = 1.0; //共有データセット(HistoryとかStandingsとか) SideMenu.Datas = {}; //共有データセットそれぞれをUpdateする関数を入れておく SideMenu.Datas.Update = {} //Datas.Update内に関数を追加 //History SideMenu.Datas.History = null; SideMenu.Datas.Update.History = (() => { var d = $.Deferred(); try { $.ajax({ url: `https://beta.atcoder.jp/users/${userScreenName}/history/json`, type: "GET", dataType: "json" }).done(history => { SideMenu.Datas.History = history; d.resolve(); }).fail(() => { d.reject(); }); } catch (e) { d.reject(e); } return d.promise(); }); //Standings SideMenu.Datas.Standings = null; SideMenu.Datas.Update.Standings = (() => { var d = $.Deferred(); try { $.ajax({ url: `https://beta.atcoder.jp/contests/${contestScreenName}/standings/json`, type: "GET", dataType: "json" }).done(standings => { SideMenu.Datas.Standings = standings; d.resolve(); }).fail(() => { d.reject(); }); } catch (e) { d.reject(); } return d.promise(); }); //APerfs SideMenu.Datas.APerfs = null; SideMenu.Datas.Update.APerfs = (() => { var d = $.Deferred(); try { $.ajax({ url: `https://ac-predictor.azurewebsites.net/api/aperfs/${contestScreenName}`, type: "GET", dataType: "json" }).done(aperfs => { SideMenu.Datas.APerfs = aperfs d.resolve(); }).fail(() => { d.reject(); }); } catch (e) { d.reject(); } return d.promise(); }); //ライブラリを追加するやつ SideMenu.appendLibrary = function (source) { var defferd = $.Deferred(); $.ajax({ url: source }).done(((src) => { $('head').append(``); defferd.resolve(); })(() => { defferd.fail(); })); return defferd.promise(); }; //モーダル関連 SideMenu.Modal = {} $('body').append(''); SideMenu.Modal.SetContent = ((content) => { $('#modal-standing').html = content; }); SideMenu.Modal.Show = (() => { $('#modal-standing').modal('show'); }); SideMenu.Modal.Hide = (() => { $('#modal-standing').modal('hide'); }); SideMenu.Modal.Toggle = (() => { $('#modal-standing').modal('toggle'); }); //通知関連 SideMenu.Notifications = {}; SideMenu.Notifications.CanSend = false; (async () => { if (Notification.permission === 'denied') return; if (Notification.permission === 'default') { await (async () => { var defferd = $.Deferred(); Notification.requestPermission((permission) => { SideMenu.Notifications.CanSend = permission === 'granted'; defferd.resolve(); })(); return defferd.promise(); }); if (!Notification.permission) return; } })(); SideMenu.appendLibrary("https://koba-e964.github.io/atcoder-rating-estimator/atcoder_rating.js"); //サイドメニュー追加(将来仕様変更が起きる可能性大) SideMenu.appendToSideMenu = async function (match, title, elemFunc) { var defferd = $.Deferred(); try { if (!match.test(location.href)) return; //アコーディオンメニュー var dom = `` $('#sidemenu').append(dom); var contents = $('.menu-content'); var contentElem = contents[contents.length - 1]; $(contentElem).parents('.menu-box').css('height', contentElem.scrollHeight) defferd.resolve(); } catch (e) { console.error(e); defferd.reject(); } return defferd.promise(); }; //サイドメニューを生成 (() => { var menuWidth = 350 var keyWidth = 50 var speed = 150 var sideMenuScript = `` var sideMenuStyle = `` var ratingScript = ``; $('#main-div').append(``); })(); //IndexedDB SideMenu.DataBase = {}; SideMenu.DataBase.Name = "PredictorDB"; SideMenu.DataBase.StoreNames = ["APerfs", "Standings"]; indexedDB.open(SideMenu.DataBase.Name, SideMenu.Version).onupgradeneeded = (event) => { var db = event.target.result; SideMenu.DataBase.StoreNames.forEach(store => { db.createObjectStore(store, { keyPath: "id" }); }); }; SideMenu.DataBase.SetData = (store, key, value) => { var defferd = $.Deferred(); try { indexedDB.open(SideMenu.DataBase.Name).onsuccess = (e) => { var db = e.target.result; var trans = db.transaction(store, 'readwrite'); var objStore = trans.objectStore(store); var data = { id: key, data: value }; var putReq = objStore.put(data); putReq.onsuccess = function () { defferd.resolve(); } } } catch (e) { defferd.reject(e); } return defferd.promise(); }; SideMenu.DataBase.GetData = (store, key) => { var defferd = $.Deferred(); try { indexedDB.open(SideMenu.DataBase.Name).onsuccess = (e) => { var db = e.target.result; var trans = db.transaction(store, 'readwrite'); var objStore = trans.objectStore(store); objStore.get(key).onsuccess = function (event) { var result = event.target.result; if (!result) defferd.reject("key was not found"); else defferd.resolve(result.data); }; } } catch (e) { defferd.reject(e); } return defferd.promise(); }; SideMenu.Files = {}; SideMenu.Files.Save = (value, name) => { var blob = new Blob([value], { type: 'text/plain' }) window.navigator.msSaveBlob(blob, name); }; SideMenu.Files.Load = (async () => { //todo }) //サイドメニュー要素の入れ物 SideMenu.Elements = {}; SideMenu.ViewOrder = ["Predictor", "Estimator"]; SideMenu.Colors = ["unrated", "gray", "brown", "green", "cyan", "blue", "yellow", "orange", "red"]; SideMenu.GetColor = (rating) => { var colorIndex = 0 if (rating > 0) { colorIndex = Math.min(Math.floor(rating / 400) + 1, 8) } return SideMenu.Colors[colorIndex] }; //Estimator SideMenu.Elements.Estimator = (async () => { await SideMenu.appendToSideMenu(/beta.atcoder.jp/,'Estimator',getElem); async function getElem() { $("#estimator-input").val(localStorage.getItem("sidemenu_estimator_value")); if (!SideMenu.Datas.History) await SideMenu.Datas.Update.History(); var js = `(() => { var estimator_state = parseInt(localStorage.getItem("sidemenu_estimator_state")); \$("#estimator-input").val(localStorage.getItem("sidemenu_estimator_value")); updateInputs(); \$("#estimator-input").keyup(updateInputs); \$("#estimator-toggle").click(function () { \$("#estimator-input").val(\$("#estimator-res").val()); estimator_state = (estimator_state + 1) % 2; updateInputs(); }) function updateInputs() { var input = \$("#estimator-input").val(); if (!isFinite(input)) { displayAlert("数字ではありません") return; } var history = SideMenu.Datas.History.filter(x => x.IsRated); history.sort(function (a, b) { if (a.EndTime < b.EndTime) return 1; if (a.EndTime > b.EndTime) return -1; return 0; }) history = history.map(x => x.InnerPerformance) var input = parseInt(input) var res = -1; if (estimator_state === 0) { \/\/ binary search var goal_rating = unpositivize_rating(input) var lo = -10000.0; var hi = 10000.0; for (var i = 0; i < 100; ++i) { var mid = (hi + lo) \/ 2; var r = calc_rating([mid].concat(history)); if (r >= goal_rating) { hi = mid; } else { lo = mid; } } res = (hi + lo) \/ 2; \$("#estimator-input-desc").text("目標レーティング"); \$("#estimator-res-desc").text("必要パフォーマンス"); } else { res = calc_rating([input].concat(history)); \$("#estimator-input-desc").text("パフォーマンス"); \$("#estimator-res-desc").text("到達レーティング"); } res = Math.round(res * 100) \/ 100 if (!isNaN(res)) \$("#estimator-res").val(res); updateLocalStorage(); updateTweetBtn(); } function updateLocalStorage() { localStorage.setItem("sidemenu_estimator_state", estimator_state); localStorage.setItem("sidemenu_estimator_value", \$("#estimator-input").val()); } function updateTweetBtn() { var tweetStr = \`AtCoderのハンドルネーム: \${userScreenName}%0A \${estimator_state == 0 ? "目標レーティング" : "パフォーマンス"}: \${\$("#estimator-input").val()}%0A \${estimator_state == 0 ? "必要パフォーマンス" : "到達レーティング"}: \${\$("#estimator-res").val()}%0A\`; \$('#estimator-tweet').attr("href", \`https:\/\/twitter.com\/share?text=\${tweetStr}&url=https:\/\/greasyfork.org\/ja\/scripts\/369954-ac-predictor\`); } function displayAlert(message) { var alertDiv = document.createElement('div') alertDiv.setAttribute("role", "alert") alertDiv.setAttribute("class", "alert alert-warning alert-dismissible") var closeButton = document.createElement('button') closeButton.setAttribute("type", "button") closeButton.setAttribute("class", "close") closeButton.setAttribute("data-dismiss", "alert") closeButton.setAttribute("aria-label", "閉じる") var closeSpan = document.createElement('span') closeSpan.setAttribute("aria-hidden", "true") closeSpan.textContent = "×" closeButton.appendChild(closeSpan) var messageContent = document.createTextNode(message) alertDiv.appendChild(closeButton) alertDiv.appendChild(messageContent) \$("#estimator-alert").append(alertDiv) } })();`; var style = ``; var dom = `
<\/div>
目標レーティング<\/span> <\/div> <\/div>
必要パフォーマンス<\/span>