CafeCoder Enhancer

CafeCoder のUIを改善し,コンテストを快適にします(たぶん)

  1. // ==UserScript==
  2. // @name CafeCoder Enhancer
  3. // @namespace iilj
  4. // @version 2020.01.05.5
  5. // @description CafeCoder のUIを改善し,コンテストを快適にします(たぶん)
  6. // @author iilj
  7. // @supportURL https://github.com/iilj/CafeCodeEnhancer/issues
  8. // @match https://www.cafecoder.top/*
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/noty/3.1.4/noty.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/mode/clike/clike.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/mode/python/python.js
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/list.js/1.5.0/list.js
  14. // @resource css_noty https://cdnjs.cloudflare.com/ajax/libs/noty/3.1.4/noty.css
  15. // @resource css_cm https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.css
  16. // @grant GM_addStyle
  17. // @grant GM_getResourceText
  18. // ==/UserScript==
  19.  
  20. /* globals CodeMirror, Noty, List */
  21.  
  22. (function () {
  23. 'use strict';
  24.  
  25. GM_addStyle(GM_getResourceText('css_noty'));
  26. GM_addStyle(GM_getResourceText('css_cm'));
  27. GM_addStyle(`
  28. /* h4 まわりの UI 改善 */
  29. h4 {
  30. margin-top: 1rem;
  31. border-bottom: 2px solid lightblue;
  32. border-left: 10px solid lightblue;
  33. padding-left: 0.5rem;
  34. }
  35.  
  36. /* ページ最下部のコンテンツが見やすいように調整する */
  37. div.card {
  38. marginBottom: 30px;
  39. }
  40.  
  41. /* コンテストページ上部のメニューを使いやすくする */
  42. div.card-body a.nav-item.nav-link {
  43. border: 1px solid #bbbbbb;
  44. margin: 0.3rem;
  45. border-radius: 0.3rem;
  46. color: #007bff;
  47. }
  48. div.card-body a.nav-item.nav-link.cce-active {
  49. background-color: #ffffff;
  50. color: rgba(0,0,0,.5);
  51. }
  52. div.card-body a.nav-item.nav-link:hover{
  53. background-color: #dddddd;
  54. }
  55.  
  56. /* 入出力サンプルのUI */
  57. .cce-myprenode {
  58. display: block;
  59. margin: 0.4rem;
  60. padding: 0.4rem;
  61. background-color: #efefef !important;
  62. border: 1px solid #bbbbbb;
  63. border-radius: 0.4rem;
  64. font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
  65. }
  66. .CodeMirror {
  67. border-top: 1px solid black;
  68. border-bottom: 1px solid black;
  69. }
  70.  
  71. /* sortable table */
  72. table.table th {
  73. padding: 6px;
  74. vertical-align: middle;
  75. }
  76. table.table tbody th {
  77. font-weight: normal; /* hotfix for unformal use of th tag */
  78. position: relative;
  79. }
  80. table thead th[data-sort] {
  81. cursor: pointer;
  82. color: #007bff;
  83. }
  84. table thead th[data-sort]:hover {
  85. background-color: #dddddd;
  86. }
  87. table thead th[data-sort].sort.desc:after {
  88. content: " ▲";
  89. color: #888;
  90. }
  91. table thead th[data-sort].sort.asc:after {
  92. content: " ▼";
  93. color: #888;
  94. }
  95.  
  96. /* result icon */
  97. span.result, th.result>span {
  98. display: inline;
  99. padding: .2em .6em .3em;
  100. font-size: 75%;
  101. font-weight: bold;
  102. line-height: 1;
  103. color: #fff;
  104. text-align: center;
  105. white-space: nowrap;
  106. vertical-align: baseline;
  107. border-radius: .25em;
  108. border: none;
  109. -webkit-text-stroke: unset;
  110. text-shadow: none;
  111. margin: 0;
  112. cursor: default;
  113. }
  114. .AC {
  115. background-color: #5cb85c;
  116. }
  117. .WA, .TLE {
  118. background-color: #f0ad4e;
  119. }
  120. .WJ {
  121. background-color: #777;
  122. }
  123.  
  124. /* ranking page */
  125. table.table.cce-ranking-table th {
  126. padding: 10px;
  127. }
  128. div.point {
  129. height: auto;
  130. width: auto;
  131. position: absolute;
  132. top: 50%;
  133. left: 50%;
  134. transform: translateY(-50%) translateX(-50%);
  135. -webkit-transform: translateY(-50%) translateX(-50%);
  136. }
  137. div.point a {
  138. color: #00AA3E;
  139. font-weight: bold;
  140. }
  141. div.point span.submit_time {
  142. margin: 0 0 3px;
  143. color: #888;
  144. font-size: 90%;
  145. font-weight: normal;
  146. }
  147. table.table th.cce-ranking-username {
  148. font-weight: bold;
  149. width: auto;
  150. height: auto;
  151. }
  152. .cce-ranking-point div.point {
  153. color: blue;
  154. font-weight: bold;
  155. }
  156.  
  157. /* icon */
  158. @font-face {
  159. font-family: 'Glyphicons Halflings';
  160. src: url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot');
  161. src: url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
  162. url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2') format('woff2'),
  163. url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff') format('woff'),
  164. url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
  165. url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')
  166. }
  167. .glyphicon {
  168. position: relative;
  169. top: 1px;
  170. display: inline-block;
  171. font-family: 'Glyphicons Halflings';
  172. font-style: normal;
  173. font-weight: normal;
  174. line-height: 1;
  175. -webkit-font-smoothing: antialiased;
  176. -moz-osx-font-smoothing: grayscale;
  177. margin-right: 0.2rem;
  178. }
  179. .glyphicon-home:before {
  180. content: "\\e021"
  181. }
  182. .glyphicon-tasks:before {
  183. content: "\\e137"
  184. }
  185. .glyphicon-sort-by-attributes-alt:before {
  186. content: "\\e156"
  187. }
  188. .glyphicon-user:before {
  189. content: "\\e008"
  190. }
  191. .glyphicon-list:before {
  192. content: "\\e056"
  193. }
  194. * {
  195. -webkit-box-sizing: border-box;
  196. -moz-box-sizing: border-box;
  197. box-sizing: border-box
  198. }
  199. *:before,
  200. *:after {
  201. -webkit-box-sizing: border-box;
  202. -moz-box-sizing: border-box;
  203. box-sizing: border-box
  204. }
  205. `);
  206.  
  207. const msg = (type, text) => {
  208. new Noty({
  209. type: type,
  210. layout: 'top',
  211. timeout: 3000,
  212. text: text
  213. }).show();
  214. };
  215.  
  216. const dqs = (selectors) => document.querySelector(selectors);
  217. const dqsa = (selectors) => document.querySelectorAll(selectors);
  218.  
  219. // add title tag when there exists no title tag
  220. let result;
  221. if (!dqs("title")) {
  222. const title = document.createElement("title");
  223. let stitle = "";
  224. if (result = location.href.match(/www\.cafecoder\.top\/([^\/]+)\/(index\.(php|html?))?$/)) {
  225. stitle += `${result[1]} (${dqs("h1").innerText.trim()})`;
  226. } else if (result = location.href.match(/www\.cafecoder\.top\/([^\/]+)\/problem_list\.(php|html?)$/)) {
  227. stitle += `${result[1]} 問題一覧`;
  228. } else if (result = location.href.match(/www\.cafecoder\.top\/([^\/]+)\/Problems\/([^\/]+)\.(php|html?)$/)) {
  229. stitle += `${result[1]}-${result[2]}`;
  230. const h3 = dqs("h3");
  231. if (h3) {
  232. stitle += " " + h3.innerText.trim();
  233. }
  234. }
  235. stitle += (stitle == "" ? "" : " : ") + "CafeCoder";
  236. title.innerText = stitle;
  237. dqs("head").insertAdjacentElement('afterbegin', title);
  238. }
  239.  
  240. // fix invalid/broken uri
  241. dqsa("a[href*='kakecoder.com']").forEach((lnk) => {
  242. lnk.href = lnk.href.replace('kakecoder.com', 'cafecoder.top');
  243. });
  244. dqsa("a[href*='.html']").forEach((lnk) => {
  245. lnk.href = lnk.href.replace('.html', '.php');
  246. });
  247. dqsa("a[href^='//'][href$='.php']").forEach((lnk) => {
  248. const href = lnk.getAttribute('href');
  249. if (result = href.match(/^\/\/([^\/]+)\.(php|html?)$/)) {
  250. lnk.setAttribute('href', href.replace('//', location.href.indexOf("/Problems/") != -1 ? '../' : './'));
  251. }
  252. });
  253.  
  254. // add icon
  255. dqsa('div.card-body a.nav-item.nav-link').forEach((lnk) => {
  256. const href = lnk.getAttribute('href');
  257. let type = 'home';
  258. if (result = href.match(/([^\/]+).(php|html?)(\?[^/]+)?$/)) {
  259. const nm = result[1];
  260. switch (nm) {
  261. case 'index':
  262. type = 'home';
  263. break;
  264. case 'problem_list':
  265. type = 'tasks';
  266. break;
  267. case 'ranking':
  268. type = 'sort-by-attributes-alt';
  269. break;
  270. case 'my_submit':
  271. type = 'user';
  272. break;
  273. case 'all_submit':
  274. type = 'list';
  275. break;
  276. }
  277. }
  278. const icon = document.createElement("span");
  279. icon.classList.add('glyphicon', `glyphicon-${type}`);
  280. icon.setAttribute('aria-hidden', 'true');
  281. lnk.insertAdjacentElement('afterbegin', icon);
  282. if (lnk.href == location.href) {
  283. lnk.classList.add('cce-active');
  284. }
  285. });
  286.  
  287. // when problem page
  288. if (location.href.indexOf("/Problems/") != -1 && dqs("h3") != null) {
  289. // improve UI/UX of I/O sample, and add sample copy button feature
  290. dqsa("span[style]:not([class]), pre[style]:not([class]), div[style]:not([class]), .sample").forEach((node, idx, _nodelist) => {
  291. if (!node.classList.contains('sample') && !node.style.backgroundColor && node.getAttribute("style").indexOf("background-color") == -1) {
  292. return;
  293. }
  294. node.classList.add('cce-myprenode');
  295. node.id = `cce-myprenode-${idx}`;
  296. if (node.firstChild.nodeName == "#text") {
  297. node.firstChild.data = node.firstChild.data.trim();
  298. }
  299. if (node.lastChild.nodeName == "#text") {
  300. node.lastChild.data = node.lastChild.data.trim();
  301. }
  302.  
  303. let btn = document.createElement("button");
  304. btn.innerText = "テキストをコピー!";
  305. btn.classList.add('btn', 'btn-primary', 'copy-sample-input');
  306. btn.style.display = "block";
  307. btn.addEventListener("click", () => {
  308. const elem = document.getElementById(node.id);
  309. document.getSelection().selectAllChildren(elem);
  310. if (document.execCommand("copy")) {
  311. msg('success', 'テキストをコピーしました!');
  312. document.getSelection().removeAllRanges();
  313. } else {
  314. msg('error', 'コピーに失敗してしまったようです.');
  315. }
  316. }, false);
  317. node.insertAdjacentElement('beforebegin', btn);
  318. });
  319.  
  320. // CodeMirror init
  321. const textarea = dqs('form[name=submit_form] textarea[name=sourcecode]');
  322. const editor = CodeMirror.fromTextArea(textarea, {
  323. mode: "text/x-c++src",
  324. lineNumbers: true,
  325. });
  326.  
  327. // CodeMirror lang selection changed event handler
  328. const selectlang = dqs('form[name=submit_form] select[name=language]');
  329. selectlang.addEventListener('change', (event) => {
  330. const modelist = [
  331. 'text/x-csrc', 'text/x-c++src', 'text/x-java', 'python', 'text/x-csharp'
  332. ];
  333. editor.setOption("mode", modelist[event.target.selectedIndex]);
  334. });
  335.  
  336. // select lang C++17 as a default
  337. selectlang.selectedIndex = 1;
  338. selectlang.classList.add('form-control');
  339.  
  340. // CodeMirror submit preprocess, remove default broken event
  341. const submitbtn = dqs('form[name=submit_form] input[type=submit]');
  342. submitbtn.removeAttribute("onclick");
  343. submitbtn.classList.add('btn-primary');
  344. document.submit_form.addEventListener('submit', (event) => {
  345. editor.save();
  346. if (textarea.value == '') {
  347. msg('warning', 'ソースコードが入力されていません');
  348. event.preventDefault();
  349. }
  350. });
  351. } else if (location.href.indexOf("/all_submit.php?") != -1 || location.href.indexOf("/my_submit.php?") != -1) {
  352. // on submit list page
  353. const parent = dqs('div.card-body');
  354. parent.id = 'cce-list-parent';
  355. const table = parent.querySelector('table.table');
  356. table.classList.add('table-striped', 'small');
  357.  
  358. const tbody = table.querySelector('tbody');
  359. tbody.classList.add('list');
  360. tbody.querySelectorAll('tr').forEach((tr) => {
  361. tr.querySelectorAll('th').forEach((td, idx, nodelist) => { /* unformal html (th here should be td) */
  362. if (idx == nodelist.length - 1) {
  363. return;
  364. }
  365. td.classList.add(`cce-list-sort-${idx}`);
  366. });
  367. });
  368.  
  369. parent.querySelector('thead').querySelectorAll('tr th').forEach((th, idx, nodelist) => {
  370. if (idx == nodelist.length - 1) {
  371. return;
  372. } else if (idx == 1) {
  373. th.classList.add('desc');
  374. }
  375. th.classList.add('sort');
  376. th.setAttribute('data-sort', `cce-list-sort-${idx}`);
  377. });
  378. const userList = new List(parent.id, {
  379. valueNames: ['cce-list-sort-0', 'cce-list-sort-1', 'cce-list-sort-2', 'cce-list-sort-3']
  380. });
  381. } else if (location.href.indexOf("/problem_list.") != -1) {
  382. // on contest problem list page
  383. const table = dqs('table.table');
  384.  
  385. const thead = table.querySelector('thead tr');
  386. const th0 = document.createElement("th");
  387. th0.innerText = '#';
  388. thead.insertAdjacentElement('afterbegin', th0);
  389.  
  390. table.querySelectorAll('tbody tr').forEach((tr) => {
  391. const tr0 = document.createElement("th");
  392. console.log(tr.querySelector('a[href]').href);
  393. const a0 = tr.querySelector('a[href]');
  394. if (result = a0.href.match(/\/([^\/])\.(php|html?)?$/)) {
  395. const pid = result[1];
  396. const a1 = document.createElement("a");
  397. a1.href = a0.href;
  398. a1.innerText = pid;
  399. tr0.insertAdjacentElement('afterbegin', a1);
  400. } else {
  401. tr0.innerText = '?';
  402. }
  403. tr.insertAdjacentElement('afterbegin', tr0);
  404. });
  405. } else if (location.href.indexOf("/ranking.php?") != -1) {
  406. // on contest ranking page
  407. const parent = dqs('div.card-body');
  408. parent.id = 'cce-list-parent';
  409. const table = parent.querySelector('table.table');
  410. table.classList.add('table-striped', 'small', 'cce-ranking-table');
  411.  
  412. const tbody = table.querySelector('tbody');
  413. tbody.classList.add('list');
  414. tbody.querySelectorAll('tr').forEach((tr, tridx) => {
  415. let endtime = '00:00:00';
  416. const submit_time = document.createElement("span");
  417. submit_time.classList.add('submit_time');
  418. tr.querySelectorAll('th').forEach((td, idx, nodelist) => { /* unformal html (th here should be td) */
  419. if (idx == 1) {
  420. td.classList.add('cce-ranking-username');
  421. } else if (idx == 2) {
  422. td.classList.add('cce-ranking-point');
  423. td.setAttribute('data-cce-list-sort-point', `${tridx}`);
  424. const divpoint = td.firstElementChild;
  425. divpoint.insertAdjacentHTML('beforeend', '<br>');
  426. divpoint.insertAdjacentElement('beforeend', submit_time);
  427. } else if (idx >= 3) {
  428. const timespan = td.querySelector('span.submit_time');
  429. if (timespan) {
  430. td.setAttribute('data-cce-list-sort-timespan', timespan.innerText);
  431. if (timespan.innerText > endtime) {
  432. endtime = timespan.innerText;
  433. }
  434. } else {
  435. td.setAttribute('data-cce-list-sort-timespan', '99:99:99');
  436. }
  437. }
  438. td.classList.add(`cce-list-sort-${idx}`);
  439. });
  440. submit_time.innerText = endtime;
  441. });
  442.  
  443. parent.querySelector('thead').querySelectorAll('tr th').forEach((th, idx, nodelist) => {
  444. if (idx == 0) {
  445. th.classList.add('asc');
  446. }
  447. th.classList.add('sort');
  448. th.setAttribute('data-sort', `cce-list-sort-${idx}`);
  449. });
  450. const userList = new List(parent.id, {
  451. valueNames: ['cce-list-sort-0', 'cce-list-sort-1',
  452. { name: 'cce-list-sort-2', attr: 'data-cce-list-sort-point' },
  453. { name: 'cce-list-sort-3', attr: 'data-cce-list-sort-timespan' },
  454. { name: 'cce-list-sort-4', attr: 'data-cce-list-sort-timespan' },
  455. { name: 'cce-list-sort-5', attr: 'data-cce-list-sort-timespan' },
  456. { name: 'cce-list-sort-6', attr: 'data-cce-list-sort-timespan' },
  457. { name: 'cce-list-sort-7', attr: 'data-cce-list-sort-timespan' },
  458. { name: 'cce-list-sort-8', attr: 'data-cce-list-sort-timespan' }]
  459. });
  460. }
  461. })();