CafeCoder Enhancer

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

当前为 2020-01-05 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         CafeCoder Enhancer
// @namespace    iilj
// @version      2020.01.05.5
// @description  CafeCoder のUIを改善し,コンテストを快適にします(たぶん)
// @author       iilj
// @supportURL   https://github.com/iilj/CafeCodeEnhancer/issues
// @match        https://www.cafecoder.top/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/noty/3.1.4/noty.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/mode/clike/clike.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/mode/python/python.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/list.js/1.5.0/list.js
// @resource     css_noty https://cdnjs.cloudflare.com/ajax/libs/noty/3.1.4/noty.css
// @resource     css_cm https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.css
// @grant        GM_addStyle
// @grant        GM_getResourceText
// ==/UserScript==

/* globals CodeMirror, Noty, List */

(function () {
    'use strict';

    GM_addStyle(GM_getResourceText('css_noty'));
    GM_addStyle(GM_getResourceText('css_cm'));
    GM_addStyle(`
/* h4 まわりの UI 改善 */
h4 {
    margin-top: 1rem;
    border-bottom: 2px solid lightblue;
    border-left: 10px solid lightblue;
    padding-left: 0.5rem;
}

/* ページ最下部のコンテンツが見やすいように調整する */
div.card {
    marginBottom: 30px;
}

/* コンテストページ上部のメニューを使いやすくする */
div.card-body a.nav-item.nav-link {
    border: 1px solid #bbbbbb;
    margin: 0.3rem;
    border-radius: 0.3rem;
    color: #007bff;
}
div.card-body a.nav-item.nav-link.cce-active {
    background-color: #ffffff;
    color: rgba(0,0,0,.5);
}
div.card-body a.nav-item.nav-link:hover{
    background-color: #dddddd;
}

/* 入出力サンプルのUI */
.cce-myprenode {
    display: block;
    margin: 0.4rem;
    padding: 0.4rem;
    background-color: #efefef !important;
    border: 1px solid #bbbbbb;
    border-radius: 0.4rem;
    font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
}
.CodeMirror {
    border-top: 1px solid black;
    border-bottom: 1px solid black;
}

/* sortable table */
table.table th {
    padding: 6px;
    vertical-align: middle;
}
table.table tbody th {
    font-weight: normal; /* hotfix for unformal use of th tag */
    position: relative;
}
table thead th[data-sort] {
    cursor: pointer;
    color: #007bff;
}
table thead th[data-sort]:hover {
    background-color: #dddddd;
}
table thead th[data-sort].sort.desc:after {
    content: " ▲";
    color: #888;
}
table thead th[data-sort].sort.asc:after {
    content: " ▼";
    color: #888;
}

/* result icon */
span.result, th.result>span {
    display: inline;
    padding: .2em .6em .3em;
    font-size: 75%;
    font-weight: bold;
    line-height: 1;
    color: #fff;
    text-align: center;
    white-space: nowrap;
    vertical-align: baseline;
    border-radius: .25em;
    border: none;
    -webkit-text-stroke: unset;
    text-shadow: none;
    margin: 0;
    cursor: default;
}
.AC {
    background-color: #5cb85c;
}
.WA, .TLE {
    background-color: #f0ad4e;
}
.WJ {
    background-color: #777;
}

/* ranking page */
table.table.cce-ranking-table th {
    padding: 10px;
}
div.point {
    height: auto;
    width: auto;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translateY(-50%) translateX(-50%);
    -webkit-transform: translateY(-50%) translateX(-50%);
}
div.point a {
    color: #00AA3E;
    font-weight: bold;
}
div.point span.submit_time {
    margin: 0 0 3px;
    color: #888;
    font-size: 90%;
    font-weight: normal;
}
table.table th.cce-ranking-username {
    font-weight: bold;
    width: auto;
    height: auto;
}
.cce-ranking-point div.point {
    color: blue;
    font-weight: bold;
}

/* icon */
@font-face {
	font-family: 'Glyphicons Halflings';
	src: url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot');
	src: url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
         url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2') format('woff2'),
         url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff') format('woff'),
         url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
         url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')
}
.glyphicon {
	position: relative;
	top: 1px;
	display: inline-block;
	font-family: 'Glyphicons Halflings';
	font-style: normal;
	font-weight: normal;
	line-height: 1;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
    margin-right: 0.2rem;
}
.glyphicon-home:before {
	content: "\\e021"
}
.glyphicon-tasks:before {
	content: "\\e137"
}
.glyphicon-sort-by-attributes-alt:before {
	content: "\\e156"
}
.glyphicon-user:before {
	content: "\\e008"
}
.glyphicon-list:before {
	content: "\\e056"
}
* {
	-webkit-box-sizing: border-box;
	-moz-box-sizing: border-box;
	box-sizing: border-box
}
*:before,
*:after {
	-webkit-box-sizing: border-box;
	-moz-box-sizing: border-box;
	box-sizing: border-box
}
    `);

    const msg = (type, text) => {
        new Noty({
            type: type,
            layout: 'top',
            timeout: 3000,
            text: text
        }).show();
    };

    const dqs = (selectors) => document.querySelector(selectors);
    const dqsa = (selectors) => document.querySelectorAll(selectors);

    // add title tag when there exists no title tag
    let result;
    if (!dqs("title")) {
        const title = document.createElement("title");
        let stitle = "";
        if (result = location.href.match(/www\.cafecoder\.top\/([^\/]+)\/(index\.(php|html?))?$/)) {
            stitle += `${result[1]} (${dqs("h1").innerText.trim()})`;
        } else if (result = location.href.match(/www\.cafecoder\.top\/([^\/]+)\/problem_list\.(php|html?)$/)) {
            stitle += `${result[1]} 問題一覧`;
        } else if (result = location.href.match(/www\.cafecoder\.top\/([^\/]+)\/Problems\/([^\/]+)\.(php|html?)$/)) {
            stitle += `${result[1]}-${result[2]}`;
            const h3 = dqs("h3");
            if (h3) {
                stitle += " " + h3.innerText.trim();
            }
        }
        stitle += (stitle == "" ? "" : " : ") + "CafeCoder";
        title.innerText = stitle;
        dqs("head").insertAdjacentElement('afterbegin', title);
    }

    // fix invalid/broken uri
    dqsa("a[href*='kakecoder.com']").forEach((lnk) => {
        lnk.href = lnk.href.replace('kakecoder.com', 'cafecoder.top');
    });
    dqsa("a[href*='.html']").forEach((lnk) => {
        lnk.href = lnk.href.replace('.html', '.php');
    });
    dqsa("a[href^='//'][href$='.php']").forEach((lnk) => {
        const href = lnk.getAttribute('href');
        if (result = href.match(/^\/\/([^\/]+)\.(php|html?)$/)) {
            lnk.setAttribute('href', href.replace('//', location.href.indexOf("/Problems/") != -1 ? '../' : './'));
        }
    });

    // add icon
    dqsa('div.card-body a.nav-item.nav-link').forEach((lnk) => {
        const href = lnk.getAttribute('href');
        let type = 'home';
        if (result = href.match(/([^\/]+).(php|html?)(\?[^/]+)?$/)) {
            const nm = result[1];
            switch (nm) {
                case 'index':
                    type = 'home';
                    break;
                case 'problem_list':
                    type = 'tasks';
                    break;
                case 'ranking':
                    type = 'sort-by-attributes-alt';
                    break;
                case 'my_submit':
                    type = 'user';
                    break;
                case 'all_submit':
                    type = 'list';
                    break;
            }
        }
        const icon = document.createElement("span");
        icon.classList.add('glyphicon', `glyphicon-${type}`);
        icon.setAttribute('aria-hidden', 'true');
        lnk.insertAdjacentElement('afterbegin', icon);
        if (lnk.href == location.href) {
            lnk.classList.add('cce-active');
        }
    });

    // when problem page
    if (location.href.indexOf("/Problems/") != -1 && dqs("h3") != null) {
        // improve UI/UX of I/O sample, and add sample copy button feature
        dqsa("span[style]:not([class]), pre[style]:not([class]), div[style]:not([class]), .sample").forEach((node, idx, _nodelist) => {
            if (!node.classList.contains('sample') && !node.style.backgroundColor && node.getAttribute("style").indexOf("background-color") == -1) {
                return;
            }
            node.classList.add('cce-myprenode');
            node.id = `cce-myprenode-${idx}`;
            if (node.firstChild.nodeName == "#text") {
                node.firstChild.data = node.firstChild.data.trim();
            }
            if (node.lastChild.nodeName == "#text") {
                node.lastChild.data = node.lastChild.data.trim();
            }

            let btn = document.createElement("button");
            btn.innerText = "テキストをコピー!";
            btn.classList.add('btn', 'btn-primary', 'copy-sample-input');
            btn.style.display = "block";
            btn.addEventListener("click", () => {
                const elem = document.getElementById(node.id);
                document.getSelection().selectAllChildren(elem);
                if (document.execCommand("copy")) {
                    msg('success', 'テキストをコピーしました!');
                    document.getSelection().removeAllRanges();
                } else {
                    msg('error', 'コピーに失敗してしまったようです.');
                }
            }, false);
            node.insertAdjacentElement('beforebegin', btn);
        });

        // CodeMirror init
        const textarea = dqs('form[name=submit_form] textarea[name=sourcecode]');
        const editor = CodeMirror.fromTextArea(textarea, {
            mode: "text/x-c++src",
            lineNumbers: true,
        });

        // CodeMirror lang selection changed event handler
        const selectlang = dqs('form[name=submit_form] select[name=language]');
        selectlang.addEventListener('change', (event) => {
            const modelist = [
                'text/x-csrc', 'text/x-c++src', 'text/x-java', 'python', 'text/x-csharp'
            ];
            editor.setOption("mode", modelist[event.target.selectedIndex]);
        });

        // select lang C++17 as a default
        selectlang.selectedIndex = 1;
        selectlang.classList.add('form-control');

        // CodeMirror submit preprocess, remove default broken event
        const submitbtn = dqs('form[name=submit_form] input[type=submit]');
        submitbtn.removeAttribute("onclick");
        submitbtn.classList.add('btn-primary');
        document.submit_form.addEventListener('submit', (event) => {
            editor.save();
            if (textarea.value == '') {
                msg('warning', 'ソースコードが入力されていません');
                event.preventDefault();
            }
        });
    } else if (location.href.indexOf("/all_submit.php?") != -1 || location.href.indexOf("/my_submit.php?") != -1) {
        // on submit list page
        const parent = dqs('div.card-body');
        parent.id = 'cce-list-parent';
        const table = parent.querySelector('table.table');
        table.classList.add('table-striped', 'small');

        const tbody = table.querySelector('tbody');
        tbody.classList.add('list');
        tbody.querySelectorAll('tr').forEach((tr) => {
            tr.querySelectorAll('th').forEach((td, idx, nodelist) => { /* unformal html (th here should be td) */
                if (idx == nodelist.length - 1) {
                    return;
                }
                td.classList.add(`cce-list-sort-${idx}`);
            });
        });

        parent.querySelector('thead').querySelectorAll('tr th').forEach((th, idx, nodelist) => {
            if (idx == nodelist.length - 1) {
                return;
            } else if (idx == 1) {
                th.classList.add('desc');
            }
            th.classList.add('sort');
            th.setAttribute('data-sort', `cce-list-sort-${idx}`);
        });
        const userList = new List(parent.id, {
            valueNames: ['cce-list-sort-0', 'cce-list-sort-1', 'cce-list-sort-2', 'cce-list-sort-3']
        });
    } else if (location.href.indexOf("/problem_list.") != -1) {
        // on contest problem list page
        const table = dqs('table.table');

        const thead = table.querySelector('thead tr');
        const th0 = document.createElement("th");
        th0.innerText = '#';
        thead.insertAdjacentElement('afterbegin', th0);

        table.querySelectorAll('tbody tr').forEach((tr) => {
            const tr0 = document.createElement("th");
            console.log(tr.querySelector('a[href]').href);
            const a0 = tr.querySelector('a[href]');
            if (result = a0.href.match(/\/([^\/])\.(php|html?)?$/)) {
                const pid = result[1];
                const a1 = document.createElement("a");
                a1.href = a0.href;
                a1.innerText = pid;
                tr0.insertAdjacentElement('afterbegin', a1);
            } else {
                tr0.innerText = '?';
            }
            tr.insertAdjacentElement('afterbegin', tr0);
        });
    } else if (location.href.indexOf("/ranking.php?") != -1) {
        // on contest ranking page
        const parent = dqs('div.card-body');
        parent.id = 'cce-list-parent';
        const table = parent.querySelector('table.table');
        table.classList.add('table-striped', 'small', 'cce-ranking-table');

        const tbody = table.querySelector('tbody');
        tbody.classList.add('list');
        tbody.querySelectorAll('tr').forEach((tr, tridx) => {
            let endtime = '00:00:00';
            const submit_time = document.createElement("span");
            submit_time.classList.add('submit_time');
            tr.querySelectorAll('th').forEach((td, idx, nodelist) => { /* unformal html (th here should be td) */
                if (idx == 1) {
                    td.classList.add('cce-ranking-username');
                } else if (idx == 2) {
                    td.classList.add('cce-ranking-point');
                    td.setAttribute('data-cce-list-sort-point', `${tridx}`);
                    const divpoint = td.firstElementChild;
                    divpoint.insertAdjacentHTML('beforeend', '<br>');
                    divpoint.insertAdjacentElement('beforeend', submit_time);
                } else if (idx >= 3) {
                    const timespan = td.querySelector('span.submit_time');
                    if (timespan) {
                        td.setAttribute('data-cce-list-sort-timespan', timespan.innerText);
                        if (timespan.innerText > endtime) {
                            endtime = timespan.innerText;
                        }
                    } else {
                        td.setAttribute('data-cce-list-sort-timespan', '99:99:99');
                    }
                }
                td.classList.add(`cce-list-sort-${idx}`);
            });
            submit_time.innerText = endtime;
        });

        parent.querySelector('thead').querySelectorAll('tr th').forEach((th, idx, nodelist) => {
            if (idx == 0) {
                th.classList.add('asc');
            }
            th.classList.add('sort');
            th.setAttribute('data-sort', `cce-list-sort-${idx}`);
        });
        const userList = new List(parent.id, {
            valueNames: ['cce-list-sort-0', 'cce-list-sort-1',
                { name: 'cce-list-sort-2', attr: 'data-cce-list-sort-point' },
                { name: 'cce-list-sort-3', attr: 'data-cce-list-sort-timespan' },
                { name: 'cce-list-sort-4', attr: 'data-cce-list-sort-timespan' },
                { name: 'cce-list-sort-5', attr: 'data-cce-list-sort-timespan' },
                { name: 'cce-list-sort-6', attr: 'data-cce-list-sort-timespan' },
                { name: 'cce-list-sort-7', attr: 'data-cce-list-sort-timespan' },
                { name: 'cce-list-sort-8', attr: 'data-cce-list-sort-timespan' }]
        });
    }
})();