CF Contest Information Viewer

Add information about the contest, such as contest times, to the sidebar.

  1. // ==UserScript==
  2. // @name CF Contest Information Viewer
  3. // @namespace https://twitter.com/kymn_
  4. // @version 0.1
  5. // @description Add information about the contest, such as contest times, to the sidebar.
  6. // @author keymoon
  7. // @match https://codeforces.com/contest/*
  8. // @match https://codeforces.com/gym/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. //#region LS
  13. function getLSCache(key, defaultObj){
  14. const str = localStorage.getItem(key);
  15. return !str ? defaultObj : JSON.parse(str);
  16. }
  17.  
  18. function setLSCache(key, obj){
  19. localStorage.setItem(key, JSON.stringify(obj));
  20. }
  21. //#endregion
  22.  
  23. //#region settings
  24. const settingsCacheKey = "__cfciv_settings";
  25.  
  26. const dataKeys =
  27. [
  28. 'id',
  29. 'name',
  30. 'type',
  31. 'phase',
  32. 'frozen',
  33. 'durationSeconds',
  34. 'startTimeSeconds',
  35. 'relativeTimeSeconds',
  36. 'preparedBy',
  37. 'websiteUrl',
  38. 'description',
  39. 'difficulty',
  40. 'kind',
  41. 'icpcRegion',
  42. 'country',
  43. 'city',
  44. 'season'
  45. ];
  46.  
  47. const defaultSettings =
  48. {
  49. id: true,
  50. name: false,
  51. type: false,
  52. phase: false,
  53. frozen: false,
  54. durationSeconds: true,
  55. startTimeSeconds: false,
  56. relativeTimeSeconds: false,
  57. preparedBy: false,
  58. websiteUrl: false,
  59. description: false,
  60. difficulty: false,
  61. kind: false,
  62. icpcRegion: false,
  63. country: false,
  64. city: false,
  65. season: false
  66. };
  67.  
  68. const shouldTrue =
  69. {
  70. id: true,
  71. name: false,
  72. type: false,
  73. phase: false,
  74. frozen: false,
  75. durationSeconds: false,
  76. startTimeSeconds: false,
  77. relativeTimeSeconds: false,
  78. preparedBy: false,
  79. websiteUrl: false,
  80. description: false,
  81. difficulty: false,
  82. kind: false,
  83. icpcRegion: false,
  84. country: false,
  85. city: false,
  86. season: false
  87. };
  88.  
  89. function validateSettings(settings){
  90. for (const key of dataKeys){
  91. if (!settings.hasOwnProperty(key)) return false;
  92. if (shouldTrue[key] && !settings[key]) return false;
  93. }
  94. return true;
  95. }
  96.  
  97. function getSettings(){
  98. return getLSCache(settingsCacheKey, defaultSettings);
  99. }
  100.  
  101. function setSettings(settings){
  102. if (!validateSettings(settings)) throw new Error("invalid settings");
  103. setLSCache(settingsCacheKey, settings);
  104. }
  105. //#endregion
  106.  
  107. //#region contests
  108. const contestsCacheKey = "__cfciv_contests";
  109.  
  110. function formatContestsData(data){
  111. const settings = getSettings();
  112. for (const item of data){
  113. for (const key of dataKeys){
  114. if (!settings[key] && item.hasOwnProperty(key)) delete item[key];
  115. }
  116. }
  117. return data;
  118. }
  119.  
  120. function fetchContestsAsync(){
  121. const contestApiURL = "https://codeforces.com/api/contest.list?gym=false";
  122. const gymApiURL = "https://codeforces.com/api/contest.list?gym=true";
  123. function _fetchContestsAsync(url){
  124. return new Promise((resolve, reject) => {
  125. const req = new XMLHttpRequest();
  126. req.open("GET", url, true);
  127. req.onload = () => {
  128. if (req.status >= 400) reject("can't fetch data : status code is ${req.status}");
  129. const obj = JSON.parse(req.responseText);
  130. if (obj.status != "OK") reject(`api status is ${obj.status}`);
  131. resolve(obj.result);
  132. };
  133. req.onerror = () => {
  134. reject("can't fetch data : Error connecting to server.");
  135. };
  136. req.send();
  137. });
  138. }
  139. return Promise.all([_fetchContestsAsync(contestApiURL), _fetchContestsAsync(gymApiURL)]).then((values) => {
  140. return values[0].concat(values[1]);
  141. });
  142. }
  143.  
  144. async function getContestsAsync(){
  145. let data = getLSCache(contestsCacheKey, undefined);
  146. if (!data) {
  147. await refreshContestsAsync();
  148. data = getLSCache(contestsCacheKey, undefined);
  149. if (!data) throw new Error("refresh failed");
  150. }
  151. formatContestsData(data);
  152. return data;
  153. }
  154.  
  155. function setContests(data){
  156. formatContestsData(data);
  157. setLSCache(contestsCacheKey, data);
  158. }
  159.  
  160. async function refreshContestsAsync(){
  161. setContests(await fetchContestsAsync());
  162. }
  163. //#endregion
  164.  
  165. //#region ui
  166. function defaultParser(data){
  167. return data.toString();
  168. }
  169.  
  170. function durationParser(sec){
  171. const grans = [60, 60, 24];
  172. const unit = ["sec(s)", "min(s)", "hour(s)", "day(s)"];
  173. const resarr = [sec];
  174. for (const gran of grans){
  175. var elem = resarr.pop();
  176. resarr.push(elem % gran);
  177. resarr.push(Math.floor(elem / gran));
  178. }
  179. let res = "";
  180. for (let i = 0; i < unit.length; i++){
  181. if (resarr[i] == 0) continue;
  182. res = `${resarr[i]} ${unit[i]},` + res;
  183. }
  184. if (res == "") res = "0 sec(s),";
  185. return res.substr(0, res.length - 1);
  186. }
  187.  
  188. function dateParser(sec){
  189. var date = new Date(sec * 1000);
  190. return date.toLocaleString();
  191. }
  192.  
  193. const parsers =
  194. {
  195. id: defaultParser,
  196. name: defaultParser,
  197. type: defaultParser,
  198. phase: defaultParser,
  199. frozen: defaultParser,
  200. durationSeconds: durationParser,
  201. startTimeSeconds: dateParser,
  202. relativeTimeSeconds: durationParser,
  203. preparedBy: defaultParser,
  204. websiteUrl: defaultParser,
  205. description: defaultParser,
  206. difficulty: defaultParser,
  207. kind: defaultParser,
  208. icpcRegion: defaultParser,
  209. country: defaultParser,
  210. city: defaultParser,
  211. season: defaultParser
  212. };
  213.  
  214. const names =
  215. {
  216. id: "id",
  217. name: "name",
  218. type: "type",
  219. phase: "phase",
  220. frozen: "frozen",
  221. durationSeconds: "duration",
  222. startTimeSeconds: "startTime",
  223. relativeTimeSeconds: "relativeTime",
  224. preparedBy: "preparedBy",
  225. websiteUrl: "websiteUrl",
  226. description: "description",
  227. difficulty: "difficulty",
  228. kind: "kind",
  229. icpcRegion: "icpcRegion",
  230. country: "country",
  231. city: "city",
  232. season: "season"
  233. };
  234.  
  235. // since there is no user input, we can use rough escape
  236. function escapeHTML(str) {
  237. return str.replace(/&/g, '&amp;')
  238. .replace(/</g, '&lt;')
  239. .replace(/>/g, '&gt;')
  240. .replace(/"/g, '&quot;')
  241. .replace(/'/g, '&#039;');
  242. }
  243.  
  244. const divid = 'cfciv_elem';
  245. function addElement(contest){
  246. const sidebar = document.getElementById("sidebar");
  247. if (!sidebar) return;
  248.  
  249. const div = `<div id="${divid}" class="roundbox sidebox sidebar-menu" style=""></div>`
  250. sidebar.insertAdjacentHTML('beforeend', div);
  251. updateElement(contest);
  252. }
  253.  
  254. async function applySettingsAsync(settings){
  255. setSettings(settings);
  256. await refreshContestsAsync();
  257. const currentContest = await getCurrentContestAsync();
  258. updateElement(currentContest);
  259. }
  260.  
  261. function updateElement(contest){
  262. function getInfoRow(key, value){
  263. const name = names[key];
  264. const parsedval = parsers[key](value);
  265. return `<li><span>${escapeHTML(name)} : ${escapeHTML(parsedval)}</span><span style="float: right;"></span><div style="clear: both;"></div></li>`;
  266. }
  267.  
  268. const checkboxIDPrefix = "cfciv_settings_checkbox_"
  269. function getSettingRow(key, state){
  270. const name = names[key];
  271. return (
  272. `<div>
  273. <input id="${checkboxIDPrefix}${key}" type="checkbox" name="${key}" ${state ? "checked" : ""} ${shouldTrue[key] ? "disabled" : ""}>
  274. <label for="${key}">${name}</label>
  275. </div>`
  276. );
  277. }
  278.  
  279. const div = document.getElementById(divid);
  280. if (!div) return;
  281.  
  282. const infolist = [];
  283. for (const key in contest){
  284. infolist.push(getInfoRow(key, contest[key]));
  285. }
  286.  
  287. const settinglist = [];
  288. const setting = getSettings();
  289. for (const key in setting){
  290. settinglist.push(getSettingRow(key, setting[key]));
  291. }
  292.  
  293. const applyButtonID = 'cfciv_settings_applybtn';
  294. const innerhtml =
  295. `<div class="roundbox-lt">&nbsp;</div>
  296. <div class="roundbox-rt">&nbsp;</div>
  297. <div class="caption titled">→ Contest Information</div>
  298. <ul>${infolist.join('')}</ul>
  299. <details style="margin:1em;">
  300. <summary>Settings</summary>
  301. <div style="margin:1em;font-size:0.8em;">
  302. You can choose which information to display. Some information may not be present in all contests.<br>
  303. Click the apply button when you are done with your settings. It may take some time to reload the information.
  304. </div>
  305. <div style="margin:0.5em 1em;">
  306. ${settinglist.join('')}
  307. <button id=${applyButtonID} style="margin:0.5em">apply</button>
  308. </div>
  309. </details>`;
  310.  
  311. div.innerHTML = innerhtml;
  312.  
  313. const elem = document.getElementById(applyButtonID);
  314. elem.onclick = async () => {
  315. const settings = getSettings();
  316. for (const key in settings){
  317. const elem = document.getElementById(checkboxIDPrefix + key);
  318. settings[key] = elem.checked;
  319. document.getElementById(checkboxIDPrefix + key).disabled = true;
  320. }
  321. applySettingsAsync(settings);
  322. };
  323. }
  324. //#endregion
  325.  
  326. //#region util
  327. function getContestID(){
  328. return parseInt(document.location.href.split('/')[4]);
  329. }
  330.  
  331. async function getCurrentContestAsync(){
  332. const contestID = getContestID();
  333. const contests = await getContestsAsync();
  334. const contest = contests.filter(x => x.id == contestID)[0];
  335. return contest;
  336. }
  337. //#endregion
  338.  
  339. (async function() {
  340. 'use strict';
  341. let contest = await getCurrentContestAsync();
  342. if (!contest){
  343. await refreshContestsAsync();
  344. contest = await getCurrentContestAsync();
  345. if (!contest) throw new Error("can't find contest information");
  346. }
  347. addElement(contest);
  348. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址