Add information about the contest, such as contest times, to the sidebar.
// ==UserScript== // @name CF Contest Information Viewer // @namespace https://twitter.com/kymn_ // @version 0.1 // @description Add information about the contest, such as contest times, to the sidebar. // @author keymoon // @match https://codeforces.com/contest/* // @match https://codeforces.com/gym/* // @grant none // ==/UserScript== //#region LS function getLSCache(key, defaultObj){ const str = localStorage.getItem(key); return !str ? defaultObj : JSON.parse(str); } function setLSCache(key, obj){ localStorage.setItem(key, JSON.stringify(obj)); } //#endregion //#region settings const settingsCacheKey = "__cfciv_settings"; const dataKeys = [ 'id', 'name', 'type', 'phase', 'frozen', 'durationSeconds', 'startTimeSeconds', 'relativeTimeSeconds', 'preparedBy', 'websiteUrl', 'description', 'difficulty', 'kind', 'i###Region', 'country', 'city', 'season' ]; const defaultSettings = { id: true, name: false, type: false, phase: false, frozen: false, durationSeconds: true, startTimeSeconds: false, relativeTimeSeconds: false, preparedBy: false, websiteUrl: false, description: false, difficulty: false, kind: false, i###Region: false, country: false, city: false, season: false }; const shouldTrue = { id: true, name: false, type: false, phase: false, frozen: false, durationSeconds: false, startTimeSeconds: false, relativeTimeSeconds: false, preparedBy: false, websiteUrl: false, description: false, difficulty: false, kind: false, i###Region: false, country: false, city: false, season: false }; function validateSettings(settings){ for (const key of dataKeys){ if (!settings.hasOwnProperty(key)) return false; if (shouldTrue[key] && !settings[key]) return false; } return true; } function getSettings(){ return getLSCache(settingsCacheKey, defaultSettings); } function setSettings(settings){ if (!validateSettings(settings)) throw new Error("invalid settings"); setLSCache(settingsCacheKey, settings); } //#endregion //#region contests const contestsCacheKey = "__cfciv_contests"; function formatContestsData(data){ const settings = getSettings(); for (const item of data){ for (const key of dataKeys){ if (!settings[key] && item.hasOwnProperty(key)) delete item[key]; } } return data; } function fetchContestsAsync(){ const contestApiURL = "https://codeforces.com/api/contest.list?gym=false"; const gymApiURL = "https://codeforces.com/api/contest.list?gym=true"; function _fetchContestsAsync(url){ return new Promise((resolve, reject) => { const req = new XMLHttpRequest(); req.open("GET", url, true); req.onload = () => { if (req.status >= 400) reject("can't fetch data : status code is ${req.status}"); const obj = JSON.parse(req.responseText); if (obj.status != "OK") reject(`api status is ${obj.status}`); resolve(obj.r###lt); }; req.onerror = () => { reject("can't fetch data : Error connecting to server."); }; req.send(); }); } return Promise.all([_fetchContestsAsync(contestApiURL), _fetchContestsAsync(gymApiURL)]).then((values) => { return values[0].concat(values[1]); }); } async function getContestsAsync(){ let data = getLSCache(contestsCacheKey, undefined); if (!data) { await refreshContestsAsync(); data = getLSCache(contestsCacheKey, undefined); if (!data) throw new Error("refresh failed"); } formatContestsData(data); return data; } function setContests(data){ formatContestsData(data); setLSCache(contestsCacheKey, data); } async function refreshContestsAsync(){ setContests(await fetchContestsAsync()); } //#endregion //#region ui function defaultParser(data){ return data.toString(); } function durationParser(sec){ const grans = [60, 60, 24]; const unit = ["sec(s)", "min(s)", "hour(s)", "day(s)"]; const resarr = [sec]; for (const gran of grans){ var elem = resarr.pop(); resarr.push(elem % gran); resarr.push(Math.floor(elem / gran)); } let res = ""; for (let i = 0; i < unit.length; i++){ if (resarr[i] == 0) continue; res = `${resarr[i]} ${unit[i]},` + res; } if (res == "") res = "0 sec(s),"; return res.substr(0, res.length - 1); } function dateParser(sec){ var date = new Date(sec * 1000); return date.toLocaleString(); } const parsers = { id: defaultParser, name: defaultParser, type: defaultParser, phase: defaultParser, frozen: defaultParser, durationSeconds: durationParser, startTimeSeconds: dateParser, relativeTimeSeconds: durationParser, preparedBy: defaultParser, websiteUrl: defaultParser, description: defaultParser, difficulty: defaultParser, kind: defaultParser, i###Region: defaultParser, country: defaultParser, city: defaultParser, season: defaultParser }; const names = { id: "id", name: "name", type: "type", phase: "phase", frozen: "frozen", durationSeconds: "duration", startTimeSeconds: "startTime", relativeTimeSeconds: "relativeTime", preparedBy: "preparedBy", websiteUrl: "websiteUrl", description: "description", difficulty: "difficulty", kind: "kind", i###Region: "i###Region", country: "country", city: "city", season: "season" }; // since there is no user input, we can use rough escape function escapeHTML(str) { return str.replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } const divid = 'cfciv_elem'; function addElement(contest){ const sidebar = document.getElementById("sidebar"); if (!sidebar) return; const div = `<div id="${divid}" class="roundbox sidebox sidebar-menu" style=""></div>` sidebar.insertAdjacentHTML('beforeend', div); updateElement(contest); } async function applySettingsAsync(settings){ setSettings(settings); await refreshContestsAsync(); const currentContest = await getCurrentContestAsync(); updateElement(currentContest); } function updateElement(contest){ function getInfoRow(key, value){ const name = names[key]; const parsedval = parsers[key](value); return `<li><span>${escapeHTML(name)} : ${escapeHTML(parsedval)}</span><span style="float: right;"></span><div style="clear: both;"></div></li>`; } const checkboxIDPrefix = "cfciv_settings_checkbox_" function getSettingRow(key, state){ const name = names[key]; return ( `<div> <input id="${checkboxIDPrefix}${key}" type="checkbox" name="${key}" ${state ? "checked" : ""} ${shouldTrue[key] ? "disabled" : ""}> <label for="${key}">${name}</label> </div>` ); } const div = document.getElementById(divid); if (!div) return; const infolist = []; for (const key in contest){ infolist.push(getInfoRow(key, contest[key])); } const settinglist = []; const setting = getSettings(); for (const key in setting){ settinglist.push(getSettingRow(key, setting[key])); } const applyButtonID = 'cfciv_settings_applybtn'; const innerhtml = `<div class="roundbox-lt"> </div> <div class="roundbox-rt"> </div> <div class="caption titled">→ Contest Information</div> <ul>${infolist.join('')}</ul> <details style="margin:1em;"> <summary>Settings</summary> <div style="margin:1em;font-size:0.8em;"> You can choose which information to display. Some information may not be present in all contests.<br> Click the apply button when you are done with your settings. It may take some time to reload the information. </div> <div style="margin:0.5em 1em;"> ${settinglist.join('')} <button id=${applyButtonID} style="margin:0.5em">apply</button> </div> </details>`; div.innerHTML = innerhtml; const elem = document.getElementById(applyButtonID); elem.onclick = async () => { const settings = getSettings(); for (const key in settings){ const elem = document.getElementById(checkboxIDPrefix + key); settings[key] = elem.checked; document.getElementById(checkboxIDPrefix + key).disabled = true; } applySettingsAsync(settings); }; } //#endregion //#region util function getContestID(){ return parseInt(document.location.href.split('/')[4]); } async function getCurrentContestAsync(){ const contestID = getContestID(); const contests = await getContestsAsync(); const contest = contests.filter(x => x.id == contestID)[0]; return contest; } //#endregion (async function() { 'use strict'; let contest = await getCurrentContestAsync(); if (!contest){ await refreshContestsAsync(); contest = await getCurrentContestAsync(); if (!contest) throw new Error("can't find contest information"); } addElement(contest); })();