您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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',
- 'icpcRegion',
- '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,
- icpcRegion: 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,
- icpcRegion: 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.result);
- };
- 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,
- icpcRegion: 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",
- icpcRegion: "icpcRegion",
- 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);
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址