CafeCoder Enhancer

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey to install this script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==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' }]
        });
    }
})();